@anfenn/zync 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +325 -367
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +51 -60
- package/dist/index.d.ts +51 -60
- package/dist/index.js +325 -368
- package/dist/index.js.map +1 -1
- package/package.json +9 -4
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/indexedDBStorage.ts"],"sourcesContent":["import { type StateCreator } from 'zustand';\nimport { persist } from 'zustand/middleware';\n\nexport { createIndexedDBStorage, type IndexedDBStorageOptions } from './indexedDBStorage';\n\nexport type SyncedRecord = { id?: any; _localId: string; updated_at: string; deleted?: boolean; [k: string]: any };\n\nexport interface ApiFunctions {\n add: (item: any) => Promise<any | undefined>;\n update: (id: any, changes: any) => Promise<boolean>; // returns true if applied, false if remote missing\n remove: (id: any) => Promise<void>;\n list: (lastUpdatedAt: Date) => Promise<any[]>; // returns changed records since timestamp (including records with deleted flag)\n firstLoad: (lastId: any) => Promise<any[]>; // returns all records with id > lastId\n}\n\ntype AfterRemoteAddCallback = (\n set: any,\n get: any,\n queue: QueueToSyncCallback,\n stateKey: string,\n item: SyncedRecord,\n) => void;\n\ntype MissingRemoteRecordDuringUpdateCallback = (\n strategy: MissingRemoteRecordDuringUpdateStrategy,\n item: SyncedRecord,\n newLocalId?: string,\n) => void;\n\nexport type MissingRemoteRecordDuringUpdateStrategy = 'deleteLocalRecord' | 'insertNewRemoteRecord';\n\ninterface SyncOptions {\n syncInterval?: number;\n logger?: Logger;\n minLogLevel?: LogLevel;\n onAfterRemoteAdd?: AfterRemoteAddCallback;\n missingRemoteRecordDuringUpdateStrategy?: MissingRemoteRecordDuringUpdateStrategy;\n onMissingRemoteRecordDuringUpdate?: MissingRemoteRecordDuringUpdateCallback;\n}\n\nexport type SyncState = {\n syncState: {\n status: 'hydrating' | 'syncing' | 'idle';\n error?: Error;\n enabled: boolean;\n enableSync: (enabled: boolean) => void;\n firstLoadDone: boolean;\n startFirstLoad: () => Promise<void>;\n };\n};\n\ntype _SyncState = {\n _pendingChanges: PendingChange[];\n _lastPulled: Record<string, string>; // stateKey -> ISO timestamp of last successful pull\n};\n\nexport enum SyncAction {\n Create = 'create',\n Update = 'update',\n Remove = 'remove',\n}\n\nexport type QueueToSyncCallback = (action: SyncAction, localId: string, stateKey: string, changes?: any) => void;\n\ntype SyncedStateCreator<TStore> = (set: any, get: any, queue: QueueToSyncCallback) => TStore;\n\ninterface PendingChange {\n stateKey: string;\n localId: string;\n action: SyncAction;\n changes?: any; // merged change set (for create/update)\n}\n\nexport interface Logger {\n debug: (...args: any[]) => void;\n info: (...args: any[]) => void;\n warn: (...args: any[]) => void;\n error: (...args: any[]) => void;\n}\n\nexport type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';\n\nconst DEFAULT_SYNC_INTERVAL_MILLIS = 5000;\nconst DEFAULT_LOGGER: Logger = console;\nconst DEFAULT_MIN_LOG_LEVEL: LogLevel = 'debug';\nconst DEFAULT_MISSING_REMOTE_RECORD_ON_UPDATE_STRATEGY: MissingRemoteRecordDuringUpdateStrategy =\n 'insertNewRemoteRecord';\nconst SYNC_FIELDS = ['id', '_localId', 'updated_at', 'deleted'] as const;\n\n/**\n * Zync creates a standard persisted Zustand store with optional background sync (e.g. via RESTful, GraphQL, etc.).\n * It provides a `queueToSync()` method to enqueue changes for syncing, which are processed on an interval.\n *\n * This is a drop-in replacement for Zustand's `persist()` middleware that wires in background sync. It's usage mirrors\n * `persist(stateCreator, options)` with optional syncing per state key.\n *\n * Can be used with any storage that Zustand Persist supports. Options include localStorage if its syncronous access doesn't\n * cause blocking issues like UI freezes, or IndexedDB with its asynchronous access for improved performance.\n *\n * When using IndexedDB the whole store is saved under one key, which means indexes cannot be used to accelerate querying. However, if this\n * becomes a performance issue due to the size of the store, then libraries like dexie.js instead of Zustand would be a better solution and\n * provide the syntax for high performance queries.\n *\n * Zync maintains the following additional state:\n *\n * [Private]\n * - _pendingChanges: A queue of changes to be synced with the backend.\n * - _lastPulled: A timestamp of the last successful pull from the backend.\n * [Public]\n * - syncState: Access to sync status, errors, and a method to enable/disable syncing.\n * i.e. const syncState = useStore((state) => state.syncState);\n *\n * Design principles:\n *\n * - Always pull (list) first each sync cycle to enable future conflict resolution. Currently last-write-wins, although any queued client changes\n * for an item will prevent it being overwritten during a pull, even if the server has a newer version.\n * - Then push queued changes in order (Create -> Update -> Remove).\n * - Queue coalescing: (Create + Update*) => single Create (merged changes); (Create + Remove) => drop both; (Update + Update) => merge; (Update + Remove) => Remove.\n *\n * Synced objects are expected to have the following server fields:\n *\n * - id: The unique identifier from the server.\n * - updated_at: A timestamp indicating when the object was last updated.\n * This field is used to determine if the object needs to be re-fetched from the server.\n * It must be set at the server (e.g. via sql trigger or in the api code).\n * Ensure the server sets a timestamp with millisecond precision, not microsecond like PostgreSQL's timestampz,\n * as Javascript's Date object is based on milliseconds, and this will be used during sync.\n * Although the client can set this locally, it is only to give a good UX, as it won't be sent\n * to the server and will be overwritten on the client during sync. The client clock is never\n * used to check for changes as it can't be guaranteed to be in sync with the server clock. Instead any item\n * that is added, updated or deleted locally is added to a queue.\n * - deleted: A boolean flag indicating whether the object has been deleted. This use of soft deletes or similar\n * is how all clients are told about deletions during sync.\n *\n * Synced objects will have the field `_localId` on the client only, which provides a stable identifier for the object.\n * It is ideal for use as JSX keys.\n *\n * @param stateCreator - The function to create the initial state.\n * @param persistOptions - Standard Zustand options for persisting the store.\n * @param syncApi - Remote API functions for syncing state. Use the same key name as the state key.\n * e.g. if your state key is called `fish`, the syncApi should be `fish: { list, add, update, remove }`\n * If you don't provide a key for a state field, it won't be synced, but will be persisted as expected.\n * @param syncOptions - Syncing options (Optional).\n */\nexport function persistWithSync<TStore extends object>(\n stateCreator: SyncedStateCreator<TStore>,\n persistOptions: any,\n syncApi: Record<string, ApiFunctions>,\n syncOptions: SyncOptions = {},\n): StateCreator<TStore & SyncState, [], []> {\n const syncInterval = syncOptions.syncInterval ?? DEFAULT_SYNC_INTERVAL_MILLIS;\n const missingStrategy =\n syncOptions.missingRemoteRecordDuringUpdateStrategy ?? DEFAULT_MISSING_REMOTE_RECORD_ON_UPDATE_STRATEGY;\n const logger = newLogger(syncOptions.logger ?? DEFAULT_LOGGER, syncOptions.minLogLevel ?? DEFAULT_MIN_LOG_LEVEL);\n\n let startSync: (() => Promise<void>) | undefined;\n let syncIntervalId: any;\n\n const baseOnRehydrate = persistOptions?.onRehydrateStorage;\n const basePartialize = persistOptions?.partialize;\n\n const wrappedPersistOptions = {\n ...persistOptions,\n onRehydrateStorage: () => {\n logger.debug('[persistWithSync] Rehydration started');\n\n return (state: any, error: any) => {\n if (error) {\n logger.error('[persistWithSync] Rehydration failed', error);\n } else {\n baseOnRehydrate?.(state, error);\n logger.debug('[persistWithSync] Rehydration complete');\n startSync?.();\n }\n };\n },\n partialize: (s: any) => {\n // Select state to be persisted\n\n const base = basePartialize ? basePartialize(s) : s;\n const { syncState: _sync, ...rest } = base || {};\n return {\n ...rest,\n syncState: {\n firstLoadDone: _sync?.firstLoadDone ?? false,\n },\n };\n },\n merge: (persisted: any, current: any) => {\n // Add unpersistable fields back e.g. functions or memory-only fields\n\n const p = persisted || {};\n const c = current || {};\n return {\n ...c,\n ...p,\n syncState: {\n ...c.syncState,\n firstLoadDone: p.syncState?.firstLoadDone ?? c.syncState?.firstLoadDone ?? false,\n status: 'idle',\n },\n };\n },\n };\n\n const creator: StateCreator<TStore & SyncState, [], []> = (set: any, get: any) => {\n async function syncOnce() {\n const state: SyncState = get();\n if (!state.syncState.enabled || state.syncState.status !== 'idle') return;\n\n set((state: any) => {\n return {\n syncState: {\n ...state.syncState,\n status: 'syncing',\n },\n } as SyncState;\n });\n\n let syncError: Error | undefined;\n\n // 1) PULL for each stateKey\n for (const stateKey of Object.keys(syncApi)) {\n try {\n const api = findApi(stateKey, syncApi);\n await pull(set, get, stateKey, api, logger);\n } catch (err) {\n syncError = syncError ?? (err as Error);\n logger.error(`[persistWithSync] Pull error for stateKey: ${stateKey}`, err);\n }\n }\n\n // 2) PUSH queued changes\n const snapshot: PendingChange[] = [...(get()._pendingChanges || [])];\n\n // Deterministic ordering: Create -> Update -> Remove so dependencies (e.g. id assignment) happen early\n snapshot.sort((a, b) => orderFor(a.action) - orderFor(b.action));\n\n for (const change of snapshot) {\n try {\n const api = findApi(change.stateKey, syncApi); \n await pushOne(\n set,\n get,\n change,\n api,\n logger,\n queueToSync,\n missingStrategy,\n syncOptions.onMissingRemoteRecordDuringUpdate,\n syncOptions.onAfterRemoteAdd,\n );\n } catch (err) {\n syncError = syncError ?? (err as Error);\n logger.error(`[persistWithSync] Push error for change: ${change}`, err);\n }\n }\n\n set((state: any) => {\n return {\n syncState: {\n ...state.syncState,\n status: 'idle',\n error: syncError,\n },\n } as SyncState;\n });\n }\n\n startSync = async () => {\n clearInterval(syncIntervalId);\n syncIntervalId = undefined;\n await syncOnce();\n syncIntervalId = setInterval(syncOnce, syncInterval);\n };\n\n function queueToSync(action: SyncAction, localId: string, stateKey: string, changes?: any) {\n set((state: any) => {\n const queue: PendingChange[] = state._pendingChanges || [];\n const idx = queue.findIndex((p) => p.localId === localId && p.stateKey === stateKey);\n if (idx >= 0) {\n const existing = queue[idx];\n switch (existing?.action) {\n case SyncAction.Create:\n if (action === SyncAction.Update) {\n existing.changes = { ...existing.changes, ...changes };\n } else if (action === SyncAction.Remove) {\n queue.splice(idx, 1); // cancel create\n }\n break;\n case SyncAction.Update:\n if (action === SyncAction.Update) {\n existing.changes = { ...existing.changes, ...changes };\n } else if (action === SyncAction.Remove) {\n existing.action = SyncAction.Remove;\n delete existing.changes;\n }\n break;\n case SyncAction.Remove:\n // terminal; ignore further updates\n break;\n }\n } else {\n if (action === SyncAction.Remove) {\n // Add id to changes as when pushOne() processes the queue it may not find the item if using IndexedDB,\n // as it's async and so may have deleted the item already\n const item = state[stateKey].find((i: any) => i._localId === localId);\n if (item) changes = { id: item.id };\n }\n\n queue.push({ stateKey, localId, action, changes });\n }\n state._pendingChanges = queue;\n return state;\n });\n syncOnce();\n }\n\n function setAndSync(partial: any) {\n set(partial);\n syncOnce();\n }\n\n function enableSync(enabled: boolean) {\n set((state: any) => {\n return {\n syncState: {\n ...state.syncState,\n enabled,\n },\n } as SyncState;\n });\n syncOnce();\n }\n\n document.addEventListener('visibilitychange', async () => {\n clearInterval(syncIntervalId);\n if (document.visibilityState === 'visible') {\n logger.debug('[persistWithSync] Sync starting now app is in foreground');\n await startSync?.();\n } else {\n logger.debug('[persistWithSync] Sync paused now app is in background');\n }\n });\n\n async function startFirstLoad() {\n let syncError: Error | undefined;\n\n for (const stateKey of Object.keys(syncApi)) {\n try {\n logger.info(`[persistWithSync] firstLoad:start for stateKey: ${stateKey}`);\n\n const api = findApi(stateKey, syncApi);\n let lastId; // Start as undefined to allow the userland api code to set the initial value+type\n\n // Batch until empty\n while (true) {\n const batch = await api.firstLoad(lastId);\n if (!batch?.length) break;\n\n // Merge batch\n set((state: any) => {\n const local: any[] = state[stateKey] || [];\n const localById = new Map<any, any>(local.filter((l) => l.id).map((l) => [l.id, l]));\n\n let newest = new Date(state._lastPulled?.[stateKey] || 0);\n const next = [...local];\n for (const remote of batch) {\n const remoteUpdated = new Date(remote.updated_at || 0);\n if (remoteUpdated > newest) newest = remoteUpdated;\n\n if (remote.deleted) continue;\n\n const localItem = remote.id ? localById.get(remote.id) : undefined;\n if (localItem) {\n const merged = { ...localItem, ...remote, _localId: localItem._localId };\n const idx = next.findIndex((i) => i._localId === localItem._localId);\n if (idx >= 0) next[idx] = merged;\n } else {\n next.push({ ...remote, _localId: nextLocalId() });\n }\n }\n\n state[stateKey] = next;\n state._lastPulled = {\n ...(state._lastPulled || {}),\n [stateKey]: newest.toISOString(),\n };\n return state;\n });\n\n lastId = batch[batch.length - 1].id;\n }\n\n logger.info(`[persistWithSync] firstLoad:done for stateKey: ${stateKey}`);\n } catch (err) {\n syncError = syncError ?? (err as Error);\n logger.error(`[persistWithSync] First load pull error for stateKey: ${stateKey}`, err);\n }\n }\n\n set((state: any) => {\n return {\n syncState: { ...state.syncState, firstLoadDone: true },\n syncError,\n };\n });\n }\n\n const userState = stateCreator(setAndSync, get, queueToSync) as TStore & SyncState & _SyncState;\n\n // Always inject sync methods and sensible defaults even if a persisted syncState object exists\n const syncState: Partial<SyncState['syncState']> = userState.syncState || {};\n return {\n ...userState,\n syncState: {\n status: syncState.status ?? 'hydrating',\n error: syncState.error,\n enabled: syncState.enabled ?? false,\n firstLoadDone: syncState.firstLoadDone ?? false,\n enableSync,\n startFirstLoad,\n },\n _pendingChanges: userState._pendingChanges ?? [],\n _lastPulled: userState._lastPulled ?? {},\n } as TStore & SyncState & _SyncState;\n };\n\n return persist(creator as any, wrappedPersistOptions) as unknown as StateCreator<TStore & SyncState, [], []>;\n}\n\nfunction orderFor(a: SyncAction): number {\n switch (a) {\n case SyncAction.Create:\n return 1;\n case SyncAction.Update:\n return 2;\n case SyncAction.Remove:\n return 3;\n }\n}\n\nasync function pull(set: any, get: any, stateKey: string, api: ApiFunctions, logger: Logger) {\n const lastPulled: Record<string, string> = get()._lastPulled || {};\n const lastPulledAt = new Date(lastPulled[stateKey] || new Date(0));\n\n logger.debug(`[persistWithSync] pull:start stateKey=${stateKey} since=${lastPulledAt.toISOString()}`);\n\n const serverData = await api.list(lastPulledAt);\n if (!serverData?.length) return;\n\n let newest = lastPulledAt;\n set((state: any) => {\n const _pendingChanges: PendingChange[] = state._pendingChanges || [];\n const local: any[] = state[stateKey] || [];\n const localById = new Map<any, any>(local.filter((l) => l.id).map((l) => [l.id, l]));\n // Collect remote ids that have a pending local Remove so we don't resurrect them before push executes\n const pendingRemovals = new Set(\n _pendingChanges\n .filter(\n (p: PendingChange) =>\n p.stateKey === stateKey &&\n p.action === SyncAction.Remove &&\n p.changes &&\n typeof p.changes.id !== 'undefined',\n )\n .map((p: PendingChange) => p.changes.id),\n );\n\n for (const remote of serverData) {\n const remoteUpdated = new Date(remote.updated_at);\n if (remoteUpdated > newest) newest = remoteUpdated;\n\n const localItem = localById.get(remote.id);\n // If a Remove is pending for this id, skip merging/adding to avoid flicker\n if (pendingRemovals.has(remote.id)) {\n logger.debug(`[persistWithSync] pull:skip-pending-remove stateKey=${stateKey} id=${remote.id}`);\n continue;\n }\n if (remote.deleted) {\n if (localItem) {\n state[stateKey] = state[stateKey].filter((i: any) => i.id !== remote.id);\n logger.debug(`[persistWithSync] pull:remove stateKey=${stateKey} id=${remote.id}`);\n }\n continue;\n }\n\n const pending = _pendingChanges.some(\n (p: PendingChange) => p.stateKey === stateKey && localItem && p.localId === localItem._localId,\n );\n if (localItem && !pending) {\n const merged = { ...localItem, ...remote, _localId: localItem._localId };\n state[stateKey] = state[stateKey].map((i: any) => (i._localId === localItem._localId ? merged : i));\n logger.debug(`[persistWithSync] pull:merge stateKey=${stateKey} id=${remote.id}`);\n } else if (!localItem) {\n // Add remote item (no local or pending collisions)\n state[stateKey] = [...state[stateKey], { ...remote, _localId: nextLocalId() }];\n logger.debug(`[persistWithSync] pull:add stateKey=${stateKey} id=${remote.id}`);\n }\n }\n\n state._lastPulled = {\n ...state._lastPulled,\n [stateKey]: newest.toISOString(),\n };\n return state;\n });\n}\n\nasync function pushOne(\n set: any,\n get: any,\n change: PendingChange,\n api: ApiFunctions,\n logger: Logger,\n queueToSync: QueueToSyncCallback,\n missingStrategy: MissingRemoteRecordDuringUpdateStrategy,\n onMissingRemoteRecordDuringUpdate?: MissingRemoteRecordDuringUpdateCallback,\n onAfterRemoteAdd?: AfterRemoteAddCallback,\n) {\n logger.debug(\n `[persistWithSync] push:attempt action=${change.action} stateKey=${change.stateKey} localId=${change.localId}`,\n );\n\n const { stateKey, localId, action, changes } = change;\n const state = get();\n const items: SyncedRecord[] = state[stateKey] || [];\n const item = items.find((i) => i._localId === localId);\n\n switch (action) {\n case SyncAction.Create: {\n if (!item) {\n removeFromPendingChanges(set, localId, stateKey);\n return;\n }\n if (item.id) {\n if (changes && Object.keys(changes).length) {\n await api.update(item.id, omitSyncFields({ ...item, ...changes }));\n }\n removeFromPendingChanges(set, localId, stateKey);\n return;\n }\n const payload = changes ? { ...item, ...changes } : item;\n const result = await api.add(omitSyncFields(payload));\n if (result) {\n logger.debug('[persistWithSync] push:create:success', { stateKey, localId, id: result.id });\n // Merge server-assigned fields (id, updated_at, etc) directly into local entity\n set((s: any) => {\n s[stateKey] = (s[stateKey] || []).map((i: any) =>\n i._localId === localId ? { ...i, ...result } : i,\n );\n return s;\n });\n // Call hook so userland can perform any cascading adjustments\n onAfterRemoteAdd?.(set, get, queueToSync, stateKey, { ...item, ...result });\n } else {\n logger.warn('[persistWithSync] push:create:no-result', { stateKey, localId });\n }\n removeFromPendingChanges(set, localId, stateKey);\n break;\n }\n case SyncAction.Update: {\n if (!item) {\n removeFromPendingChanges(set, localId, stateKey);\n return;\n }\n if (!item.id) {\n // promote to create\n set((s: any) => {\n const q: PendingChange[] = s._pendingChanges || [];\n const e = q.find((p) => p.localId === localId && p.stateKey === stateKey);\n if (e) e.action = SyncAction.Create;\n return s;\n });\n return;\n }\n const changed = await api.update(item.id, omitSyncFields({ ...changes }));\n if (!changed) {\n logger.warn('[persistWithSync] push:update:missingRemote', { stateKey, localId, id: item.id });\n const oldRecord = { ...item } as SyncedRecord;\n let newLocalId: string | undefined;\n switch (missingStrategy) {\n case 'deleteLocalRecord':\n set((s: any) => {\n s[stateKey] = (s[stateKey] || []).filter((i: any) => i._localId !== localId);\n return s;\n });\n removeFromPendingChanges(set, localId, stateKey);\n break;\n case 'insertNewRemoteRecord': {\n const freshLocalId = nextLocalId();\n newLocalId = freshLocalId;\n set((s: any) => {\n // remove old, add new copy without id so it becomes a Create\n s[stateKey] = (s[stateKey] || []).filter((i: any) => i._localId !== localId);\n s[stateKey].push({\n ...omitSyncFields(oldRecord),\n _localId: freshLocalId,\n updated_at: new Date().toISOString(),\n });\n // update queue entry\n const q: PendingChange[] = s._pendingChanges || [];\n const e = q.find((p) => p.localId === localId && p.stateKey === stateKey);\n if (e) {\n e.localId = freshLocalId;\n e.action = SyncAction.Create;\n } else {\n q.push({ stateKey, localId: freshLocalId, action: SyncAction.Create, changes });\n }\n s._pendingChanges = q;\n return s;\n });\n break;\n }\n }\n // Call hook so userland can alert the user etc.\n onMissingRemoteRecordDuringUpdate?.(missingStrategy, oldRecord, newLocalId);\n } else {\n logger.debug('[persistWithSync] push:update:success', { stateKey, localId, id: item.id });\n removeFromPendingChanges(set, localId, stateKey);\n }\n break;\n }\n case SyncAction.Remove: {\n const id = changes?.id;\n if (id) {\n await api.remove(id);\n logger.debug('[persistWithSync] push:remove:success', { stateKey, localId, id });\n }\n removeFromPendingChanges(set, localId, stateKey);\n break;\n }\n }\n}\n\nfunction removeFromPendingChanges(set: any, localId: string, stateKey: string) {\n set((s: any) => {\n s._pendingChanges = (s._pendingChanges || []).filter(\n (p: PendingChange) => !(p.localId === localId && p.stateKey === stateKey),\n );\n return s;\n });\n}\n\nfunction omitSyncFields(item: any) {\n const result = { ...item };\n for (const k of SYNC_FIELDS) delete result[k];\n return result;\n}\n\nexport function nextLocalId(): string {\n return crypto.randomUUID();\n}\n\nfunction newLogger(logger: Logger, min: LogLevel): Logger {\n const order: Record<LogLevel, number> = { debug: 10, info: 20, warn: 30, error: 40, none: 100 };\n const threshold = order[min];\n const enabled = (lvl: LogLevel) => order[lvl] >= threshold;\n return {\n debug: (...a) => enabled('debug') && logger.debug?.(...a),\n info: (...a) => enabled('info') && logger.info?.(...a),\n warn: (...a) => enabled('warn') && logger.warn?.(...a),\n error: (...a) => enabled('error') && logger.error?.(...a),\n };\n}\n\nfunction findApi(stateKey: string, syncApi: Record<string, ApiFunctions>) {\n const api = syncApi[stateKey];\n if (!api || !api.add || !api.update || !api.remove || !api.list || !api.firstLoad) {\n throw new Error(`Missing API function(s) for state key: ${stateKey}.`);\n }\n return api;\n}\n\n","/**\n * IndexedDB storage adapter for Zustand's persist middleware.\n *\n * Usage:\n * import { persist, createJSONStorage } from 'zustand/middleware';\n * import { createIndexedDBStorage } from './indexedDBStorage';\n *\n * const storage = createJSONStorage(() => createIndexedDBStorage({ dbName: 'app', storeName: 'persist' }));\n *\n * persist(myCreator, { name: 'store', storage })\n *\n * Design goals:\n * - Non‑blocking: all operations are async and off the main microtask once IndexedDB request queued.\n * - Reuses a single opened IDBDatabase instance (lazy) to avoid repeated open costs.\n * - Graceful fallback to localStorage if IndexedDB unavailable or open fails (e.g. Safari private mode).\n * - Small, dependency free, typed.\n */\n\nexport interface IndexedDBStorageOptions {\n /** Database name (default: 'zustand-persist') */\n dbName?: string;\n /** Object store name (default: 'keyval') */\n storeName?: string;\n /** IndexedDB version (default: 1) */\n version?: number;\n /** Optional logger (console-like) */\n logger?: { warn: (...a: any[]) => void; error: (...a: any[]) => void };\n}\n\nexport type ZustandStateStorage = {\n getItem: (name: string) => Promise<string | null> | string | null;\n setItem: (name: string, value: string) => Promise<void> | void;\n removeItem: (name: string) => Promise<void> | void;\n};\n\nexport function createIndexedDBStorage(options: IndexedDBStorageOptions = {}): ZustandStateStorage {\n // Provide a minimal localStorage polyfill for non-browser (test / SSR) environments.\n if (typeof globalThis.localStorage === 'undefined') {\n const mem: Record<string, string> = {};\n (globalThis as any).localStorage = {\n getItem: (k: string) => (k in mem ? mem[k] : null),\n setItem: (k: string, v: string) => {\n mem[k] = v;\n },\n removeItem: (k: string) => {\n delete mem[k];\n },\n clear: () => {\n Object.keys(mem).forEach((k) => delete mem[k]);\n },\n } as Storage;\n }\n const dbName = options.dbName ?? 'zustand-persist';\n const storeName = options.storeName ?? 'keyval';\n const version = options.version ?? 1;\n const log = options.logger ?? { warn: () => {}, error: () => {} };\n\n // Fallback detection\n const canUseIDB = typeof window !== 'undefined' && 'indexedDB' in window;\n if (!canUseIDB) {\n log.warn('[indexedDBStorage] IndexedDB not available – falling back to localStorage');\n return localStorageFallback();\n }\n\n let dbPromise: Promise<IDBDatabase | undefined> | undefined;\n\n function openDB(): Promise<IDBDatabase | undefined> {\n if (dbPromise) return dbPromise;\n dbPromise = new Promise<IDBDatabase | undefined>((resolve, reject) => {\n try {\n const req = indexedDB.open(dbName, version);\n req.onupgradeneeded = () => {\n const db = req.result;\n if (!db.objectStoreNames.contains(storeName)) {\n db.createObjectStore(storeName);\n }\n };\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n req.onblocked = () => log.warn('[indexedDBStorage] open blocked');\n } catch (e) {\n reject(e);\n }\n }).catch((e) => {\n log.error('[indexedDBStorage] Failed to open DB, falling back to localStorage', e);\n return undefined; // handled below\n });\n return dbPromise;\n }\n\n async function withStore<T>(mode: IDBTransactionMode, fn: (store: IDBObjectStore) => T | Promise<T>): Promise<T> {\n const db = await openDB();\n if (!db) {\n // Fallback path (should be rare: open failed)\n return (fn as any)(null);\n }\n return new Promise<T>((resolve, reject) => {\n let tx: IDBTransaction;\n try {\n tx = db.transaction(storeName, mode);\n } catch (_e) {\n // Possibly closed / version change: clear cached promise & retry once\n dbPromise = undefined;\n openDB().then((fresh) => {\n try {\n if (!fresh) {\n // Fall back to invoking fn with null (localStorage path)\n const result = (fn as any)(null);\n Promise.resolve(result).then(resolve, reject);\n return;\n }\n const retryTx = fresh.transaction(storeName, mode);\n const store = retryTx.objectStore(storeName);\n const result = fn(store);\n Promise.resolve(result).then(resolve, reject);\n } catch (err) {\n reject(err);\n }\n }, reject);\n return;\n }\n const store = tx.objectStore(storeName);\n let result: any;\n try {\n result = fn(store);\n } catch (err) {\n reject(err);\n return;\n }\n tx.oncomplete = () => resolve(result);\n tx.onerror = () => reject(tx.error);\n tx.onabort = () => reject(tx.error || new Error('Transaction aborted'));\n });\n }\n\n async function getItem(name: string): Promise<string | null> {\n return withStore('readonly', (store) => {\n console.log('READ STORE');\n if (!store) return localStorage.getItem(name);\n return new Promise<string | null>((resolve, reject) => {\n const req = store.get(name);\n req.onsuccess = () => resolve(req.result ?? null);\n req.onerror = () => reject(req.error);\n });\n });\n }\n\n async function setItem(name: string, value: string): Promise<void> {\n return withStore('readwrite', (store) => {\n console.log('WRITE STORE');\n if (!store) {\n localStorage.setItem(name, value);\n return;\n }\n return new Promise<void>((resolve, reject) => {\n const req = store.put(value, name);\n req.onsuccess = () => resolve();\n req.onerror = () => reject(req.error);\n });\n });\n }\n\n async function removeItem(name: string): Promise<void> {\n return withStore('readwrite', (store) => {\n if (!store) {\n localStorage.removeItem(name);\n return;\n }\n return new Promise<void>((resolve, reject) => {\n const req = store.delete(name);\n req.onsuccess = () => resolve();\n req.onerror = () => reject(req.error);\n });\n });\n }\n\n return { getItem, setItem, removeItem };\n}\n\nfunction localStorageFallback(): ZustandStateStorage {\n // Capture reference now so if global is cleaned up (test teardown) we still work\n const ls: any = (globalThis as any).localStorage;\n return {\n getItem: (n) => Promise.resolve(ls?.getItem ? ls.getItem(n) : null),\n setItem: (n, v) => {\n ls?.setItem?.(n, v);\n return Promise.resolve();\n },\n removeItem: (n) => {\n ls?.removeItem?.(n);\n return Promise.resolve();\n },\n };\n}\n"],"mappings":"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","names":["result","store","SyncAction","state"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/indexedDBStorage.ts"],"sourcesContent":["import { create, type StateCreator, type StoreApi, type UseBoundStore } from 'zustand';\nimport { persist, type PersistOptions } from 'zustand/middleware';\n\nexport { createIndexedDBStorage } from './indexedDBStorage';\n\nexport type SyncedRecord = {\n id?: any;\n _localId: string;\n updated_at: string;\n deleted?: boolean;\n [k: string]: any;\n};\n\nexport interface ApiFunctions {\n add: (item: any) => Promise<any | undefined>;\n update: (id: any, changes: any) => Promise<boolean>; // returns true if applied, false if remote missing\n remove: (id: any) => Promise<void>;\n list: (lastUpdatedAt: Date) => Promise<any[]>; // returns changed records since timestamp (including records with deleted flag)\n firstLoad: (lastId: any) => Promise<any[]>; // returns all records with id > lastId\n}\n\ntype AfterRemoteAddCallback = (set: any, get: any, queue: QueueToSyncCallback, stateKey: string, item: SyncedRecord) => void;\n\ntype MissingRemoteRecordDuringUpdateCallback = (strategy: MissingRemoteRecordDuringUpdateStrategy, item: SyncedRecord, newLocalId?: string) => void;\n\nexport type MissingRemoteRecordDuringUpdateStrategy = 'ignore' | 'deleteLocalRecord' | 'insertNewRemoteRecord';\n\ninterface SyncOptions {\n syncInterval?: number;\n logger?: Logger;\n minLogLevel?: LogLevel;\n onAfterRemoteAdd?: AfterRemoteAddCallback;\n missingRemoteRecordDuringUpdateStrategy?: MissingRemoteRecordDuringUpdateStrategy;\n onMissingRemoteRecordDuringUpdate?: MissingRemoteRecordDuringUpdateCallback;\n}\n\nexport type SyncState = {\n syncState: {\n status: 'hydrating' | 'syncing' | 'idle';\n error?: Error;\n enabled: boolean;\n firstLoadDone: boolean;\n pendingChanges: PendingChange[];\n lastPulled: Record<string, string>; // stateKey -> ISO timestamp of last successful pull\n };\n};\n\nexport enum SyncAction {\n CreateOrUpdate = 'createOrUpdate',\n Remove = 'remove',\n}\n\nexport type QueueToSyncCallback = (action: SyncAction, stateKey: string, ...localIds: string[]) => void;\n\ntype SyncedStateCreator<TStore> = (set: any, get: any, queue: QueueToSyncCallback) => TStore;\n\ninterface PendingChange {\n action: SyncAction;\n stateKey: string;\n localId: string;\n id?: any;\n version: number;\n}\n\nexport interface Logger {\n debug: (...args: any[]) => void;\n info: (...args: any[]) => void;\n warn: (...args: any[]) => void;\n error: (...args: any[]) => void;\n}\n\nexport type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';\n\nconst DEFAULT_SYNC_INTERVAL_MILLIS = 5000;\nconst DEFAULT_LOGGER: Logger = console;\nconst DEFAULT_MIN_LOG_LEVEL: LogLevel = 'debug';\nconst DEFAULT_MISSING_REMOTE_RECORD_ON_UPDATE_STRATEGY: MissingRemoteRecordDuringUpdateStrategy = 'ignore';\nconst SYNC_FIELDS = ['id', '_localId', 'updated_at', 'deleted'] as const;\n\n/**\n * Zync creates a standard persisted Zustand store with optional background sync (e.g. via RESTful, GraphQL, etc.).\n * It provides a `queueToSync()` method to enqueue changes for syncing, which are processed on an interval.\n *\n * This is a drop-in replacement for Zustand's `persist()` middleware that wires in background sync. It's usage mirrors\n * `persist(stateCreator, options)` with optional syncing per state key.\n *\n * Can be used with any storage that Zustand Persist supports. Options include localStorage if its syncronous access doesn't\n * cause blocking issues like UI freezes, or IndexedDB with its asynchronous access for improved performance.\n *\n * When using IndexedDB the whole store is saved under one key, which means indexes cannot be used to accelerate querying. However, if this\n * becomes a performance issue due to the size of the store, then libraries like dexie.js instead of Zustand would be a better solution and\n * provide the syntax for high performance queries.\n *\n * Zync's api is:\n * - `syncState` object for reactive access to internal state i.e.: const syncState = useStore((state) => state.syncState);\n * - `sync` object for control methods e.g.: useStore.sync.enable(true|false);\n *\n * Synced objects will have the client only field `_localId`, which provides a stable identifier for the object.\n * It is ideal for use as JSX keys.\n *\n * Synced objects are expected to have the following server fields:\n *\n * - id: The unique identifier from the server.\n * - updated_at: A timestamp indicating when the object was last updated.\n * This field is used to determine if the object needs to be re-fetched from the server.\n * It must be set at the server (e.g. via sql trigger or in the api code).\n * Ensure the server sets a timestamp with millisecond precision, not microsecond like PostgreSQL's timestampz,\n * as Javascript's Date object is based on milliseconds, and this will be used during sync.\n * Although the client can set this locally, it is only to give a good UX, as it won't be sent\n * to the server and will be overwritten on the client during sync. The client clock is never\n * used to check for changes as it can't be guaranteed to be in sync with the server clock. Instead any item\n * that is added, updated or deleted locally is added to a queue.\n * - deleted: A boolean flag indicating whether the object has been deleted. This use of soft deletes or similar\n * is how all clients are told about deletions during sync.\n *\n * Design principles:\n *\n * - Always pull (list) first each sync cycle to enable future conflict resolution. Currently last-write-wins, although any queued client changes\n * for an item will prevent it being overwritten during a pull, even if the server has a newer version.\n * - Then push queued changes in order (Create -> Update -> Remove).\n * - Queue coalescing: (Create + Update*) => single Create (merged changes); (Create + Remove) => drop both; (Update + Update) => merge; (Update + Remove) => Remove.\n *\n *\n * @param stateCreator - The function to create the initial state.\n * @param persistOptions - Standard Zustand options for persisting the store.\n * @param syncApi - Remote API functions for syncing state. Use the same key name as the state key.\n * e.g. if your state key is called `fish`, the syncApi should be `fish: { list, add, update, remove }`\n * If you don't provide a key for a state field, it won't be synced, but will be persisted as expected.\n * @param syncOptions - Syncing options (Optional).\n */\nexport function persistWithSync<TStore extends object>(\n stateCreator: SyncedStateCreator<TStore>,\n persistOptions: any,\n syncApi: Record<string, ApiFunctions>,\n syncOptions: SyncOptions = {},\n) {\n const syncInterval = syncOptions.syncInterval ?? DEFAULT_SYNC_INTERVAL_MILLIS;\n const missingStrategy = syncOptions.missingRemoteRecordDuringUpdateStrategy ?? DEFAULT_MISSING_REMOTE_RECORD_ON_UPDATE_STRATEGY;\n const logger = newLogger(syncOptions.logger ?? DEFAULT_LOGGER, syncOptions.minLogLevel ?? DEFAULT_MIN_LOG_LEVEL);\n\n const baseOnRehydrate = persistOptions?.onRehydrateStorage;\n const basePartialize = persistOptions?.partialize;\n\n const wrappedPersistOptions = {\n ...persistOptions,\n onRehydrateStorage: () => {\n logger.debug('[persistWithSync] Rehydration started');\n\n return (state: any, error: any) => {\n if (error) {\n logger.error('[persistWithSync] Rehydration failed', error);\n } else {\n baseOnRehydrate?.(state, error);\n state.syncState.status = 'idle';\n logger.debug('[persistWithSync] Rehydration complete');\n }\n };\n },\n partialize: (s: any) => {\n // Select state to be persisted\n\n const base = basePartialize ? basePartialize(s) : s;\n const { syncState, ...rest } = base || {};\n return {\n ...rest,\n syncState: {\n firstLoadDone: syncState.firstLoadDone,\n pendingChanges: syncState.pendingChanges,\n lastPulled: syncState.lastPulled,\n },\n };\n },\n // merge: (persisted: any, current: any) => {\n // // Add unpersistable fields back e.g. functions or memory-only fields\n\n // const p = persisted || {};\n // const c = current || {};\n // return {\n // ...c,\n // ...p,\n // syncState: {\n // ...c.syncState,\n // ...p.syncState,\n // status: 'idle',\n // //firstLoadDone: p.syncState?.firstLoadDone ?? c.syncState.firstLoadDone ?? false,\n // //pendingChanges: p.syncState?.pendingChanges ?? c.syncState.pendingChanges ?? [],\n // //lastPulled: p.syncState?.lastPulled ?? c.syncState.lastPulled ?? {},\n // },\n // };\n // },\n };\n\n const creator: StateCreator<TStore & SyncState, [], []> = (set: any, get: any, storeApi: any) => {\n let syncIntervalId: any;\n\n async function syncOnce() {\n const state: SyncState = get();\n if (!state.syncState.enabled || state.syncState.status !== 'idle') return;\n\n set((state: any) => ({\n syncState: {\n ...(state.syncState || {}),\n status: 'syncing',\n },\n }));\n\n let syncError: Error | undefined;\n\n // 1) PULL for each stateKey\n for (const stateKey of Object.keys(syncApi)) {\n try {\n const api = findApi(stateKey, syncApi);\n await pull(set, get, stateKey, api, logger);\n } catch (err) {\n syncError = syncError ?? (err as Error);\n logger.error(`[persistWithSync] Pull error for stateKey: ${stateKey}`, err);\n }\n }\n\n // 2) PUSH queued changes\n const snapshot: PendingChange[] = [...(get().syncState.pendingChanges || [])];\n\n // Deterministic ordering: Create -> Update -> Remove so dependencies (e.g. id assignment) happen early\n snapshot.sort((a, b) => orderFor(a.action) - orderFor(b.action));\n\n for (const change of snapshot) {\n try {\n const api = findApi(change.stateKey, syncApi);\n await pushOne(\n set,\n get,\n change,\n api,\n logger,\n queueToSync,\n missingStrategy,\n syncOptions.onMissingRemoteRecordDuringUpdate,\n syncOptions.onAfterRemoteAdd,\n );\n } catch (err) {\n syncError = syncError ?? (err as Error);\n logger.error(`[persistWithSync] Push error for change: ${change}`, err);\n }\n }\n\n if (get().syncState.pendingChanges.length > 0 && !syncError) {\n return await syncOnce();\n }\n\n set((state: any) => ({\n syncState: {\n ...(state.syncState || {}),\n status: 'idle',\n error: syncError,\n },\n }));\n }\n\n function queueToSync(action: SyncAction, stateKey: string, ...localIds: string[]) {\n set((state: any) => {\n const queue: PendingChange[] = state.syncState.pendingChanges || [];\n\n for (const localId of localIds) {\n const item = state[stateKey].find((i: any) => i._localId === localId);\n if (!item) {\n logger.error('[persistWithSync] queueToSync:no-local-item', {\n stateKey,\n localId,\n });\n continue;\n }\n if (action === SyncAction.Remove && !item.id) {\n logger.warn('[persistWithSync] queueToSync:remove-no-id', {\n stateKey,\n localId,\n });\n continue;\n }\n\n const queueItem = queue.find((p) => p.localId === localId && p.stateKey === stateKey);\n if (queueItem) {\n queueItem.version += 1;\n\n if (queueItem.action === SyncAction.CreateOrUpdate && action === SyncAction.Remove && item.id) {\n // Convert pending Create -> Remove so the intent to\n // delete is preserved even if the create hasn't\n // been pushed yet. This prevents races where a\n // create is removed from the queue but still\n // ends up being pushed by an in-flight run.\n queueItem.action = SyncAction.Remove;\n queueItem.id = item.id;\n }\n } else {\n queue.push({ action, stateKey, localId, id: item.id, version: 1 });\n }\n }\n\n return {\n syncState: {\n ...(state.syncState || {}),\n pendingChanges: queue,\n },\n };\n });\n syncOnce(); // Unawaited async\n }\n\n function setAndSync(partial: any) {\n // Allow passing either a partial object or an updater function\n if (typeof partial === 'function') {\n set((state: any) => ({ ...(partial as Function)(state) }));\n } else {\n set(partial);\n }\n syncOnce(); // Unawaited async\n }\n\n function enable(enabled: boolean) {\n set((state: any) => ({\n syncState: {\n ...(state.syncState || {}),\n enabled,\n },\n }));\n\n enableSyncTimer(enabled);\n addVisibilityChangeListener(enabled);\n }\n\n function enableSyncTimer(enabled: boolean) {\n clearInterval(syncIntervalId);\n syncIntervalId = undefined;\n if (enabled) {\n syncIntervalId = setInterval(syncOnce, syncInterval);\n syncOnce(); // Unawaited async\n }\n }\n\n function addVisibilityChangeListener(add: boolean) {\n if (add) {\n document.addEventListener('visibilitychange', onVisibilityChange);\n } else {\n document.removeEventListener('visibilitychange', onVisibilityChange);\n }\n }\n\n function onVisibilityChange() {\n if (document.visibilityState === 'visible') {\n logger.debug('[persistWithSync] Sync started now app is in foreground');\n enableSyncTimer(true);\n } else {\n logger.debug('[persistWithSync] Sync paused now app is in background');\n enableSyncTimer(false);\n }\n }\n\n async function startFirstLoad() {\n let syncError: Error | undefined;\n\n for (const stateKey of Object.keys(syncApi)) {\n try {\n logger.info(`[persistWithSync] firstLoad:start for stateKey: ${stateKey}`);\n\n const api = findApi(stateKey, syncApi);\n let lastId; // Start as undefined to allow the userland api code to set the initial value+type\n\n // Batch until empty\n while (true) {\n const batch = await api.firstLoad(lastId);\n if (!batch?.length) break;\n\n // Merge batch\n set((state: any) => {\n const local: any[] = state[stateKey] || [];\n const localById = new Map<any, any>(local.filter((l) => l.id).map((l) => [l.id, l]));\n\n let newest = new Date(state.syncState.lastPulled[stateKey] || 0);\n const next = [...local];\n for (const remote of batch) {\n const remoteUpdated = new Date(remote.updated_at || 0);\n if (remoteUpdated > newest) newest = remoteUpdated;\n\n if (remote.deleted) continue;\n\n const localItem = remote.id ? localById.get(remote.id) : undefined;\n if (localItem) {\n const merged = {\n ...localItem,\n ...remote,\n _localId: localItem._localId,\n };\n const idx = next.findIndex((i) => i._localId === localItem._localId);\n if (idx >= 0) next[idx] = merged;\n } else {\n next.push({\n ...remote,\n _localId: nextLocalId(),\n });\n }\n }\n\n return {\n [stateKey]: next,\n syncState: {\n ...(state.syncState || {}),\n lastPulled: {\n ...(state.syncState.lastPulled || {}),\n [stateKey]: newest.toISOString(),\n },\n },\n };\n });\n\n lastId = batch[batch.length - 1].id;\n }\n\n logger.info(`[persistWithSync] firstLoad:done for stateKey: ${stateKey}`);\n } catch (err) {\n syncError = syncError ?? (err as Error);\n logger.error(`[persistWithSync] First load pull error for stateKey: ${stateKey}`, err);\n }\n }\n\n set((state: any) => ({\n syncState: {\n ...(state.syncState || {}),\n firstLoadDone: true,\n error: syncError,\n },\n }));\n }\n\n storeApi.sync = {\n enable,\n startFirstLoad,\n };\n\n const userState = stateCreator(setAndSync, get, queueToSync) as TStore;\n\n return {\n ...userState,\n syncState: {\n // set defaults\n status: 'hydrating',\n error: undefined,\n enabled: false,\n firstLoadDone: false,\n pendingChanges: [],\n lastPulled: {},\n },\n } as TStore & SyncState;\n };\n\n return persist(creator, wrappedPersistOptions);\n}\n\nexport function createStoreWithSync<TStore extends object>(\n stateCreator: SyncedStateCreator<TStore>,\n persistOptions: any,\n syncApi: Record<string, ApiFunctions>,\n syncOptions: SyncOptions = {},\n): UseStoreWithSync<TStore> {\n return create(persistWithSync(stateCreator, persistOptions, syncApi, syncOptions)) as UseStoreWithSync<TStore>;\n}\n\nexport type UseStoreWithSync<T> = UseBoundStore<\n StoreApi<T & SyncState> & {\n sync: {\n enable: (e: boolean) => void;\n startFirstLoad: () => Promise<void>;\n };\n persist: {\n setOptions: (options: Partial<PersistOptions<T, any, any>>) => void;\n clearStorage: () => void;\n rehydrate: () => Promise<void> | void;\n hasHydrated: () => boolean;\n onHydrate: (fn: (state: T) => void) => () => void;\n onFinishHydration: (fn: (state: T) => void) => () => void;\n getOptions: () => Partial<PersistOptions<T, any, any>>;\n };\n }\n>;\n\nfunction orderFor(a: SyncAction): number {\n switch (a) {\n case SyncAction.CreateOrUpdate:\n return 1;\n case SyncAction.Remove:\n return 2;\n }\n}\n\nasync function pull(set: any, get: any, stateKey: string, api: ApiFunctions, logger: Logger) {\n const lastPulled: Record<string, string> = get().syncState.lastPulled || {};\n const lastPulledAt = new Date(lastPulled[stateKey] || new Date(0));\n\n logger.debug(`[persistWithSync] pull:start stateKey=${stateKey} since=${lastPulledAt.toISOString()}`);\n\n const serverData = await api.list(lastPulledAt);\n if (!serverData?.length) return;\n\n let newest = lastPulledAt;\n set((state: any) => {\n const pendingChanges: PendingChange[] = state.syncState.pendingChanges || [];\n const localItems: any[] = state[stateKey] || [];\n let nextItems = [...localItems];\n const localById = new Map<any, any>(localItems.filter((l) => l.id).map((l) => [l.id, l]));\n\n // Collect remote ids that have a pending local Remove so we don't resurrect them before push executes\n const pendingRemovalIds = new Set();\n for (const change of pendingChanges) {\n if (change.stateKey === stateKey && change.action === SyncAction.Remove) {\n const item = localItems.find((i: any) => i._localId === change.localId);\n if (item && item.id) pendingRemovalIds.add(item.id);\n }\n }\n\n for (const remote of serverData) {\n const remoteUpdated = new Date(remote.updated_at);\n if (remoteUpdated > newest) newest = remoteUpdated;\n\n const localItem = localById.get(remote.id);\n // If a Remove is pending for this id, skip merging/adding to avoid flicker\n if (pendingRemovalIds.has(remote.id)) {\n logger.debug(`[persistWithSync] pull:skip-pending-remove stateKey=${stateKey} id=${remote.id}`);\n continue;\n }\n if (remote.deleted) {\n if (localItem) {\n nextItems = nextItems.filter((i: any) => i.id !== remote.id);\n logger.debug(`[persistWithSync] pull:remove stateKey=${stateKey} id=${remote.id}`);\n }\n continue;\n }\n\n const pending = localItem && pendingChanges.some((p: PendingChange) => p.stateKey === stateKey && p.localId === localItem._localId);\n if (localItem && !pending) {\n const merged = {\n ...localItem,\n ...remote,\n _localId: localItem._localId,\n };\n nextItems = nextItems.map((i: any) => (i._localId === localItem._localId ? merged : i));\n logger.debug(`[persistWithSync] pull:merge stateKey=${stateKey} id=${remote.id}`);\n } else if (!localItem) {\n // Add remote item (no local or pending collisions)\n nextItems = [...nextItems, { ...remote, _localId: nextLocalId() }];\n logger.debug(`[persistWithSync] pull:add stateKey=${stateKey} id=${remote.id}`);\n }\n }\n\n return {\n [stateKey]: nextItems,\n syncState: {\n ...(state.syncState || {}),\n lastPulled: {\n ...(state.syncState.lastPulled || {}),\n [stateKey]: newest.toISOString(),\n },\n },\n };\n });\n}\n\nasync function pushOne(\n set: any,\n get: any,\n change: PendingChange,\n api: ApiFunctions,\n logger: Logger,\n queueToSync: QueueToSyncCallback,\n missingStrategy: MissingRemoteRecordDuringUpdateStrategy,\n onMissingRemoteRecordDuringUpdate?: MissingRemoteRecordDuringUpdateCallback,\n onAfterRemoteAdd?: AfterRemoteAddCallback,\n) {\n logger.debug(`[persistWithSync] push:attempt action=${change.action} stateKey=${change.stateKey} localId=${change.localId}`);\n\n const { action, stateKey, localId, id, version } = change;\n\n switch (action) {\n case SyncAction.Remove:\n await api.remove(id);\n logger.debug(`[persistWithSync] push:remove:success ${stateKey} ${localId} ${id}`);\n removeFromPendingChanges(set, localId, stateKey);\n break;\n\n case SyncAction.CreateOrUpdate:\n const state = get();\n const items: SyncedRecord[] = state[stateKey] || [];\n const item = items.find((i) => i._localId === localId);\n if (!item) {\n logger.warn(`[persistWithSync] push:${action}:no-local-item`, {\n stateKey,\n localId,\n });\n removeFromPendingChanges(set, localId, stateKey);\n return;\n }\n\n let omittedItem = omitSyncFields(item);\n if (item.id) {\n // Update\n const changed = await api.update(item.id, omittedItem);\n if (changed) {\n logger.debug('[persistWithSync] push:update:success', {\n stateKey,\n localId,\n id: item.id,\n });\n if (samePendingVersion(get, stateKey, localId, version)) {\n removeFromPendingChanges(set, localId, stateKey);\n }\n return;\n } else {\n logger.warn('[persistWithSync] push:update:missingRemote', {\n stateKey,\n localId,\n id: item.id,\n });\n\n switch (missingStrategy) {\n case 'deleteLocalRecord':\n set((s: any) => ({\n [stateKey]: (s[stateKey] || []).filter((i: any) => i._localId !== localId),\n }));\n break;\n\n case 'insertNewRemoteRecord': {\n omittedItem._localId = nextLocalId();\n omittedItem.updated_at = new Date().toISOString();\n\n // replace old with new copy without id so it becomes a Create\n set((s: any) => ({\n [stateKey]: (s[stateKey] || []).map((i: any) => (i._localId === localId ? omittedItem : i)),\n }));\n\n queueToSync(SyncAction.CreateOrUpdate, stateKey, omittedItem._localId);\n break;\n }\n }\n removeFromPendingChanges(set, localId, stateKey);\n // Call hook so userland can alert the user etc.\n onMissingRemoteRecordDuringUpdate?.(missingStrategy, omittedItem, omittedItem._localId);\n }\n return;\n }\n\n // Create\n const result = await api.add(omittedItem);\n if (result) {\n logger.debug('[persistWithSync] push:create:success', {\n stateKey,\n localId,\n id: result.id,\n });\n\n // Merge server-assigned fields (id, updated_at, etc) directly into local entity\n set((s: any) => ({\n [stateKey]: (s[stateKey] || []).map((i: any) => (i._localId === localId ? { ...i, ...result } : i)),\n }));\n if (samePendingVersion(get, stateKey, localId, version)) {\n removeFromPendingChanges(set, localId, stateKey);\n }\n // Call hook so userland can perform any cascading adjustments\n onAfterRemoteAdd?.(set, get, queueToSync, stateKey, {\n ...item,\n ...result,\n });\n } else {\n logger.warn('[persistWithSync] push:create:no-result', {\n stateKey,\n localId,\n });\n if (samePendingVersion(get, stateKey, localId, version)) {\n removeFromPendingChanges(set, localId, stateKey);\n }\n }\n break;\n }\n}\n\nfunction samePendingVersion(get: any, stateKey: string, localId: string, version: number): boolean {\n // Check if the pending entry is still the same (i.e. wasn't modified while the request was in-flight)\n const q: PendingChange[] = get().syncState.pendingChanges || [];\n const curChange = q.find((p) => p.localId === localId && p.stateKey === stateKey);\n return curChange?.version === version;\n}\n\nfunction removeFromPendingChanges(set: any, localId: string, stateKey: string) {\n set((s: any) => {\n const queue: PendingChange[] = (s.syncState.pendingChanges || []).filter((p: PendingChange) => !(p.localId === localId && p.stateKey === stateKey));\n return {\n syncState: {\n ...(s.syncState || {}),\n pendingChanges: queue,\n },\n };\n });\n}\n\nfunction omitSyncFields(item: any) {\n const result = { ...item };\n for (const k of SYNC_FIELDS) delete result[k];\n return result;\n}\n\nexport function nextLocalId(): string {\n return crypto.randomUUID();\n}\n\nfunction newLogger(logger: Logger, min: LogLevel): Logger {\n const order: Record<LogLevel, number> = {\n debug: 10,\n info: 20,\n warn: 30,\n error: 40,\n none: 100,\n };\n const threshold = order[min];\n const enabled = (lvl: LogLevel) => order[lvl] >= threshold;\n return {\n debug: (...a) => enabled('debug') && logger.debug?.(...a),\n info: (...a) => enabled('info') && logger.info?.(...a),\n warn: (...a) => enabled('warn') && logger.warn?.(...a),\n error: (...a) => enabled('error') && logger.error?.(...a),\n };\n}\n\nfunction findApi(stateKey: string, syncApi: Record<string, ApiFunctions>) {\n const api = syncApi[stateKey];\n if (!api || !api.add || !api.update || !api.remove || !api.list || !api.firstLoad) {\n throw new Error(`Missing API function(s) for state key: ${stateKey}.`);\n }\n return api;\n}\n","import { openDB, type IDBPDatabase } from 'idb';\n\nexport function createIndexedDBStorage(options: { dbName: string; storeName: string }) {\n const dbName = options.dbName;\n const storeName = options.storeName;\n\n // Start with version 1; we may bump if the requested object store is missing\n let dbPromise: Promise<IDBPDatabase<any>> = openDB(dbName, 1, {\n upgrade(db) {\n if (!db.objectStoreNames.contains(storeName)) {\n db.createObjectStore(storeName);\n }\n },\n });\n\n async function ensureStore(): Promise<void> {\n const db = await dbPromise;\n if (db.objectStoreNames.contains(storeName)) return;\n const nextVersion = (db.version || 0) + 1;\n try {\n db.close();\n } catch (e) {\n // ignore\n }\n // Bump version to force onupgradeneeded and create the missing store\n dbPromise = openDB(dbName, nextVersion, {\n upgrade(upg) {\n if (!upg.objectStoreNames.contains(storeName)) upg.createObjectStore(storeName);\n },\n });\n await dbPromise;\n }\n\n async function withRetry<T>(fn: (db: IDBPDatabase<any>) => Promise<T>): Promise<T> {\n try {\n const db = await dbPromise;\n return await fn(db);\n } catch (err: any) {\n // If the object store is missing, reopen DB with bumped version and retry once\n const msg = String(err && err.message ? err.message : err);\n if (err && (err.name === 'NotFoundError' || /objectStore/i.test(msg))) {\n await ensureStore();\n const db2 = await dbPromise;\n return await fn(db2);\n }\n throw err;\n }\n }\n\n return {\n getItem: async (name: string): Promise<string | null> => {\n return withRetry(async (db) => {\n let v = await db.get(storeName, name);\n v = v ?? null;\n console.log('getItem:', db.objectStoreNames, storeName, name, v);\n return v;\n });\n },\n setItem: async (name: string, value: string): Promise<void> => {\n return withRetry(async (db) => {\n console.log('setItem', name, value);\n await db.put(storeName, value, name);\n });\n },\n removeItem: async (name: string): Promise<void> => {\n return withRetry(async (db) => {\n console.log('removeItem', name);\n await db.delete(storeName, name);\n });\n },\n };\n}\n"],"mappings":"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","names":["SyncAction","state"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,41 +1,14 @@
|
|
|
1
|
-
import { StateCreator } from 'zustand';
|
|
1
|
+
import { StateCreator, UseBoundStore, StoreApi } from 'zustand';
|
|
2
|
+
import { PersistOptions } from 'zustand/middleware';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* const storage = createJSONStorage(() => createIndexedDBStorage({ dbName: 'app', storeName: 'persist' }));
|
|
11
|
-
*
|
|
12
|
-
* persist(myCreator, { name: 'store', storage })
|
|
13
|
-
*
|
|
14
|
-
* Design goals:
|
|
15
|
-
* - Non‑blocking: all operations are async and off the main microtask once IndexedDB request queued.
|
|
16
|
-
* - Reuses a single opened IDBDatabase instance (lazy) to avoid repeated open costs.
|
|
17
|
-
* - Graceful fallback to localStorage if IndexedDB unavailable or open fails (e.g. Safari private mode).
|
|
18
|
-
* - Small, dependency free, typed.
|
|
19
|
-
*/
|
|
20
|
-
interface IndexedDBStorageOptions {
|
|
21
|
-
/** Database name (default: 'zustand-persist') */
|
|
22
|
-
dbName?: string;
|
|
23
|
-
/** Object store name (default: 'keyval') */
|
|
24
|
-
storeName?: string;
|
|
25
|
-
/** IndexedDB version (default: 1) */
|
|
26
|
-
version?: number;
|
|
27
|
-
/** Optional logger (console-like) */
|
|
28
|
-
logger?: {
|
|
29
|
-
warn: (...a: any[]) => void;
|
|
30
|
-
error: (...a: any[]) => void;
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
type ZustandStateStorage = {
|
|
34
|
-
getItem: (name: string) => Promise<string | null> | string | null;
|
|
35
|
-
setItem: (name: string, value: string) => Promise<void> | void;
|
|
36
|
-
removeItem: (name: string) => Promise<void> | void;
|
|
4
|
+
declare function createIndexedDBStorage(options: {
|
|
5
|
+
dbName: string;
|
|
6
|
+
storeName: string;
|
|
7
|
+
}): {
|
|
8
|
+
getItem: (name: string) => Promise<string | null>;
|
|
9
|
+
setItem: (name: string, value: string) => Promise<void>;
|
|
10
|
+
removeItem: (name: string) => Promise<void>;
|
|
37
11
|
};
|
|
38
|
-
declare function createIndexedDBStorage(options?: IndexedDBStorageOptions): ZustandStateStorage;
|
|
39
12
|
|
|
40
13
|
type SyncedRecord = {
|
|
41
14
|
id?: any;
|
|
@@ -53,7 +26,7 @@ interface ApiFunctions {
|
|
|
53
26
|
}
|
|
54
27
|
type AfterRemoteAddCallback = (set: any, get: any, queue: QueueToSyncCallback, stateKey: string, item: SyncedRecord) => void;
|
|
55
28
|
type MissingRemoteRecordDuringUpdateCallback = (strategy: MissingRemoteRecordDuringUpdateStrategy, item: SyncedRecord, newLocalId?: string) => void;
|
|
56
|
-
type MissingRemoteRecordDuringUpdateStrategy = 'deleteLocalRecord' | 'insertNewRemoteRecord';
|
|
29
|
+
type MissingRemoteRecordDuringUpdateStrategy = 'ignore' | 'deleteLocalRecord' | 'insertNewRemoteRecord';
|
|
57
30
|
interface SyncOptions {
|
|
58
31
|
syncInterval?: number;
|
|
59
32
|
logger?: Logger;
|
|
@@ -67,18 +40,24 @@ type SyncState = {
|
|
|
67
40
|
status: 'hydrating' | 'syncing' | 'idle';
|
|
68
41
|
error?: Error;
|
|
69
42
|
enabled: boolean;
|
|
70
|
-
enableSync: (enabled: boolean) => void;
|
|
71
43
|
firstLoadDone: boolean;
|
|
72
|
-
|
|
44
|
+
pendingChanges: PendingChange[];
|
|
45
|
+
lastPulled: Record<string, string>;
|
|
73
46
|
};
|
|
74
47
|
};
|
|
75
48
|
declare enum SyncAction {
|
|
76
|
-
|
|
77
|
-
Update = "update",
|
|
49
|
+
CreateOrUpdate = "createOrUpdate",
|
|
78
50
|
Remove = "remove"
|
|
79
51
|
}
|
|
80
|
-
type QueueToSyncCallback = (action: SyncAction,
|
|
52
|
+
type QueueToSyncCallback = (action: SyncAction, stateKey: string, ...localIds: string[]) => void;
|
|
81
53
|
type SyncedStateCreator<TStore> = (set: any, get: any, queue: QueueToSyncCallback) => TStore;
|
|
54
|
+
interface PendingChange {
|
|
55
|
+
action: SyncAction;
|
|
56
|
+
stateKey: string;
|
|
57
|
+
localId: string;
|
|
58
|
+
id?: any;
|
|
59
|
+
version: number;
|
|
60
|
+
}
|
|
82
61
|
interface Logger {
|
|
83
62
|
debug: (...args: any[]) => void;
|
|
84
63
|
info: (...args: any[]) => void;
|
|
@@ -100,21 +79,12 @@ type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
|
|
|
100
79
|
* becomes a performance issue due to the size of the store, then libraries like dexie.js instead of Zustand would be a better solution and
|
|
101
80
|
* provide the syntax for high performance queries.
|
|
102
81
|
*
|
|
103
|
-
* Zync
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* - _pendingChanges: A queue of changes to be synced with the backend.
|
|
107
|
-
* - _lastPulled: A timestamp of the last successful pull from the backend.
|
|
108
|
-
* [Public]
|
|
109
|
-
* - syncState: Access to sync status, errors, and a method to enable/disable syncing.
|
|
110
|
-
* i.e. const syncState = useStore((state) => state.syncState);
|
|
82
|
+
* Zync's api is:
|
|
83
|
+
* - `syncState` object for reactive access to internal state i.e.: const syncState = useStore((state) => state.syncState);
|
|
84
|
+
* - `sync` object for control methods e.g.: useStore.sync.enable(true|false);
|
|
111
85
|
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* - Always pull (list) first each sync cycle to enable future conflict resolution. Currently last-write-wins, although any queued client changes
|
|
115
|
-
* for an item will prevent it being overwritten during a pull, even if the server has a newer version.
|
|
116
|
-
* - Then push queued changes in order (Create -> Update -> Remove).
|
|
117
|
-
* - Queue coalescing: (Create + Update*) => single Create (merged changes); (Create + Remove) => drop both; (Update + Update) => merge; (Update + Remove) => Remove.
|
|
86
|
+
* Synced objects will have the client only field `_localId`, which provides a stable identifier for the object.
|
|
87
|
+
* It is ideal for use as JSX keys.
|
|
118
88
|
*
|
|
119
89
|
* Synced objects are expected to have the following server fields:
|
|
120
90
|
*
|
|
@@ -131,8 +101,13 @@ type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
|
|
|
131
101
|
* - deleted: A boolean flag indicating whether the object has been deleted. This use of soft deletes or similar
|
|
132
102
|
* is how all clients are told about deletions during sync.
|
|
133
103
|
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
104
|
+
* Design principles:
|
|
105
|
+
*
|
|
106
|
+
* - Always pull (list) first each sync cycle to enable future conflict resolution. Currently last-write-wins, although any queued client changes
|
|
107
|
+
* for an item will prevent it being overwritten during a pull, even if the server has a newer version.
|
|
108
|
+
* - Then push queued changes in order (Create -> Update -> Remove).
|
|
109
|
+
* - Queue coalescing: (Create + Update*) => single Create (merged changes); (Create + Remove) => drop both; (Update + Update) => merge; (Update + Remove) => Remove.
|
|
110
|
+
*
|
|
136
111
|
*
|
|
137
112
|
* @param stateCreator - The function to create the initial state.
|
|
138
113
|
* @param persistOptions - Standard Zustand options for persisting the store.
|
|
@@ -141,7 +116,23 @@ type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
|
|
|
141
116
|
* If you don't provide a key for a state field, it won't be synced, but will be persisted as expected.
|
|
142
117
|
* @param syncOptions - Syncing options (Optional).
|
|
143
118
|
*/
|
|
144
|
-
declare function persistWithSync<TStore extends object>(stateCreator: SyncedStateCreator<TStore>, persistOptions: any, syncApi: Record<string, ApiFunctions>, syncOptions?: SyncOptions): StateCreator<TStore & SyncState, [], []>;
|
|
119
|
+
declare function persistWithSync<TStore extends object>(stateCreator: SyncedStateCreator<TStore>, persistOptions: any, syncApi: Record<string, ApiFunctions>, syncOptions?: SyncOptions): StateCreator<TStore & SyncState, [], [["zustand/persist", TStore & SyncState]]>;
|
|
120
|
+
declare function createStoreWithSync<TStore extends object>(stateCreator: SyncedStateCreator<TStore>, persistOptions: any, syncApi: Record<string, ApiFunctions>, syncOptions?: SyncOptions): UseStoreWithSync<TStore>;
|
|
121
|
+
type UseStoreWithSync<T> = UseBoundStore<StoreApi<T & SyncState> & {
|
|
122
|
+
sync: {
|
|
123
|
+
enable: (e: boolean) => void;
|
|
124
|
+
startFirstLoad: () => Promise<void>;
|
|
125
|
+
};
|
|
126
|
+
persist: {
|
|
127
|
+
setOptions: (options: Partial<PersistOptions<T, any, any>>) => void;
|
|
128
|
+
clearStorage: () => void;
|
|
129
|
+
rehydrate: () => Promise<void> | void;
|
|
130
|
+
hasHydrated: () => boolean;
|
|
131
|
+
onHydrate: (fn: (state: T) => void) => () => void;
|
|
132
|
+
onFinishHydration: (fn: (state: T) => void) => () => void;
|
|
133
|
+
getOptions: () => Partial<PersistOptions<T, any, any>>;
|
|
134
|
+
};
|
|
135
|
+
}>;
|
|
145
136
|
declare function nextLocalId(): string;
|
|
146
137
|
|
|
147
|
-
export { type ApiFunctions, type
|
|
138
|
+
export { type ApiFunctions, type LogLevel, type Logger, type MissingRemoteRecordDuringUpdateStrategy, type QueueToSyncCallback, SyncAction, type SyncState, type SyncedRecord, type UseStoreWithSync, createIndexedDBStorage, createStoreWithSync, nextLocalId, persistWithSync };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,41 +1,14 @@
|
|
|
1
|
-
import { StateCreator } from 'zustand';
|
|
1
|
+
import { StateCreator, UseBoundStore, StoreApi } from 'zustand';
|
|
2
|
+
import { PersistOptions } from 'zustand/middleware';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* const storage = createJSONStorage(() => createIndexedDBStorage({ dbName: 'app', storeName: 'persist' }));
|
|
11
|
-
*
|
|
12
|
-
* persist(myCreator, { name: 'store', storage })
|
|
13
|
-
*
|
|
14
|
-
* Design goals:
|
|
15
|
-
* - Non‑blocking: all operations are async and off the main microtask once IndexedDB request queued.
|
|
16
|
-
* - Reuses a single opened IDBDatabase instance (lazy) to avoid repeated open costs.
|
|
17
|
-
* - Graceful fallback to localStorage if IndexedDB unavailable or open fails (e.g. Safari private mode).
|
|
18
|
-
* - Small, dependency free, typed.
|
|
19
|
-
*/
|
|
20
|
-
interface IndexedDBStorageOptions {
|
|
21
|
-
/** Database name (default: 'zustand-persist') */
|
|
22
|
-
dbName?: string;
|
|
23
|
-
/** Object store name (default: 'keyval') */
|
|
24
|
-
storeName?: string;
|
|
25
|
-
/** IndexedDB version (default: 1) */
|
|
26
|
-
version?: number;
|
|
27
|
-
/** Optional logger (console-like) */
|
|
28
|
-
logger?: {
|
|
29
|
-
warn: (...a: any[]) => void;
|
|
30
|
-
error: (...a: any[]) => void;
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
type ZustandStateStorage = {
|
|
34
|
-
getItem: (name: string) => Promise<string | null> | string | null;
|
|
35
|
-
setItem: (name: string, value: string) => Promise<void> | void;
|
|
36
|
-
removeItem: (name: string) => Promise<void> | void;
|
|
4
|
+
declare function createIndexedDBStorage(options: {
|
|
5
|
+
dbName: string;
|
|
6
|
+
storeName: string;
|
|
7
|
+
}): {
|
|
8
|
+
getItem: (name: string) => Promise<string | null>;
|
|
9
|
+
setItem: (name: string, value: string) => Promise<void>;
|
|
10
|
+
removeItem: (name: string) => Promise<void>;
|
|
37
11
|
};
|
|
38
|
-
declare function createIndexedDBStorage(options?: IndexedDBStorageOptions): ZustandStateStorage;
|
|
39
12
|
|
|
40
13
|
type SyncedRecord = {
|
|
41
14
|
id?: any;
|
|
@@ -53,7 +26,7 @@ interface ApiFunctions {
|
|
|
53
26
|
}
|
|
54
27
|
type AfterRemoteAddCallback = (set: any, get: any, queue: QueueToSyncCallback, stateKey: string, item: SyncedRecord) => void;
|
|
55
28
|
type MissingRemoteRecordDuringUpdateCallback = (strategy: MissingRemoteRecordDuringUpdateStrategy, item: SyncedRecord, newLocalId?: string) => void;
|
|
56
|
-
type MissingRemoteRecordDuringUpdateStrategy = 'deleteLocalRecord' | 'insertNewRemoteRecord';
|
|
29
|
+
type MissingRemoteRecordDuringUpdateStrategy = 'ignore' | 'deleteLocalRecord' | 'insertNewRemoteRecord';
|
|
57
30
|
interface SyncOptions {
|
|
58
31
|
syncInterval?: number;
|
|
59
32
|
logger?: Logger;
|
|
@@ -67,18 +40,24 @@ type SyncState = {
|
|
|
67
40
|
status: 'hydrating' | 'syncing' | 'idle';
|
|
68
41
|
error?: Error;
|
|
69
42
|
enabled: boolean;
|
|
70
|
-
enableSync: (enabled: boolean) => void;
|
|
71
43
|
firstLoadDone: boolean;
|
|
72
|
-
|
|
44
|
+
pendingChanges: PendingChange[];
|
|
45
|
+
lastPulled: Record<string, string>;
|
|
73
46
|
};
|
|
74
47
|
};
|
|
75
48
|
declare enum SyncAction {
|
|
76
|
-
|
|
77
|
-
Update = "update",
|
|
49
|
+
CreateOrUpdate = "createOrUpdate",
|
|
78
50
|
Remove = "remove"
|
|
79
51
|
}
|
|
80
|
-
type QueueToSyncCallback = (action: SyncAction,
|
|
52
|
+
type QueueToSyncCallback = (action: SyncAction, stateKey: string, ...localIds: string[]) => void;
|
|
81
53
|
type SyncedStateCreator<TStore> = (set: any, get: any, queue: QueueToSyncCallback) => TStore;
|
|
54
|
+
interface PendingChange {
|
|
55
|
+
action: SyncAction;
|
|
56
|
+
stateKey: string;
|
|
57
|
+
localId: string;
|
|
58
|
+
id?: any;
|
|
59
|
+
version: number;
|
|
60
|
+
}
|
|
82
61
|
interface Logger {
|
|
83
62
|
debug: (...args: any[]) => void;
|
|
84
63
|
info: (...args: any[]) => void;
|
|
@@ -100,21 +79,12 @@ type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
|
|
|
100
79
|
* becomes a performance issue due to the size of the store, then libraries like dexie.js instead of Zustand would be a better solution and
|
|
101
80
|
* provide the syntax for high performance queries.
|
|
102
81
|
*
|
|
103
|
-
* Zync
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* - _pendingChanges: A queue of changes to be synced with the backend.
|
|
107
|
-
* - _lastPulled: A timestamp of the last successful pull from the backend.
|
|
108
|
-
* [Public]
|
|
109
|
-
* - syncState: Access to sync status, errors, and a method to enable/disable syncing.
|
|
110
|
-
* i.e. const syncState = useStore((state) => state.syncState);
|
|
82
|
+
* Zync's api is:
|
|
83
|
+
* - `syncState` object for reactive access to internal state i.e.: const syncState = useStore((state) => state.syncState);
|
|
84
|
+
* - `sync` object for control methods e.g.: useStore.sync.enable(true|false);
|
|
111
85
|
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* - Always pull (list) first each sync cycle to enable future conflict resolution. Currently last-write-wins, although any queued client changes
|
|
115
|
-
* for an item will prevent it being overwritten during a pull, even if the server has a newer version.
|
|
116
|
-
* - Then push queued changes in order (Create -> Update -> Remove).
|
|
117
|
-
* - Queue coalescing: (Create + Update*) => single Create (merged changes); (Create + Remove) => drop both; (Update + Update) => merge; (Update + Remove) => Remove.
|
|
86
|
+
* Synced objects will have the client only field `_localId`, which provides a stable identifier for the object.
|
|
87
|
+
* It is ideal for use as JSX keys.
|
|
118
88
|
*
|
|
119
89
|
* Synced objects are expected to have the following server fields:
|
|
120
90
|
*
|
|
@@ -131,8 +101,13 @@ type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
|
|
|
131
101
|
* - deleted: A boolean flag indicating whether the object has been deleted. This use of soft deletes or similar
|
|
132
102
|
* is how all clients are told about deletions during sync.
|
|
133
103
|
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
104
|
+
* Design principles:
|
|
105
|
+
*
|
|
106
|
+
* - Always pull (list) first each sync cycle to enable future conflict resolution. Currently last-write-wins, although any queued client changes
|
|
107
|
+
* for an item will prevent it being overwritten during a pull, even if the server has a newer version.
|
|
108
|
+
* - Then push queued changes in order (Create -> Update -> Remove).
|
|
109
|
+
* - Queue coalescing: (Create + Update*) => single Create (merged changes); (Create + Remove) => drop both; (Update + Update) => merge; (Update + Remove) => Remove.
|
|
110
|
+
*
|
|
136
111
|
*
|
|
137
112
|
* @param stateCreator - The function to create the initial state.
|
|
138
113
|
* @param persistOptions - Standard Zustand options for persisting the store.
|
|
@@ -141,7 +116,23 @@ type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
|
|
|
141
116
|
* If you don't provide a key for a state field, it won't be synced, but will be persisted as expected.
|
|
142
117
|
* @param syncOptions - Syncing options (Optional).
|
|
143
118
|
*/
|
|
144
|
-
declare function persistWithSync<TStore extends object>(stateCreator: SyncedStateCreator<TStore>, persistOptions: any, syncApi: Record<string, ApiFunctions>, syncOptions?: SyncOptions): StateCreator<TStore & SyncState, [], []>;
|
|
119
|
+
declare function persistWithSync<TStore extends object>(stateCreator: SyncedStateCreator<TStore>, persistOptions: any, syncApi: Record<string, ApiFunctions>, syncOptions?: SyncOptions): StateCreator<TStore & SyncState, [], [["zustand/persist", TStore & SyncState]]>;
|
|
120
|
+
declare function createStoreWithSync<TStore extends object>(stateCreator: SyncedStateCreator<TStore>, persistOptions: any, syncApi: Record<string, ApiFunctions>, syncOptions?: SyncOptions): UseStoreWithSync<TStore>;
|
|
121
|
+
type UseStoreWithSync<T> = UseBoundStore<StoreApi<T & SyncState> & {
|
|
122
|
+
sync: {
|
|
123
|
+
enable: (e: boolean) => void;
|
|
124
|
+
startFirstLoad: () => Promise<void>;
|
|
125
|
+
};
|
|
126
|
+
persist: {
|
|
127
|
+
setOptions: (options: Partial<PersistOptions<T, any, any>>) => void;
|
|
128
|
+
clearStorage: () => void;
|
|
129
|
+
rehydrate: () => Promise<void> | void;
|
|
130
|
+
hasHydrated: () => boolean;
|
|
131
|
+
onHydrate: (fn: (state: T) => void) => () => void;
|
|
132
|
+
onFinishHydration: (fn: (state: T) => void) => () => void;
|
|
133
|
+
getOptions: () => Partial<PersistOptions<T, any, any>>;
|
|
134
|
+
};
|
|
135
|
+
}>;
|
|
145
136
|
declare function nextLocalId(): string;
|
|
146
137
|
|
|
147
|
-
export { type ApiFunctions, type
|
|
138
|
+
export { type ApiFunctions, type LogLevel, type Logger, type MissingRemoteRecordDuringUpdateStrategy, type QueueToSyncCallback, SyncAction, type SyncState, type SyncedRecord, type UseStoreWithSync, createIndexedDBStorage, createStoreWithSync, nextLocalId, persistWithSync };
|