aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2025-12-31 18:40:56 +0900
committernsfisis <nsfisis@gmail.com>2025-12-31 18:41:36 +0900
commit539e8f25b12d719084c8a986fcf65cf7848314b3 (patch)
tree3e12eeb5a26b1cb33298ea9e54280461e1128f84
parent4755779975a140660e66acfcec2cc899179b0b71 (diff)
downloadkioku-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.ts102
-rw-r--r--src/client/sync/conflict.ts155
-rw-r--r--src/client/sync/index.ts1
-rw-r--r--src/client/sync/manager.test.ts2
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 () => {