From 5a2c25409c6a3ed82db11fa9ae33dc1117fc4b46 Mon Sep 17 00:00:00 2001 From: Alexander Adam Date: Fri, 20 Mar 2026 19:38:17 +0100 Subject: [PATCH 1/2] fix(syncing-server): skip conflict when content is identical bypass sync_conflict in TimeDifferenceFilter if encrypted content and enc_item_key match between incoming and existing item, even when timestamps differ. prevents unecessary duplicate creation. --- .../Item/SaveRule/TimeDifferenceFilter.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) 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: From b965c80bc16ba183d07d74390317e651ea103182 Mon Sep 17 00:00:00 2001 From: Alexander Adam Date: Fri, 20 Mar 2026 20:11:42 +0100 Subject: [PATCH 2/2] add tests for content-identical bypass in TimeDifferenceFilter --- .../SaveRule/TimeDifferenceFilter.spec.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) 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,