aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/client/sync/pull.ts
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2025-12-07 19:29:46 +0900
committernsfisis <nsfisis@gmail.com>2025-12-07 19:29:46 +0900
commit864495bd4d7156ee433cbc12adda4bdebd43f6fe (patch)
treefe6f8a628d2812ce77ddf9bcaca4e8ad26544dfe /src/client/sync/pull.ts
parent842c74fdc2bf06a020868f5b4e504fec0da8715d (diff)
downloadkioku-864495bd4d7156ee433cbc12adda4bdebd43f6fe.tar.gz
kioku-864495bd4d7156ee433cbc12adda4bdebd43f6fe.tar.zst
kioku-864495bd4d7156ee433cbc12adda4bdebd43f6fe.zip
feat(client): add pull service for sync implementation
Implement PullService class to pull changes from server: - Fetch changes since last sync version - Convert server data format to local IndexedDB format - Apply pulled decks, cards, and review logs to local database - Update sync version after successful pull 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'src/client/sync/pull.ts')
-rw-r--r--src/client/sync/pull.ts222
1 files changed, 222 insertions, 0 deletions
diff --git a/src/client/sync/pull.ts b/src/client/sync/pull.ts
new file mode 100644
index 0000000..333782c
--- /dev/null
+++ b/src/client/sync/pull.ts
@@ -0,0 +1,222 @@
+import type {
+ CardStateType,
+ LocalCard,
+ LocalDeck,
+ LocalReviewLog,
+ RatingType,
+} from "../db/index";
+import type { SyncQueue } from "./queue";
+
+/**
+ * Server deck data format from pull response
+ */
+export interface ServerDeck {
+ id: string;
+ userId: string;
+ name: string;
+ description: string | null;
+ newCardsPerDay: number;
+ createdAt: Date;
+ updatedAt: Date;
+ deletedAt: Date | null;
+ syncVersion: number;
+}
+
+/**
+ * Server card data format from pull response
+ */
+export interface ServerCard {
+ id: string;
+ deckId: string;
+ front: string;
+ back: string;
+ state: number;
+ due: Date;
+ stability: number;
+ difficulty: number;
+ elapsedDays: number;
+ scheduledDays: number;
+ reps: number;
+ lapses: number;
+ lastReview: Date | null;
+ createdAt: Date;
+ updatedAt: Date;
+ deletedAt: Date | null;
+ syncVersion: number;
+}
+
+/**
+ * Server review log data format from pull response
+ */
+export interface ServerReviewLog {
+ id: string;
+ cardId: string;
+ userId: string;
+ rating: number;
+ state: number;
+ scheduledDays: number;
+ elapsedDays: number;
+ reviewedAt: Date;
+ durationMs: number | null;
+ syncVersion: number;
+}
+
+/**
+ * Response from pull endpoint
+ */
+export interface SyncPullResult {
+ decks: ServerDeck[];
+ cards: ServerCard[];
+ reviewLogs: ServerReviewLog[];
+ currentSyncVersion: number;
+}
+
+/**
+ * Options for creating a pull service
+ */
+export interface PullServiceOptions {
+ syncQueue: SyncQueue;
+ pullFromServer: (lastSyncVersion: number) => Promise<SyncPullResult>;
+}
+
+/**
+ * Convert server deck to local deck format
+ */
+function serverDeckToLocal(deck: ServerDeck): LocalDeck {
+ return {
+ id: deck.id,
+ userId: deck.userId,
+ name: deck.name,
+ description: deck.description,
+ newCardsPerDay: deck.newCardsPerDay,
+ createdAt: new Date(deck.createdAt),
+ updatedAt: new Date(deck.updatedAt),
+ deletedAt: deck.deletedAt ? new Date(deck.deletedAt) : null,
+ syncVersion: deck.syncVersion,
+ _synced: true,
+ };
+}
+
+/**
+ * Convert server card to local card format
+ */
+function serverCardToLocal(card: ServerCard): LocalCard {
+ return {
+ id: card.id,
+ deckId: card.deckId,
+ front: card.front,
+ back: card.back,
+ state: card.state as CardStateType,
+ due: new Date(card.due),
+ stability: card.stability,
+ difficulty: card.difficulty,
+ elapsedDays: card.elapsedDays,
+ scheduledDays: card.scheduledDays,
+ reps: card.reps,
+ lapses: card.lapses,
+ lastReview: card.lastReview ? new Date(card.lastReview) : null,
+ createdAt: new Date(card.createdAt),
+ updatedAt: new Date(card.updatedAt),
+ deletedAt: card.deletedAt ? new Date(card.deletedAt) : null,
+ syncVersion: card.syncVersion,
+ _synced: true,
+ };
+}
+
+/**
+ * Convert server review log to local review log format
+ */
+function serverReviewLogToLocal(log: ServerReviewLog): LocalReviewLog {
+ return {
+ id: log.id,
+ cardId: log.cardId,
+ userId: log.userId,
+ rating: log.rating as RatingType,
+ state: log.state as CardStateType,
+ scheduledDays: log.scheduledDays,
+ elapsedDays: log.elapsedDays,
+ reviewedAt: new Date(log.reviewedAt),
+ durationMs: log.durationMs,
+ syncVersion: log.syncVersion,
+ _synced: true,
+ };
+}
+
+/**
+ * Convert server pull result to local format for storage
+ */
+export function pullResultToLocalData(result: SyncPullResult): {
+ decks: LocalDeck[];
+ cards: LocalCard[];
+ reviewLogs: LocalReviewLog[];
+} {
+ return {
+ decks: result.decks.map(serverDeckToLocal),
+ cards: result.cards.map(serverCardToLocal),
+ reviewLogs: result.reviewLogs.map(serverReviewLogToLocal),
+ };
+}
+
+/**
+ * Pull sync service
+ *
+ * Handles pulling changes from the server:
+ * 1. Get last sync version from sync queue
+ * 2. Request changes from server since that version
+ * 3. Convert server data to local format
+ * 4. Apply changes to local database
+ * 5. Update sync version
+ */
+export class PullService {
+ private syncQueue: SyncQueue;
+ private pullFromServer: (lastSyncVersion: number) => Promise<SyncPullResult>;
+
+ constructor(options: PullServiceOptions) {
+ this.syncQueue = options.syncQueue;
+ this.pullFromServer = options.pullFromServer;
+ }
+
+ /**
+ * Pull changes from the server
+ *
+ * @returns Result containing pulled items and new sync version
+ * @throws Error if pull fails
+ */
+ async pull(): Promise<SyncPullResult> {
+ const lastSyncVersion = this.syncQueue.getLastSyncVersion();
+
+ // Pull changes from server
+ const result = await this.pullFromServer(lastSyncVersion);
+
+ // If there are changes, apply them to local database
+ if (
+ result.decks.length > 0 ||
+ result.cards.length > 0 ||
+ result.reviewLogs.length > 0
+ ) {
+ const localData = pullResultToLocalData(result);
+ await this.syncQueue.applyPulledChanges(localData);
+ }
+
+ // Update sync version even if no changes (to mark we synced up to this point)
+ if (result.currentSyncVersion > lastSyncVersion) {
+ await this.syncQueue.completeSync(result.currentSyncVersion);
+ }
+
+ return result;
+ }
+
+ /**
+ * Get the last sync version
+ */
+ getLastSyncVersion(): number {
+ return this.syncQueue.getLastSyncVersion();
+ }
+}
+
+/**
+ * Create a pull service with the given options
+ */
+export function createPullService(options: PullServiceOptions): PullService {
+ return new PullService(options);
+}