aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/client/stores/sync.tsx
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-01-04 17:43:59 +0900
committernsfisis <nsfisis@gmail.com>2026-01-04 19:09:58 +0900
commitf8e4be9b36a16969ac53bd9ce12ce8064be10196 (patch)
treeb2cf350d2e2e52803ff809311effb40da767d859 /src/client/stores/sync.tsx
parente1c9e5e89bb91bca2586470c786510c3e1c03826 (diff)
downloadkioku-f8e4be9b36a16969ac53bd9ce12ce8064be10196.tar.gz
kioku-f8e4be9b36a16969ac53bd9ce12ce8064be10196.tar.zst
kioku-f8e4be9b36a16969ac53bd9ce12ce8064be10196.zip
refactor(client): migrate state management from React Context to Jotai
Replace AuthProvider and SyncProvider with Jotai atoms for more granular state management and better performance. This migration: - Creates atoms for auth, sync, decks, cards, noteTypes, and study state - Uses atomFamily for parameterized state (e.g., cards by deckId) - Introduces StoreInitializer component for subscription initialization - Updates all components and pages to use useAtomValue/useSetAtom - Updates all tests to use Jotai Provider with createStore pattern 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'src/client/stores/sync.tsx')
-rw-r--r--src/client/stores/sync.tsx303
1 files changed, 0 insertions, 303 deletions
diff --git a/src/client/stores/sync.tsx b/src/client/stores/sync.tsx
deleted file mode 100644
index 9b46302..0000000
--- a/src/client/stores/sync.tsx
+++ /dev/null
@@ -1,303 +0,0 @@
-import {
- createContext,
- type ReactNode,
- useCallback,
- useContext,
- useEffect,
- useMemo,
- useState,
-} from "react";
-import { apiClient } from "../api/client";
-import {
- conflictResolver,
- createPullService,
- createPushService,
- createSyncManager,
- type SyncManagerEvent,
- type SyncQueueState,
- type SyncResult,
- SyncStatus,
- syncQueue,
-} from "../sync";
-import type {
- ServerCard,
- ServerDeck,
- ServerNote,
- ServerNoteFieldType,
- ServerNoteFieldValue,
- ServerNoteType,
- ServerReviewLog,
- SyncPullResult,
-} from "../sync/pull";
-import type { SyncPushData, SyncPushResult } from "../sync/push";
-
-export interface SyncState {
- isOnline: boolean;
- isSyncing: boolean;
- pendingCount: number;
- lastSyncAt: Date | null;
- lastError: string | null;
- status: SyncQueueState["status"];
-}
-
-export interface SyncActions {
- sync: () => Promise<SyncResult>;
-}
-
-export type SyncContextValue = SyncState & SyncActions;
-
-const SyncContext = createContext<SyncContextValue | null>(null);
-
-export interface SyncProviderProps {
- children: ReactNode;
-}
-
-interface PullResponse {
- decks: Array<
- Omit<ServerDeck, "createdAt" | "updatedAt" | "deletedAt"> & {
- createdAt: string;
- updatedAt: string;
- deletedAt: string | null;
- }
- >;
- cards: Array<
- Omit<
- ServerCard,
- "due" | "lastReview" | "createdAt" | "updatedAt" | "deletedAt"
- > & {
- due: string;
- lastReview: string | null;
- createdAt: string;
- updatedAt: string;
- deletedAt: string | null;
- }
- >;
- reviewLogs: Array<
- Omit<ServerReviewLog, "reviewedAt"> & {
- reviewedAt: string;
- }
- >;
- noteTypes: Array<
- Omit<ServerNoteType, "createdAt" | "updatedAt" | "deletedAt"> & {
- createdAt: string;
- updatedAt: string;
- deletedAt: string | null;
- }
- >;
- noteFieldTypes: Array<
- Omit<ServerNoteFieldType, "createdAt" | "updatedAt" | "deletedAt"> & {
- createdAt: string;
- updatedAt: string;
- deletedAt: string | null;
- }
- >;
- notes: Array<
- Omit<ServerNote, "createdAt" | "updatedAt" | "deletedAt"> & {
- createdAt: string;
- updatedAt: string;
- deletedAt: string | null;
- }
- >;
- noteFieldValues: Array<
- Omit<ServerNoteFieldValue, "createdAt" | "updatedAt"> & {
- createdAt: string;
- updatedAt: string;
- }
- >;
- currentSyncVersion: number;
-}
-
-async function pushToServer(data: SyncPushData): Promise<SyncPushResult> {
- const res = await apiClient.authenticatedFetch("/api/sync/push", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify(data),
- });
-
- if (!res.ok) {
- const errorBody = (await res.json().catch(() => ({}))) as {
- error?: string;
- };
- throw new Error(errorBody.error || `Push failed with status ${res.status}`);
- }
-
- return res.json() as Promise<SyncPushResult>;
-}
-
-async function pullFromServer(
- lastSyncVersion: number,
-): Promise<SyncPullResult> {
- const res = await apiClient.authenticatedFetch(
- `/api/sync/pull?lastSyncVersion=${lastSyncVersion}`,
- );
-
- if (!res.ok) {
- const errorBody = (await res.json().catch(() => ({}))) as {
- error?: string;
- };
- throw new Error(errorBody.error || `Pull failed with status ${res.status}`);
- }
-
- const data = (await res.json()) as PullResponse;
-
- return {
- decks: data.decks.map((d) => ({
- ...d,
- createdAt: new Date(d.createdAt),
- updatedAt: new Date(d.updatedAt),
- deletedAt: d.deletedAt ? new Date(d.deletedAt) : null,
- })),
- cards: data.cards.map((c) => ({
- ...c,
- due: new Date(c.due),
- lastReview: c.lastReview ? new Date(c.lastReview) : null,
- createdAt: new Date(c.createdAt),
- updatedAt: new Date(c.updatedAt),
- deletedAt: c.deletedAt ? new Date(c.deletedAt) : null,
- })),
- reviewLogs: data.reviewLogs.map((r) => ({
- ...r,
- reviewedAt: new Date(r.reviewedAt),
- })),
- noteTypes: data.noteTypes.map((n) => ({
- ...n,
- createdAt: new Date(n.createdAt),
- updatedAt: new Date(n.updatedAt),
- deletedAt: n.deletedAt ? new Date(n.deletedAt) : null,
- })),
- noteFieldTypes: data.noteFieldTypes.map((f) => ({
- ...f,
- createdAt: new Date(f.createdAt),
- updatedAt: new Date(f.updatedAt),
- deletedAt: f.deletedAt ? new Date(f.deletedAt) : null,
- })),
- notes: data.notes.map((n) => ({
- ...n,
- createdAt: new Date(n.createdAt),
- updatedAt: new Date(n.updatedAt),
- deletedAt: n.deletedAt ? new Date(n.deletedAt) : null,
- })),
- noteFieldValues: data.noteFieldValues.map((v) => ({
- ...v,
- createdAt: new Date(v.createdAt),
- updatedAt: new Date(v.updatedAt),
- })),
- currentSyncVersion: data.currentSyncVersion,
- };
-}
-
-const pushService = createPushService({
- syncQueue,
- pushToServer,
-});
-
-const pullService = createPullService({
- syncQueue,
- pullFromServer,
-});
-
-const syncManager = createSyncManager({
- syncQueue,
- pushService,
- pullService,
- conflictResolver,
-});
-
-export function SyncProvider({ children }: SyncProviderProps) {
- const [isOnline, setIsOnline] = useState(
- typeof navigator !== "undefined" ? navigator.onLine : true,
- );
- const [isSyncing, setIsSyncing] = useState(false);
- const [pendingCount, setPendingCount] = useState(0);
- const [lastSyncAt, setLastSyncAt] = useState<Date | null>(null);
- const [lastError, setLastError] = useState<string | null>(null);
- const [status, setStatus] = useState<SyncQueueState["status"]>(
- SyncStatus.Idle,
- );
-
- useEffect(() => {
- syncManager.start();
-
- const unsubscribeManager = syncManager.subscribe(
- (event: SyncManagerEvent) => {
- switch (event.type) {
- case "online":
- setIsOnline(true);
- break;
- case "offline":
- setIsOnline(false);
- break;
- case "sync_start":
- setIsSyncing(true);
- setLastError(null);
- setStatus(SyncStatus.Syncing);
- break;
- case "sync_complete":
- setIsSyncing(false);
- setLastSyncAt(new Date());
- setStatus(SyncStatus.Idle);
- break;
- case "sync_error":
- setIsSyncing(false);
- setLastError(event.error);
- setStatus(SyncStatus.Error);
- break;
- }
- },
- );
-
- const unsubscribeQueue = syncQueue.subscribe((state: SyncQueueState) => {
- setPendingCount(state.pendingCount);
- if (state.lastSyncAt) {
- setLastSyncAt(state.lastSyncAt);
- }
- if (state.lastError) {
- setLastError(state.lastError);
- }
- setStatus(state.status);
- });
-
- // Initialize state from queue
- syncQueue.getState().then((state) => {
- setPendingCount(state.pendingCount);
- setLastSyncAt(state.lastSyncAt);
- setLastError(state.lastError);
- setStatus(state.status);
- });
-
- return () => {
- unsubscribeManager();
- unsubscribeQueue();
- syncManager.stop();
- };
- }, []);
-
- const sync = useCallback(async () => {
- return syncManager.sync();
- }, []);
-
- const value = useMemo<SyncContextValue>(
- () => ({
- isOnline,
- isSyncing,
- pendingCount,
- lastSyncAt,
- lastError,
- status,
- sync,
- }),
- [isOnline, isSyncing, pendingCount, lastSyncAt, lastError, status, sync],
- );
-
- return <SyncContext.Provider value={value}>{children}</SyncContext.Provider>;
-}
-
-export function useSync(): SyncContextValue {
- const context = useContext(SyncContext);
- if (!context) {
- throw new Error("useSync must be used within a SyncProvider");
- }
- return context;
-}