aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/client/sync/push.ts
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2025-12-07 19:26:57 +0900
committernsfisis <nsfisis@gmail.com>2025-12-07 19:26:57 +0900
commit842c74fdc2bf06a020868f5b4e504fec0da8715d (patch)
tree8620a9ddb4211f449faaae98776a62ed8101fae3 /src/client/sync/push.ts
parent83be5ccd5d64c64f65c7efbfb9feb94ab0f75ce6 (diff)
downloadkioku-842c74fdc2bf06a020868f5b4e504fec0da8715d.tar.gz
kioku-842c74fdc2bf06a020868f5b4e504fec0da8715d.tar.zst
kioku-842c74fdc2bf06a020868f5b4e504fec0da8715d.zip
feat(client): add push service for sync implementation
Implement PushService class to push local changes to server: - Convert local decks, cards, and review logs to API format - Push pending changes to server endpoint - Mark items as synced after successful push - Return conflicts reported by server (LWW resolution) 🤖 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/push.ts')
-rw-r--r--src/client/sync/push.ts211
1 files changed, 211 insertions, 0 deletions
diff --git a/src/client/sync/push.ts b/src/client/sync/push.ts
new file mode 100644
index 0000000..7702583
--- /dev/null
+++ b/src/client/sync/push.ts
@@ -0,0 +1,211 @@
+import type { LocalCard, LocalDeck, LocalReviewLog } from "../db/index";
+import type { PendingChanges, SyncQueue } from "./queue";
+
+/**
+ * Data format for push request to server
+ */
+export interface SyncPushData {
+ decks: SyncDeckData[];
+ cards: SyncCardData[];
+ reviewLogs: SyncReviewLogData[];
+}
+
+export interface SyncDeckData {
+ id: string;
+ name: string;
+ description: string | null;
+ newCardsPerDay: number;
+ createdAt: string;
+ updatedAt: string;
+ deletedAt: string | null;
+}
+
+export interface SyncCardData {
+ id: string;
+ deckId: string;
+ front: string;
+ back: string;
+ state: number;
+ due: string;
+ stability: number;
+ difficulty: number;
+ elapsedDays: number;
+ scheduledDays: number;
+ reps: number;
+ lapses: number;
+ lastReview: string | null;
+ createdAt: string;
+ updatedAt: string;
+ deletedAt: string | null;
+}
+
+export interface SyncReviewLogData {
+ id: string;
+ cardId: string;
+ rating: number;
+ state: number;
+ scheduledDays: number;
+ elapsedDays: number;
+ reviewedAt: string;
+ durationMs: number | null;
+}
+
+/**
+ * Response from push endpoint
+ */
+export interface SyncPushResult {
+ decks: { id: string; syncVersion: number }[];
+ cards: { id: string; syncVersion: number }[];
+ reviewLogs: { id: string; syncVersion: number }[];
+ conflicts: {
+ decks: string[];
+ cards: string[];
+ };
+}
+
+/**
+ * Options for creating a push service
+ */
+export interface PushServiceOptions {
+ syncQueue: SyncQueue;
+ pushToServer: (data: SyncPushData) => Promise<SyncPushResult>;
+}
+
+/**
+ * Convert local deck to sync format
+ */
+function deckToSyncData(deck: LocalDeck): SyncDeckData {
+ return {
+ id: deck.id,
+ name: deck.name,
+ description: deck.description,
+ newCardsPerDay: deck.newCardsPerDay,
+ createdAt: deck.createdAt.toISOString(),
+ updatedAt: deck.updatedAt.toISOString(),
+ deletedAt: deck.deletedAt?.toISOString() ?? null,
+ };
+}
+
+/**
+ * Convert local card to sync format
+ */
+function cardToSyncData(card: LocalCard): SyncCardData {
+ return {
+ id: card.id,
+ deckId: card.deckId,
+ front: card.front,
+ back: card.back,
+ state: card.state,
+ due: card.due.toISOString(),
+ stability: card.stability,
+ difficulty: card.difficulty,
+ elapsedDays: card.elapsedDays,
+ scheduledDays: card.scheduledDays,
+ reps: card.reps,
+ lapses: card.lapses,
+ lastReview: card.lastReview?.toISOString() ?? null,
+ createdAt: card.createdAt.toISOString(),
+ updatedAt: card.updatedAt.toISOString(),
+ deletedAt: card.deletedAt?.toISOString() ?? null,
+ };
+}
+
+/**
+ * Convert local review log to sync format
+ */
+function reviewLogToSyncData(log: LocalReviewLog): SyncReviewLogData {
+ return {
+ id: log.id,
+ cardId: log.cardId,
+ rating: log.rating,
+ state: log.state,
+ scheduledDays: log.scheduledDays,
+ elapsedDays: log.elapsedDays,
+ reviewedAt: log.reviewedAt.toISOString(),
+ durationMs: log.durationMs,
+ };
+}
+
+/**
+ * Convert pending changes to sync push data format
+ */
+export function pendingChangesToPushData(changes: PendingChanges): SyncPushData {
+ return {
+ decks: changes.decks.map(deckToSyncData),
+ cards: changes.cards.map(cardToSyncData),
+ reviewLogs: changes.reviewLogs.map(reviewLogToSyncData),
+ };
+}
+
+/**
+ * Push sync service
+ *
+ * Handles pushing local changes to the server:
+ * 1. Get pending changes from sync queue
+ * 2. Convert to API format
+ * 3. Send to server
+ * 4. Mark items as synced on success
+ * 5. Handle conflicts (server wins for LWW)
+ */
+export class PushService {
+ private syncQueue: SyncQueue;
+ private pushToServer: (data: SyncPushData) => Promise<SyncPushResult>;
+
+ constructor(options: PushServiceOptions) {
+ this.syncQueue = options.syncQueue;
+ this.pushToServer = options.pushToServer;
+ }
+
+ /**
+ * Push all pending changes to the server
+ *
+ * @returns Result containing synced items and conflicts
+ * @throws Error if push fails
+ */
+ async push(): Promise<SyncPushResult> {
+ const pendingChanges = await this.syncQueue.getPendingChanges();
+
+ // If no pending changes, return empty result
+ if (
+ pendingChanges.decks.length === 0 &&
+ pendingChanges.cards.length === 0 &&
+ pendingChanges.reviewLogs.length === 0
+ ) {
+ return {
+ decks: [],
+ cards: [],
+ reviewLogs: [],
+ conflicts: { decks: [], cards: [] },
+ };
+ }
+
+ // Convert to API format
+ const pushData = pendingChangesToPushData(pendingChanges);
+
+ // Push to server
+ const result = await this.pushToServer(pushData);
+
+ // Mark successfully synced items
+ await this.syncQueue.markSynced({
+ decks: result.decks,
+ cards: result.cards,
+ reviewLogs: result.reviewLogs,
+ });
+
+ return result;
+ }
+
+ /**
+ * Check if there are pending changes to push
+ */
+ async hasPendingChanges(): Promise<boolean> {
+ return this.syncQueue.hasPendingChanges();
+ }
+}
+
+/**
+ * Create a push service with the given options
+ */
+export function createPushService(options: PushServiceOptions): PushService {
+ return new PushService(options);
+}