aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/client/sync
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/sync')
-rw-r--r--src/client/sync/conflict.test.ts3
-rw-r--r--src/client/sync/conflict.ts24
-rw-r--r--src/client/sync/index.ts69
-rw-r--r--src/client/sync/manager.test.ts23
-rw-r--r--src/client/sync/manager.ts3
-rw-r--r--src/client/sync/pull.test.ts10
-rw-r--r--src/client/sync/push.test.ts6
-rw-r--r--src/client/sync/push.ts4
-rw-r--r--src/client/sync/queue.test.ts8
-rw-r--r--src/client/sync/queue.ts71
10 files changed, 125 insertions, 96 deletions
diff --git a/src/client/sync/conflict.test.ts b/src/client/sync/conflict.test.ts
index 7f86953..211f410 100644
--- a/src/client/sync/conflict.test.ts
+++ b/src/client/sync/conflict.test.ts
@@ -466,7 +466,8 @@ describe("ConflictResolver", () => {
expect(result.decks).toHaveLength(1);
expect(result.decks[0]?.resolution).toBe("server_wins");
- const insertedDeck = await localDeckRepository.findById("non-existent-deck");
+ const insertedDeck =
+ await localDeckRepository.findById("non-existent-deck");
expect(insertedDeck?.name).toBe("Server Deck");
});
diff --git a/src/client/sync/conflict.ts b/src/client/sync/conflict.ts
index 365ef3c..4e0e3ef 100644
--- a/src/client/sync/conflict.ts
+++ b/src/client/sync/conflict.ts
@@ -1,8 +1,5 @@
import type { LocalCard, LocalDeck } from "../db/index";
-import {
- localCardRepository,
- localDeckRepository,
-} from "../db/repositories";
+import { localCardRepository, localDeckRepository } from "../db/repositories";
import type { ServerCard, ServerDeck, SyncPullResult } from "./pull";
import type { SyncPushResult } from "./push";
@@ -39,10 +36,7 @@ export interface ConflictResolverOptions {
* Compare timestamps for LWW resolution
* Returns true if server data is newer or equal
*/
-function isServerNewer(
- serverUpdatedAt: Date,
- localUpdatedAt: Date,
-): boolean {
+function isServerNewer(serverUpdatedAt: Date, localUpdatedAt: Date): boolean {
return serverUpdatedAt.getTime() >= localUpdatedAt.getTime();
}
@@ -222,7 +216,10 @@ export class ConflictResolver {
const serverDeck = pullResult.decks.find((d) => d.id === deckId);
if (localDeck && serverDeck) {
- const resolution = await this.resolveDeckConflict(localDeck, serverDeck);
+ const resolution = await this.resolveDeckConflict(
+ localDeck,
+ serverDeck,
+ );
result.decks.push(resolution);
} else if (serverDeck) {
// Local doesn't exist, apply server data
@@ -239,7 +236,10 @@ export class ConflictResolver {
const serverCard = pullResult.cards.find((c) => c.id === cardId);
if (localCard && serverCard) {
- const resolution = await this.resolveCardConflict(localCard, serverCard);
+ const resolution = await this.resolveCardConflict(
+ localCard,
+ serverCard,
+ );
result.cards.push(resolution);
} else if (serverCard) {
// Local doesn't exist, apply server data
@@ -266,4 +266,6 @@ export function createConflictResolver(
/**
* Default conflict resolver using LWW (server wins) strategy
*/
-export const conflictResolver = new ConflictResolver({ strategy: "server_wins" });
+export const conflictResolver = new ConflictResolver({
+ strategy: "server_wins",
+});
diff --git a/src/client/sync/index.ts b/src/client/sync/index.ts
index a602753..c3ddab4 100644
--- a/src/client/sync/index.ts
+++ b/src/client/sync/index.ts
@@ -1,50 +1,47 @@
export {
- SyncQueue,
- SyncStatus,
- syncQueue,
- type PendingChanges,
- type SyncQueueListener,
- type SyncQueueState,
- type SyncStatusType,
-} from "./queue";
-
+ type ConflictResolutionItem,
+ type ConflictResolutionResult,
+ ConflictResolver,
+ type ConflictResolverOptions,
+ conflictResolver,
+ createConflictResolver,
+} from "./conflict";
export {
- createPushService,
- pendingChangesToPushData,
- PushService,
- type PushServiceOptions,
- type SyncCardData,
- type SyncDeckData,
- type SyncPushData,
- type SyncPushResult,
- type SyncReviewLogData,
-} from "./push";
+ createSyncManager,
+ SyncManager,
+ type SyncManagerEvent,
+ type SyncManagerListener,
+ type SyncManagerOptions,
+ type SyncResult,
+} from "./manager";
export {
createPullService,
- pullResultToLocalData,
PullService,
type PullServiceOptions,
+ pullResultToLocalData,
type ServerCard,
type ServerDeck,
type ServerReviewLog,
type SyncPullResult,
} from "./pull";
-
export {
- ConflictResolver,
- conflictResolver,
- createConflictResolver,
- type ConflictResolutionItem,
- type ConflictResolutionResult,
- type ConflictResolverOptions,
-} from "./conflict";
-
+ createPushService,
+ PushService,
+ type PushServiceOptions,
+ pendingChangesToPushData,
+ type SyncCardData,
+ type SyncDeckData,
+ type SyncPushData,
+ type SyncPushResult,
+ type SyncReviewLogData,
+} from "./push";
export {
- createSyncManager,
- SyncManager,
- type SyncManagerEvent,
- type SyncManagerListener,
- type SyncManagerOptions,
- type SyncResult,
-} from "./manager";
+ type PendingChanges,
+ SyncQueue,
+ type SyncQueueListener,
+ type SyncQueueState,
+ SyncStatus,
+ type SyncStatusType,
+ syncQueue,
+} from "./queue";
diff --git a/src/client/sync/manager.test.ts b/src/client/sync/manager.test.ts
index 1e53bd4..96fb97d 100644
--- a/src/client/sync/manager.test.ts
+++ b/src/client/sync/manager.test.ts
@@ -8,8 +8,8 @@ import {
describe,
expect,
it,
- vi,
type Mock,
+ vi,
} from "vitest";
import { db } from "../db/index";
import { localDeckRepository } from "../db/repositories";
@@ -42,9 +42,8 @@ describe("SyncManager", () => {
/**
* Create a pending deck in the database that will need to be synced
*/
- async function createPendingDeck(id = "deck-1") {
+ async function createPendingDeck() {
return localDeckRepository.create({
- id,
userId: "user-1",
name: "Test Deck",
description: null,
@@ -169,12 +168,10 @@ describe("SyncManager", () => {
manager.start();
// Should only be called once for each event type
- expect(
- addSpy.mock.calls.filter((c) => c[0] === "online").length,
- ).toBe(1);
- expect(
- addSpy.mock.calls.filter((c) => c[0] === "offline").length,
- ).toBe(1);
+ expect(addSpy.mock.calls.filter((c) => c[0] === "online").length).toBe(1);
+ expect(addSpy.mock.calls.filter((c) => c[0] === "offline").length).toBe(
+ 1,
+ );
manager.stop();
addSpy.mockRestore();
@@ -354,20 +351,20 @@ describe("SyncManager", () => {
it("should resolve conflicts when present", async () => {
// Create pending data so pushToServer will be called
- await createPendingDeck();
+ const deck = await createPendingDeck();
const pushResult: SyncPushResult = {
- decks: [{ id: "deck-1", syncVersion: 1 }],
+ decks: [{ id: deck.id, syncVersion: 1 }],
cards: [],
reviewLogs: [],
- conflicts: { decks: ["deck-1"], cards: [] },
+ conflicts: { decks: [deck.id], cards: [] },
};
pushToServer.mockResolvedValue(pushResult);
const pullResult: SyncPullResult = {
decks: [
{
- id: "deck-1",
+ id: deck.id,
userId: "user-1",
name: "Server Deck",
description: null,
diff --git a/src/client/sync/manager.ts b/src/client/sync/manager.ts
index d24fda4..d935a3b 100644
--- a/src/client/sync/manager.ts
+++ b/src/client/sync/manager.ts
@@ -239,8 +239,7 @@ export class SyncManager {
pushResult,
pullResult,
);
- conflictsResolved =
- resolution.decks.length + resolution.cards.length;
+ conflictsResolved = resolution.decks.length + resolution.cards.length;
}
const result: SyncResult = {
diff --git a/src/client/sync/pull.test.ts b/src/client/sync/pull.test.ts
index 1aaac84..23c64ef 100644
--- a/src/client/sync/pull.test.ts
+++ b/src/client/sync/pull.test.ts
@@ -5,7 +5,7 @@ import "fake-indexeddb/auto";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { CardState, db, Rating } from "../db/index";
import { localCardRepository, localDeckRepository } from "../db/repositories";
-import { pullResultToLocalData, PullService } from "./pull";
+import { PullService, pullResultToLocalData } from "./pull";
import { SyncQueue } from "./queue";
describe("pullResultToLocalData", () => {
@@ -68,7 +68,9 @@ describe("pullResultToLocalData", () => {
currentSyncVersion: 3,
});
- expect(result.decks[0]?.deletedAt).toEqual(new Date("2024-01-03T12:00:00Z"));
+ expect(result.decks[0]?.deletedAt).toEqual(
+ new Date("2024-01-03T12:00:00Z"),
+ );
});
it("should convert server cards to local format", () => {
@@ -410,7 +412,9 @@ describe("PullService", () => {
});
it("should throw error if pull fails", async () => {
- const pullFromServer = vi.fn().mockRejectedValue(new Error("Network error"));
+ const pullFromServer = vi
+ .fn()
+ .mockRejectedValue(new Error("Network error"));
const pullService = new PullService({
syncQueue,
diff --git a/src/client/sync/push.test.ts b/src/client/sync/push.test.ts
index 79a9d4a..911a8d3 100644
--- a/src/client/sync/push.test.ts
+++ b/src/client/sync/push.test.ts
@@ -9,7 +9,7 @@ import {
localDeckRepository,
localReviewLogRepository,
} from "../db/repositories";
-import { pendingChangesToPushData, PushService } from "./push";
+import { PushService, pendingChangesToPushData } from "./push";
import { SyncQueue } from "./queue";
describe("pendingChangesToPushData", () => {
@@ -450,7 +450,9 @@ describe("PushService", () => {
newCardsPerDay: 20,
});
- const pushToServer = vi.fn().mockRejectedValue(new Error("Network error"));
+ const pushToServer = vi
+ .fn()
+ .mockRejectedValue(new Error("Network error"));
const pushService = new PushService({
syncQueue,
diff --git a/src/client/sync/push.ts b/src/client/sync/push.ts
index 7702583..2493e4e 100644
--- a/src/client/sync/push.ts
+++ b/src/client/sync/push.ts
@@ -129,7 +129,9 @@ function reviewLogToSyncData(log: LocalReviewLog): SyncReviewLogData {
/**
* Convert pending changes to sync push data format
*/
-export function pendingChangesToPushData(changes: PendingChanges): SyncPushData {
+export function pendingChangesToPushData(
+ changes: PendingChanges,
+): SyncPushData {
return {
decks: changes.decks.map(deckToSyncData),
cards: changes.cards.map(cardToSyncData),
diff --git a/src/client/sync/queue.test.ts b/src/client/sync/queue.test.ts
index d35ae32..f6a3019 100644
--- a/src/client/sync/queue.test.ts
+++ b/src/client/sync/queue.test.ts
@@ -230,13 +230,17 @@ describe("SyncQueue", () => {
const state = await syncQueue.getState();
expect(state.lastSyncAt).not.toBeNull();
- expect(state.lastSyncAt?.getTime()).toBeGreaterThanOrEqual(before.getTime());
+ expect(state.lastSyncAt?.getTime()).toBeGreaterThanOrEqual(
+ before.getTime(),
+ );
});
it("should persist state to localStorage", async () => {
await syncQueue.completeSync(10);
- const stored = JSON.parse(localStorage.getItem("kioku_sync_state") ?? "{}");
+ const stored = JSON.parse(
+ localStorage.getItem("kioku_sync_state") ?? "{}",
+ );
expect(stored.lastSyncVersion).toBe(10);
expect(stored.lastSyncAt).toBeDefined();
});
diff --git a/src/client/sync/queue.ts b/src/client/sync/queue.ts
index f0b112a..01c62cc 100644
--- a/src/client/sync/queue.ts
+++ b/src/client/sync/queue.ts
@@ -1,4 +1,9 @@
-import { db, type LocalCard, type LocalDeck, type LocalReviewLog } from "../db/index";
+import {
+ db,
+ type LocalCard,
+ type LocalDeck,
+ type LocalReviewLog,
+} from "../db/index";
import {
localCardRepository,
localDeckRepository,
@@ -41,7 +46,10 @@ const SYNC_STATE_KEY = "kioku_sync_state";
/**
* Load sync state from localStorage
*/
-function loadSyncState(): Pick<SyncQueueState, "lastSyncVersion" | "lastSyncAt"> {
+function loadSyncState(): Pick<
+ SyncQueueState,
+ "lastSyncVersion" | "lastSyncAt"
+> {
const stored = localStorage.getItem(SYNC_STATE_KEY);
if (!stored) {
return { lastSyncVersion: 0, lastSyncAt: null };
@@ -137,7 +145,9 @@ export class SyncQueue {
*/
async getPendingCount(): Promise<number> {
const changes = await this.getPendingChanges();
- return changes.decks.length + changes.cards.length + changes.reviewLogs.length;
+ return (
+ changes.decks.length + changes.cards.length + changes.reviewLogs.length
+ );
}
/**
@@ -205,17 +215,24 @@ export class SyncQueue {
cards: { id: string; syncVersion: number }[];
reviewLogs: { id: string; syncVersion: number }[];
}): Promise<void> {
- await db.transaction("rw", [db.decks, db.cards, db.reviewLogs], async () => {
- for (const deck of results.decks) {
- await localDeckRepository.markSynced(deck.id, deck.syncVersion);
- }
- for (const card of results.cards) {
- await localCardRepository.markSynced(card.id, card.syncVersion);
- }
- for (const reviewLog of results.reviewLogs) {
- await localReviewLogRepository.markSynced(reviewLog.id, reviewLog.syncVersion);
- }
- });
+ await db.transaction(
+ "rw",
+ [db.decks, db.cards, db.reviewLogs],
+ async () => {
+ for (const deck of results.decks) {
+ await localDeckRepository.markSynced(deck.id, deck.syncVersion);
+ }
+ for (const card of results.cards) {
+ await localCardRepository.markSynced(card.id, card.syncVersion);
+ }
+ for (const reviewLog of results.reviewLogs) {
+ await localReviewLogRepository.markSynced(
+ reviewLog.id,
+ reviewLog.syncVersion,
+ );
+ }
+ },
+ );
await this.notifyListeners();
}
@@ -227,17 +244,21 @@ export class SyncQueue {
cards: LocalCard[];
reviewLogs: LocalReviewLog[];
}): Promise<void> {
- await db.transaction("rw", [db.decks, db.cards, db.reviewLogs], async () => {
- for (const deck of data.decks) {
- await localDeckRepository.upsertFromServer(deck);
- }
- for (const card of data.cards) {
- await localCardRepository.upsertFromServer(card);
- }
- for (const reviewLog of data.reviewLogs) {
- await localReviewLogRepository.upsertFromServer(reviewLog);
- }
- });
+ await db.transaction(
+ "rw",
+ [db.decks, db.cards, db.reviewLogs],
+ async () => {
+ for (const deck of data.decks) {
+ await localDeckRepository.upsertFromServer(deck);
+ }
+ for (const card of data.cards) {
+ await localCardRepository.upsertFromServer(card);
+ }
+ for (const reviewLog of data.reviewLogs) {
+ await localReviewLogRepository.upsertFromServer(reviewLog);
+ }
+ },
+ );
await this.notifyListeners();
}