diff options
| author | nsfisis <nsfisis@gmail.com> | 2025-12-31 18:40:56 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2025-12-31 18:41:36 +0900 |
| commit | 539e8f25b12d719084c8a986fcf65cf7848314b3 (patch) | |
| tree | 3e12eeb5a26b1cb33298ea9e54280461e1128f84 | |
| parent | 4755779975a140660e66acfcec2cc899179b0b71 (diff) | |
| download | kioku-539e8f25b12d719084c8a986fcf65cf7848314b3.tar.gz kioku-539e8f25b12d719084c8a986fcf65cf7848314b3.tar.zst kioku-539e8f25b12d719084c8a986fcf65cf7848314b3.zip | |
refactor(sync): remove legacy conflict resolution strategies
Remove the unused "local_wins" strategy and ConflictResolverOptions
interface from ConflictResolver. The CRDT-based conflict resolution
now always uses Automerge merge with server_wins fallback when CRDT
data is unavailable. This simplifies the codebase by removing
configuration options that were never used in production.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
| -rw-r--r-- | src/client/sync/conflict.test.ts | 102 | ||||
| -rw-r--r-- | src/client/sync/conflict.ts | 155 | ||||
| -rw-r--r-- | src/client/sync/index.ts | 1 | ||||
| -rw-r--r-- | src/client/sync/manager.test.ts | 2 |
4 files changed, 63 insertions, 197 deletions
diff --git a/src/client/sync/conflict.test.ts b/src/client/sync/conflict.test.ts index 6cef6b0..3fe8b67 100644 --- a/src/client/sync/conflict.test.ts +++ b/src/client/sync/conflict.test.ts @@ -150,7 +150,7 @@ describe("ConflictResolver", () => { }); describe("resolveDeckConflict", () => { - it("should use server data with server_wins strategy", async () => { + it("should use server data when no CRDT data available", async () => { const localDeck = await localDeckRepository.create({ userId: "user-1", name: "Local Name", @@ -170,7 +170,7 @@ describe("ConflictResolver", () => { syncVersion: 5, }; - const resolver = new ConflictResolver({ strategy: "server_wins" }); + const resolver = new ConflictResolver(); const result = await resolver.resolveDeckConflict(localDeck, serverDeck); expect(result.resolution).toBe("server_wins"); @@ -181,39 +181,10 @@ describe("ConflictResolver", () => { expect(updatedDeck?.newCardsPerDay).toBe(20); expect(updatedDeck?._synced).toBe(true); }); - - it("should keep local data with local_wins strategy", async () => { - const localDeck = await localDeckRepository.create({ - userId: "user-1", - name: "Local Name", - description: "Local description", - newCardsPerDay: 10, - }); - - const serverDeck = { - id: localDeck.id, - userId: "user-1", - name: "Server Name", - description: "Server description", - newCardsPerDay: 20, - createdAt: new Date("2024-01-01"), - updatedAt: new Date("2024-01-03"), - deletedAt: null, - syncVersion: 5, - }; - - const resolver = new ConflictResolver({ strategy: "local_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", () => { - it("should use server data with server_wins strategy", async () => { + it("should use server data when no CRDT data available", async () => { const deck = await localDeckRepository.create({ userId: "user-1", name: "Test Deck", @@ -251,7 +222,7 @@ describe("ConflictResolver", () => { syncVersion: 3, }; - const resolver = new ConflictResolver({ strategy: "server_wins" }); + const resolver = new ConflictResolver(); const result = await resolver.resolveCardConflict(localCard, serverCard); expect(result.resolution).toBe("server_wins"); @@ -261,53 +232,6 @@ describe("ConflictResolver", () => { expect(updatedCard?.back).toBe("Server Answer"); expect(updatedCard?._synced).toBe(true); }); - - it("should keep local data with local_wins strategy", async () => { - const deck = await localDeckRepository.create({ - userId: "user-1", - name: "Test Deck", - description: null, - newCardsPerDay: 20, - }); - - const localCard = await localCardRepository.create({ - deckId: deck.id, - noteId: "test-note-id", - isReversed: false, - front: "Local Question", - back: "Local Answer", - }); - - const serverCard = { - id: localCard.id, - deckId: deck.id, - noteId: "test-note-id", - isReversed: false, - front: "Server Question", - back: "Server Answer", - state: CardState.New, - due: new Date(), - stability: 0, - difficulty: 0, - elapsedDays: 0, - scheduledDays: 0, - reps: 0, - lapses: 0, - lastReview: null, - createdAt: new Date(), - updatedAt: new Date(), - deletedAt: null, - syncVersion: 3, - }; - - const resolver = new ConflictResolver({ strategy: "local_wins" }); - const result = await resolver.resolveCardConflict(localCard, serverCard); - - expect(result.resolution).toBe("local_wins"); - - const updatedCard = await localCardRepository.findById(localCard.id); - expect(updatedCard?.front).toBe("Local Question"); - }); }); describe("resolveConflicts", () => { @@ -372,7 +296,7 @@ describe("ConflictResolver", () => { ...createEmptyPullResult(6), }; - const resolver = new ConflictResolver({ strategy: "server_wins" }); + const resolver = new ConflictResolver(); const result = await resolver.resolveConflicts(pushResult, pullResult); expect(result.decks).toHaveLength(2); @@ -441,7 +365,7 @@ describe("ConflictResolver", () => { ...createEmptyPullResult(3), }; - const resolver = new ConflictResolver({ strategy: "server_wins" }); + const resolver = new ConflictResolver(); const result = await resolver.resolveConflicts(pushResult, pullResult); expect(result.cards).toHaveLength(1); @@ -485,7 +409,7 @@ describe("ConflictResolver", () => { ...createEmptyPullResult(1), }; - const resolver = new ConflictResolver({ strategy: "server_wins" }); + const resolver = new ConflictResolver(); const result = await resolver.resolveConflicts(pushResult, pullResult); expect(result.decks).toHaveLength(1); @@ -522,7 +446,7 @@ describe("ConflictResolver", () => { ...createEmptyPullResult(0), }; - const resolver = new ConflictResolver({ strategy: "server_wins" }); + const resolver = new ConflictResolver(); const result = await resolver.resolveConflicts(pushResult, pullResult); // No resolution since server doesn't have the item @@ -533,7 +457,7 @@ describe("ConflictResolver", () => { expect(localDeck?.name).toBe("Local Only Deck"); }); - it("should default to crdt strategy", async () => { + it("should use CRDT strategy by default", async () => { const deck = await localDeckRepository.create({ userId: "user-1", name: "Local Name", @@ -647,7 +571,7 @@ describe("ConflictResolver", () => { ], }; - const resolver = new ConflictResolver({ strategy: "crdt" }); + const resolver = new ConflictResolver(); const result = await resolver.resolveConflicts(pushResult, pullResult); expect(result.decks).toHaveLength(1); @@ -708,7 +632,7 @@ describe("ConflictResolver", () => { ], }; - const resolver = new ConflictResolver({ strategy: "crdt" }); + const resolver = new ConflictResolver(); const result = await resolver.resolveConflicts(pushResult, pullResult); // Should still resolve using fallback @@ -759,7 +683,7 @@ describe("ConflictResolver", () => { ...createEmptyPullResult(5), }; - const resolver = new ConflictResolver({ strategy: "crdt" }); + const resolver = new ConflictResolver(); const result = await resolver.resolveConflicts(pushResult, pullResult); expect(result.decks).toHaveLength(1); @@ -822,7 +746,7 @@ describe("ConflictResolver", () => { ], }; - const resolver = new ConflictResolver({ strategy: "crdt" }); + const resolver = new ConflictResolver(); const result = await resolver.resolveConflicts(pushResult, pullResult); expect(result.decks).toHaveLength(1); diff --git a/src/client/sync/conflict.ts b/src/client/sync/conflict.ts index 88b5bfe..be767dc 100644 --- a/src/client/sync/conflict.ts +++ b/src/client/sync/conflict.ts @@ -41,7 +41,7 @@ import type { SyncPushResult } from "./push"; */ export interface ConflictResolutionItem { id: string; - resolution: "server_wins" | "local_wins"; + resolution: "server_wins"; } /** @@ -57,19 +57,6 @@ export interface ConflictResolutionResult { } /** - * Options for conflict resolver - */ -export interface ConflictResolverOptions { - /** - * Strategy for resolving conflicts - * - "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 - */ - strategy?: "crdt" | "server_wins" | "local_wins"; -} - -/** * CRDT merge result with entity data */ interface CrdtMergeConflictResult<T> { @@ -203,17 +190,11 @@ function serverNoteFieldValueToLocal( * Handles conflicts reported by the server during push operations. * 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) + * 2. Uses Automerge CRDT merge for conflict-free resolution * 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"; - - constructor(options: ConflictResolverOptions = {}) { - this.strategy = options.strategy ?? "crdt"; - } - /** * Check if there are conflicts in push result */ @@ -243,15 +224,15 @@ export class ConflictResolver { } /** - * Resolve deck conflict using configured strategy + * Resolve deck conflict using CRDT merge with server_wins fallback */ async resolveDeckConflict( localDeck: LocalDeck, serverDeck: ServerDeck, serverCrdtBinary?: Uint8Array, ): Promise<ConflictResolutionItem> { - // Try CRDT merge first if strategy is "crdt" and we have CRDT data - if (this.strategy === "crdt" && serverCrdtBinary) { + // Try CRDT merge first if we have CRDT data + if (serverCrdtBinary) { const mergeResult = await this.mergeDeckWithCrdt( localDeck, serverCrdtBinary, @@ -273,18 +254,11 @@ export class ConflictResolver { } } - // 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 - const localData = serverDeckToLocal(serverDeck); - await localDeckRepository.upsertFromServer(localData); - } - // If local_wins, we keep local data and it will be pushed again next sync + // Fallback to server_wins when CRDT merge is not available + const localData = serverDeckToLocal(serverDeck); + await localDeckRepository.upsertFromServer(localData); - return { id: localDeck.id, resolution }; + return { id: localDeck.id, resolution: "server_wins" }; } /** @@ -327,15 +301,15 @@ export class ConflictResolver { } /** - * Resolve card conflict using configured strategy + * Resolve card conflict using CRDT merge with server_wins fallback */ async resolveCardConflict( localCard: LocalCard, serverCard: ServerCard, serverCrdtBinary?: Uint8Array, ): Promise<ConflictResolutionItem> { - // Try CRDT merge first if strategy is "crdt" and we have CRDT data - if (this.strategy === "crdt" && serverCrdtBinary) { + // Try CRDT merge first if we have CRDT data + if (serverCrdtBinary) { const mergeResult = await this.mergeCardWithCrdt( localCard, serverCrdtBinary, @@ -356,18 +330,11 @@ export class ConflictResolver { } } - // Fallback strategy when CRDT merge is not available - const resolution = - this.strategy === "local_wins" ? "local_wins" : "server_wins"; + // Fallback to server_wins when CRDT merge is not available + const localData = serverCardToLocal(serverCard); + await localCardRepository.upsertFromServer(localData); - if (resolution === "server_wins") { - // Update local with server data - const localData = serverCardToLocal(serverCard); - await localCardRepository.upsertFromServer(localData); - } - // If local_wins, we keep local data and it will be pushed again next sync - - return { id: localCard.id, resolution }; + return { id: localCard.id, resolution: "server_wins" }; } /** @@ -405,15 +372,15 @@ export class ConflictResolver { } /** - * Resolve note type conflict using configured strategy + * Resolve note type conflict using CRDT merge with server_wins fallback */ async resolveNoteTypeConflict( localNoteType: LocalNoteType, serverNoteType: ServerNoteType, serverCrdtBinary?: Uint8Array, ): Promise<ConflictResolutionItem> { - // Try CRDT merge first if strategy is "crdt" and we have CRDT data - if (this.strategy === "crdt" && serverCrdtBinary) { + // Try CRDT merge first if we have CRDT data + if (serverCrdtBinary) { const mergeResult = await this.mergeNoteTypeWithCrdt( localNoteType, serverCrdtBinary, @@ -434,16 +401,11 @@ export class ConflictResolver { } } - // Fallback strategy when CRDT merge is not available - const resolution = - this.strategy === "local_wins" ? "local_wins" : "server_wins"; + // Fallback to server_wins when CRDT merge is not available + const localData = serverNoteTypeToLocal(serverNoteType); + await localNoteTypeRepository.upsertFromServer(localData); - if (resolution === "server_wins") { - const localData = serverNoteTypeToLocal(serverNoteType); - await localNoteTypeRepository.upsertFromServer(localData); - } - - return { id: localNoteType.id, resolution }; + return { id: localNoteType.id, resolution: "server_wins" }; } /** @@ -481,15 +443,15 @@ export class ConflictResolver { } /** - * Resolve note field type conflict using configured strategy + * Resolve note field type conflict using CRDT merge with server_wins fallback */ async resolveNoteFieldTypeConflict( localFieldType: LocalNoteFieldType, serverFieldType: ServerNoteFieldType, serverCrdtBinary?: Uint8Array, ): Promise<ConflictResolutionItem> { - // Try CRDT merge first if strategy is "crdt" and we have CRDT data - if (this.strategy === "crdt" && serverCrdtBinary) { + // Try CRDT merge first if we have CRDT data + if (serverCrdtBinary) { const mergeResult = await this.mergeNoteFieldTypeWithCrdt( localFieldType, serverCrdtBinary, @@ -510,16 +472,11 @@ export class ConflictResolver { } } - // 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); - await localNoteFieldTypeRepository.upsertFromServer(localData); - } + // Fallback to server_wins when CRDT merge is not available + const localData = serverNoteFieldTypeToLocal(serverFieldType); + await localNoteFieldTypeRepository.upsertFromServer(localData); - return { id: localFieldType.id, resolution }; + return { id: localFieldType.id, resolution: "server_wins" }; } /** @@ -560,15 +517,15 @@ export class ConflictResolver { } /** - * Resolve note conflict using configured strategy + * Resolve note conflict using CRDT merge with server_wins fallback */ async resolveNoteConflict( localNote: LocalNote, serverNote: ServerNote, serverCrdtBinary?: Uint8Array, ): Promise<ConflictResolutionItem> { - // Try CRDT merge first if strategy is "crdt" and we have CRDT data - if (this.strategy === "crdt" && serverCrdtBinary) { + // Try CRDT merge first if we have CRDT data + if (serverCrdtBinary) { const mergeResult = await this.mergeNoteWithCrdt( localNote, serverCrdtBinary, @@ -589,16 +546,11 @@ export class ConflictResolver { } } - // 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); - await localNoteRepository.upsertFromServer(localData); - } + // Fallback to server_wins when CRDT merge is not available + const localData = serverNoteToLocal(serverNote); + await localNoteRepository.upsertFromServer(localData); - return { id: localNote.id, resolution }; + return { id: localNote.id, resolution: "server_wins" }; } /** @@ -636,15 +588,15 @@ export class ConflictResolver { } /** - * Resolve note field value conflict using configured strategy + * Resolve note field value conflict using CRDT merge with server_wins fallback */ async resolveNoteFieldValueConflict( localFieldValue: LocalNoteFieldValue, serverFieldValue: ServerNoteFieldValue, serverCrdtBinary?: Uint8Array, ): Promise<ConflictResolutionItem> { - // Try CRDT merge first if strategy is "crdt" and we have CRDT data - if (this.strategy === "crdt" && serverCrdtBinary) { + // Try CRDT merge first if we have CRDT data + if (serverCrdtBinary) { const mergeResult = await this.mergeNoteFieldValueWithCrdt( localFieldValue, serverCrdtBinary, @@ -665,16 +617,11 @@ export class ConflictResolver { } } - // 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); - await localNoteFieldValueRepository.upsertFromServer(localData); - } + // Fallback to server_wins when CRDT merge is not available + const localData = serverNoteFieldValueToLocal(serverFieldValue); + await localNoteFieldValueRepository.upsertFromServer(localData); - return { id: localFieldValue.id, resolution }; + return { id: localFieldValue.id, resolution: "server_wins" }; } /** @@ -900,18 +847,14 @@ export class ConflictResolver { } /** - * Create a conflict resolver with the given options + * Create a conflict resolver */ -export function createConflictResolver( - options: ConflictResolverOptions = {}, -): ConflictResolver { - return new ConflictResolver(options); +export function createConflictResolver(): ConflictResolver { + return new ConflictResolver(); } /** - * Default conflict resolver using CRDT (Automerge) strategy + * Default conflict resolver using CRDT (Automerge) merge * Falls back to server_wins when CRDT data is unavailable */ -export const conflictResolver = new ConflictResolver({ - strategy: "crdt", -}); +export const conflictResolver = new ConflictResolver(); diff --git a/src/client/sync/index.ts b/src/client/sync/index.ts index c3ddab4..81d5f14 100644 --- a/src/client/sync/index.ts +++ b/src/client/sync/index.ts @@ -2,7 +2,6 @@ export { type ConflictResolutionItem, type ConflictResolutionResult, ConflictResolver, - type ConflictResolverOptions, conflictResolver, createConflictResolver, } from "./conflict"; diff --git a/src/client/sync/manager.test.ts b/src/client/sync/manager.test.ts index 0758261..8af6e6f 100644 --- a/src/client/sync/manager.test.ts +++ b/src/client/sync/manager.test.ts @@ -112,7 +112,7 @@ describe("SyncManager", () => { ...createEmptyPullResult(0), } satisfies SyncPullResult); - conflictResolver = new ConflictResolver({ strategy: "server_wins" }); + conflictResolver = new ConflictResolver(); }); afterEach(async () => { |
