From e3e9392b15c314ca71e693d9d48e6469415d9a28 Mon Sep 17 00:00:00 2001 From: d-oit <6849456+d-oit@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:56:05 +0000 Subject: [PATCH 1/2] perf(sync): optimize local entity lookup in collectUpdates Pre-build Map indexes for local entities and claims in applyConflictResolution and collectUpdates, reducing entity/claim lookup complexity from O(C * N) to O(C + N) during conflict resolution. --- src/lib/sync/bridge.ts | 20 +++--- src/lib/sync/merge.perf.test.ts | 118 ++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 8 deletions(-) create mode 100644 src/lib/sync/merge.perf.test.ts diff --git a/src/lib/sync/bridge.ts b/src/lib/sync/bridge.ts index ef36f54d0..f175a3053 100644 --- a/src/lib/sync/bridge.ts +++ b/src/lib/sync/bridge.ts @@ -230,26 +230,29 @@ export function applyConflictResolution( const sync = getSyncDoc() const doc = getDoc() + const localEntityMap = new Map(localEntities.map((e) => [e.id, e])) + const localClaimMap = new Map(localClaims.map((c) => [c.id, c])) + const entityUpdates = collectUpdates( conflicts.filter((c) => c.entityType === 'entity'), resolutions, - localEntities, + localEntityMap, ) const claimUpdates = collectUpdates( conflicts.filter((c) => c.entityType === 'claim'), resolutions, - localClaims, + localClaimMap, ) doc.transact(() => { for (const [id, updates] of entityUpdates) { - const local = localEntities.find((e) => e.id === id) + const local = localEntityMap.get(id) if (local) { sync.entities.set(id, entityToYMap({ ...local, ...updates, updatedAt: new Date().toISOString() })) } } for (const [id, updates] of claimUpdates) { - const local = localClaims.find((c) => c.id === id) + const local = localClaimMap.get(id) if (local) { sync.claims.set(id, claimToYMap({ ...local, ...updates })) } @@ -257,16 +260,17 @@ export function applyConflictResolution( }, ORIGIN_OUTBOUND) } -function collectUpdates( +/** Collect field-level conflict update overrides into a Map indexed by ID. */ +const collectUpdates = ( conflicts: import('./merge').FieldConflict[], resolutions: Map, - locals: T[], -): Map { + localMap: Map, +): Map => { const updates = new Map() for (const conflict of conflicts) { const key = `${conflict.entityId}:${conflict.field}` if ((resolutions.get(key) ?? conflict.winner) === 'local') continue - const local = locals.find((item) => item.id === conflict.entityId) + const local = localMap.get(conflict.entityId) if (!local) continue const existing = updates.get(conflict.entityId) ?? { ...local } const field = conflict.field as keyof T diff --git a/src/lib/sync/merge.perf.test.ts b/src/lib/sync/merge.perf.test.ts new file mode 100644 index 000000000..326797669 --- /dev/null +++ b/src/lib/sync/merge.perf.test.ts @@ -0,0 +1,118 @@ +import { describe, it, expect } from 'vitest' +import { mergeEntities, mergeClaims } from './merge' +import type { Entity, Claim } from '@/lib/studio/types' + +function generateEntities(count: number): { local: Entity[]; remote: Entity[] } { + const local: Entity[] = [] + const remote: Entity[] = [] + + for (let i = 0; i < count; i++) { + const id = `entity-${i}` + const timestamp = new Date(1700000000000 + i * 1000).toISOString() + const remoteTimestamp = new Date(1700000000000 + i * 1000 + 500).toISOString() + + const links = [ + { targetId: `entity-${(i + 1) % count}`, relation: 'relates_to' }, + { targetId: `entity-${(i + 2) % count}`, relation: 'depends_on' }, + ] + + local.push({ + id, + name: `Entity ${i}`, + type: 'concept', + description: `Description ${i}`, + content: `Content for entity ${i}`, + sourceUrl: `https://example.com/${i}`, + tags: ['tag1', 'tag2', `tag-${i}`], + createdAt: timestamp, + updatedAt: timestamp, + links, + }) + + remote.push({ + id, + name: `Entity Remote ${i}`, + type: 'concept', + description: `Description ${i}`, + content: `Content for entity ${i}`, + sourceUrl: `https://example.com/${i}`, + tags: ['tag1', 'tag2', `tag-remote-${i}`], + createdAt: timestamp, + updatedAt: remoteTimestamp, + links: [ + ...links, + { targetId: `entity-${(i + 3) % count}`, relation: 'mentions' }, + ], + }) + } + + return { local, remote } +} + +function generateClaims(count: number): { local: Claim[]; remote: Claim[] } { + const local: Claim[] = [] + const remote: Claim[] = [] + + for (let i = 0; i < count; i++) { + const id = `claim-${i}` + const timestamp = new Date(1700000000000 + i * 1000).toISOString() + const remoteTimestamp = new Date(1700000000000 + i * 1000 + 500).toISOString() + + local.push({ + id, + entityId: `entity-${i % 100}`, + statement: `Statement ${i}`, + evidence: `Evidence ${i}`, + confidence: 0.8, + verification: 'verified', + source: `Source ${i}`, + createdAt: timestamp, + updatedAt: timestamp, + version: 1, + editHistory: [ + { statement: `Initial statement ${i}`, editedAt: timestamp }, + ], + }) + + remote.push({ + id, + entityId: `entity-${i % 100}`, + statement: `Remote Statement ${i}`, + evidence: `Evidence ${i}`, + confidence: 0.85, + verification: 'verified', + source: `Source ${i}`, + createdAt: timestamp, + updatedAt: remoteTimestamp, + version: 2, + editHistory: [ + { statement: `Initial statement ${i}`, editedAt: timestamp }, + { statement: `Remote Statement ${i}`, editedAt: remoteTimestamp }, + ], + }) + } + + return { local, remote } +} + +describe('merge performance', () => { + it('merges 1000 entities fast', () => { + const { local, remote } = generateEntities(1000) + const start = performance.now() + const result = mergeEntities(local, remote) + const duration = performance.now() - start + + console.error(`mergeEntities (1000 entities): ${duration.toFixed(2)}ms`) + expect(result.merged).toHaveLength(1000) + }) + + it('merges 1000 claims fast', () => { + const { local, remote } = generateClaims(1000) + const start = performance.now() + const result = mergeClaims(local, remote) + const duration = performance.now() - start + + console.error(`mergeClaims (1000 claims): ${duration.toFixed(2)}ms`) + expect(result.merged).toHaveLength(1000) + }) +}) From 3949127ba1c102ab67116c54c836e86fbfd15d37 Mon Sep 17 00:00:00 2001 From: d-oit <6849456+d-oit@users.noreply.github.com> Date: Wed, 7 Oct 2026 01:05:38 +0000 Subject: [PATCH 2/2] perf(sync): optimize local entity lookup in collectUpdates Pre-build Map indexes for local entities and claims in applyConflictResolution and collectUpdates, reducing entity/claim lookup complexity from O(C * N) to O(C + N) during conflict resolution.