diff --git a/packages/syncing-server/src/Domain/Item/SaveRule/TimeDifferenceFilter.spec.ts b/packages/syncing-server/src/Domain/Item/SaveRule/TimeDifferenceFilter.spec.ts index 17c97e0fb..0f5b5619c 100644 --- a/packages/syncing-server/src/Domain/Item/SaveRule/TimeDifferenceFilter.spec.ts +++ b/packages/syncing-server/src/Domain/Item/SaveRule/TimeDifferenceFilter.spec.ts @@ -221,6 +221,51 @@ describe('TimeDifferenceFilter', () => { }) }) + it('should pass items with different timestamp but identical content (microsecond precision)', async () => { + itemHash = ItemHash.create({ + ...itemHash.props, + content: 'foobar', + enc_item_key: undefined, + updated_at_timestamp: existingItem.props.timestamps.updatedAt + 1, + }).getValue() + + const result = await createFilter().check({ + userUuid: '1-2-3', + apiVersion: ApiVersion.v20200115, + snjsVersion: '2.200.0', + itemHash, + existingItem, + }) + + expect(result).toEqual({ + passed: true, + }) + }) + + it('should still conflict items with different timestamp AND different content', async () => { + itemHash = ItemHash.create({ + ...itemHash.props, + content: 'totally different stuff', + updated_at_timestamp: existingItem.props.timestamps.updatedAt + 1, + }).getValue() + + const result = await createFilter().check({ + userUuid: '1-2-3', + apiVersion: ApiVersion.v20200115, + snjsVersion: '2.200.0', + itemHash, + existingItem, + }) + + expect(result).toEqual({ + passed: false, + conflict: { + serverItem: existingItem, + type: 'sync_conflict', + }, + }) + }) + it('should leave items having update at timestamp different by less than a millisecond', async () => { itemHash = ItemHash.create({ ...itemHash.props, diff --git a/packages/syncing-server/src/Domain/Item/SaveRule/TimeDifferenceFilter.ts b/packages/syncing-server/src/Domain/Item/SaveRule/TimeDifferenceFilter.ts index ce9fef98b..028bd77d3 100644 --- a/packages/syncing-server/src/Domain/Item/SaveRule/TimeDifferenceFilter.ts +++ b/packages/syncing-server/src/Domain/Item/SaveRule/TimeDifferenceFilter.ts @@ -37,6 +37,10 @@ export class TimeDifferenceFilter implements ItemSaveRuleInterface { if (this.itemHashHasMicrosecondsPrecision(dto.itemHash)) { const passed = difference === 0 + if (!passed && this.contentIsIdentical(dto)) { + return { passed: true } + } + return { passed, conflict: passed @@ -50,6 +54,10 @@ export class TimeDifferenceFilter implements ItemSaveRuleInterface { const passed = Math.abs(difference) < this.getMinimalConflictIntervalMicroseconds(dto.apiVersion) + if (!passed && this.contentIsIdentical(dto)) { + return { passed: true } + } + return { passed, conflict: passed @@ -69,6 +77,18 @@ export class TimeDifferenceFilter implements ItemSaveRuleInterface { return itemHash.props.updated_at_timestamp !== undefined } + private contentIsIdentical(dto: ItemSaveValidationDTO): boolean { + if (!dto.existingItem) { + return false + } + const incomingContent = dto.itemHash.props.content ?? null + const existingContent = dto.existingItem.props.content + const incomingEncKey = dto.itemHash.props.enc_item_key ?? null + const existingEncKey = dto.existingItem.props.encItemKey + + return incomingContent !== null && incomingContent === existingContent && incomingEncKey === existingEncKey + } + private getMinimalConflictIntervalMicroseconds(apiVersion?: string): number { switch (apiVersion) { case ApiVersion.v20161215: