diff options
| author | nsfisis <nsfisis@gmail.com> | 2025-12-31 18:25:56 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2025-12-31 18:25:56 +0900 |
| commit | 4755779975a140660e66acfcec2cc899179b0b71 (patch) | |
| tree | 37491529031459767a9e1dfdf2e4570a977ab1e5 | |
| parent | d7b55e7d9a3dce5e3d7a71d5266aa2787c6ad5f8 (diff) | |
| download | kioku-4755779975a140660e66acfcec2cc899179b0b71.tar.gz kioku-4755779975a140660e66acfcec2cc899179b0b71.tar.zst kioku-4755779975a140660e66acfcec2cc899179b0b71.zip | |
refactor(sync): remove legacy LWW conflict resolution strategy
Remove the deprecated "newer_wins" strategy from the conflict resolver
as CRDT is now the default and validated approach. The system now uses
only "crdt", "server_wins", and "local_wins" strategies.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
| -rw-r--r-- | docs/dev/roadmap.md | 2 | ||||
| -rw-r--r-- | src/client/sync/conflict.test.ts | 58 | ||||
| -rw-r--r-- | src/client/sync/conflict.ts | 174 | ||||
| -rw-r--r-- | src/client/sync/push.ts | 2 |
4 files changed, 38 insertions, 198 deletions
diff --git a/docs/dev/roadmap.md b/docs/dev/roadmap.md index 28facb6..022e09e 100644 --- a/docs/dev/roadmap.md +++ b/docs/dev/roadmap.md @@ -45,4 +45,4 @@ Replace the current Last-Write-Wins (LWW) conflict resolution with Automerge CRD - [x] Add unit tests for CRDT operations - [x] Add integration tests for concurrent edit scenarios -- [ ] Remove legacy LWW code after validation +- [x] Remove legacy LWW code after validation diff --git a/src/client/sync/conflict.test.ts b/src/client/sync/conflict.test.ts index e648373..6cef6b0 100644 --- a/src/client/sync/conflict.test.ts +++ b/src/client/sync/conflict.test.ts @@ -210,64 +210,6 @@ describe("ConflictResolver", () => { const updatedDeck = await localDeckRepository.findById(localDeck.id); expect(updatedDeck?.name).toBe("Local Name"); }); - - it("should use server data when server is newer with newer_wins strategy", async () => { - const localDeck = await localDeckRepository.create({ - userId: "user-1", - name: "Local Name", - description: null, - newCardsPerDay: 10, - }); - - const serverDeck = { - id: localDeck.id, - userId: "user-1", - name: "Server Name", - description: null, - newCardsPerDay: 20, - createdAt: new Date("2024-01-01"), - updatedAt: new Date(Date.now() + 10000), // Server is newer - deletedAt: null, - syncVersion: 5, - }; - - const resolver = new ConflictResolver({ strategy: "newer_wins" }); - const result = await resolver.resolveDeckConflict(localDeck, serverDeck); - - expect(result.resolution).toBe("server_wins"); - - const updatedDeck = await localDeckRepository.findById(localDeck.id); - expect(updatedDeck?.name).toBe("Server Name"); - }); - - it("should use local data when local is newer with newer_wins strategy", async () => { - const localDeck = await localDeckRepository.create({ - userId: "user-1", - name: "Local Name", - description: null, - newCardsPerDay: 10, - }); - - const serverDeck = { - id: localDeck.id, - userId: "user-1", - name: "Server Name", - description: null, - newCardsPerDay: 20, - createdAt: new Date("2024-01-01"), - updatedAt: new Date("2024-01-01"), // Server is older - deletedAt: null, - syncVersion: 5, - }; - - const resolver = new ConflictResolver({ strategy: "newer_wins" }); - const result = await resolver.resolveDeckConflict(localDeck, serverDeck); - - expect(result.resolution).toBe("local_wins"); - - const updatedDeck = await localDeckRepository.findById(localDeck.id); - expect(updatedDeck?.name).toBe("Local Name"); - }); }); describe("resolveCardConflict", () => { diff --git a/src/client/sync/conflict.ts b/src/client/sync/conflict.ts index a49e9b2..88b5bfe 100644 --- a/src/client/sync/conflict.ts +++ b/src/client/sync/conflict.ts @@ -65,17 +65,8 @@ export interface ConflictResolverOptions { * - "crdt": Use Automerge CRDT merge for conflict-free resolution (default) * - "server_wins": Always use server data (fallback when no CRDT data) * - "local_wins": Always use local data - * - "newer_wins": Compare timestamps and use newer data (legacy LWW) */ - strategy?: "crdt" | "server_wins" | "local_wins" | "newer_wins"; -} - -/** - * Compare timestamps for LWW resolution (legacy fallback) - * Returns true if server data is newer or equal - */ -function isServerNewer(serverUpdatedAt: Date, localUpdatedAt: Date): boolean { - return serverUpdatedAt.getTime() >= localUpdatedAt.getTime(); + strategy?: "crdt" | "server_wins" | "local_wins"; } /** @@ -213,11 +204,11 @@ function serverNoteFieldValueToLocal( * When a conflict occurs (server has newer data), this resolver: * 1. Identifies conflicting items from push result * 2. Uses Automerge CRDT merge for conflict-free resolution (default) - * 3. Falls back to LWW strategies when CRDT data is unavailable + * 3. Falls back to server_wins when CRDT data is unavailable * 4. Updates local database accordingly */ export class ConflictResolver { - private strategy: "crdt" | "server_wins" | "local_wins" | "newer_wins"; + private strategy: "crdt" | "server_wins" | "local_wins"; constructor(options: ConflictResolverOptions = {}) { this.strategy = options.strategy ?? "crdt"; @@ -282,26 +273,9 @@ export class ConflictResolver { } } - // Fallback to LWW strategies - let resolution: "server_wins" | "local_wins"; - - switch (this.strategy) { - case "crdt": - case "server_wins": - resolution = "server_wins"; - break; - case "local_wins": - resolution = "local_wins"; - break; - case "newer_wins": - resolution = isServerNewer( - new Date(serverDeck.updatedAt), - localDeck.updatedAt, - ) - ? "server_wins" - : "local_wins"; - break; - } + // Fallback strategy when CRDT merge is not available + const resolution = + this.strategy === "local_wins" ? "local_wins" : "server_wins"; if (resolution === "server_wins") { // Update local with server data @@ -344,7 +318,10 @@ export class ConflictResolver { hadLocalDocument: localBinary !== null, }; } catch (error) { - console.warn("CRDT merge failed for deck, falling back to LWW:", error); + console.warn( + "CRDT merge failed for deck, falling back to server_wins:", + error, + ); return null; } } @@ -379,26 +356,9 @@ export class ConflictResolver { } } - // Fallback to LWW strategies - let resolution: "server_wins" | "local_wins"; - - switch (this.strategy) { - case "crdt": - case "server_wins": - resolution = "server_wins"; - break; - case "local_wins": - resolution = "local_wins"; - break; - case "newer_wins": - resolution = isServerNewer( - new Date(serverCard.updatedAt), - localCard.updatedAt, - ) - ? "server_wins" - : "local_wins"; - break; - } + // Fallback strategy when CRDT merge is not available + const resolution = + this.strategy === "local_wins" ? "local_wins" : "server_wins"; if (resolution === "server_wins") { // Update local with server data @@ -436,7 +396,10 @@ export class ConflictResolver { hadLocalDocument: localBinary !== null, }; } catch (error) { - console.warn("CRDT merge failed for card, falling back to LWW:", error); + console.warn( + "CRDT merge failed for card, falling back to server_wins:", + error, + ); return null; } } @@ -471,26 +434,9 @@ export class ConflictResolver { } } - // Fallback to LWW strategies - let resolution: "server_wins" | "local_wins"; - - switch (this.strategy) { - case "crdt": - case "server_wins": - resolution = "server_wins"; - break; - case "local_wins": - resolution = "local_wins"; - break; - case "newer_wins": - resolution = isServerNewer( - new Date(serverNoteType.updatedAt), - localNoteType.updatedAt, - ) - ? "server_wins" - : "local_wins"; - break; - } + // Fallback strategy when CRDT merge is not available + const resolution = + this.strategy === "local_wins" ? "local_wins" : "server_wins"; if (resolution === "server_wins") { const localData = serverNoteTypeToLocal(serverNoteType); @@ -527,7 +473,7 @@ export class ConflictResolver { }; } catch (error) { console.warn( - "CRDT merge failed for note type, falling back to LWW:", + "CRDT merge failed for note type, falling back to server_wins:", error, ); return null; @@ -564,26 +510,9 @@ export class ConflictResolver { } } - // Fallback to LWW strategies - let resolution: "server_wins" | "local_wins"; - - switch (this.strategy) { - case "crdt": - case "server_wins": - resolution = "server_wins"; - break; - case "local_wins": - resolution = "local_wins"; - break; - case "newer_wins": - resolution = isServerNewer( - new Date(serverFieldType.updatedAt), - localFieldType.updatedAt, - ) - ? "server_wins" - : "local_wins"; - break; - } + // Fallback strategy when CRDT merge is not available + const resolution = + this.strategy === "local_wins" ? "local_wins" : "server_wins"; if (resolution === "server_wins") { const localData = serverNoteFieldTypeToLocal(serverFieldType); @@ -623,7 +552,7 @@ export class ConflictResolver { }; } catch (error) { console.warn( - "CRDT merge failed for note field type, falling back to LWW:", + "CRDT merge failed for note field type, falling back to server_wins:", error, ); return null; @@ -660,26 +589,9 @@ export class ConflictResolver { } } - // Fallback to LWW strategies - let resolution: "server_wins" | "local_wins"; - - switch (this.strategy) { - case "crdt": - case "server_wins": - resolution = "server_wins"; - break; - case "local_wins": - resolution = "local_wins"; - break; - case "newer_wins": - resolution = isServerNewer( - new Date(serverNote.updatedAt), - localNote.updatedAt, - ) - ? "server_wins" - : "local_wins"; - break; - } + // Fallback strategy when CRDT merge is not available + const resolution = + this.strategy === "local_wins" ? "local_wins" : "server_wins"; if (resolution === "server_wins") { const localData = serverNoteToLocal(serverNote); @@ -715,7 +627,10 @@ export class ConflictResolver { hadLocalDocument: localBinary !== null, }; } catch (error) { - console.warn("CRDT merge failed for note, falling back to LWW:", error); + console.warn( + "CRDT merge failed for note, falling back to server_wins:", + error, + ); return null; } } @@ -750,26 +665,9 @@ export class ConflictResolver { } } - // Fallback to LWW strategies - let resolution: "server_wins" | "local_wins"; - - switch (this.strategy) { - case "crdt": - case "server_wins": - resolution = "server_wins"; - break; - case "local_wins": - resolution = "local_wins"; - break; - case "newer_wins": - resolution = isServerNewer( - new Date(serverFieldValue.updatedAt), - localFieldValue.updatedAt, - ) - ? "server_wins" - : "local_wins"; - break; - } + // Fallback strategy when CRDT merge is not available + const resolution = + this.strategy === "local_wins" ? "local_wins" : "server_wins"; if (resolution === "server_wins") { const localData = serverNoteFieldValueToLocal(serverFieldValue); @@ -809,7 +707,7 @@ export class ConflictResolver { }; } catch (error) { console.warn( - "CRDT merge failed for note field value, falling back to LWW:", + "CRDT merge failed for note field value, falling back to server_wins:", error, ); return null; diff --git a/src/client/sync/push.ts b/src/client/sync/push.ts index eea671b..61c7f30 100644 --- a/src/client/sync/push.ts +++ b/src/client/sync/push.ts @@ -381,7 +381,7 @@ export function pendingChangesToPushData( * 2. Convert to API format * 3. Send to server * 4. Mark items as synced on success - * 5. Handle conflicts (server wins for LWW) + * 5. Handle conflicts using CRDT merge (fallback to server_wins) */ export class PushService { private syncQueue: SyncQueue; |
