diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-01-04 17:43:59 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-01-04 19:09:58 +0900 |
| commit | f8e4be9b36a16969ac53bd9ce12ce8064be10196 (patch) | |
| tree | b2cf350d2e2e52803ff809311effb40da767d859 /src/client/stores/sync.tsx | |
| parent | e1c9e5e89bb91bca2586470c786510c3e1c03826 (diff) | |
| download | kioku-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.tsx | 303 |
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; -} |
