aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2025-12-31 18:25:56 +0900
committernsfisis <nsfisis@gmail.com>2025-12-31 18:25:56 +0900
commit4755779975a140660e66acfcec2cc899179b0b71 (patch)
tree37491529031459767a9e1dfdf2e4570a977ab1e5
parentd7b55e7d9a3dce5e3d7a71d5266aa2787c6ad5f8 (diff)
downloadkioku-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.md2
-rw-r--r--src/client/sync/conflict.test.ts58
-rw-r--r--src/client/sync/conflict.ts174
-rw-r--r--src/client/sync/push.ts2
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;