@drakkar.software/starfish-client 3.0.0-alpha.37 → 3.0.0-alpha.39

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/bindings/zustand.ts", "../../../../../node_modules/.pnpm/zustand@5.0.11_@types+react@19.2.14_immer@11.1.4_react@19.2.4_use-sync-external-store@1.6.0_react@19.2.4_/node_modules/zustand/esm/middleware.mjs", "../../src/client.ts", "../../src/types.ts", "../../src/fetch.ts", "../../src/sync.ts", "../../src/validate.ts", "../../src/broadcast.ts"],
4
- "sourcesContent": ["import { createStore, type StoreApi } from \"zustand/vanilla\"\nimport { useStore } from \"zustand\"\nimport {\n persist,\n subscribeWithSelector,\n createJSONStorage,\n type StateStorage,\n} from \"zustand/middleware\"\nimport type { DevtoolsOptions } from \"zustand/middleware\"\nimport { useEffect, useRef, useState, useCallback } from \"react\"\nimport type { Encryptor, PullResult } from \"@drakkar.software/starfish-protocol\"\nimport { StarfishClient } from \"../client.js\"\nimport { SyncManager } from \"../sync.js\"\nimport { classifyError } from \"../fetch.js\"\nimport { AppendLogCursor, type AppendElement } from \"../append-log.js\"\nimport { setupCrossTabSync, type BroadcastableStore } from \"../broadcast.js\"\nimport type { StarfishClientOptions, StarfishCapProvider, ConflictResolver, PullCache } from \"../types.js\"\nimport type { SyncLogger } from \"../logger.js\"\nimport type { Validator } from \"../validate.js\"\n\nexport interface StarfishState {\n data: Record<string, unknown>\n syncing: boolean\n online: boolean\n dirty: boolean\n error: string | null\n /** Last-known server hash, persisted alongside `data`/`dirty`. Restored into the bound SyncManager on hydration. */\n hash: string | null\n /**\n * True when the currently-shown `data` came from the offline read-through\n * cache (a cache-first {@link StarfishActions.seed} or a {@link StarfishActions.pull}\n * the client served from cache because the transport was unreachable) rather\n * than a live server response. A successful live pull/flush clears it. Use it\n * to drive an \"offline / showing last-synced data\" indicator.\n */\n stale: boolean\n}\n\nexport interface StarfishActions {\n pull: () => Promise<void>\n set: (modifier: (current: Record<string, unknown>) => Record<string, unknown>) => void\n /** Update data without marking dirty or triggering flush. Use for restoring pulled data into the store. */\n restore: (data: Record<string, unknown>) => void\n flush: () => Promise<void>\n setOnline: (online: boolean) => void\n /**\n * Cache-first paint: populate `data` from the client's offline read-through\n * cache (decrypting in memory for E2E collections) without touching the\n * network. A no-op when the client has no cache configured or there's no\n * (unexpired) entry. {@link useSyncInit} calls this once before the initial\n * pull; the live pull then supersedes the seeded snapshot.\n */\n seed: () => Promise<void>\n /**\n * Apply a freshly-fetched `PullResult` to the store WITHOUT firing a network\n * request. Decrypts in memory for E2E collections, conflict-merges against\n * any local optimistic writes (same logic as a live pull), and clears `stale`.\n *\n * Primarily called automatically by the binding when\n * {@link StarfishClientOptions.onRevalidated} fires (background revalidation\n * delivered a fresh snapshot after a 429/5xx hit or an SWR-on-read). Also\n * available for manual use when a caller holds a fresh `PullResult` it wants\n * to push into the store without a second network round-trip.\n */\n mergeResult: (result: PullResult) => Promise<void>\n}\n\nexport type StarfishStore = StarfishState & StarfishActions\n\nexport interface CreateStarfishStoreOptions {\n /** Unique name used as the persistence key (prefixed with `starfish-`) */\n name: string\n syncManager: SyncManager\n /** Pass `false` to disable persistence. Defaults to `localStorage` in browsers. */\n storage?: StateStorage | false\n /**\n * Wrap the store with Redux DevTools. Import `devtools` from `'zustand/middleware'`\n * and pass it directly \u2014 this keeps the import in your code, preventing\n * `import.meta.env` from being bundled in Metro/Hermes environments.\n *\n * @example\n * import { devtools } from 'zustand/middleware'\n * createStarfishStore({ devtools: (fn) => devtools(fn, { name: 'my-app' }) })\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n devtools?: (storeCreator: any) => any\n /** Pass `produce` from `immer` to enable draft-based mutations in `set()`. */\n produce?: <T>(base: T, recipe: (draft: T) => T | void) => T\n /**\n * Called when remote data arrives via `pull()` \u2014 **not** called for local `set()` writes.\n *\n * Use this to restore domain stores after a pull without worrying about feedback loops.\n * The callback fires **after** the Starfish store state is updated, so the store already\n * reflects the new data when this runs.\n *\n * Replaces the manual `isRestoring` flag pattern:\n * ```ts\n * createStarfishStore({\n * name: \"app\",\n * syncManager,\n * onRemoteUpdate: (data) => {\n * taskStore.setState({ tasks: data.tasks as Task[] })\n * settingsStore.setState({ settings: data.settings as Settings })\n * },\n * })\n * ```\n */\n onRemoteUpdate?: (data: Record<string, unknown>) => void\n /**\n * Auto re-attempt a failed flush with exponential backoff while the store\n * stays dirty + online. Omit to keep the current no-retry behavior.\n *\n * Defaults when the option is present: `maxRetries: 5`, `initialDelayMs: 500`,\n * `maxDelayMs: 30_000`. Backoff is `min(initial * 2^attempt, max) + jitter(100ms)`.\n * A successful flush resets the counter. Going offline cancels any pending retry.\n * `AbortError`s are never retried.\n */\n flushRetry?: {\n maxRetries?: number\n initialDelayMs?: number\n maxDelayMs?: number\n }\n}\n\n// Re-export DevtoolsOptions for convenience\nexport type { DevtoolsOptions }\n\nexport function createStarfishStore(\n options: CreateStarfishStoreOptions,\n): StoreApi<StarfishStore> {\n const { name, syncManager, storage } = options\n\n type NamedSet = (partial: Partial<StarfishStore>, replace?: boolean, action?: string) => void\n\n const storeCreator = (\n rawSet: StoreApi<StarfishStore>[\"setState\"],\n get: StoreApi<StarfishStore>[\"getState\"],\n ): StarfishStore => {\n const set = rawSet as NamedSet\n\n // Self-healing flush retry (only active when options.flushRetry is set).\n let retryTimer: ReturnType<typeof setTimeout> | undefined\n let retryAttempt = 0\n\n const scheduleFlushRetry = () => {\n const retryOpts = options.flushRetry\n if (!retryOpts) return\n const maxRetries = retryOpts.maxRetries ?? 5\n if (retryAttempt >= maxRetries) return\n const initialMs = retryOpts.initialDelayMs ?? 500\n const maxMs = retryOpts.maxDelayMs ?? 30_000\n // Jittered exponential backoff \u2014 mirrors the push-conflict recovery pattern.\n const delayMs = Math.min(initialMs * Math.pow(2, retryAttempt), maxMs) + Math.random() * 100\n retryAttempt++\n clearTimeout(retryTimer)\n retryTimer = setTimeout(() => {\n if (get().dirty && get().online && !get().syncing) {\n get().flush().catch(() => {})\n }\n }, delayMs)\n }\n\n const cancelFlushRetry = () => {\n clearTimeout(retryTimer)\n retryTimer = undefined\n retryAttempt = 0\n }\n\n // Shared commit for any remote snapshot landing in the store \u2014 used by both\n // `pull()` (live response) and `mergeResult()` (background revalidation).\n // Reads the already-updated SyncManager state, conflict-merges if dirty, then\n // writes to the store and notifies domain stores via `onRemoteUpdate`.\n const commitRemote = (label: string) => {\n const remote = syncManager.getData()\n // A remote snapshot would overwrite any local optimistic writes (they live\n // only in store.data, never in localData until a push succeeds). When dirty,\n // merge them back via the same resolver the push-conflict path uses so a\n // pull racing a send can't lose the write.\n const newData = get().dirty ? syncManager.resolve(get().data, remote) : remote\n set({ data: newData, syncing: false, hash: syncManager.getHash(), stale: syncManager.getLastPullFromCache() }, false, label)\n // The preserved write still needs to reach the server, and nothing else\n // will trigger that here. Kick a flush, gated on dirty + online exactly\n // like setOnline \u2014 a successful flush clears dirty, so there's no ping-pong.\n if (get().online && get().dirty) get().flush().catch(() => {})\n // Fire after state update so domain stores can read the updated Starfish state.\n // Calling set() inside onRemoteUpdate does NOT re-enter pull/mergeResult, no feedback loop.\n options.onRemoteUpdate?.(newData)\n }\n\n return {\n data: {},\n syncing: false,\n online: true,\n dirty: false,\n error: null,\n hash: null,\n stale: false,\n\n seed: async () => {\n try {\n const seeded = await syncManager.seedFromCache()\n if (!seeded) return\n // Don't clobber data a live pull/local set already produced (the seed is\n // a fast local read, but guard the race anyway): only seed while empty\n // and not yet dirty.\n if (get().dirty || Object.keys(get().data).length > 0) return\n set({ data: syncManager.getData(), hash: syncManager.getHash(), stale: true }, false, \"seed\")\n } catch {\n /* a cache miss / decrypt failure must never block the live pull */\n }\n },\n\n pull: async () => {\n // Gap D: when the store already shows stale (seeded or previously offline)\n // data, keep it on screen without a syncing flash \u2014 the user sees useful\n // content and the live pull supersedes it silently. Only raise the spinner\n // when the store is empty (fresh mount with no seed data).\n set(get().stale ? { error: null } : { syncing: true, error: null }, false, \"pull/start\")\n try {\n await syncManager.pull()\n commitRemote(\"pull/success\")\n } catch (err) {\n // Transport unreachable (offline / DNS / timeout): the persisted `data` is still\n // the last-synced snapshot, so keep it on screen and flag it stale rather than\n // surfacing an error. This replaces the client pull-cache's offline fallback \u2014\n // a persist-backed store is offline-first on its own, no client `cache` needed.\n if (classifyError(err) === \"network\") {\n set({ syncing: false, stale: true }, false, \"pull/offline\")\n return\n }\n set({ syncing: false, error: err instanceof Error ? err.message : String(err) }, false, \"pull/error\")\n }\n },\n\n mergeResult: async (result) => {\n await syncManager.ingest(result)\n commitRemote(\"merge/success\")\n },\n\n set: (modifier) => {\n try {\n const next = options.produce\n ? options.produce(get().data, modifier as (draft: Record<string, unknown>) => Record<string, unknown> | void)\n : modifier(get().data)\n // Fresh write: reset retry budget so this change gets a full set of attempts.\n retryAttempt = 0\n clearTimeout(retryTimer)\n set({ data: next, dirty: true, error: null }, false, \"set\")\n if (get().online) get().flush().catch(() => {})\n } catch (err) {\n set({ error: err instanceof Error ? err.message : String(err) }, false, \"set/error\")\n }\n },\n\n restore: (data) => {\n set({ data }, false, \"restore\")\n },\n\n flush: async () => {\n if (get().syncing || !get().dirty) return\n set({ syncing: true, error: null }, false, \"flush/start\")\n try {\n await syncManager.push(get().data)\n cancelFlushRetry()\n set({ data: syncManager.getData(), syncing: false, dirty: false, hash: syncManager.getHash(), stale: false }, false, \"flush/success\")\n } catch (err) {\n // AbortErrors (e.g. tab close, unmount) are not retriable.\n const isAbort = err instanceof Error && (err.name === \"AbortError\" ||\n (typeof DOMException !== \"undefined\" && err instanceof DOMException && err.name === \"AbortError\"))\n set({ syncing: false, error: err instanceof Error ? err.message : String(err) }, false, \"flush/error\")\n if (!isAbort) scheduleFlushRetry()\n }\n },\n\n setOnline: (online) => {\n set({ online }, false, \"setOnline\")\n if (online && get().dirty) {\n get().flush().catch(() => {})\n } else if (!online) {\n // Cancel pending retry \u2014 no point retrying while offline.\n cancelFlushRetry()\n }\n },\n }}\n\n const withPersist = storage === false\n ? storeCreator\n : persist(storeCreator, {\n name: `starfish-${name}`,\n storage: storage ? createJSONStorage(() => storage) : undefined,\n partialize: (state) => ({\n data: state.data,\n dirty: state.dirty,\n hash: state.hash,\n }),\n onRehydrateStorage: () => (state) => {\n // Only restore if the manager hasn't already received a hash from a live pull/push.\n // With async storage, pull() may resolve before hydration completes \u2014 the server's\n // hash always wins over the persisted one.\n if (state?.hash && syncManager.getHash() === null) syncManager.setHash(state.hash)\n },\n })\n\n const withSelector = subscribeWithSelector(withPersist)\n\n return createStore<StarfishStore>()(\n options.devtools ? options.devtools(withSelector) : withSelector,\n )\n}\n\n// \u2500\u2500 React hooks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Derived sync status for UI display. */\nexport type SyncStatus = \"synced\" | \"syncing\" | \"pending\" | \"error\" | \"offline\"\n\n/** Derive a single sync status from store state. */\nexport function deriveSyncStatus(state: StarfishState): SyncStatus {\n if (!state.online) return \"offline\"\n if (state.error) return \"error\"\n if (state.syncing) return \"syncing\"\n if (state.dirty) return \"pending\"\n return \"synced\"\n}\n\n/**\n * Aggregate multiple sync statuses into a single worst-case status.\n * Priority (worst first): error > syncing > pending > offline > synced.\n */\nexport function aggregateSyncStatus(statuses: SyncStatus[]): SyncStatus {\n if (statuses.includes(\"error\")) return \"error\"\n if (statuses.includes(\"syncing\")) return \"syncing\"\n if (statuses.includes(\"pending\")) return \"pending\"\n if (statuses.includes(\"offline\")) return \"offline\"\n return \"synced\"\n}\n\n/** Use the full Starfish store state and actions. */\nexport function useStarfish(store: StoreApi<StarfishStore>): StarfishStore {\n return useStore(store)\n}\n\n/** Subscribe to a fine-grained slice of Starfish store state. Avoids re-renders on unrelated field changes. */\nexport function useStarfishState<T>(\n store: StoreApi<StarfishStore>,\n selector: (state: StarfishState) => T,\n): T {\n return useStore(store, selector)\n}\n\n/** Use only the synced data, with an optional selector for fine-grained subscriptions. */\nexport function useStarfishData<T = Record<string, unknown>>(\n store: StoreApi<StarfishStore>,\n selector?: (data: Record<string, unknown>) => T,\n): T {\n return useStore(store, (state) =>\n selector ? selector(state.data) : (state.data as unknown as T),\n )\n}\n\n/** Use the derived sync status (synced | syncing | pending | error | offline). */\nexport function useSyncStatus(store: StoreApi<StarfishStore>): SyncStatus {\n return useStore(store, deriveSyncStatus)\n}\n\n/**\n * Subscribe to sync status changes outside of React.\n *\n * Framework-agnostic \u2014 works in React Native, Node.js, or anywhere hooks are unavailable.\n * The callback is invoked immediately with the current status and then on every change.\n *\n * ```ts\n * const unsub = subscribeSyncStatus(store, (status) => {\n * updateStatusBar(status)\n * })\n *\n * // Later, to stop listening:\n * unsub()\n * ```\n */\nexport function subscribeSyncStatus(\n store: StoreApi<StarfishStore>,\n callback: (status: SyncStatus) => void,\n): () => void {\n let prev = deriveSyncStatus(store.getState())\n callback(prev)\n return store.subscribe((state) => {\n const next = deriveSyncStatus(state)\n if (next !== prev) {\n prev = next\n callback(next)\n }\n })\n}\n\n/** Sets up cross-tab sync for a Starfish store. Cleans up on unmount. */\nexport function useCrossTabSync(\n store: StoreApi<StarfishStore>,\n name: string,\n): void {\n useEffect(() => {\n return setupCrossTabSync(store as unknown as BroadcastableStore, name)\n }, [store, name])\n}\n\n/** Binds browser online/offline events to the store's setOnline action. Cleans up on unmount. */\nexport function useConnectivity(store: StoreApi<StarfishStore>): void {\n useEffect(() => {\n const handleOnline = () => store.getState().setOnline(true)\n const handleOffline = () => store.getState().setOnline(false)\n\n window.addEventListener(\"online\", handleOnline)\n window.addEventListener(\"offline\", handleOffline)\n\n return () => {\n window.removeEventListener(\"online\", handleOnline)\n window.removeEventListener(\"offline\", handleOffline)\n }\n }, [store])\n}\n\n/** Returns a human-readable \"last synced\" label that updates every 5 seconds. */\nexport function useLastSynced(store: StoreApi<StarfishStore>): string {\n const lastSyncedAt = useRef<number | null>(null)\n const [label, setLabel] = useState(\"Never synced\")\n\n const computeLabel = useCallback(() => {\n if (lastSyncedAt.current === null) return \"Never synced\"\n const seconds = Math.floor((Date.now() - lastSyncedAt.current) / 1000)\n if (seconds < 10) return \"Just now\"\n if (seconds < 60) return `${seconds}s ago`\n return `${Math.floor(seconds / 60)}m ago`\n }, [])\n\n // Track sync completion\n useEffect(() => {\n let prevSyncing = store.getState().syncing\n const unsub = store.subscribe((state) => {\n if (prevSyncing && !state.syncing && !state.error) {\n lastSyncedAt.current = Date.now()\n setLabel(computeLabel())\n }\n prevSyncing = state.syncing\n })\n return unsub\n }, [store, computeLabel])\n\n // Update label periodically \u2014 skip when the tab is hidden\n useEffect(() => {\n const timer = setInterval(() => {\n if (!document.hidden) setLabel(computeLabel())\n }, 5000)\n return () => clearInterval(timer)\n }, [computeLabel])\n\n return label\n}\n\n// \u2500\u2500 SyncInitializer hook \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface SyncInitConfig {\n serverUrl: string\n /**\n * Optional server namespace, forwarded to the underlying {@link StarfishClient}\n * so `pullPath`/`pushPath` are rewritten to `/v1/<namespace>/\u2026` (signed AND sent).\n * Leave unset for a root-mounted server. Pass the bare name (e.g. `\"octochat\"`),\n * not `/v1/octochat` \u2014 the `/v1/` is added by the client.\n */\n namespace?: string\n capProvider?: StarfishCapProvider\n pullPath: string\n pushPath: string\n /** Pre-built encryptor for E2E collections (build via `createKeyringEncryptor`). */\n encryptor?: Encryptor\n onConflict?: ConflictResolver\n /** Called when pulled data arrives. Use to restore domain stores. */\n onData?: (data: Record<string, unknown>) => void\n storeName?: string\n storage?: StateStorage | false\n fetch?: typeof globalThis.fetch\n /**\n * Offline-first read-through cache for the underlying {@link StarfishClient}\n * (see {@link StarfishClientOptions.cache}). When set, the store seeds from the\n * last-synced ciphertext on creation (cache-first paint, decrypted in memory)\n * and the live pull falls back to it when the transport is unreachable; the\n * store's `stale` flag reflects whether the shown data is from cache.\n */\n cache?: PullCache\n /** Max age (ms) for {@link cache} entries; see {@link StarfishClientOptions.cacheMaxAgeMs}. */\n cacheMaxAgeMs?: number\n /**\n * HTTP status codes for which pulls fall back to the last-cached snapshot rather than\n * throwing \u2014 stale-while-revalidate for transient server failures.\n * See {@link StarfishClientOptions.cacheFallbackStatuses}.\n * Recommended set for offline-first apps: `[429, 500, 502, 503, 504]`.\n */\n cacheFallbackStatuses?: number[]\n /**\n * Called after a background revalidation following a {@link cacheFallbackStatuses} hit:\n * the server returned a live response and the fresh snapshot has been written through.\n * See {@link StarfishClientOptions.onRevalidated}.\n */\n onRevalidated?: StarfishClientOptions[\"onRevalidated\"]\n logger?: SyncLogger\n validate?: Validator\n}\n\n/**\n * React hook that manages the full Starfish sync lifecycle.\n *\n * Creates StarfishClient \u2192 SyncManager \u2192 Zustand store, pulls on mount,\n * calls `onData` when remote data arrives, and tears down on unmount or\n * config change.\n *\n * Pass `null` to disable sync (returns `null`).\n */\nexport function useSyncInit(config: SyncInitConfig | null): StoreApi<StarfishStore> | null {\n const [store, setStore] = useState<StoreApi<StarfishStore> | null>(null)\n const onDataRef = useRef(config?.onData)\n onDataRef.current = config?.onData\n\n useEffect(() => {\n if (!config) return\n\n const client = new StarfishClient({\n baseUrl: config.serverUrl,\n namespace: config.namespace,\n capProvider: config.capProvider,\n fetch: config.fetch,\n cache: config.cache,\n cacheMaxAgeMs: config.cacheMaxAgeMs,\n cacheFallbackStatuses: config.cacheFallbackStatuses,\n // Auto-merge: when a background revalidation delivers a fresh snapshot,\n // push it into the store so the UI heals without waiting for the next pull.\n // newStore is referenced by closure \u2014 safe because onRevalidated only fires\n // asynchronously, well after the store is created below.\n onRevalidated: (path, result) => {\n newStore.getState().mergeResult(result).catch(() => {})\n config.onRevalidated?.(path, result)\n },\n })\n\n const syncManager = new SyncManager({\n client,\n pullPath: config.pullPath,\n pushPath: config.pushPath,\n encryptor: config.encryptor,\n onConflict: config.onConflict,\n logger: config.logger,\n validate: config.validate,\n })\n\n const newStore = createStarfishStore({\n name: config.storeName ?? \"sync\",\n syncManager,\n storage: config.storage,\n // onRemoteUpdate fires only for pull() results, never for local set() writes \u2014\n // so no isRestoring flag is needed.\n onRemoteUpdate: (data) => {\n try {\n onDataRef.current?.(data)\n } catch (err) {\n newStore.setState({\n error: `onData failed: ${err instanceof Error ? err.message : String(err)}`,\n })\n }\n },\n })\n\n setStore(newStore)\n\n // Cache-first paint, then revalidate: seed synced data from the offline\n // cache (a fast local read; no-op without a cache) BEFORE the initial pull,\n // so the UI shows last-synced data immediately and the live pull supersedes\n // it. Errors in either are captured into state \u2014 never thrown here.\n newStore.getState().seed().finally(() => {\n newStore.getState().pull().catch(() => {})\n })\n\n return () => {\n setStore(null)\n }\n // Intentionally depend on serializable config values, not the object reference\n // eslint-disable-next-line react-doctor/exhaustive-deps\n }, [\n config?.serverUrl,\n config?.pullPath,\n config?.pushPath,\n config?.encryptor,\n config?.storeName,\n ])\n\n return config ? store : null\n}\n\n// \u2500\u2500 Shared sync-store registry \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Problem: `useSyncInit` mints a brand-new StarfishClient + SyncManager + zustand\n// store on every React mount \u2014 `storeName` is only a label, not a dedup key. When\n// multiple components mount the same logical document (e.g. a shared object index),\n// each gets its own store and each fires its own pull(), producing a request storm.\n//\n// Solution: a module-level Map keyed by `storeName`. The first acquire constructs\n// and seeds/pulls the store; subsequent acquires share the live instance (refCount++).\n// Release decrements; on 0 the entry is dropped and GC'd, mirroring useSyncInit's\n// own teardown (which just drops its local store ref \u2014 no explicit destroy needed).\n//\n// When to use useSharedSyncStore vs useSyncInit:\n// - useSharedSyncStore: same document, multiple components \u2014 guaranteed one pull,\n// shared live state, correct reactivity for every subscriber.\n// - useSyncInit: unique document per mount, or when you need per-instance onData.\n\n/**\n * Config for a shared sync store \u2014 identical to {@link SyncInitConfig} EXCEPT:\n * - `onData` is omitted: it is not safe to fan out a single `onRemoteUpdate`\n * callback to multiple independent subscribers. Consumers should instead\n * subscribe to the returned store via `store.subscribe(...)`.\n * - `storeName` is REQUIRED: it is the registry key, not an optional label.\n */\nexport type SharedSyncConfig = Omit<SyncInitConfig, \"onData\" | \"storeName\"> & {\n /** Registry key AND store persistence label. Required; there is no default. */\n storeName: string\n}\n\ninterface _RegistryEntry {\n store: StoreApi<StarfishStore>\n refCount: number\n}\n\nconst _syncStoreRegistry = new Map<string, _RegistryEntry>()\n\n/**\n * Return (or create) the shared zustand store for `config.storeName`.\n *\n * On the **first** acquire, constructs `StarfishClient` \u2192 `SyncManager` \u2192 store\n * (forwarding all config fields, including `cacheFallbackStatuses` and `onRevalidated`\n * for native stale-while-revalidate), then fires `seed().finally(pull())`. On every\n * subsequent acquire of the same `storeName`, the existing store is returned \u2014 **no**\n * new pull fires.\n *\n * Always pair with {@link releaseSyncStore}. Call {@link clearSyncStoreRegistry}\n * on account switch / sign-out.\n */\nexport function acquireSyncStore(config: SharedSyncConfig): StoreApi<StarfishStore> {\n const existing = _syncStoreRegistry.get(config.storeName)\n if (existing) {\n existing.refCount += 1\n return existing.store\n }\n\n const client = new StarfishClient({\n baseUrl: config.serverUrl,\n namespace: config.namespace,\n capProvider: config.capProvider,\n fetch: config.fetch,\n cache: config.cache,\n cacheMaxAgeMs: config.cacheMaxAgeMs,\n cacheFallbackStatuses: config.cacheFallbackStatuses,\n // Auto-merge: push fresh revalidated snapshots into the store.\n // store is referenced by closure \u2014 safe because onRevalidated only fires\n // asynchronously, well after the store is created below.\n onRevalidated: (path, result) => {\n store.getState().mergeResult(result).catch(() => {})\n config.onRevalidated?.(path, result)\n },\n })\n const syncManager = new SyncManager({\n client,\n pullPath: config.pullPath,\n pushPath: config.pushPath,\n encryptor: config.encryptor,\n onConflict: config.onConflict,\n logger: config.logger,\n validate: config.validate,\n })\n const store = createStarfishStore({\n name: config.storeName,\n syncManager,\n storage: config.storage,\n // No onRemoteUpdate: consumers subscribe via store.subscribe() \u2014 see module comment.\n })\n\n const entry: _RegistryEntry = { store, refCount: 1 }\n _syncStoreRegistry.set(config.storeName, entry)\n\n // Cache-first paint then one network pull, fire-and-forget:\n // - errors must NOT evict the entry (the store stays usable; SWR retries in background)\n // - identity guard stops a stale pull firing after a registry clear (account switch)\n store.getState().seed().finally(() => {\n if (_syncStoreRegistry.get(config.storeName) === entry) {\n store.getState().pull().catch(() => {})\n }\n })\n\n return store\n}\n\n/**\n * Release a previously acquired store. Decrements the refCount; on 0 the entry is\n * evicted \u2014 the store, client, and sync manager are dropped and GC'd (mirrors\n * `useSyncInit`'s own teardown, which simply drops the local store reference).\n */\nexport function releaseSyncStore(storeName: string): void {\n const entry = _syncStoreRegistry.get(storeName)\n if (!entry) return\n entry.refCount -= 1\n if (entry.refCount <= 0) _syncStoreRegistry.delete(storeName)\n}\n\n/**\n * Clear all registry entries.\n *\n * Call on account switch or sign-out alongside any other per-session cache clears.\n * An identity guard inside {@link acquireSyncStore} prevents any in-flight pull from\n * firing against the old session's cap after this is called.\n */\nexport function clearSyncStoreRegistry(): void {\n _syncStoreRegistry.clear()\n}\n\n/**\n * React hook that returns (or creates) the shared zustand store for\n * `config.storeName` \u2014 a drop-in replacement for {@link useSyncInit} when the\n * same logical document is consumed from multiple components.\n *\n * **Key design decision \u2014 effect deps include only `storeName`:** config identity\n * churn (fresh `capProvider`/`encryptor` refs per render) is intentionally ignored.\n * For a given `(user, space)` the cap and keyring are functionally equivalent across\n * refs, and no `onData` fan-out is needed, so the shared store never needs to rebuild\n * on churn. The `configRef` pattern ensures the latest config values are captured at\n * acquire-time without re-running the effect.\n *\n * Pass `null` to disable sync (returns `null`).\n */\nexport function useSharedSyncStore(\n config: SharedSyncConfig | null,\n): StoreApi<StarfishStore> | null {\n const [store, setStore] = useState<StoreApi<StarfishStore> | null>(null)\n const storeName = config?.storeName ?? null\n\n // Hold the latest config in a ref so the effect (keyed on storeName only) reads\n // current values at acquire-time without re-running on identity churn.\n const configRef = useRef<SharedSyncConfig | null>(config)\n configRef.current = config\n\n useEffect(() => {\n if (!storeName) return\n const acquired = acquireSyncStore(configRef.current!)\n setStore(acquired)\n return () => {\n releaseSyncStore(storeName)\n setStore(null)\n }\n // Intentionally depend only on storeName \u2014 see JSDoc above.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [storeName])\n\n return storeName ? store : null\n}\n\n\n//\n// The reactive counterpart for an append-only collection, backed by an\n// `AppendLogCursor` instead of a `SyncManager`. A log only grows, so the\n// store is read-only \u2014 there is no `set`/`flush`/`dirty`/conflict surface,\n// and no `persist` middleware: the cursor owns the items + checkpoint, so\n// persist by reading `getItems()` and rehydrate by constructing the cursor\n// with `initialItems` (see `AppendLogCursor`).\n//\n// The store assumes it is the SOLE driver of its cursor: it seeds `items` from\n// `cursor.getItems()` at construction and updates only via its own `pull()`.\n// Don't also call `cursor.pull()` directly on the same cursor, or the store's\n// `items`/`checkpoint` will go stale.\n\nexport interface StarfishLogState {\n /** The full accumulated log, newest appended last. */\n items: AppendElement[]\n /** A `pull()` is in flight. */\n loading: boolean\n online: boolean\n error: string | null\n /** The cursor's checkpoint (max `ts` held). */\n checkpoint: number\n}\n\nexport interface StarfishLogActions {\n /** Pull elements newer than the checkpoint, append them, and return the new\n * batch. Errors are captured into `error` (mirroring the SyncManager store). */\n pull: () => Promise<AppendElement[]>\n setOnline: (online: boolean) => void\n}\n\nexport type StarfishLogStore = StarfishLogState & StarfishLogActions\n\nexport interface CreateStarfishLogOptions {\n cursor: AppendLogCursor\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n devtools?: (storeCreator: any) => any\n}\n\nexport function createStarfishLog(\n options: CreateStarfishLogOptions,\n): StoreApi<StarfishLogStore> {\n const { cursor } = options\n\n type NamedSet = (partial: Partial<StarfishLogStore>, replace?: boolean, action?: string) => void\n\n const storeCreator = (\n rawSet: StoreApi<StarfishLogStore>[\"setState\"],\n get: StoreApi<StarfishLogStore>[\"getState\"],\n ): StarfishLogStore => {\n const set = rawSet as NamedSet\n return {\n // Seed from the cursor so a warm-started cursor's items show immediately.\n items: cursor.getItems(),\n loading: false,\n online: true,\n error: null,\n checkpoint: cursor.getCheckpoint(),\n\n pull: async () => {\n if (get().loading) return []\n set({ loading: true, error: null }, false, \"log/pull/start\")\n try {\n const batch = await cursor.pull()\n set(\n { items: cursor.getItems(), checkpoint: cursor.getCheckpoint(), loading: false },\n false,\n \"log/pull/success\",\n )\n return batch\n } catch (err) {\n set({ loading: false, error: err instanceof Error ? err.message : String(err) }, false, \"log/pull/error\")\n return []\n }\n },\n\n setOnline: (online) => {\n set({ online }, false, \"log/setOnline\")\n },\n }\n }\n\n const withSelector = subscribeWithSelector(storeCreator)\n return createStore<StarfishLogStore>()(\n options.devtools ? options.devtools(withSelector) : withSelector,\n )\n}\n\n/** Derived status for an append-log store. */\nexport type LogStatus = \"idle\" | \"loading\" | \"error\" | \"offline\"\n\n/** Derive a single status from log store state. */\nexport function deriveLogStatus(state: StarfishLogState): LogStatus {\n if (!state.online) return \"offline\"\n if (state.error) return \"error\"\n if (state.loading) return \"loading\"\n return \"idle\"\n}\n\n/** Use the full append-log store state and actions. */\nexport function useStarfishLog(store: StoreApi<StarfishLogStore>): StarfishLogStore {\n return useStore(store)\n}\n\n/** Use only the accumulated items, with an optional selector for fine-grained subscriptions. */\nexport function useStarfishLogItems<T = AppendElement[]>(\n store: StoreApi<StarfishLogStore>,\n selector?: (items: AppendElement[]) => T,\n): T {\n return useStore(store, (state) =>\n selector ? selector(state.items) : (state.items as unknown as T),\n )\n}\n\n/** Use the derived log status (idle | loading | error | offline). */\nexport function useLogStatus(store: StoreApi<StarfishLogStore>): LogStatus {\n return useStore(store, deriveLogStatus)\n}\n\n/** Subscribe to log status changes outside of React. Invoked immediately with the\n * current status, then on every change. Returns an unsubscribe function. */\nexport function subscribeLogStatus(\n store: StoreApi<StarfishLogStore>,\n callback: (status: LogStatus) => void,\n): () => void {\n let prev = deriveLogStatus(store.getState())\n callback(prev)\n return store.subscribe((state) => {\n const next = deriveLogStatus(state)\n if (next !== prev) {\n prev = next\n callback(next)\n }\n })\n}\n\n/** Binds browser online/offline events to the log store's setOnline action. Cleans up on unmount. */\nexport function useLogConnectivity(store: StoreApi<StarfishLogStore>): void {\n useEffect(() => {\n const handleOnline = () => store.getState().setOnline(true)\n const handleOffline = () => store.getState().setOnline(false)\n window.addEventListener(\"online\", handleOnline)\n window.addEventListener(\"offline\", handleOffline)\n return () => {\n window.removeEventListener(\"online\", handleOnline)\n window.removeEventListener(\"offline\", handleOffline)\n }\n }, [store])\n}\n", "const reduxImpl = (reducer, initial) => (set, _get, api) => {\n api.dispatch = (action) => {\n set((state) => reducer(state, action), false, action);\n return action;\n };\n api.dispatchFromDevtools = true;\n return { dispatch: (...args) => api.dispatch(...args), ...initial };\n};\nconst redux = reduxImpl;\n\nconst shouldDispatchFromDevtools = (api) => !!api.dispatchFromDevtools && typeof api.dispatch === \"function\";\nconst trackedConnections = /* @__PURE__ */ new Map();\nconst getTrackedConnectionState = (name) => {\n const api = trackedConnections.get(name);\n if (!api) return {};\n return Object.fromEntries(\n Object.entries(api.stores).map(([key, api2]) => [key, api2.getState()])\n );\n};\nconst extractConnectionInformation = (store, extensionConnector, options) => {\n if (store === void 0) {\n return {\n type: \"untracked\",\n connection: extensionConnector.connect(options)\n };\n }\n const existingConnection = trackedConnections.get(options.name);\n if (existingConnection) {\n return { type: \"tracked\", store, ...existingConnection };\n }\n const newConnection = {\n connection: extensionConnector.connect(options),\n stores: {}\n };\n trackedConnections.set(options.name, newConnection);\n return { type: \"tracked\", store, ...newConnection };\n};\nconst removeStoreFromTrackedConnections = (name, store) => {\n if (store === void 0) return;\n const connectionInfo = trackedConnections.get(name);\n if (!connectionInfo) return;\n delete connectionInfo.stores[store];\n if (Object.keys(connectionInfo.stores).length === 0) {\n trackedConnections.delete(name);\n }\n};\nconst findCallerName = (stack) => {\n var _a, _b;\n if (!stack) return void 0;\n const traceLines = stack.split(\"\\n\");\n const apiSetStateLineIndex = traceLines.findIndex(\n (traceLine) => traceLine.includes(\"api.setState\")\n );\n if (apiSetStateLineIndex < 0) return void 0;\n const callerLine = ((_a = traceLines[apiSetStateLineIndex + 1]) == null ? void 0 : _a.trim()) || \"\";\n return (_b = /.+ (.+) .+/.exec(callerLine)) == null ? void 0 : _b[1];\n};\nconst devtoolsImpl = (fn, devtoolsOptions = {}) => (set, get, api) => {\n const { enabled, anonymousActionType, store, ...options } = devtoolsOptions;\n let extensionConnector;\n try {\n extensionConnector = (enabled != null ? enabled : (import.meta.env ? import.meta.env.MODE : void 0) !== \"production\") && window.__REDUX_DEVTOOLS_EXTENSION__;\n } catch (e) {\n }\n if (!extensionConnector) {\n return fn(set, get, api);\n }\n const { connection, ...connectionInformation } = extractConnectionInformation(store, extensionConnector, options);\n let isRecording = true;\n api.setState = ((state, replace, nameOrAction) => {\n const r = set(state, replace);\n if (!isRecording) return r;\n const action = nameOrAction === void 0 ? {\n type: anonymousActionType || findCallerName(new Error().stack) || \"anonymous\"\n } : typeof nameOrAction === \"string\" ? { type: nameOrAction } : nameOrAction;\n if (store === void 0) {\n connection == null ? void 0 : connection.send(action, get());\n return r;\n }\n connection == null ? void 0 : connection.send(\n {\n ...action,\n type: `${store}/${action.type}`\n },\n {\n ...getTrackedConnectionState(options.name),\n [store]: api.getState()\n }\n );\n return r;\n });\n api.devtools = {\n cleanup: () => {\n if (connection && typeof connection.unsubscribe === \"function\") {\n connection.unsubscribe();\n }\n removeStoreFromTrackedConnections(options.name, store);\n }\n };\n const setStateFromDevtools = (...a) => {\n const originalIsRecording = isRecording;\n isRecording = false;\n set(...a);\n isRecording = originalIsRecording;\n };\n const initialState = fn(api.setState, get, api);\n if (connectionInformation.type === \"untracked\") {\n connection == null ? void 0 : connection.init(initialState);\n } else {\n connectionInformation.stores[connectionInformation.store] = api;\n connection == null ? void 0 : connection.init(\n Object.fromEntries(\n Object.entries(connectionInformation.stores).map(([key, store2]) => [\n key,\n key === connectionInformation.store ? initialState : store2.getState()\n ])\n )\n );\n }\n if (shouldDispatchFromDevtools(api)) {\n let didWarnAboutReservedActionType = false;\n const originalDispatch = api.dispatch;\n api.dispatch = (...args) => {\n if ((import.meta.env ? import.meta.env.MODE : void 0) !== \"production\" && args[0].type === \"__setState\" && !didWarnAboutReservedActionType) {\n console.warn(\n '[zustand devtools middleware] \"__setState\" action type is reserved to set state from the devtools. Avoid using it.'\n );\n didWarnAboutReservedActionType = true;\n }\n originalDispatch(...args);\n };\n }\n connection.subscribe((message) => {\n var _a;\n switch (message.type) {\n case \"ACTION\":\n if (typeof message.payload !== \"string\") {\n console.error(\n \"[zustand devtools middleware] Unsupported action format\"\n );\n return;\n }\n return parseJsonThen(\n message.payload,\n (action) => {\n if (action.type === \"__setState\") {\n if (store === void 0) {\n setStateFromDevtools(action.state);\n return;\n }\n if (Object.keys(action.state).length !== 1) {\n console.error(\n `\n [zustand devtools middleware] Unsupported __setState action format.\n When using 'store' option in devtools(), the 'state' should have only one key, which is a value of 'store' that was passed in devtools(),\n and value of this only key should be a state object. Example: { \"type\": \"__setState\", \"state\": { \"abc123Store\": { \"foo\": \"bar\" } } }\n `\n );\n }\n const stateFromDevtools = action.state[store];\n if (stateFromDevtools === void 0 || stateFromDevtools === null) {\n return;\n }\n if (JSON.stringify(api.getState()) !== JSON.stringify(stateFromDevtools)) {\n setStateFromDevtools(stateFromDevtools);\n }\n return;\n }\n if (shouldDispatchFromDevtools(api)) {\n api.dispatch(action);\n }\n }\n );\n case \"DISPATCH\":\n switch (message.payload.type) {\n case \"RESET\":\n setStateFromDevtools(initialState);\n if (store === void 0) {\n return connection == null ? void 0 : connection.init(api.getState());\n }\n return connection == null ? void 0 : connection.init(getTrackedConnectionState(options.name));\n case \"COMMIT\":\n if (store === void 0) {\n connection == null ? void 0 : connection.init(api.getState());\n return;\n }\n return connection == null ? void 0 : connection.init(getTrackedConnectionState(options.name));\n case \"ROLLBACK\":\n return parseJsonThen(message.state, (state) => {\n if (store === void 0) {\n setStateFromDevtools(state);\n connection == null ? void 0 : connection.init(api.getState());\n return;\n }\n setStateFromDevtools(state[store]);\n connection == null ? void 0 : connection.init(getTrackedConnectionState(options.name));\n });\n case \"JUMP_TO_STATE\":\n case \"JUMP_TO_ACTION\":\n return parseJsonThen(message.state, (state) => {\n if (store === void 0) {\n setStateFromDevtools(state);\n return;\n }\n if (JSON.stringify(api.getState()) !== JSON.stringify(state[store])) {\n setStateFromDevtools(state[store]);\n }\n });\n case \"IMPORT_STATE\": {\n const { nextLiftedState } = message.payload;\n const lastComputedState = (_a = nextLiftedState.computedStates.slice(-1)[0]) == null ? void 0 : _a.state;\n if (!lastComputedState) return;\n if (store === void 0) {\n setStateFromDevtools(lastComputedState);\n } else {\n setStateFromDevtools(lastComputedState[store]);\n }\n connection == null ? void 0 : connection.send(\n null,\n // FIXME no-any\n nextLiftedState\n );\n return;\n }\n case \"PAUSE_RECORDING\":\n return isRecording = !isRecording;\n }\n return;\n }\n });\n return initialState;\n};\nconst devtools = devtoolsImpl;\nconst parseJsonThen = (stringified, fn) => {\n let parsed;\n try {\n parsed = JSON.parse(stringified);\n } catch (e) {\n console.error(\n \"[zustand devtools middleware] Could not parse the received json\",\n e\n );\n }\n if (parsed !== void 0) fn(parsed);\n};\n\nconst subscribeWithSelectorImpl = (fn) => (set, get, api) => {\n const origSubscribe = api.subscribe;\n api.subscribe = ((selector, optListener, options) => {\n let listener = selector;\n if (optListener) {\n const equalityFn = (options == null ? void 0 : options.equalityFn) || Object.is;\n let currentSlice = selector(api.getState());\n listener = (state) => {\n const nextSlice = selector(state);\n if (!equalityFn(currentSlice, nextSlice)) {\n const previousSlice = currentSlice;\n optListener(currentSlice = nextSlice, previousSlice);\n }\n };\n if (options == null ? void 0 : options.fireImmediately) {\n optListener(currentSlice, currentSlice);\n }\n }\n return origSubscribe(listener);\n });\n const initialState = fn(set, get, api);\n return initialState;\n};\nconst subscribeWithSelector = subscribeWithSelectorImpl;\n\nfunction combine(initialState, create) {\n return (...args) => Object.assign({}, initialState, create(...args));\n}\n\nfunction createJSONStorage(getStorage, options) {\n let storage;\n try {\n storage = getStorage();\n } catch (e) {\n return;\n }\n const persistStorage = {\n getItem: (name) => {\n var _a;\n const parse = (str2) => {\n if (str2 === null) {\n return null;\n }\n return JSON.parse(str2, options == null ? void 0 : options.reviver);\n };\n const str = (_a = storage.getItem(name)) != null ? _a : null;\n if (str instanceof Promise) {\n return str.then(parse);\n }\n return parse(str);\n },\n setItem: (name, newValue) => storage.setItem(name, JSON.stringify(newValue, options == null ? void 0 : options.replacer)),\n removeItem: (name) => storage.removeItem(name)\n };\n return persistStorage;\n}\nconst toThenable = (fn) => (input) => {\n try {\n const result = fn(input);\n if (result instanceof Promise) {\n return result;\n }\n return {\n then(onFulfilled) {\n return toThenable(onFulfilled)(result);\n },\n catch(_onRejected) {\n return this;\n }\n };\n } catch (e) {\n return {\n then(_onFulfilled) {\n return this;\n },\n catch(onRejected) {\n return toThenable(onRejected)(e);\n }\n };\n }\n};\nconst persistImpl = (config, baseOptions) => (set, get, api) => {\n let options = {\n storage: createJSONStorage(() => window.localStorage),\n partialize: (state) => state,\n version: 0,\n merge: (persistedState, currentState) => ({\n ...currentState,\n ...persistedState\n }),\n ...baseOptions\n };\n let hasHydrated = false;\n let hydrationVersion = 0;\n const hydrationListeners = /* @__PURE__ */ new Set();\n const finishHydrationListeners = /* @__PURE__ */ new Set();\n let storage = options.storage;\n if (!storage) {\n return config(\n (...args) => {\n console.warn(\n `[zustand persist middleware] Unable to update item '${options.name}', the given storage is currently unavailable.`\n );\n set(...args);\n },\n get,\n api\n );\n }\n const setItem = () => {\n const state = options.partialize({ ...get() });\n return storage.setItem(options.name, {\n state,\n version: options.version\n });\n };\n const savedSetState = api.setState;\n api.setState = (state, replace) => {\n savedSetState(state, replace);\n return setItem();\n };\n const configResult = config(\n (...args) => {\n set(...args);\n return setItem();\n },\n get,\n api\n );\n api.getInitialState = () => configResult;\n let stateFromStorage;\n const hydrate = () => {\n var _a, _b;\n if (!storage) return;\n const currentVersion = ++hydrationVersion;\n hasHydrated = false;\n hydrationListeners.forEach((cb) => {\n var _a2;\n return cb((_a2 = get()) != null ? _a2 : configResult);\n });\n const postRehydrationCallback = ((_b = options.onRehydrateStorage) == null ? void 0 : _b.call(options, (_a = get()) != null ? _a : configResult)) || void 0;\n return toThenable(storage.getItem.bind(storage))(options.name).then((deserializedStorageValue) => {\n if (deserializedStorageValue) {\n if (typeof deserializedStorageValue.version === \"number\" && deserializedStorageValue.version !== options.version) {\n if (options.migrate) {\n const migration = options.migrate(\n deserializedStorageValue.state,\n deserializedStorageValue.version\n );\n if (migration instanceof Promise) {\n return migration.then((result) => [true, result]);\n }\n return [true, migration];\n }\n console.error(\n `State loaded from storage couldn't be migrated since no migrate function was provided`\n );\n } else {\n return [false, deserializedStorageValue.state];\n }\n }\n return [false, void 0];\n }).then((migrationResult) => {\n var _a2;\n if (currentVersion !== hydrationVersion) {\n return;\n }\n const [migrated, migratedState] = migrationResult;\n stateFromStorage = options.merge(\n migratedState,\n (_a2 = get()) != null ? _a2 : configResult\n );\n set(stateFromStorage, true);\n if (migrated) {\n return setItem();\n }\n }).then(() => {\n if (currentVersion !== hydrationVersion) {\n return;\n }\n postRehydrationCallback == null ? void 0 : postRehydrationCallback(stateFromStorage, void 0);\n stateFromStorage = get();\n hasHydrated = true;\n finishHydrationListeners.forEach((cb) => cb(stateFromStorage));\n }).catch((e) => {\n if (currentVersion !== hydrationVersion) {\n return;\n }\n postRehydrationCallback == null ? void 0 : postRehydrationCallback(void 0, e);\n });\n };\n api.persist = {\n setOptions: (newOptions) => {\n options = {\n ...options,\n ...newOptions\n };\n if (newOptions.storage) {\n storage = newOptions.storage;\n }\n },\n clearStorage: () => {\n storage == null ? void 0 : storage.removeItem(options.name);\n },\n getOptions: () => options,\n rehydrate: () => hydrate(),\n hasHydrated: () => hasHydrated,\n onHydrate: (cb) => {\n hydrationListeners.add(cb);\n return () => {\n hydrationListeners.delete(cb);\n };\n },\n onFinishHydration: (cb) => {\n finishHydrationListeners.add(cb);\n return () => {\n finishHydrationListeners.delete(cb);\n };\n }\n };\n if (!options.skipHydration) {\n hydrate();\n }\n return stateFromStorage || configResult;\n};\nconst persist = persistImpl;\n\nfunction ssrSafe(config, isSSR = typeof window === \"undefined\") {\n return (set, get, api) => {\n if (!isSSR) {\n return config(set, get, api);\n }\n const ssrSet = () => {\n throw new Error(\"Cannot set state of Zustand store in SSR\");\n };\n api.setState = ssrSet;\n return config(ssrSet, get, api);\n };\n}\n\nexport { combine, createJSONStorage, devtools, persist, redux, subscribeWithSelector, ssrSafe as unstable_ssrSafe };\n", "import type { PullResult, PushSuccess } from \"@drakkar.software/starfish-protocol\"\nimport {\n AUTHOR_PUBKEY_FIELD,\n AUTHOR_SIGNATURE_FIELD,\n DATA_FIELD,\n TS_FIELD,\n BASE_HASH_FIELD,\n PUSH_PATH_PREFIX,\n HEADER_AUTHORIZATION,\n HEADER_SIG,\n HEADER_TS,\n HEADER_NONCE,\n HEADER_PUB,\n HEADER_CONTENT_TYPE,\n HEADER_ACCEPT,\n signAppendAuthor,\n signRequest,\n stableStringify,\n type AppendAuthor,\n type SignableMethod,\n type SignableRequest,\n} from \"@drakkar.software/starfish-protocol\"\nimport type {\n StarfishClientOptions,\n StarfishCapProvider,\n PullCache,\n} from \"./types.js\"\nimport { AppendHttpError, ConflictError, StarfishHttpError } from \"./types.js\"\nimport { parseRetryAfterMs } from \"./fetch.js\"\n\nconst APPEND_DEFAULT_FIELD = \"items\"\nconst MAX_REVALIDATE_ATTEMPTS = 5\nconst REVALIDATE_INITIAL_DELAY_MS = 1_000\nconst REVALIDATE_MAX_DELAY_MS = 30_000\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/**\n * Shape persisted in a {@link PullCache} for one document: the raw server\n * `PullResult` fields. For E2E collections `data` is the sealed ciphertext.\n */\ninterface CachedPull {\n data: Record<string, unknown>\n hash: string\n timestamp: number\n /** Wall-clock ms when this snapshot was written \u2014 for {@link StarfishClientOptions.cacheMaxAgeMs} expiry. */\n cachedAt: number\n}\n\n/**\n * The cache key for a pull `pathAndQuery`: the path with any query string\n * dropped, so a checkpoint'd or `withKeyring` pull and a plain pull of the same\n * document share one stable key (the document identity, not the request shape).\n */\nfunction pullCacheKey(pathAndQuery: string): string {\n const q = pathAndQuery.indexOf(\"?\")\n return q === -1 ? pathAndQuery : pathAndQuery.slice(0, q)\n}\n\n/**\n * Whether a {@link PullResult} was served from the offline read-through cache\n * (the transport was unreachable) rather than a live server response. Used by\n * {@link SyncManager} to surface a `stale` flag to the UI without treating a\n * cache hit as proof the server is reachable.\n */\nexport function pullWasFromCache(result: PullResult): boolean {\n return (result as { fromCache?: boolean }).fromCache === true\n}\n\n/** The storage `documentKey` for a push `path`: the path with the `/push/`\n * action prefix stripped (the namespace lives only in the URL). The author\n * signature binds to this key. */\nexport function stripPushPrefix(path: string): string {\n return path.startsWith(PUSH_PATH_PREFIX) ? path.slice(PUSH_PATH_PREFIX.length) : path\n}\n\n/** Result of pulling a binary blob from the server. */\nexport interface BlobPullResult {\n data: ArrayBuffer\n /** Content hash from the ETag header. Null if the server didn't include an ETag. */\n hash: string | null\n contentType: string\n}\n\n/** Result of pushing a binary blob to the server. */\nexport interface BlobPushResult {\n hash: string\n}\n\n/** Options for append-only pull \u2014 extracts a single array field from the response. */\nexport interface AppendPullOptions {\n /** Array field name in `data`. Defaults to `\"items\"`. */\n appendField?: string\n /** Only return items appended after this timestamp (ms). Sent as `?checkpoint=`. */\n since?: number\n /** Return only the last K items (applied after `since` filter). Sent as `?last=`. */\n last?: number\n /** Return only the last K items. Alias of `last`; sent as `?limit=`. When both\n * are given, `limit` wins. */\n limit?: number\n /** Explicitly fetch the whole collection (sent as `?full=true`). Mutually\n * exclusive with `since`/`limit`/`last` \u2014 the server requires a pull to declare\n * exactly one of {checkpoint, limit/last, full}. */\n full?: boolean\n}\n\n/**\n * Options for a structured (non-append) pull.\n *\n * `withKeyring: true` appends `?withKeyring=1` so the server includes the\n * collection's sibling `<collection>/_keyring` document in the response,\n * saving a cold-start round-trip. The cap-cert scope MUST cover BOTH the\n * data path and `<collection>/_keyring` \u2014 `scopes.writer(collection)` denies\n * the keyring path and will produce a 403; use `scopes.readWrite()` or grant\n * the keyring path explicitly when opting in.\n */\nexport interface PullOptions {\n /** Server timestamp of the last successful pull (ms). Sent as `?checkpoint=`. */\n checkpoint?: number\n /** Include the sibling `_keyring` document in the response. Defaults to false. */\n withKeyring?: boolean\n /**\n * Serve the last-synced cached snapshot immediately (tagged via\n * {@link pullWasFromCache}) and revalidate in the background. Requires a\n * {@link StarfishClientOptions.cache} to be configured; without one the option\n * is a no-op and the pull goes to the network as usual.\n *\n * On a cache hit: returns the stale snapshot at once, kicks a background fetch,\n * and on success writes the fresh snapshot to cache and fires\n * {@link StarfishClientOptions.onRevalidated}. Uses the same dedup set as the\n * {@link StarfishClientOptions.cacheFallbackStatuses} revalidation path \u2014 a\n * concurrent error-triggered loop for the same document is not duplicated.\n *\n * On a cache miss: falls through to the normal network-first pull unchanged.\n */\n staleWhileRevalidate?: boolean\n}\n\n/** Per-collection result in a {@link BatchPullResult}: either the pulled\n * document (`data`/`hash`/`timestamp`) or a per-collection `error` string. */\nexport interface BatchPullEntry {\n data?: unknown\n hash?: string\n timestamp?: number\n error?: string\n}\n\n/** Response of {@link StarfishClient.batchPull}: a map of requested collection\n * name \u2192 an ARRAY of {@link BatchPullEntry}, one per requested param-set, in\n * request order. A collection read with no params yields a one-element array. */\nexport interface BatchPullResult {\n collections: Record<string, BatchPullEntry[]>\n}\n\n/** Options for {@link StarfishClient.batchPull}. */\nexport interface BatchPullOptions {\n /** Per-collection path params: collection name \u2192 an ARRAY of param-sets, one\n * per document to read from that collection, e.g.\n * `{ profile: [{ identity: \"a\" }, { identity: \"b\" }] }` reads two profiles in\n * one round-trip. Serialized to a URL-encoded JSON `params` query. The\n * `{identity}` param is auto-filled by the server from the authenticated\n * caller when a set omits it, so a single self-doc read can pass `[{}]` \u2014 or\n * omit the collection from `params` entirely (an unlisted collection reads one\n * auto-filled doc). Results come back under the same name in request order. */\n params?: Record<string, Record<string, string>[]>\n}\n\n/**\n * Base64-encode the canonical stable-stringification of a cap-cert.\n *\n * Used as the value of the `Authorization: Cap <\u2026>` header in v3.0. We rely\n * on the host's `btoa` for browsers and fall back to `Buffer` in Node so the\n * client stays free of native dependencies.\n */\nfunction encodeCapAuth(cap: unknown): string {\n const json = stableStringify(cap as Record<string, unknown>)\n if (typeof btoa === \"function\") {\n return btoa(json)\n }\n const bufCtor = (globalThis as { Buffer?: { from: (s: string, enc: string) => { toString: (enc: string) => string } } }).Buffer\n if (bufCtor) return bufCtor.from(json, \"utf-8\").toString(\"base64\")\n throw new Error(\"No base64 encoder available\")\n}\n\n/**\n * Low-level HTTP client for the Starfish sync protocol.\n * Handles auth headers and response parsing.\n */\nexport class StarfishClient {\n private readonly baseUrl: string\n private readonly namespace?: string\n private readonly capProvider?: StarfishCapProvider\n private readonly fetch: typeof globalThis.fetch\n private readonly cache?: PullCache\n private readonly cacheMaxAgeMs?: number\n private readonly cacheFallbackStatuses?: ReadonlyArray<number>\n private readonly onRevalidated?: (path: string, result: PullResult) => void\n private readonly revalidating = new Set<string>()\n /**\n * In-memory mirror of the latest document timestamp written to each cache\n * key via {@link writeCache}. Updated synchronously so {@link revalidateLoop}\n * can guard against stale overwrites without an extra async cache read.\n */\n private readonly latestCacheTimestamp = new Map<string, number>()\n /**\n * Installed client-side plugins. Currently stored as inert data; no\n * hooks fire yet. Extensions can inspect this list if needed.\n */\n public readonly plugins: ReadonlyArray<import(\"./types.js\").ClientPlugin>\n\n constructor(options: StarfishClientOptions) {\n this.baseUrl = options.baseUrl.replace(/\\/$/, \"\")\n // Empty string \u21D2 no namespace (treat like unset), so a falsy env value\n // doesn't produce a malformed `/v1//\u2026` path.\n this.namespace = options.namespace || undefined\n this.capProvider = options.capProvider\n this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis)\n this.cache = options.cache\n this.cacheMaxAgeMs = options.cacheMaxAgeMs\n this.cacheFallbackStatuses = options.cacheFallbackStatuses\n this.onRevalidated = options.onRevalidated\n this.plugins = options.plugins ? [...options.plugins] : []\n }\n\n /**\n * Mark a `PullResult` as having been served from the offline read-through\n * cache (transport was unreachable). Non-enumerable so it doesn't leak into\n * JSON / equality / re-caching; read via {@link pullWasFromCache}.\n */\n private tagFromCache(result: PullResult): PullResult {\n Object.defineProperty(result, \"fromCache\", { value: true, enumerable: false })\n return result\n }\n\n /**\n * Resolve the host portion of the URL the client will send to. The host\n * is folded into the signed canonical input as the `h` field so the\n * server can refuse a signature that was minted against a different\n * Starfish host (replay-across-servers defence).\n *\n * When `baseUrl` is relative \u2014 e.g. the consumer passed a custom `fetch`\n * that resolves relative URLs in its own context \u2014 there is no parseable\n * host; we return `\"\"` so signing still proceeds. The server-side\n * verifier will also reconstruct host from its inbound URL, so the\n * empty-host case still verifies symmetrically when both sides agree.\n */\n private signingHost(): string {\n try {\n return new URL(this.baseUrl).host\n } catch {\n return \"\"\n }\n }\n\n /**\n * Rewrite a request path for the configured namespace. A no-op when no\n * namespace is set; otherwise `/{action}/\u2026` becomes `/v1/{namespace}/{action}/\u2026`\n * (the `/v1` protocol-version segment is part of the namespaced route, matching\n * the Python client and the server's namespace mount).\n *\n * Applied to the path used for BOTH the signature and the URL so the canonical\n * path the client signs equals the path the server reconstructs from the URL.\n * Covers SDK-helper-built paths too \u2014 that's the point: a namespace-unaware\n * helper passing `/push/spaces/x/_keyring` reaches `/v1/{ns}/push/spaces/x/_keyring`.\n */\n private applyNamespace(path: string): string {\n return this.namespace ? `/v1/${this.namespace}${path}` : path\n }\n\n /**\n * Build auth headers for a request. When a `capProvider` is set, signs the\n * request with the device's Ed25519 private key and returns the v3 header\n * set (`Authorization: Cap \u2026`, `X-Starfish-Sig`, `X-Starfish-Ts`,\n * `X-Starfish-Nonce`). Empty when no provider is configured (public reads).\n *\n * Body bytes signed MUST equal the bytes sent on the wire \u2014 callers pass\n * the already-serialized body string here so signing and transmission agree.\n * The host bound into the signature is derived from `baseUrl` once per call.\n */\n private async buildAuthHeaders(\n method: SignableMethod,\n pathAndQuery: string,\n body: string | undefined,\n ): Promise<Record<string, string>> {\n if (!this.capProvider) return {}\n const capCtx = await this.capProvider.getCap()\n return this.capRequestHeaders(capCtx, method, pathAndQuery, body)\n }\n\n /**\n * Build the request-signing headers from an ALREADY-fetched cap context. Split\n * out of {@link buildAuthHeaders} so {@link append} can fetch the cap once and\n * reuse it for BOTH the author signature (over the element data) and the\n * request signature (over the body), without redeeming the cap twice \u2014 a\n * second `getCap()` could rotate keys and break the `authorPubkey ===\n * presenter` bind the server checks.\n */\n private async capRequestHeaders(\n capCtx: Awaited<ReturnType<StarfishCapProvider[\"getCap\"]>>,\n method: SignableMethod,\n pathAndQuery: string,\n body: string | undefined,\n ): Promise<Record<string, string>> {\n const { cap, devEdPrivHex, pubHex } = capCtx\n const req: SignableRequest = {\n method,\n pathAndQuery,\n body,\n host: this.signingHost(),\n }\n const { sig, ts, nonce } = await signRequest(req, devEdPrivHex)\n const headers: Record<string, string> = {\n [HEADER_AUTHORIZATION]: `Cap ${encodeCapAuth(cap)}`,\n [HEADER_SIG]: sig,\n [HEADER_TS]: String(ts),\n [HEADER_NONCE]: nonce,\n }\n // Audience (public-link) caps bind no single subject, so the server needs\n // the presenter's pubkey to verify the signature and check the allow-list.\n if (pubHex !== undefined) headers[HEADER_PUB] = pubHex\n return headers\n }\n\n /**\n * Resolve the author public key to attach to a signed append: the redeemer's\n * `pubHex` for an audience cap, else the cert subject `cap.sub` for a\n * device/member cap. This is the SAME key that signs the request, so a server\n * enforcing author proof can bind the stored element to its writer. Returns\n * undefined only for a (malformed) cap with neither \u2014 the append then goes\n * unsigned and a server requiring signatures rejects it.\n */\n private appendAuthorKey(\n capCtx: Awaited<ReturnType<StarfishCapProvider[\"getCap\"]>>,\n ): { authorPubHex: string } | null {\n const { cap, pubHex } = capCtx\n const authorPubHex = pubHex ?? cap.sub\n if (authorPubHex === undefined) return null\n return { authorPubHex }\n }\n\n /** Pull synced data from the server. Returns the raw `PullResult`. */\n async pull(path: string, checkpoint?: number): Promise<PullResult>\n /** Pull synced data with structured options (e.g. `{withKeyring: true}`). */\n async pull(path: string, options: PullOptions): Promise<PullResult>\n /** Pull an append-only collection. Extracts and returns `data[appendField]` as `T[]`. */\n async pull<T = unknown>(path: string, options: AppendPullOptions): Promise<T[]>\n async pull<T = unknown>(\n path: string,\n checkpointOrOptions?: number | AppendPullOptions | PullOptions,\n ): Promise<PullResult | T[]> {\n let pathAndQuery = this.applyNamespace(path)\n let appendField: string | undefined\n let swr = false\n\n if (typeof checkpointOrOptions === \"number\") {\n if (checkpointOrOptions) pathAndQuery += `?checkpoint=${checkpointOrOptions}`\n } else if (checkpointOrOptions != null) {\n // Disambiguate AppendPullOptions vs PullOptions.\n //\n // PullOptions are identified by the presence of `withKeyring`, `checkpoint`,\n // or `staleWhileRevalidate` keys (which AppendPullOptions does not have \u2014\n // append uses `since`, not `checkpoint`). Anything else, including an empty\n // `{}` object, retains the historical behavior of AppendPullOptions\n // (extracts `data.items` with `?` query).\n const opts = checkpointOrOptions as AppendPullOptions & PullOptions\n const isPullOptions =\n opts.withKeyring !== undefined ||\n opts.checkpoint !== undefined ||\n opts.staleWhileRevalidate !== undefined\n const params = new URLSearchParams()\n\n if (isPullOptions) {\n if (opts.checkpoint != null && opts.checkpoint > 0) {\n params.set(\"checkpoint\", String(opts.checkpoint))\n }\n if (opts.withKeyring) {\n params.set(\"withKeyring\", \"1\")\n }\n swr = opts.staleWhileRevalidate === true\n } else {\n appendField = opts.appendField ?? APPEND_DEFAULT_FIELD\n // `full` means \"the whole collection\" \u2014 it cannot be combined with a bound.\n if (opts.full && (opts.since != null || opts.limit != null || opts.last != null)) {\n throw new Error(\"full cannot be combined with since, limit, or last\")\n }\n if (opts.since != null) {\n if (opts.since < 0) throw new Error(\"since must be non-negative\")\n params.set(\"checkpoint\", String(opts.since))\n }\n if (opts.limit != null) {\n if (opts.limit < 0) throw new Error(\"limit must be non-negative\")\n params.set(\"limit\", String(opts.limit))\n }\n if (opts.last != null) {\n if (opts.last < 0) throw new Error(\"last must be non-negative\")\n params.set(\"last\", String(opts.last))\n }\n if (opts.full) {\n params.set(\"full\", \"true\")\n }\n }\n if (params.size > 0) pathAndQuery += `?${params.toString()}`\n }\n\n const url = `${this.baseUrl}${pathAndQuery}`\n const authHeaders = await this.buildAuthHeaders(\"GET\", pathAndQuery, undefined)\n\n // Read-through cache: only for structured (non-append) pulls. Append\n // collections own their own warm-start persistence via AppendLogCursor.\n const cacheKey =\n this.cache && appendField === undefined ? pullCacheKey(pathAndQuery) : undefined\n\n // staleWhileRevalidate: serve the cached snapshot immediately (cache-first\n // paint without a zustand store), kick background revalidation, return stale.\n // Falls through to network-first when there is no cache hit (miss or expired).\n if (swr && cacheKey) {\n const cached = await this.readCache(cacheKey)\n if (cached) {\n this.scheduleRevalidate(cacheKey, pathAndQuery, null, /* immediate */ true)\n return cached\n }\n }\n\n let res: Response\n try {\n res = await this.fetch(url, {\n method: \"GET\",\n headers: { [HEADER_ACCEPT]: \"application/json\", ...authHeaders },\n })\n } catch (err) {\n // The TRANSPORT failed (offline / DNS / timeout) \u2014 fall back to the last\n // cached snapshot for this document if we have one, tagged so callers can\n // tell it's stale. A real HTTP error (below) is a genuine server answer\n // and never gets here; 403 and 404 always propagate. 429 and 5xx\n // propagate by default too, but can fall back to cache when\n // `cacheFallbackStatuses` is set \u2014 see the stale-while-revalidate branch.\n if (cacheKey) {\n const cached = await this.readCache(cacheKey)\n if (cached) return cached\n }\n throw err\n }\n if (!res.ok) {\n const status = res.status\n if (cacheKey && this.cacheFallbackStatuses?.includes(status)) {\n // Stale-while-revalidate: serve the last-synced snapshot immediately and\n // retry in the background. 403/404 are not in the configured set so they\n // still propagate as genuine answers.\n const retryAfterHeader = res.headers.get(\"Retry-After\")\n this.scheduleRevalidate(cacheKey, pathAndQuery, retryAfterHeader)\n const cached = await this.readCache(cacheKey)\n if (cached) {\n // Discard the response body so the underlying connection can be reused.\n void res.body?.cancel()\n return cached\n }\n }\n throw new StarfishHttpError(status, await res.text())\n }\n\n const result = await res.json() as PullResult\n if (appendField !== undefined) {\n const list = (result.data as Record<string, unknown> | null)?.[appendField]\n return (Array.isArray(list) ? list : []) as T[]\n }\n if (cacheKey) this.writeCache(cacheKey, result)\n return result\n }\n\n /**\n * Write a pull snapshot to the cache. Fire-and-forget; errors are swallowed\n * so a failing cache never blocks the caller. No-op when no cache is configured.\n */\n private writeCache(\n cacheKey: string,\n result: { data: Record<string, unknown>; hash: string; timestamp: number },\n ): void {\n if (!this.cache) return\n // Track the newest document timestamp written so revalidateLoop can check\n // staleness synchronously (without an async cache read adding extra ticks).\n if (result.timestamp > (this.latestCacheTimestamp.get(cacheKey) ?? -1)) {\n this.latestCacheTimestamp.set(cacheKey, result.timestamp)\n }\n const snapshot: CachedPull = {\n data: result.data,\n hash: result.hash,\n timestamp: result.timestamp,\n cachedAt: Date.now(),\n }\n void this.cache.set(cacheKey, JSON.stringify(snapshot)).catch(() => {})\n }\n\n /** Build the URL + auth headers for one revalidation GET. Shared between\n * {@link pull} and {@link revalidateLoop} to avoid duplicated fetch setup. */\n private async revalidateFetch(pathAndQuery: string): Promise<Response> {\n const url = `${this.baseUrl}${pathAndQuery}`\n const authHeaders = await this.buildAuthHeaders(\"GET\", pathAndQuery, undefined)\n return this.fetch(url, {\n method: \"GET\",\n headers: { [HEADER_ACCEPT]: \"application/json\", ...authHeaders },\n })\n }\n\n /**\n * Deduplicated fire-and-forget: starts one revalidation loop per cacheKey.\n * Used by both the {@link cacheFallbackStatuses} error path (delayed first\n * attempt, honoring `Retry-After`) and the {@link PullOptions.staleWhileRevalidate}\n * read path (`immediate: true` \u2014 no initial delay on the first attempt). The\n * `revalidating` set deduplicates across both triggers so a concurrent\n * error-triggered loop and an SWR-on-read loop for the same key collapse to one.\n */\n private scheduleRevalidate(\n cacheKey: string,\n pathAndQuery: string,\n retryAfterHeader: string | null,\n immediate = false,\n ): void {\n if (this.revalidating.has(cacheKey)) return\n this.revalidating.add(cacheKey)\n void this.revalidateLoop(cacheKey, pathAndQuery, retryAfterHeader, immediate).finally(() => {\n this.revalidating.delete(cacheKey)\n })\n }\n\n /**\n * Background revalidation loop shared by both {@link cacheFallbackStatuses}\n * hits and {@link PullOptions.staleWhileRevalidate} reads.\n *\n * Retries (honoring `Retry-After`) up to {@link MAX_REVALIDATE_ATTEMPTS} times.\n * When `immediate` is true the first attempt fires without any initial delay\n * (SWR-on-read path). On a live 2xx the fresh snapshot is written to cache and\n * {@link onRevalidated} fires. Stops early on a non-fallback status (403/404).\n */\n private async revalidateLoop(\n cacheKey: string,\n pathAndQuery: string,\n firstRetryAfter: string | null,\n immediate = false,\n ): Promise<void> {\n let retryAfterHeader = firstRetryAfter\n const fallbackSet = this.cacheFallbackStatuses ? new Set(this.cacheFallbackStatuses) : null\n for (let attempt = 0; attempt < MAX_REVALIDATE_ATTEMPTS; attempt++) {\n // Skip the initial delay for the first attempt when immediate mode is set\n // (staleWhileRevalidate path). Subsequent attempts always backoff normally.\n if (!immediate || attempt > 0) {\n const delay = parseRetryAfterMs(retryAfterHeader, {\n fallbackMs: Math.min(\n REVALIDATE_INITIAL_DELAY_MS * Math.pow(2, attempt),\n REVALIDATE_MAX_DELAY_MS,\n ),\n maxMs: REVALIDATE_MAX_DELAY_MS,\n })\n await sleep(delay)\n }\n\n try {\n const res = await this.revalidateFetch(pathAndQuery)\n\n if (res.ok) {\n const result = (await res.json()) as PullResult\n // Guard against stale overwrites: if push() wrote a newer snapshot\n // while this revalidation was in-flight, the in-memory tracker\n // reflects the current latest-written timestamp synchronously (no\n // extra async tick). We drop the revalidation result and leave the\n // cache \u2014 and onRevalidated \u2014 untouched so the pushed edit survives.\n const latestTs = this.latestCacheTimestamp.get(cacheKey) ?? -1\n if (result.timestamp >= latestTs) {\n this.writeCache(cacheKey, result)\n this.onRevalidated?.(pathAndQuery, result)\n }\n return\n }\n\n if (!fallbackSet?.has(res.status)) {\n // Genuine server answer (e.g. 403 or 404) \u2014 stop retrying.\n return\n }\n\n retryAfterHeader = res.headers.get(\"Retry-After\")\n } catch {\n // Transport failure \u2014 keep retrying with exponential backoff.\n retryAfterHeader = null\n }\n }\n }\n\n /**\n * Read the cached snapshot for a document `path` WITHOUT hitting the network \u2014\n * the basis for cache-first paint (seed the UI from the last-synced snapshot,\n * then revalidate with a live {@link pull}). Returns the tagged `PullResult`,\n * or null when no cache is configured / there's no entry. Namespacing matches\n * {@link pull}, so the key lines up with whatever `pull` wrote.\n */\n async peekCache(path: string): Promise<PullResult | null> {\n if (!this.cache) return null\n return this.readCache(pullCacheKey(this.applyNamespace(path)))\n }\n\n /** Read + parse a cached pull snapshot, tagged {@link tagFromCache}. Returns\n * null on a miss or an unparseable blob (never throws \u2014 a corrupt cache entry\n * must not break a pull, just miss). */\n private async readCache(cacheKey: string): Promise<PullResult | null> {\n try {\n const raw = await this.cache!.get(cacheKey)\n if (!raw) return null\n const parsed = JSON.parse(raw) as CachedPull\n if (!parsed || typeof parsed.hash !== \"string\") return null\n // Expiry: a snapshot older than the configured max age is a miss. Entries\n // written before this field existed (cachedAt missing) count as age 0 \u21D2\n // expired under any TTL, forcing a fresh pull once.\n if (this.cacheMaxAgeMs != null && Date.now() - (parsed.cachedAt ?? 0) > this.cacheMaxAgeMs) {\n return null\n }\n return this.tagFromCache({ data: parsed.data ?? {}, hash: parsed.hash, timestamp: parsed.timestamp ?? 0 })\n } catch {\n return null\n }\n }\n\n /**\n * Pull several documents in one round-trip via `/batch/pull`. `collections` is\n * the list of distinct collection names; `opts.params` supplies, per collection,\n * an ARRAY of path-param sets \u2014 one per document to read \u2014 so the SAME collection\n * can fan in many documents (e.g. many users' `profile`) in a single request.\n * The server auto-fills the `{identity}` param from the authenticated caller for\n * any set that omits it, so a self-doc collection needs no params. Returns a map\n * of collection name \u2192 an ARRAY of pulled documents (or per-document `{ error }`),\n * in request order. Honors the configured namespace.\n *\n * For the common \"many docs of one collection\" case prefer {@link batchPullMany}.\n *\n * Note: not append/checkpoint-aware \u2014 for incremental append-only reads use\n * `pull(path, { since })` (or `AppendLogCursor`) per collection.\n */\n async batchPull(\n collections: string[],\n opts: BatchPullOptions = {},\n ): Promise<BatchPullResult> {\n const search = new URLSearchParams()\n search.set(\"collections\", collections.join(\",\"))\n if (opts.params && Object.keys(opts.params).length > 0) {\n search.set(\"params\", JSON.stringify(opts.params))\n }\n const pathAndQuery = `${this.applyNamespace(\"/batch/pull\")}?${search.toString()}`\n const url = `${this.baseUrl}${pathAndQuery}`\n const authHeaders = await this.buildAuthHeaders(\"GET\", pathAndQuery, undefined)\n\n const res = await this.fetch(url, {\n method: \"GET\",\n headers: { [HEADER_ACCEPT]: \"application/json\", ...authHeaders },\n })\n if (!res.ok) {\n throw new StarfishHttpError(res.status, await res.text())\n }\n return await res.json() as BatchPullResult\n }\n\n /**\n * Convenience over {@link batchPull} for reading MANY documents of ONE\n * collection in a single round-trip: pass the per-document param-sets and get\n * back the {@link BatchPullEntry} array aligned to `paramsList` by index (each\n * entry is `{ data, hash, timestamp }` or `{ error }`). An empty `paramsList`\n * issues no request and returns `[]`.\n */\n async batchPullMany(\n collection: string,\n paramsList: Record<string, string>[],\n ): Promise<BatchPullEntry[]> {\n if (paramsList.length === 0) return []\n const res = await this.batchPull([collection], { params: { [collection]: paramsList } })\n return res.collections[collection] ?? []\n }\n\n /**\n * Push synced data to the server.\n * @param path - The push endpoint path (e.g. \"/push/users/abc/settings\")\n * @param data - The full document data to push\n * @param baseHash - Hash of the document this push is based on (null for first push)\n *\n * v3 author proof (`authorPubkey` + `authorSignature`) is passed via `author`\n * (produced by `SyncManager` when a `signer` is configured) and sent as\n * top-level body siblings of `data`, where the server verifies it.\n * @throws {ConflictError} if the server detects a hash mismatch (409)\n */\n async push(\n path: string,\n data: Record<string, unknown>,\n baseHash: string | null,\n author?: AppendAuthor,\n ): Promise<PushSuccess> {\n const body = JSON.stringify({\n [DATA_FIELD]: data,\n [BASE_HASH_FIELD]: baseHash,\n ...(author && {\n [AUTHOR_PUBKEY_FIELD]: author.authorPubkey,\n [AUTHOR_SIGNATURE_FIELD]: author.authorSignature,\n }),\n })\n\n const sendPath = this.applyNamespace(path)\n const authHeaders = await this.buildAuthHeaders(\"POST\", sendPath, body)\n\n const res = await this.fetch(`${this.baseUrl}${sendPath}`, {\n method: \"POST\",\n headers: {\n [HEADER_CONTENT_TYPE]: \"application/json\",\n [HEADER_ACCEPT]: \"application/json\",\n ...authHeaders,\n },\n body,\n })\n\n if (res.status === 409) {\n throw new ConflictError()\n }\n if (!res.ok) {\n throw new StarfishHttpError(res.status, await res.text())\n }\n const result = (await res.json()) as PushSuccess\n // Write-through: update the pull cache with the pushed data so an offline\n // restart reads the just-written state rather than the pre-push snapshot.\n // The push path is /push/X; the corresponding pull cache key is /pull/X.\n if (this.cache) {\n const pullPath = sendPath.replace(\"/push/\", \"/pull/\")\n this.writeCache(pullCacheKey(pullPath), { data, hash: result.hash, timestamp: result.timestamp })\n }\n return result\n }\n\n /**\n * Append an element to an appendOnly (`by_timestamp`) collection.\n *\n * Unlike {@link push}, appendOnly writes carry no hash/conflict check \u2014 an\n * authorized append is always accepted. Each element is stored server-side as\n * `{ts, data}` and pulls can filter by `ts` via `since`/`checkpoint`.\n *\n * @param path - the push endpoint (e.g. \"/push/events\")\n * @param data - the element payload. For a `delegated` collection, encrypt it\n * first (e.g. `createKeyringEncryptor(keyring, kem).encrypt(data)`); the\n * server stores it opaquely and never reads it.\n * @param opts.ts - optional client-supplied element timestamp (ms). Must be a\n * non-negative integer strictly greater than the latest stored element's ts\n * (else the server responds 409). Omit to let the server assign one.\n * @throws {StarfishHttpError} on a non-2xx response \u2014 e.g. 409\n * `{ error: \"non_monotonic_timestamp\" }` for a non-monotonic timestamp, or\n * `{ error: \"append_limit_exceeded\", limit }` if the collection's `maxItems`\n * cap is reached (partition by a path parameter for higher volume).\n */\n async append(\n path: string,\n data: Record<string, unknown>,\n opts: { ts?: number } = {},\n ): Promise<PushSuccess> {\n const sendPath = this.applyNamespace(path)\n const bodyObj: Record<string, unknown> = { [DATA_FIELD]: data }\n if (opts.ts !== undefined) bodyObj[TS_FIELD] = opts.ts\n\n // Author proof. Fetch the cap ONCE and reuse it for both the author\n // signature (over the element `data`) and the request signature (over the\n // final body) \u2014 see {@link capRequestHeaders}. The author fields are signed\n // with the same key that authenticates the request, so a collection with\n // `requireAuthorSignature` (the default) binds the stored element to its\n // writer. Without a cap provider the append is sent unsigned and such a\n // collection rejects it.\n const capCtx = this.capProvider ? await this.capProvider.getCap() : null\n if (capCtx) {\n const authorKey = this.appendAuthorKey(capCtx)\n if (authorKey) {\n // The signature binds the author to BOTH the element data AND the\n // document it is written to (the storage path = `path` minus the\n // `/push/` action prefix; the namespace lives only in the URL).\n const documentKey = stripPushPrefix(path)\n const { authorPubkey, authorSignature } = signAppendAuthor(\n documentKey,\n data,\n authorKey.authorPubHex,\n capCtx.devEdPrivHex,\n )\n bodyObj[AUTHOR_PUBKEY_FIELD] = authorPubkey\n bodyObj[AUTHOR_SIGNATURE_FIELD] = authorSignature\n }\n }\n\n const body = JSON.stringify(bodyObj)\n const authHeaders = capCtx\n ? await this.capRequestHeaders(capCtx, \"POST\", sendPath, body)\n : {}\n\n const res = await this.fetch(`${this.baseUrl}${sendPath}`, {\n method: \"POST\",\n headers: {\n [HEADER_CONTENT_TYPE]: \"application/json\",\n [HEADER_ACCEPT]: \"application/json\",\n ...authHeaders,\n },\n body,\n })\n\n if (!res.ok) {\n throw new StarfishHttpError(res.status, await res.text())\n }\n return res.json() as Promise<PushSuccess>\n }\n\n /**\n * Append one element to a **public-write** append-only collection with an\n * Ed25519 author proof but **no cap `Authorization` header**.\n *\n * Unlike {@link append}, which always attaches a cap-signed `Authorization`\n * header from the configured `capProvider`, this method signs only the\n * append-author proof (binding the element to the writer's Ed25519 key) and\n * sends the request without authentication headers. This is required for\n * collections with `writeRoles: [\"public\"]` \u2014 the server's cap-scope check\n * would reject a request carrying a cap whose scope does not cover the path.\n *\n * Typical use-case: writing a sealed invitation to another user's\n * public-write inbox collection without needing a cap scoped to the\n * recipient's namespace. The author proof is optional on the server side\n * (`requireAuthorSignature: false` for a public inbox), but signing anyway\n * binds the stored element to the sender's Ed25519 key for verification in\n * the receive path.\n *\n * The element is sent as `{ data, authorPubkey, authorSignature }`.\n *\n * @param path The push path, e.g. `/push/inbox/{userId}/{shard}`.\n * @param element The JSON element to append.\n * @param signer The sender's Ed25519 keypair (signs the author proof).\n *\n * @throws {AppendHttpError} on a non-2xx response.\n */\n async appendAnonymous(\n path: string,\n element: Record<string, unknown>,\n signer: { edPubHex: string; edPrivHex: string },\n ): Promise<void> {\n const sendPath = this.applyNamespace(path)\n const documentKey = stripPushPrefix(path)\n const { authorPubkey, authorSignature } = signAppendAuthor(\n documentKey,\n element,\n signer.edPubHex,\n signer.edPrivHex,\n )\n const body = JSON.stringify({\n [DATA_FIELD]: element,\n [AUTHOR_PUBKEY_FIELD]: authorPubkey,\n [AUTHOR_SIGNATURE_FIELD]: authorSignature,\n })\n const res = await this.fetch(`${this.baseUrl}${sendPath}`, {\n method: \"POST\",\n headers: {\n [HEADER_CONTENT_TYPE]: \"application/json\",\n [HEADER_ACCEPT]: \"application/json\",\n },\n body,\n })\n if (!res.ok) {\n const detail = await res.text().catch(() => \"\")\n throw new AppendHttpError(\n res.status,\n `anonymous append failed: HTTP ${res.status} ${detail}`.trim(),\n )\n }\n }\n\n /**\n * Pull binary data from a blob collection.\n * Returns raw bytes with the content hash from the ETag header.\n */\n async pullBlob(path: string): Promise<BlobPullResult> {\n const sendPath = this.applyNamespace(path)\n const authHeaders = await this.buildAuthHeaders(\"GET\", sendPath, undefined)\n\n const res = await this.fetch(`${this.baseUrl}${sendPath}`, {\n method: \"GET\",\n headers: { [HEADER_ACCEPT]: \"*/*\", ...authHeaders },\n })\n if (!res.ok) {\n throw new StarfishHttpError(res.status, await res.text())\n }\n\n const etag = res.headers.get(\"ETag\")?.replace(/\"/g, \"\") ?? null\n const contentType = res.headers.get(HEADER_CONTENT_TYPE) ?? \"application/octet-stream\"\n const data = await res.arrayBuffer()\n\n return { data, hash: etag, contentType }\n }\n\n /**\n * Push binary data to a blob collection.\n * Binary collections use last-write-wins (no conflict detection).\n */\n async pushBlob(\n path: string,\n data: ArrayBuffer | Uint8Array | Blob,\n contentType: string,\n ): Promise<BlobPushResult> {\n // Blobs are not JSON; we leave body undefined when signing \u2014 server-side\n // verification is expected to use a separate path for blob uploads.\n const sendPath = this.applyNamespace(path)\n const authHeaders = await this.buildAuthHeaders(\"POST\", sendPath, undefined)\n\n const res = await this.fetch(`${this.baseUrl}${sendPath}`, {\n method: \"POST\",\n headers: {\n [HEADER_CONTENT_TYPE]: contentType,\n [HEADER_ACCEPT]: \"application/json\",\n ...authHeaders,\n },\n body: data as BodyInit,\n })\n if (!res.ok) {\n throw new StarfishHttpError(res.status, await res.text())\n }\n return res.json() as Promise<BlobPushResult>\n }\n}\n", "import type { CapCert, PullResult } from \"@drakkar.software/starfish-protocol\"\n\n/** Push conflict error (HTTP 409). */\nexport class ConflictError extends Error {\n constructor() {\n super(\"hash_mismatch\")\n this.name = \"ConflictError\"\n }\n}\n\n/** HTTP error from the Starfish server. */\nexport class StarfishHttpError extends Error {\n constructor(\n public readonly status: number,\n public readonly body: string\n ) {\n super(`HTTP ${status}: ${body}`)\n this.name = \"StarfishHttpError\"\n }\n}\n\n/**\n * Non-2xx HTTP error from an anonymous (cap-less) append call.\n *\n * Distinct from {@link StarfishHttpError} so callers can distinguish \"the\n * anonymous write was rejected\" (e.g. auth required, payload too large) from\n * other client errors without pattern-matching on the error message.\n */\nexport class AppendHttpError extends Error {\n constructor(\n /** HTTP status returned by the server. */\n public readonly status: number,\n message: string,\n ) {\n super(message)\n this.name = \"AppendHttpError\"\n }\n}\n\n/**\n * v3.0 cap-cert provider for `StarfishClient`. Returns the device's cap-cert and\n * the matching Ed25519 private key (hex). The client calls `getCap()` once per\n * outgoing request; implementations are expected to cache so this is cheap.\n *\n * When set, the client signs every outgoing request: each call carries\n * `Authorization: Cap <base64(stableStringify(cap))>` plus `X-Starfish-Sig`,\n * `X-Starfish-Ts`, `X-Starfish-Nonce`.\n */\nexport interface StarfishCapProvider {\n /**\n * Returns the device's cap-cert and its Ed25519 private key (hex).\n * Implementations are expected to cache; the client may call this once per\n * authenticated request.\n *\n * For an `audience` (public-link) cap, which binds no single subject, also\n * return `pubHex` \u2014 the redeemer's own Ed25519 pubkey matching `devEdPrivHex`.\n * The client then sends it as `X-Starfish-Pub` so the server can verify the\n * request signature against it and check the cap's `aud` allow-list. Omit\n * `pubHex` for device/member caps (the server uses `cap.sub`).\n */\n getCap(): Promise<{\n cap: CapCert\n devEdPrivHex: string\n pubHex?: string\n }>\n}\n\n/**\n * A minimal async key-value store the client uses as a read-through cache for\n * {@link StarfishClient.pull} (offline-first reads). Host-provided so the SDK\n * stays storage-agnostic \u2014 back it by `localStorage`, `AsyncStorage`, a file,\n * etc. Shaped like a subset of zustand's `StateStorage` so an existing adapter\n * fits.\n *\n * IMPORTANT \u2014 what gets stored: the client caches the RAW server response only\n * (`data`/`hash`/`timestamp`). For E2E (`delegated`) collections that payload is\n * the SEALED ciphertext the server holds \u2014 never the decrypted form \u2014 so this\n * cache is ciphertext-at-rest by construction. Decryption always happens in\n * memory on read (see {@link SyncManager}). Public/plaintext collections cache\n * their plaintext, exactly as the server stores it.\n */\nexport interface PullCache {\n /** Return the previously-stored string for `key`, or null if absent. Must not throw. */\n get(key: string): Promise<string | null>\n /** Store `value` under `key`. Must not throw (failures are swallowed by the client). */\n set(key: string, value: string): Promise<void>\n}\n\n/** Options for creating a StarfishClient. */\nexport interface StarfishClientOptions {\n /** Base URL of the Starfish server (e.g. \"https://api.example.com/v1\"). */\n baseUrl: string\n /**\n * Optional namespace for a namespace-mounted server. When set, every request\n * path `/{action}/\u2026` is rewritten to `/v1/{namespace}/{action}/\u2026` for BOTH the\n * URL the client hits AND the canonical path it signs, so the signature the\n * server reconstructs from the namespaced URL verifies (no rewrite layer\n * needed). Mirrors the Python client's `namespace` parameter.\n *\n * Crucially this also rewrites the paths that namespace-unaware SDK helpers\n * build internally (e.g. `starfish-keyring`'s `addCollectionRecipient`, blob\n * uploads), so consumers no longer hand-prefix paths or wrap the client to\n * reach a namespaced deployment. Leave unset (default) for a root-mounted\n * server \u2014 paths pass through unchanged, byte-identical to before.\n *\n * Pass the bare namespace name (e.g. `\"octochat\"`); `baseUrl` then carries only\n * the origin (and any reverse-proxy mount the proxy strips), not the `/v1`\n * version segment. Must match `[A-Za-z0-9_-]+` and not be a reserved route name\n * (`pull`, `push`, `health`, `batch`).\n */\n namespace?: string\n /**\n * Cap-cert provider. When set, requests are signed with Ed25519 and carry\n * `Authorization: Cap <\u2026>`. Omit for unauthenticated public-read collections.\n */\n capProvider?: StarfishCapProvider\n /** Optional fetch implementation (defaults to global fetch). */\n fetch?: typeof fetch\n /**\n * Optional read-through cache for {@link StarfishClient.pull} \u2014 the basis for\n * offline-first reads. When set, every successful non-append pull is written\n * through to the cache (keyed by document path), and a pull that fails because\n * the TRANSPORT is unreachable (offline / DNS / timeout \u2014 `fetch` rejects)\n * falls back to the cached response, tagged so callers can tell it's stale.\n *\n * A real HTTP error (404/403/5xx) is a genuine server answer and always\n * propagates \u2014 the cache is NOT consulted \u2014 so \"no document yet\" and\n * \"access denied\" keep their meaning. Caches ciphertext for E2E collections\n * (the server only ever holds sealed payloads); never decrypted data.\n */\n cache?: PullCache\n /**\n * Optional max age (ms) for {@link cache} entries. An entry older than this is\n * treated as a cache MISS on every read \u2014 both cache-first paint and the\n * offline fallback \u2014 so a stale-beyond-policy snapshot is never served (the\n * pull then goes to the network, or rethrows the transport error offline).\n * Each cached snapshot records its write time; expiry is `now - cachedAt >\n * cacheMaxAgeMs`. Omit (default) for entries that never expire \u2014 recommended\n * for an offline-first app where any last-synced data beats none.\n */\n cacheMaxAgeMs?: number\n /**\n * HTTP status codes for which a structured `pull()` falls back to the\n * last-synced cached snapshot rather than throwing `StarfishHttpError` \u2014\n * a **stale-while-revalidate** strategy for transient server failures.\n *\n * When a pull returns one of these statuses AND a {@link cache} is configured\n * AND a cached snapshot exists for the document, `pull()` returns the cached\n * result immediately (tagged stale via `pullWasFromCache`) and spawns a\n * background revalidation loop that retries until it gets a live response.\n * On success the fresh snapshot is written through and {@link onRevalidated}\n * fires. When no cached snapshot exists the error propagates as usual.\n *\n * Applies only to structured (non-append) pulls. Do NOT include `403` or `404`\n * \u2014 they are genuine server answers (access denied / no document yet).\n *\n * Default `undefined` \u2014 every non-2xx status throws as before.\n *\n * Recommended set for offline-first apps: `[429, 500, 502, 503, 504]`.\n */\n cacheFallbackStatuses?: number[]\n /**\n * Called after a background revalidation delivers a fresh snapshot to the\n * cache. Fires for two revalidation paths:\n *\n * 1. **Error-triggered** ({@link cacheFallbackStatuses} hit): the server\n * returned a transient error (429/5xx), `pull()` served the stale cache,\n * and the background retry loop eventually got a live response.\n * 2. **SWR-on-read** ({@link PullOptions.staleWhileRevalidate}): `pull()`\n * served the cache immediately and the background fetch completed.\n *\n * In both cases `result` is the fresh `PullResult` just written to cache.\n * Use this to signal reachability recovery and/or push the fresh data into\n * any store that is showing the stale snapshot.\n *\n * `path` is the namespaced document path (namespace prefix + path + query\n * string, matching the cache key written by {@link StarfishClient.pull}).\n */\n onRevalidated?: (path: string, result: PullResult) => void\n /**\n * Optional list of client-side plugins. The list is stored on the client\n * instance but does not fire any hooks yet \u2014 the contract is plumbed so\n * extension packages (`starfish-identities`, `starfish-keyring`,\n * `starfish-sharing`, \u2026) can register against it later without a breaking\n * API change.\n *\n * The current set of hooks is purposely empty; extensions that need to\n * react to mint events or transport actions today can wrap the client\n * directly. Future hook additions will be additive.\n */\n plugins?: ClientPlugin[]\n}\n\n/**\n * Client-side plugin contract.\n *\n * A placeholder shape: the interface intentionally has no required hooks\n * yet; extensions declare a plugin object with `name` and opt into\n * specific lifecycle hooks once those exist. Apps wire plugins via\n * `new StarfishClient({ plugins: [...] })`.\n */\nexport interface ClientPlugin {\n /** Human-readable name. Used in error messages and audit output. */\n name: string\n /**\n * Reserved for future hook fields. Plugins typically declare only\n * `name`. Hook additions are additive \u2014 extensions implementing a\n * future hook will populate the relevant optional property without\n * affecting existing zero-hook plugins.\n */\n}\n\n/** Conflict resolver: given local and remote data, return merged result. */\nexport type ConflictResolver = (\n local: Record<string, unknown>,\n remote: Record<string, unknown>\n) => Record<string, unknown>\n", "/**\n * Parse a `Retry-After` header value into milliseconds.\n *\n * - Numeric string (`\"30\"`) \u2014 treated as seconds \u00D7 1000.\n * - HTTP-date string \u2014 delta from now in ms (floored to 0).\n * - `null`, empty, or unparseable \u2014 returns `opts.fallbackMs`.\n *\n * All results are clamped to `[0, opts.maxMs]`.\n */\nexport function parseRetryAfterMs(\n header: string | null | undefined,\n opts: { fallbackMs: number; maxMs: number },\n): number {\n const { fallbackMs, maxMs } = opts\n const trimmed = header?.trim()\n if (trimmed) {\n const seconds = Number(trimmed)\n if (!isNaN(seconds)) return Math.min(seconds * 1000, maxMs)\n const date = Date.parse(trimmed)\n if (!isNaN(date)) return Math.min(Math.max(date - Date.now(), 0), maxMs)\n }\n return Math.min(fallbackMs, maxMs)\n}\n\n/** Error category returned by classifyError. */\nexport type ErrorCategory =\n | \"network\"\n | \"auth\"\n | \"conflict\"\n | \"rate-limited\"\n | \"server\"\n | \"client\"\n | \"unknown\"\n\n/** Classify an error from a fetch response or network failure. */\nexport function classifyError(err: unknown): ErrorCategory {\n if (err instanceof Response || (err && typeof err === \"object\" && \"status\" in err)) {\n const status = (err as { status: unknown }).status\n if (typeof status !== \"number\" || isNaN(status)) return \"unknown\"\n if (status === 0) return \"network\"\n if (status === 401 || status === 403) return \"auth\"\n if (status === 409) return \"conflict\"\n if (status === 429) return \"rate-limited\"\n if (status >= 500) return \"server\"\n if (status >= 400) return \"client\"\n }\n if (err instanceof Error && /failed to fetch|fetch failed|network|load failed|ECONNREFUSED|ENOTFOUND/i.test(err.message)) return \"network\"\n return \"unknown\"\n}\n\nexport interface RetryOptions {\n /** Max number of retries (default: 3). */\n maxRetries?: number\n /** Initial delay in ms before first retry (default: 500). */\n initialDelayMs?: number\n /** Maximum delay in ms (default: 10000). */\n maxDelayMs?: number\n}\n\n/**\n * Wraps a fetch function with automatic retry for retriable errors\n * (network failures, 429, 5xx). Respects Retry-After headers.\n */\nexport function createRetryFetch(options?: RetryOptions): typeof globalThis.fetch {\n const maxRetries = Math.max(0, options?.maxRetries ?? 3)\n const initialDelay = options?.initialDelayMs ?? 500\n const maxDelay = options?.maxDelayMs ?? 10_000\n\n return async (input, init?) => {\n let attempt = 0\n while (true) {\n try {\n const res = await globalThis.fetch(input, init)\n if (res.ok || attempt >= maxRetries) return res\n\n const category = classifyError(res)\n if (category !== \"rate-limited\" && category !== \"server\") return res\n\n const retryAfterHeader = res.headers.get(\"Retry-After\")\n const exponentialDelay = Math.min(initialDelay * Math.pow(2, attempt), maxDelay)\n // When the header is present but unparseable, original falls back to\n // initialDelay (not exponential). Preserve that by checking presence first.\n const delay = parseRetryAfterMs(retryAfterHeader, {\n fallbackMs: retryAfterHeader?.trim() ? initialDelay : exponentialDelay,\n maxMs: maxDelay,\n })\n\n await new Promise<void>((r) => setTimeout(r, delay))\n attempt++\n } catch (err) {\n if (attempt >= maxRetries) throw err\n const category = classifyError(err)\n if (category !== \"network\") throw err\n\n const delay = Math.min(initialDelay * Math.pow(2, attempt), maxDelay)\n await new Promise<void>((r) => setTimeout(r, delay))\n attempt++\n }\n }\n }\n}\n\ntype BreakerState = \"closed\" | \"open\" | \"half-open\"\n\nexport interface CircuitBreakerOptions {\n /** Number of consecutive failures to open the circuit (default: 5). */\n threshold?: number\n /** Cooldown in ms before transitioning from open to half-open (default: 30000). */\n cooldownMs?: number\n}\n\n/** Circuit breaker that prevents requests when the backend is unavailable. */\nexport class CircuitBreaker {\n private state: BreakerState = \"closed\"\n private failures = 0\n private openedAt = 0\n private readonly threshold: number\n private readonly cooldownMs: number\n\n constructor(options?: CircuitBreakerOptions) {\n this.threshold = options?.threshold ?? 5\n this.cooldownMs = options?.cooldownMs ?? 30_000\n }\n\n getState(): BreakerState {\n this.maybeTransition()\n return this.state\n }\n\n isOpen(): boolean {\n return this.getState() === \"open\"\n }\n\n recordSuccess(): void {\n this.failures = 0\n this.state = \"closed\"\n }\n\n recordFailure(): void {\n this.failures++\n if (this.state === \"half-open\" || this.failures >= this.threshold) {\n this.state = \"open\"\n this.openedAt = Date.now()\n }\n }\n\n private maybeTransition(): void {\n if (this.state === \"open\" && Date.now() - this.openedAt >= this.cooldownMs) {\n this.state = \"half-open\"\n }\n }\n}\n\n/**\n * Wraps fetch to gzip-compress string request bodies using the CompressionStream API.\n * Adds Content-Encoding: gzip header. Non-string bodies (ArrayBuffer, Blob, etc.)\n * are passed through uncompressed. Requires CompressionStream (browsers, Node.js 18+, Deno).\n */\nexport function createCompressedFetch(inner?: typeof globalThis.fetch): typeof globalThis.fetch {\n const baseFetch = inner ?? globalThis.fetch.bind(globalThis)\n return async (input, init?) => {\n if (!init?.body || typeof CompressionStream === \"undefined\") {\n return baseFetch(input, init)\n }\n\n const bodyText = typeof init.body === \"string\" ? init.body : null\n if (!bodyText) return baseFetch(input, init)\n\n try {\n const stream = new Blob([bodyText]).stream().pipeThrough(new CompressionStream(\"gzip\"))\n const compressed = await new Response(stream).arrayBuffer()\n\n const normalized = Object.fromEntries(new Headers(init.headers as HeadersInit).entries())\n normalized[\"content-encoding\"] = \"gzip\"\n\n return baseFetch(input, {\n ...init,\n body: compressed,\n headers: normalized,\n })\n } catch {\n return baseFetch(input, init)\n }\n }\n}\n\n/**\n * Wrap `fetch` to bound the **connect / Time-to-First-Byte** phase with a\n * timeout. The timer is cleared as soon as the response HEADERS arrive (i.e.\n * the `fetch()` promise resolves), so a slow large-body download after a fast\n * connection is not interrupted. Only the initial \"will the server even\n * respond?\" window is bounded.\n *\n * The wrapper composes with the caller's `AbortSignal`: if the caller's signal\n * fires first the request is still aborted and the timeout timer is cleaned up.\n *\n * @param timeoutMs How long (in ms) to wait for the server to start\n * responding before aborting. Default `10 000`.\n * @param inner Optional underlying `fetch` to wrap (defaults to\n * `globalThis.fetch`).\n *\n * @example\n * ```ts\n * import { createTimeoutFetch, createResilientFetch } from \"@drakkar.software/starfish-client/fetch\"\n *\n * const { fetch: resilient } = createResilientFetch()\n * const client = new StarfishClient({\n * baseUrl: \"https://api.example.com\",\n * fetch: createTimeoutFetch(8_000, resilient),\n * })\n * ```\n */\nexport function createTimeoutFetch(\n timeoutMs = 10_000,\n inner?: typeof globalThis.fetch,\n): typeof globalThis.fetch {\n const baseFetch = inner ?? globalThis.fetch.bind(globalThis)\n return async (input, init?) => {\n const timeoutCtrl = new AbortController()\n const timer = setTimeout(() => timeoutCtrl.abort(new Error(`connect timeout after ${timeoutMs}ms`)), timeoutMs)\n\n // Compose with a caller-supplied AbortSignal if present.\n const callerSignal = init?.signal as AbortSignal | null | undefined\n let combinedSignal: AbortSignal\n\n if (callerSignal) {\n if (typeof AbortSignal.any === \"function\") {\n combinedSignal = AbortSignal.any([timeoutCtrl.signal, callerSignal])\n } else {\n // Polyfill for environments without AbortSignal.any.\n const combo = new AbortController()\n const onCallerAbort = () => combo.abort(callerSignal.reason)\n const onTimeout = () => combo.abort(timeoutCtrl.signal.reason)\n callerSignal.addEventListener(\"abort\", onCallerAbort, { once: true })\n timeoutCtrl.signal.addEventListener(\"abort\", onTimeout, { once: true })\n combinedSignal = combo.signal\n }\n } else {\n combinedSignal = timeoutCtrl.signal\n }\n\n try {\n const res = await baseFetch(input, { ...init, signal: combinedSignal })\n clearTimeout(timer) // Headers arrived \u2014 clear the connect timeout.\n return res\n } catch (err) {\n clearTimeout(timer)\n throw err\n }\n }\n}\n\n/**\n * Combines retry and circuit breaker into a single resilient fetch wrapper.\n * Rejects immediately when the circuit is open.\n */\nexport function createResilientFetch(\n retryOptions?: RetryOptions,\n breakerOptions?: CircuitBreakerOptions,\n): { fetch: typeof globalThis.fetch; breaker: CircuitBreaker } {\n const breaker = new CircuitBreaker(breakerOptions)\n const retryFetch = createRetryFetch(retryOptions)\n\n const resilientFetch: typeof globalThis.fetch = async (input, init?) => {\n if (breaker.isOpen()) {\n const cooldown = Math.ceil((breakerOptions?.cooldownMs ?? 30_000) / 1000)\n throw new Error(`Request blocked: too many consecutive failures. Retry in ${cooldown}s.`)\n }\n\n try {\n const res = await retryFetch(input, init)\n if (res.status >= 500) {\n breaker.recordFailure()\n } else {\n breaker.recordSuccess()\n }\n return res\n } catch (err) {\n breaker.recordFailure()\n throw err\n }\n }\n\n return { fetch: resilientFetch, breaker }\n}\n", "import type { PullResult } from \"@drakkar.software/starfish-protocol\"\nimport {\n AUTHOR_PUBKEY_FIELD,\n AUTHOR_SIGNATURE_FIELD,\n PUSH_PATH_PREFIX,\n deepMerge,\n docAuthorCanonicalInput,\n getBase64,\n type AppendAuthor,\n} from \"@drakkar.software/starfish-protocol\"\nimport type { ConflictResolver } from \"./types.js\"\nimport { ConflictError } from \"./types.js\"\nimport type { Encryptor } from \"@drakkar.software/starfish-protocol\"\nimport { StarfishClient, stripPushPrefix, pullWasFromCache } from \"./client.js\"\nimport type { SyncLogger } from \"./logger.js\"\nimport type { Validator } from \"./validate.js\"\nimport { ValidationError } from \"./validate.js\"\n\nexport class AbortError extends Error {\n constructor() {\n super(\"SyncManager was aborted\")\n this.name = \"AbortError\"\n }\n}\n\n/**\n * v3.0 author-signature plumbing for `SyncManager`.\n *\n * Returns the device's Ed25519 public key (hex) and a function that signs\n * arbitrary payload bytes. `SyncManager` calls `getSigner()` once per push\n * and uses the returned `sign` to produce a base64-encoded signature over\n * the canonical stringification of the encrypted payload (sans author fields).\n *\n * Implementations typically wrap the same Ed25519 private key used by\n * `StarfishCapProvider` so that `cap.sub === devEdPubHex`.\n */\nexport interface SyncSigner {\n /**\n * Returns the device's `cap.sub` (Ed25519 pubkey, hex) and a payload signer.\n * The `sign` function receives the canonical signing input bytes and must\n * return the raw 64-byte Ed25519 signature.\n */\n getSigner(): Promise<{ devEdPubHex: string; sign(payload: Uint8Array): Promise<Uint8Array> }>\n}\n\n\nexport interface SyncManagerOptions {\n client: StarfishClient\n pullPath: string\n pushPath: string\n /** Custom conflict resolver. Defaults to remote-wins deep merge. Arrays are atomic. */\n onConflict?: ConflictResolver\n /** Max conflict retry attempts (default: 3). */\n maxRetries?: number\n /**\n * Encryptor for client-side E2E encryption. For v3 `delegated` collections,\n * build it via `createKeyringEncryptor(keyring, deviceKemKeys)`.\n */\n encryptor?: Encryptor\n /**\n * v3 author-signature plumbing. When set, every push attaches\n * `authorPubkey` (= `cap.sub`) and `authorSignature` (= base64 Ed25519 over\n * stable-stringify of the encrypted payload minus author fields).\n */\n signer?: SyncSigner\n /** Structured logger for sync events. */\n logger?: SyncLogger\n /** Name passed to logger methods (default: derived from pullPath). */\n loggerName?: string\n /** Validate data before push. Throws ValidationError on failure. */\n validate?: Validator\n}\n\nexport class SyncManager {\n private readonly client: StarfishClient\n private readonly pullPath: string\n private readonly pushPath: string\n private readonly onConflict: ConflictResolver\n private readonly maxRetries: number\n private readonly encryptor: Encryptor | null\n private readonly signer?: SyncSigner\n private readonly logger?: SyncLogger\n private readonly loggerName: string\n private readonly validate?: Validator\n\n private lastHash: string | null = null\n private lastCheckpoint: number = 0\n private localData: Record<string, unknown> = {}\n private aborted: boolean = false\n private lastFromCache: boolean = false\n\n constructor(options: SyncManagerOptions) {\n this.client = options.client\n this.pullPath = options.pullPath\n this.pushPath = options.pushPath\n this.onConflict = options.onConflict ?? deepMerge\n this.maxRetries = options.maxRetries ?? 3\n this.signer = options.signer\n this.logger = options.logger\n this.loggerName = options.loggerName ?? options.pullPath.split(\"/\").filter(Boolean).pop() ?? options.pullPath\n this.validate = options.validate\n this.encryptor = options.encryptor ?? null\n }\n\n abort(): void {\n this.aborted = true\n }\n\n get isAborted(): boolean {\n return this.aborted\n }\n\n getData(): Record<string, unknown> {\n return { ...this.localData }\n }\n\n /**\n * Merge a remote snapshot with local (optimistic) data using this manager's\n * conflict resolver \u2014 the same resolver the push-conflict path uses. A plain\n * {@link pull} overwrites the store's data with the server snapshot, which\n * would drop un-pushed local writes (they live only in the store, never in\n * `localData` until a push succeeds). The zustand binding calls this on pull\n * while the store is dirty so those writes survive. `local` wins by the same\n * rules as a push conflict.\n */\n resolve(\n local: Record<string, unknown>,\n remote: Record<string, unknown>,\n ): Record<string, unknown> {\n return this.onConflict(local, remote)\n }\n\n getHash(): string | null {\n return this.lastHash\n }\n\n /** Set the last-known server hash. Used by persistence layers to restore state across restarts. */\n setHash(hash: string | null): void {\n this.lastHash = hash\n }\n\n /**\n * Whether the most recent {@link pull} (or {@link seedFromCache}) was served\n * from the client's offline read-through cache rather than a live server\n * response. The binding surfaces this as a `stale` flag so the UI can show an\n * offline indicator without treating a cache hit as \"reachable\". Reset to\n * false by the next successful network pull.\n */\n getLastPullFromCache(): boolean {\n return this.lastFromCache\n }\n\n /**\n * Cache-first paint: seed `localData` from the client's read-through cache\n * WITHOUT touching the network, decrypting in memory for E2E collections.\n * Returns whether anything was seeded (false on a miss, an expired entry, or\n * a decrypt failure \u2014 e.g. keyring skew). Call once on store creation before\n * the initial live {@link pull}, which then supersedes the seeded snapshot.\n * Requires the client to have been built with a `cache`.\n */\n async seedFromCache(): Promise<boolean> {\n if (this.aborted) return false\n const cached = await this.client.peekCache(this.pullPath)\n if (!cached) return false\n let data: Record<string, unknown>\n try {\n data = this.encryptor ? await this.encryptor.decrypt(cached.data) : cached.data\n } catch {\n return false // undecryptable (keyring skew / foreign epoch) \u2014 seed nothing\n }\n if (this.aborted) return false\n this.localData = data\n this.lastHash = cached.hash\n // Leave lastCheckpoint at 0 so the first live pull is a full (re)sync, not a\n // delta against a stale cached checkpoint.\n this.lastFromCache = true\n return true\n }\n\n getCheckpoint(): number {\n return this.lastCheckpoint\n }\n\n /**\n * Apply a freshly-fetched `PullResult` to this manager's state WITHOUT\n * firing a network request. Used by the zustand binding's `mergeResult`\n * action to absorb a background revalidation result (delivered via\n * {@link StarfishClientOptions.onRevalidated}) into the store.\n *\n * Like {@link pull}, `ingest` conflict-merges the snapshot against the\n * established baseline via `this.onConflict` when a checkpoint exists \u2014 so a\n * union-merge store does not lose array items when a revalidation result\n * (e.g. a stale cache-fallback on 429/5xx) is a shorter snapshot. The first\n * ingest (no checkpoint yet) takes the snapshot wholesale. Sets\n * `lastFromCache = false` (a revalidation is a live response) so the binding\n * can clear its `stale` flag.\n *\n * **Staleness guard**: if a `push()` advanced `lastCheckpoint` between the\n * time the revalidation request was sent and the time it resolves, the\n * result is from an older document version. Ingesting it would clobber the\n * user's just-saved edit and reset `lastHash` to a stale server hash\n * (causing a spurious 409 on the next push). We silently drop the result in\n * that case \u2014 the store's post-push state is already correct.\n */\n async ingest(result: PullResult): Promise<void> {\n if (this.aborted) return\n // Drop a revalidation result that is older than our current local state.\n // `lastCheckpoint` is advanced by every successful push() and pull(); a\n // revalidation snapshot whose document timestamp is strictly less than the\n // current checkpoint is stale relative to a concurrent push.\n if (result.timestamp < this.lastCheckpoint) return\n let incoming: Record<string, unknown>\n if (this.encryptor) {\n incoming = await this.encryptor.decrypt(result.data)\n if (this.aborted) return\n } else {\n incoming = result.data\n }\n // Honor the configured conflict resolver against the established baseline\n // (same as pull()). The first ingest takes the snapshot wholesale.\n this.localData = this.lastCheckpoint > 0 ? this.onConflict(this.localData, incoming) : incoming\n this.lastHash = result.hash\n this.lastCheckpoint = result.timestamp\n this.lastFromCache = false\n }\n\n async pull(): Promise<PullResult> {\n if (this.aborted) throw new AbortError()\n this.logger?.pullStart(this.loggerName)\n const start = performance.now()\n try {\n // NOTE: `SyncManager.pull` does NOT auto-enable `withKeyring`. Clients\n // that drive the keyring helpers from `recipients.ts` and want to save\n // the cold-start round-trip should call `client.pull(path, {withKeyring: true})`\n // directly. We keep `SyncManager` keyring-agnostic so it stays usable\n // for collections that don't use delegated encryption.\n const result = await this.client.pull(this.pullPath, this.lastCheckpoint)\n if (this.aborted) throw new AbortError()\n // True when the client served this from its offline cache (transport was\n // unreachable); a live response clears it. Surfaced as `stale` by the binding.\n this.lastFromCache = pullWasFromCache(result)\n\n let incoming: Record<string, unknown>\n if (this.encryptor) {\n incoming = await this.encryptor.decrypt(result.data)\n if (this.aborted) throw new AbortError()\n } else {\n incoming = result.data\n }\n // Honor the configured conflict resolver against the established baseline \u2014\n // the same resolver the push-conflict path (push 409 / resolve()) already\n // uses. A union-merge store must not lose array items when a pull returns a\n // shorter/stale snapshot (cache-fallback on 429/5xx or a momentarily-short\n // concurrent write). The first pull (no checkpoint yet) takes the snapshot\n // wholesale so a cache seed is superseded by a full resync.\n // onConflict defaults to deepMerge \u21D2 no behavior change for stores without\n // a custom resolver (plaintext incremental merges and E2EE snapshots behave\n // exactly as before).\n this.localData = this.lastCheckpoint > 0 ? this.onConflict(this.localData, incoming) : incoming\n result.data = this.localData\n\n this.lastHash = result.hash\n this.lastCheckpoint = result.timestamp\n this.logger?.pullSuccess(this.loggerName, Math.round(performance.now() - start))\n return result\n } catch (err) {\n this.logger?.pullError(this.loggerName, err instanceof Error ? err.message : String(err))\n throw err\n }\n }\n\n async push(data: Record<string, unknown>): Promise<{ hash: string; timestamp: number }> {\n if (this.aborted) throw new AbortError()\n if (this.validate) {\n const result = this.validate(data)\n if (result !== true) throw new ValidationError(result)\n }\n this.logger?.pushStart(this.loggerName)\n const start = performance.now()\n let attempt = 0\n let pendingData = data\n\n while (attempt <= this.maxRetries) {\n try {\n const sealed = this.encryptor\n ? await this.encryptor.encrypt(pendingData)\n : pendingData\n if (this.aborted) throw new AbortError()\n\n // v3.0 signer path: sign the document author proof over the doc-author\n // canonical input (domain-tagged, bound to documentKey) and pass it as\n // top-level body siblings of `data` (NOT inside `data`), where the server\n // verifies it and stores the raw author pubkey.\n let author: AppendAuthor | undefined\n if (this.signer) {\n const { devEdPubHex, sign } = await this.signer.getSigner()\n if (this.aborted) throw new AbortError()\n const documentKey = stripPushPrefix(this.pushPath)\n const canonical = docAuthorCanonicalInput(documentKey, sealed as Record<string, unknown>)\n const sigBytes = await sign(new TextEncoder().encode(canonical))\n if (this.aborted) throw new AbortError()\n author = {\n [AUTHOR_PUBKEY_FIELD]: devEdPubHex,\n [AUTHOR_SIGNATURE_FIELD]: getBase64().encode(sigBytes),\n }\n }\n\n const result = await this.client.push(\n this.pushPath,\n sealed as Record<string, unknown>,\n this.lastHash,\n author,\n )\n if (this.aborted) throw new AbortError()\n this.lastHash = result.hash\n this.lastCheckpoint = result.timestamp\n this.localData = pendingData\n this.logger?.pushSuccess(this.loggerName, Math.round(performance.now() - start))\n return result\n } catch (err) {\n if (err instanceof AbortError) throw err\n if (!(err instanceof ConflictError) || attempt >= this.maxRetries) {\n this.logger?.pushError(this.loggerName, err instanceof Error ? err.message : String(err))\n throw err\n }\n this.logger?.conflict(this.loggerName, attempt + 1)\n try {\n const remote = await this.client.pull(this.pullPath)\n if (this.aborted) throw new AbortError()\n const remoteData = this.encryptor\n ? await this.encryptor.decrypt(remote.data)\n : remote.data\n if (this.aborted) throw new AbortError()\n this.lastHash = remote.hash\n this.lastCheckpoint = remote.timestamp\n pendingData = this.onConflict(pendingData, remoteData)\n } catch (resolveErr) {\n if (resolveErr instanceof AbortError) throw resolveErr\n const msg = resolveErr instanceof Error ? resolveErr.message : String(resolveErr)\n this.logger?.pushError(this.loggerName, `Conflict resolution failed (attempt ${attempt + 1}): ${msg}`)\n throw resolveErr\n }\n await new Promise<void>(resolve => setTimeout(resolve, Math.min(100 * Math.pow(2, attempt), 2000) + Math.random() * 100))\n attempt++\n }\n }\n throw new ConflictError()\n }\n\n async update(\n modifier: (current: Record<string, unknown>) => Record<string, unknown>\n ): Promise<{ hash: string; timestamp: number }> {\n await this.pull()\n const updated = modifier(this.localData)\n return this.push(updated)\n }\n}\n", "/** Validation result: true if valid, or an array of error messages. */\nexport type ValidationResult = true | string[]\n\n/** A function that validates data before push. */\nexport type Validator = (data: Record<string, unknown>) => ValidationResult\n\n/** Error thrown when pre-push validation fails. */\nexport class ValidationError extends Error {\n constructor(public readonly errors: string[]) {\n super(`Validation failed: ${errors.join(\"; \")}`)\n this.name = \"ValidationError\"\n }\n}\n\n/**\n * Creates a validator from a JSON Schema object.\n * Requires an Ajv-compatible validate function.\n *\n * @example\n * ```ts\n * import Ajv from \"ajv\"\n * const ajv = new Ajv()\n * const validator = createSchemaValidator(ajv, mySchema)\n * ```\n */\nexport function createSchemaValidator(\n ajv: { compile: (schema: object) => { (data: unknown): boolean; errors?: unknown }; errorsText: (errors?: unknown) => string },\n schema: object,\n): Validator {\n const validate = ajv.compile(schema)\n return (data) => {\n if (validate(data)) return true\n return [ajv.errorsText(validate.errors)]\n }\n}\n", "/** Minimal store interface for cross-tab sync. Works with both Zustand and Legend bindings. */\nexport interface BroadcastableStore {\n getState(): { data: Record<string, unknown>; dirty: boolean }\n setState(partial: { data: Record<string, unknown>; dirty: boolean }): void\n subscribe(listener: (state: { data: Record<string, unknown>; dirty: boolean }, prev: { data: Record<string, unknown>; dirty: boolean }) => void): () => void\n}\n\ninterface BroadcastPayload {\n data: Record<string, unknown>\n dirty: boolean\n}\n\n/**\n * Syncs a Starfish store across browser tabs using BroadcastChannel.\n * Works with any store that has getState/setState/subscribe (Zustand, Legend adapters, etc.).\n * Returns a cleanup function that closes the channel.\n */\nexport function setupBroadcastSync(\n store: BroadcastableStore,\n name: string,\n): () => void {\n const channel = new BroadcastChannel(`starfish-${name}`)\n let lastReceivedData: Record<string, unknown> | null = null\n\n channel.onmessage = (event: MessageEvent<unknown>) => {\n const payload = event.data as BroadcastPayload | undefined\n if (!payload || typeof payload !== \"object\" || !payload.data || typeof payload.data !== \"object\") return\n lastReceivedData = payload.data\n store.setState({ data: payload.data, dirty: !!payload.dirty })\n }\n\n const unsub = store.subscribe((state, prev) => {\n if (state.data === lastReceivedData) return\n if (state.data !== prev.data || state.dirty !== prev.dirty) {\n try {\n channel.postMessage({ data: state.data, dirty: state.dirty } satisfies BroadcastPayload)\n } catch { /* non-serializable data \u2014 skip broadcast */ }\n }\n })\n\n return () => {\n unsub()\n channel.close()\n }\n}\n\n/**\n * Syncs a Starfish store across browser tabs using storage events.\n * Fallback for environments without BroadcastChannel.\n * Returns a cleanup function.\n */\nexport function setupStorageFallback(\n store: BroadcastableStore,\n name: string,\n): () => void {\n const storageKey = `starfish-broadcast-${name}`\n let lastReceivedData: Record<string, unknown> | null = null\n\n const onStorage = (e: StorageEvent) => {\n if (e.key !== storageKey || !e.newValue) return\n let payload: BroadcastPayload\n try {\n payload = JSON.parse(e.newValue)\n } catch {\n return\n }\n if (!payload || typeof payload !== \"object\" || !payload.data || typeof payload.data !== \"object\") return\n lastReceivedData = payload.data\n store.setState({ data: payload.data, dirty: !!payload.dirty })\n }\n\n globalThis.addEventListener(\"storage\", onStorage)\n\n const unsub = store.subscribe((state, prev) => {\n if (state.data === lastReceivedData) return\n if (state.data !== prev.data || state.dirty !== prev.dirty) {\n try {\n localStorage.setItem(\n storageKey,\n JSON.stringify({ data: state.data, dirty: state.dirty } satisfies BroadcastPayload),\n )\n } catch { /* quota exceeded or non-serializable \u2014 skip */ }\n }\n })\n\n return () => {\n unsub()\n globalThis.removeEventListener(\"storage\", onStorage)\n }\n}\n\n/**\n * Auto-detects the best cross-tab sync mechanism and sets it up.\n * Uses BroadcastChannel when available, falls back to storage events.\n * Returns a cleanup function.\n */\nexport function setupCrossTabSync(\n store: BroadcastableStore,\n name: string,\n): () => void {\n if (typeof BroadcastChannel !== \"undefined\") {\n return setupBroadcastSync(store, name)\n }\n if (typeof globalThis.addEventListener === \"function\" && typeof localStorage !== \"undefined\") {\n return setupStorageFallback(store, name)\n }\n return () => {}\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { createStore, type StoreApi } from \"zustand/vanilla\"\nimport { useStore } from \"zustand\"\nimport {\n persist,\n subscribeWithSelector,\n createJSONStorage,\n type StateStorage,\n} from \"zustand/middleware\"\nimport type { DevtoolsOptions } from \"zustand/middleware\"\nimport { useEffect, useRef, useState, useCallback } from \"react\"\nimport type { Encryptor, PullResult } from \"@drakkar.software/starfish-protocol\"\nimport { StarfishClient } from \"../client.js\"\nimport { SyncManager } from \"../sync.js\"\nimport { classifyError } from \"../fetch.js\"\nimport { AppendLogCursor, type AppendElement } from \"../append-log.js\"\nimport { setupCrossTabSync, type BroadcastableStore } from \"../broadcast.js\"\nimport type { StarfishClientOptions, StarfishCapProvider, ConflictResolver, PullCache } from \"../types.js\"\nimport type { SyncLogger } from \"../logger.js\"\nimport type { Validator } from \"../validate.js\"\n\nexport interface StarfishState {\n data: Record<string, unknown>\n syncing: boolean\n online: boolean\n dirty: boolean\n error: string | null\n /** Last-known server hash, persisted alongside `data`/`dirty`. Restored into the bound SyncManager on hydration. */\n hash: string | null\n /**\n * True when the currently-shown `data` came from the offline read-through\n * cache (a cache-first {@link StarfishActions.seed} or a {@link StarfishActions.pull}\n * the client served from cache because the transport was unreachable) rather\n * than a live server response. A successful live pull/flush clears it. Use it\n * to drive an \"offline / showing last-synced data\" indicator.\n */\n stale: boolean\n}\n\nexport interface StarfishActions {\n pull: () => Promise<void>\n set: (modifier: (current: Record<string, unknown>) => Record<string, unknown>) => void\n /** Update data without marking dirty or triggering flush. Use for restoring pulled data into the store. */\n restore: (data: Record<string, unknown>) => void\n flush: () => Promise<void>\n setOnline: (online: boolean) => void\n /**\n * Cache-first paint: populate `data` from the client's offline read-through\n * cache (decrypting in memory for E2E collections) without touching the\n * network. A no-op when the client has no cache configured or there's no\n * (unexpired) entry. {@link useSyncInit} calls this once before the initial\n * pull; the live pull then supersedes the seeded snapshot.\n */\n seed: () => Promise<void>\n /**\n * Apply a freshly-fetched `PullResult` to the store WITHOUT firing a network\n * request. Decrypts in memory for E2E collections, conflict-merges against\n * any local optimistic writes (same logic as a live pull), and clears `stale`.\n *\n * Primarily called automatically by the binding when\n * {@link StarfishClientOptions.onRevalidated} fires (background revalidation\n * delivered a fresh snapshot after a 429/5xx hit or an SWR-on-read). Also\n * available for manual use when a caller holds a fresh `PullResult` it wants\n * to push into the store without a second network round-trip.\n */\n mergeResult: (result: PullResult) => Promise<void>\n}\n\nexport type StarfishStore = StarfishState & StarfishActions\n\nexport interface CreateStarfishStoreOptions {\n /** Unique name used as the persistence key (prefixed with `starfish-`) */\n name: string\n syncManager: SyncManager\n /** Pass `false` to disable persistence. Defaults to `localStorage` in browsers. */\n storage?: StateStorage | false\n /**\n * Wrap the store with Redux DevTools. Import `devtools` from `'zustand/middleware'`\n * and pass it directly \u2014 this keeps the import in your code, preventing\n * `import.meta.env` from being bundled in Metro/Hermes environments.\n *\n * @example\n * import { devtools } from 'zustand/middleware'\n * createStarfishStore({ devtools: (fn) => devtools(fn, { name: 'my-app' }) })\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n devtools?: (storeCreator: any) => any\n /** Pass `produce` from `immer` to enable draft-based mutations in `set()`. */\n produce?: <T>(base: T, recipe: (draft: T) => T | void) => T\n /**\n * Called when remote data arrives via `pull()` \u2014 **not** called for local `set()` writes.\n *\n * Use this to restore domain stores after a pull without worrying about feedback loops.\n * The callback fires **after** the Starfish store state is updated, so the store already\n * reflects the new data when this runs.\n *\n * Replaces the manual `isRestoring` flag pattern:\n * ```ts\n * createStarfishStore({\n * name: \"app\",\n * syncManager,\n * onRemoteUpdate: (data) => {\n * taskStore.setState({ tasks: data.tasks as Task[] })\n * settingsStore.setState({ settings: data.settings as Settings })\n * },\n * })\n * ```\n */\n onRemoteUpdate?: (data: Record<string, unknown>) => void\n /**\n * Auto re-attempt a failed flush with exponential backoff while the store\n * stays dirty + online. Omit to keep the current no-retry behavior.\n *\n * Defaults when the option is present: `maxRetries: 5`, `initialDelayMs: 500`,\n * `maxDelayMs: 30_000`. Backoff is `min(initial * 2^attempt, max) + jitter(100ms)`.\n * A successful flush resets the counter. Going offline cancels any pending retry.\n * `AbortError`s are never retried.\n */\n flushRetry?: {\n maxRetries?: number\n initialDelayMs?: number\n maxDelayMs?: number\n }\n}\n\n// Re-export DevtoolsOptions for convenience\nexport type { DevtoolsOptions }\n\nexport function createStarfishStore(\n options: CreateStarfishStoreOptions,\n): StoreApi<StarfishStore> {\n const { name, syncManager, storage } = options\n\n type NamedSet = (partial: Partial<StarfishStore>, replace?: boolean, action?: string) => void\n\n const storeCreator = (\n rawSet: StoreApi<StarfishStore>[\"setState\"],\n get: StoreApi<StarfishStore>[\"getState\"],\n ): StarfishStore => {\n const set = rawSet as NamedSet\n\n // Self-healing flush retry (only active when options.flushRetry is set).\n let retryTimer: ReturnType<typeof setTimeout> | undefined\n let retryAttempt = 0\n\n const scheduleFlushRetry = () => {\n const retryOpts = options.flushRetry\n if (!retryOpts) return\n const maxRetries = retryOpts.maxRetries ?? 5\n if (retryAttempt >= maxRetries) return\n const initialMs = retryOpts.initialDelayMs ?? 500\n const maxMs = retryOpts.maxDelayMs ?? 30_000\n // Jittered exponential backoff \u2014 mirrors the push-conflict recovery pattern.\n const delayMs = Math.min(initialMs * Math.pow(2, retryAttempt), maxMs) + Math.random() * 100\n retryAttempt++\n clearTimeout(retryTimer)\n retryTimer = setTimeout(() => {\n if (get().dirty && get().online && !get().syncing) {\n get().flush().catch(() => {})\n }\n }, delayMs)\n }\n\n const cancelFlushRetry = () => {\n clearTimeout(retryTimer)\n retryTimer = undefined\n retryAttempt = 0\n }\n\n // Shared commit for any remote snapshot landing in the store \u2014 used by both\n // `pull()` (live response) and `mergeResult()` (background revalidation).\n // Reads the already-updated SyncManager state, conflict-merges if dirty, then\n // writes to the store and notifies domain stores via `onRemoteUpdate`.\n const commitRemote = (label: string) => {\n const remote = syncManager.getData()\n // A remote snapshot would overwrite any local optimistic writes (they live\n // only in store.data, never in localData until a push succeeds). When dirty,\n // merge them back via the same resolver the push-conflict path uses so a\n // pull racing a send can't lose the write.\n const newData = get().dirty ? syncManager.resolve(get().data, remote) : remote\n set({ data: newData, syncing: false, hash: syncManager.getHash(), stale: syncManager.getLastPullFromCache() }, false, label)\n // The preserved write still needs to reach the server, and nothing else\n // will trigger that here. Kick a flush, gated on dirty + online exactly\n // like setOnline \u2014 a successful flush clears dirty, so there's no ping-pong.\n if (get().online && get().dirty) get().flush().catch(() => {})\n // Fire after state update so domain stores can read the updated Starfish state.\n // Calling set() inside onRemoteUpdate does NOT re-enter pull/mergeResult, no feedback loop.\n options.onRemoteUpdate?.(newData)\n }\n\n return {\n data: {},\n syncing: false,\n online: true,\n dirty: false,\n error: null,\n hash: null,\n stale: false,\n\n seed: async () => {\n try {\n const seeded = await syncManager.seedFromCache()\n if (!seeded) return\n // Don't clobber data a live pull/local set already produced (the seed is\n // a fast local read, but guard the race anyway): only seed while empty\n // and not yet dirty.\n if (get().dirty || Object.keys(get().data).length > 0) return\n set({ data: syncManager.getData(), hash: syncManager.getHash(), stale: true }, false, \"seed\")\n } catch {\n /* a cache miss / decrypt failure must never block the live pull */\n }\n },\n\n pull: async () => {\n // Gap D: when the store already shows stale (seeded or previously offline)\n // data, keep it on screen without a syncing flash \u2014 the user sees useful\n // content and the live pull supersedes it silently. Only raise the spinner\n // when the store is empty (fresh mount with no seed data).\n set(get().stale ? { error: null } : { syncing: true, error: null }, false, \"pull/start\")\n try {\n await syncManager.pull()\n commitRemote(\"pull/success\")\n } catch (err) {\n // Transport unreachable (offline / DNS / timeout): the persisted `data` is still\n // the last-synced snapshot, so keep it on screen and flag it stale rather than\n // surfacing an error. This replaces the client pull-cache's offline fallback \u2014\n // a persist-backed store is offline-first on its own, no client `cache` needed.\n if (classifyError(err) === \"network\") {\n set({ syncing: false, stale: true }, false, \"pull/offline\")\n return\n }\n set({ syncing: false, error: err instanceof Error ? err.message : String(err) }, false, \"pull/error\")\n }\n },\n\n mergeResult: async (result) => {\n await syncManager.ingest(result)\n commitRemote(\"merge/success\")\n },\n\n set: (modifier) => {\n try {\n const next = options.produce\n ? options.produce(get().data, modifier as (draft: Record<string, unknown>) => Record<string, unknown> | void)\n : modifier(get().data)\n // Fresh write: reset retry budget so this change gets a full set of attempts.\n retryAttempt = 0\n clearTimeout(retryTimer)\n set({ data: next, dirty: true, error: null }, false, \"set\")\n if (get().online) get().flush().catch(() => {})\n } catch (err) {\n set({ error: err instanceof Error ? err.message : String(err) }, false, \"set/error\")\n }\n },\n\n restore: (data) => {\n set({ data }, false, \"restore\")\n },\n\n flush: async () => {\n if (get().syncing || !get().dirty) return\n set({ syncing: true, error: null }, false, \"flush/start\")\n try {\n await syncManager.push(get().data)\n cancelFlushRetry()\n set({ data: syncManager.getData(), syncing: false, dirty: false, hash: syncManager.getHash(), stale: false }, false, \"flush/success\")\n } catch (err) {\n // AbortErrors (e.g. tab close, unmount) are not retriable.\n const isAbort = err instanceof Error && (err.name === \"AbortError\" ||\n (typeof DOMException !== \"undefined\" && err instanceof DOMException && err.name === \"AbortError\"))\n set({ syncing: false, error: err instanceof Error ? err.message : String(err) }, false, \"flush/error\")\n if (!isAbort) scheduleFlushRetry()\n }\n },\n\n setOnline: (online) => {\n set({ online }, false, \"setOnline\")\n if (online && get().dirty) {\n get().flush().catch(() => {})\n } else if (!online) {\n // Cancel pending retry \u2014 no point retrying while offline.\n cancelFlushRetry()\n }\n },\n }}\n\n const withPersist = storage === false\n ? storeCreator\n : persist(storeCreator, {\n name: `starfish-${name}`,\n storage: storage ? createJSONStorage(() => storage) : undefined,\n partialize: (state) => ({\n data: state.data,\n dirty: state.dirty,\n hash: state.hash,\n }),\n onRehydrateStorage: () => (state) => {\n // Only restore if the manager hasn't already received a hash from a live pull/push.\n // With async storage, pull() may resolve before hydration completes \u2014 the server's\n // hash always wins over the persisted one.\n if (state?.hash && syncManager.getHash() === null) syncManager.setHash(state.hash)\n },\n })\n\n const withSelector = subscribeWithSelector(withPersist)\n\n return createStore<StarfishStore>()(\n options.devtools ? options.devtools(withSelector) : withSelector,\n )\n}\n\n// \u2500\u2500 React hooks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Derived sync status for UI display. */\nexport type SyncStatus = \"synced\" | \"syncing\" | \"pending\" | \"error\" | \"offline\"\n\n/** Derive a single sync status from store state. */\nexport function deriveSyncStatus(state: StarfishState): SyncStatus {\n if (!state.online) return \"offline\"\n if (state.error) return \"error\"\n if (state.syncing) return \"syncing\"\n if (state.dirty) return \"pending\"\n return \"synced\"\n}\n\n/**\n * Aggregate multiple sync statuses into a single worst-case status.\n * Priority (worst first): error > syncing > pending > offline > synced.\n */\nexport function aggregateSyncStatus(statuses: SyncStatus[]): SyncStatus {\n if (statuses.includes(\"error\")) return \"error\"\n if (statuses.includes(\"syncing\")) return \"syncing\"\n if (statuses.includes(\"pending\")) return \"pending\"\n if (statuses.includes(\"offline\")) return \"offline\"\n return \"synced\"\n}\n\n/** Use the full Starfish store state and actions. */\nexport function useStarfish(store: StoreApi<StarfishStore>): StarfishStore {\n return useStore(store)\n}\n\n/** Subscribe to a fine-grained slice of Starfish store state. Avoids re-renders on unrelated field changes. */\nexport function useStarfishState<T>(\n store: StoreApi<StarfishStore>,\n selector: (state: StarfishState) => T,\n): T {\n return useStore(store, selector)\n}\n\n/** Use only the synced data, with an optional selector for fine-grained subscriptions. */\nexport function useStarfishData<T = Record<string, unknown>>(\n store: StoreApi<StarfishStore>,\n selector?: (data: Record<string, unknown>) => T,\n): T {\n return useStore(store, (state) =>\n selector ? selector(state.data) : (state.data as unknown as T),\n )\n}\n\n/** Use the derived sync status (synced | syncing | pending | error | offline). */\nexport function useSyncStatus(store: StoreApi<StarfishStore>): SyncStatus {\n return useStore(store, deriveSyncStatus)\n}\n\n/**\n * Subscribe to sync status changes outside of React.\n *\n * Framework-agnostic \u2014 works in React Native, Node.js, or anywhere hooks are unavailable.\n * The callback is invoked immediately with the current status and then on every change.\n *\n * ```ts\n * const unsub = subscribeSyncStatus(store, (status) => {\n * updateStatusBar(status)\n * })\n *\n * // Later, to stop listening:\n * unsub()\n * ```\n */\nexport function subscribeSyncStatus(\n store: StoreApi<StarfishStore>,\n callback: (status: SyncStatus) => void,\n): () => void {\n let prev = deriveSyncStatus(store.getState())\n callback(prev)\n return store.subscribe((state) => {\n const next = deriveSyncStatus(state)\n if (next !== prev) {\n prev = next\n callback(next)\n }\n })\n}\n\n/** Sets up cross-tab sync for a Starfish store. Cleans up on unmount. */\nexport function useCrossTabSync(\n store: StoreApi<StarfishStore>,\n name: string,\n): void {\n useEffect(() => {\n return setupCrossTabSync(store as unknown as BroadcastableStore, name)\n }, [store, name])\n}\n\n/** Binds browser online/offline events to the store's setOnline action. Cleans up on unmount. */\nexport function useConnectivity(store: StoreApi<StarfishStore>): void {\n useEffect(() => {\n const handleOnline = () => store.getState().setOnline(true)\n const handleOffline = () => store.getState().setOnline(false)\n\n window.addEventListener(\"online\", handleOnline)\n window.addEventListener(\"offline\", handleOffline)\n\n return () => {\n window.removeEventListener(\"online\", handleOnline)\n window.removeEventListener(\"offline\", handleOffline)\n }\n }, [store])\n}\n\n/** Returns a human-readable \"last synced\" label that updates every 5 seconds. */\nexport function useLastSynced(store: StoreApi<StarfishStore>): string {\n const lastSyncedAt = useRef<number | null>(null)\n const [label, setLabel] = useState(\"Never synced\")\n\n const computeLabel = useCallback(() => {\n if (lastSyncedAt.current === null) return \"Never synced\"\n const seconds = Math.floor((Date.now() - lastSyncedAt.current) / 1000)\n if (seconds < 10) return \"Just now\"\n if (seconds < 60) return `${seconds}s ago`\n return `${Math.floor(seconds / 60)}m ago`\n }, [])\n\n // Track sync completion\n useEffect(() => {\n let prevSyncing = store.getState().syncing\n const unsub = store.subscribe((state) => {\n if (prevSyncing && !state.syncing && !state.error) {\n lastSyncedAt.current = Date.now()\n setLabel(computeLabel())\n }\n prevSyncing = state.syncing\n })\n return unsub\n }, [store, computeLabel])\n\n // Update label periodically \u2014 skip when the tab is hidden\n useEffect(() => {\n const timer = setInterval(() => {\n if (!document.hidden) setLabel(computeLabel())\n }, 5000)\n return () => clearInterval(timer)\n }, [computeLabel])\n\n return label\n}\n\n// \u2500\u2500 SyncInitializer hook \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface SyncInitConfig {\n serverUrl: string\n /**\n * Optional server namespace, forwarded to the underlying {@link StarfishClient}\n * so `pullPath`/`pushPath` are rewritten to `/v1/<namespace>/\u2026` (signed AND sent).\n * Leave unset for a root-mounted server. Pass the bare name (e.g. `\"octochat\"`),\n * not `/v1/octochat` \u2014 the `/v1/` is added by the client.\n */\n namespace?: string\n capProvider?: StarfishCapProvider\n pullPath: string\n pushPath: string\n /** Pre-built encryptor for E2E collections (build via `createKeyringEncryptor`). */\n encryptor?: Encryptor\n onConflict?: ConflictResolver\n /** Called when pulled data arrives. Use to restore domain stores. */\n onData?: (data: Record<string, unknown>) => void\n storeName?: string\n storage?: StateStorage | false\n fetch?: typeof globalThis.fetch\n /**\n * Offline-first read-through cache for the underlying {@link StarfishClient}\n * (see {@link StarfishClientOptions.cache}). When set, the store seeds from the\n * last-synced ciphertext on creation (cache-first paint, decrypted in memory)\n * and the live pull falls back to it when the transport is unreachable; the\n * store's `stale` flag reflects whether the shown data is from cache.\n */\n cache?: PullCache\n /** Max age (ms) for {@link cache} entries; see {@link StarfishClientOptions.cacheMaxAgeMs}. */\n cacheMaxAgeMs?: number\n /**\n * HTTP status codes for which pulls fall back to the last-cached snapshot rather than\n * throwing \u2014 stale-while-revalidate for transient server failures.\n * See {@link StarfishClientOptions.cacheFallbackStatuses}.\n * Recommended set for offline-first apps: `[429, 500, 502, 503, 504]`.\n */\n cacheFallbackStatuses?: number[]\n /**\n * Called after a background revalidation following a {@link cacheFallbackStatuses} hit:\n * the server returned a live response and the fresh snapshot has been written through.\n * See {@link StarfishClientOptions.onRevalidated}.\n */\n onRevalidated?: StarfishClientOptions[\"onRevalidated\"]\n logger?: SyncLogger\n validate?: Validator\n}\n\n/**\n * React hook that manages the full Starfish sync lifecycle.\n *\n * Creates StarfishClient \u2192 SyncManager \u2192 Zustand store, pulls on mount,\n * calls `onData` when remote data arrives, and tears down on unmount or\n * config change.\n *\n * Pass `null` to disable sync (returns `null`).\n */\nexport function useSyncInit(config: SyncInitConfig | null): StoreApi<StarfishStore> | null {\n const [store, setStore] = useState<StoreApi<StarfishStore> | null>(null)\n const onDataRef = useRef(config?.onData)\n onDataRef.current = config?.onData\n\n useEffect(() => {\n if (!config) return\n\n const client = new StarfishClient({\n baseUrl: config.serverUrl,\n namespace: config.namespace,\n capProvider: config.capProvider,\n fetch: config.fetch,\n cache: config.cache,\n cacheMaxAgeMs: config.cacheMaxAgeMs,\n cacheFallbackStatuses: config.cacheFallbackStatuses,\n // Auto-merge: when a background revalidation delivers a fresh snapshot,\n // push it into the store so the UI heals without waiting for the next pull.\n // newStore is referenced by closure \u2014 safe because onRevalidated only fires\n // asynchronously, well after the store is created below.\n onRevalidated: (path, result) => {\n newStore.getState().mergeResult(result).catch(() => {})\n config.onRevalidated?.(path, result)\n },\n })\n\n const syncManager = new SyncManager({\n client,\n pullPath: config.pullPath,\n pushPath: config.pushPath,\n encryptor: config.encryptor,\n onConflict: config.onConflict,\n logger: config.logger,\n validate: config.validate,\n })\n\n const newStore = createStarfishStore({\n name: config.storeName ?? \"sync\",\n syncManager,\n storage: config.storage,\n // onRemoteUpdate fires only for pull() results, never for local set() writes \u2014\n // so no isRestoring flag is needed.\n onRemoteUpdate: (data) => {\n try {\n onDataRef.current?.(data)\n } catch (err) {\n newStore.setState({\n error: `onData failed: ${err instanceof Error ? err.message : String(err)}`,\n })\n }\n },\n })\n\n setStore(newStore)\n\n // Cache-first paint, then revalidate: seed synced data from the offline\n // cache (a fast local read; no-op without a cache) BEFORE the initial pull,\n // so the UI shows last-synced data immediately and the live pull supersedes\n // it. Errors in either are captured into state \u2014 never thrown here.\n newStore.getState().seed().finally(() => {\n newStore.getState().pull().catch(() => {})\n })\n\n return () => {\n setStore(null)\n }\n // Intentionally depend on serializable config values, not the object reference\n // eslint-disable-next-line react-doctor/exhaustive-deps\n }, [\n config?.serverUrl,\n config?.pullPath,\n config?.pushPath,\n config?.encryptor,\n config?.storeName,\n ])\n\n return config ? store : null\n}\n\n// \u2500\u2500 Shared sync-store registry \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Problem: `useSyncInit` mints a brand-new StarfishClient + SyncManager + zustand\n// store on every React mount \u2014 `storeName` is only a label, not a dedup key. When\n// multiple components mount the same logical document (e.g. a shared object index),\n// each gets its own store and each fires its own pull(), producing a request storm.\n//\n// Solution: a module-level Map keyed by `storeName`. The first acquire constructs\n// and seeds/pulls the store; subsequent acquires share the live instance (refCount++).\n// Release decrements; on 0 the entry is dropped and GC'd, mirroring useSyncInit's\n// own teardown (which just drops its local store ref \u2014 no explicit destroy needed).\n//\n// When to use useSharedSyncStore vs useSyncInit:\n// - useSharedSyncStore: same document, multiple components \u2014 guaranteed one pull,\n// shared live state, correct reactivity for every subscriber.\n// - useSyncInit: unique document per mount, or when you need per-instance onData.\n\n/**\n * Config for a shared sync store \u2014 identical to {@link SyncInitConfig} EXCEPT:\n * - `onData` is omitted: it is not safe to fan out a single `onRemoteUpdate`\n * callback to multiple independent subscribers. Consumers should instead\n * subscribe to the returned store via `store.subscribe(...)`.\n * - `storeName` is REQUIRED: it is the registry key, not an optional label.\n */\nexport type SharedSyncConfig = Omit<SyncInitConfig, \"onData\" | \"storeName\"> & {\n /** Registry key AND store persistence label. Required; there is no default. */\n storeName: string\n}\n\ninterface _RegistryEntry {\n store: StoreApi<StarfishStore>\n refCount: number\n}\n\nconst _syncStoreRegistry = new Map<string, _RegistryEntry>()\n\n/**\n * Return (or create) the shared zustand store for `config.storeName`.\n *\n * On the **first** acquire, constructs `StarfishClient` \u2192 `SyncManager` \u2192 store\n * (forwarding all config fields, including `cacheFallbackStatuses` and `onRevalidated`\n * for native stale-while-revalidate), then fires `seed().finally(pull())`. On every\n * subsequent acquire of the same `storeName`, the existing store is returned \u2014 **no**\n * new pull fires.\n *\n * Always pair with {@link releaseSyncStore}. Call {@link clearSyncStoreRegistry}\n * on account switch / sign-out.\n */\nexport function acquireSyncStore(config: SharedSyncConfig): StoreApi<StarfishStore> {\n const existing = _syncStoreRegistry.get(config.storeName)\n if (existing) {\n existing.refCount += 1\n return existing.store\n }\n\n const client = new StarfishClient({\n baseUrl: config.serverUrl,\n namespace: config.namespace,\n capProvider: config.capProvider,\n fetch: config.fetch,\n cache: config.cache,\n cacheMaxAgeMs: config.cacheMaxAgeMs,\n cacheFallbackStatuses: config.cacheFallbackStatuses,\n // Auto-merge: push fresh revalidated snapshots into the store.\n // store is referenced by closure \u2014 safe because onRevalidated only fires\n // asynchronously, well after the store is created below.\n onRevalidated: (path, result) => {\n store.getState().mergeResult(result).catch(() => {})\n config.onRevalidated?.(path, result)\n },\n })\n const syncManager = new SyncManager({\n client,\n pullPath: config.pullPath,\n pushPath: config.pushPath,\n encryptor: config.encryptor,\n onConflict: config.onConflict,\n logger: config.logger,\n validate: config.validate,\n })\n const store = createStarfishStore({\n name: config.storeName,\n syncManager,\n storage: config.storage,\n // No onRemoteUpdate: consumers subscribe via store.subscribe() \u2014 see module comment.\n })\n\n const entry: _RegistryEntry = { store, refCount: 1 }\n _syncStoreRegistry.set(config.storeName, entry)\n\n // Cache-first paint then one network pull, fire-and-forget:\n // - errors must NOT evict the entry (the store stays usable; SWR retries in background)\n // - identity guard stops a stale pull firing after a registry clear (account switch)\n store.getState().seed().finally(() => {\n if (_syncStoreRegistry.get(config.storeName) === entry) {\n store.getState().pull().catch(() => {})\n }\n })\n\n return store\n}\n\n/**\n * Release a previously acquired store. Decrements the refCount; on 0 the entry is\n * evicted \u2014 the store, client, and sync manager are dropped and GC'd (mirrors\n * `useSyncInit`'s own teardown, which simply drops the local store reference).\n */\nexport function releaseSyncStore(storeName: string): void {\n const entry = _syncStoreRegistry.get(storeName)\n if (!entry) return\n entry.refCount -= 1\n if (entry.refCount <= 0) _syncStoreRegistry.delete(storeName)\n}\n\n/**\n * Clear all registry entries.\n *\n * Call on account switch or sign-out alongside any other per-session cache clears.\n * An identity guard inside {@link acquireSyncStore} prevents any in-flight pull from\n * firing against the old session's cap after this is called.\n */\nexport function clearSyncStoreRegistry(): void {\n _syncStoreRegistry.clear()\n}\n\n/**\n * React hook that returns (or creates) the shared zustand store for\n * `config.storeName` \u2014 a drop-in replacement for {@link useSyncInit} when the\n * same logical document is consumed from multiple components.\n *\n * **Key design decision \u2014 effect deps include only `storeName`:** config identity\n * churn (fresh `capProvider`/`encryptor` refs per render) is intentionally ignored.\n * For a given `(user, space)` the cap and keyring are functionally equivalent across\n * refs, and no `onData` fan-out is needed, so the shared store never needs to rebuild\n * on churn. The `configRef` pattern ensures the latest config values are captured at\n * acquire-time without re-running the effect.\n *\n * Pass `null` to disable sync (returns `null`).\n */\nexport function useSharedSyncStore(\n config: SharedSyncConfig | null,\n): StoreApi<StarfishStore> | null {\n const [store, setStore] = useState<StoreApi<StarfishStore> | null>(null)\n const storeName = config?.storeName ?? null\n\n // Hold the latest config in a ref so the effect (keyed on storeName only) reads\n // current values at acquire-time without re-running on identity churn.\n const configRef = useRef<SharedSyncConfig | null>(config)\n configRef.current = config\n\n useEffect(() => {\n if (!storeName) return\n const acquired = acquireSyncStore(configRef.current!)\n setStore(acquired)\n return () => {\n releaseSyncStore(storeName)\n setStore(null)\n }\n // Intentionally depend only on storeName \u2014 see JSDoc above.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [storeName])\n\n return storeName ? store : null\n}\n\n\n//\n// The reactive counterpart for an append-only collection, backed by an\n// `AppendLogCursor` instead of a `SyncManager`. A log only grows, so the\n// store is read-only \u2014 there is no `set`/`flush`/`dirty`/conflict surface,\n// and no `persist` middleware: the cursor owns the items + checkpoint, so\n// persist by reading `getItems()` and rehydrate by constructing the cursor\n// with `initialItems` (see `AppendLogCursor`).\n//\n// The store assumes it is the SOLE driver of its cursor: it seeds `items` from\n// `cursor.getItems()` at construction and updates only via its own `pull()`.\n// Don't also call `cursor.pull()` directly on the same cursor, or the store's\n// `items`/`checkpoint` will go stale.\n\nexport interface StarfishLogState {\n /** The full accumulated log, newest appended last. */\n items: AppendElement[]\n /** A `pull()` is in flight. */\n loading: boolean\n online: boolean\n error: string | null\n /** The cursor's checkpoint (max `ts` held). */\n checkpoint: number\n}\n\nexport interface StarfishLogActions {\n /** Pull elements newer than the checkpoint, append them, and return the new\n * batch. Errors are captured into `error` (mirroring the SyncManager store). */\n pull: () => Promise<AppendElement[]>\n setOnline: (online: boolean) => void\n}\n\nexport type StarfishLogStore = StarfishLogState & StarfishLogActions\n\nexport interface CreateStarfishLogOptions {\n cursor: AppendLogCursor\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n devtools?: (storeCreator: any) => any\n}\n\nexport function createStarfishLog(\n options: CreateStarfishLogOptions,\n): StoreApi<StarfishLogStore> {\n const { cursor } = options\n\n type NamedSet = (partial: Partial<StarfishLogStore>, replace?: boolean, action?: string) => void\n\n const storeCreator = (\n rawSet: StoreApi<StarfishLogStore>[\"setState\"],\n get: StoreApi<StarfishLogStore>[\"getState\"],\n ): StarfishLogStore => {\n const set = rawSet as NamedSet\n return {\n // Seed from the cursor so a warm-started cursor's items show immediately.\n items: cursor.getItems(),\n loading: false,\n online: true,\n error: null,\n checkpoint: cursor.getCheckpoint(),\n\n pull: async () => {\n if (get().loading) return []\n set({ loading: true, error: null }, false, \"log/pull/start\")\n try {\n const batch = await cursor.pull()\n set(\n { items: cursor.getItems(), checkpoint: cursor.getCheckpoint(), loading: false },\n false,\n \"log/pull/success\",\n )\n return batch\n } catch (err) {\n set({ loading: false, error: err instanceof Error ? err.message : String(err) }, false, \"log/pull/error\")\n return []\n }\n },\n\n setOnline: (online) => {\n set({ online }, false, \"log/setOnline\")\n },\n }\n }\n\n const withSelector = subscribeWithSelector(storeCreator)\n return createStore<StarfishLogStore>()(\n options.devtools ? options.devtools(withSelector) : withSelector,\n )\n}\n\n/** Derived status for an append-log store. */\nexport type LogStatus = \"idle\" | \"loading\" | \"error\" | \"offline\"\n\n/** Derive a single status from log store state. */\nexport function deriveLogStatus(state: StarfishLogState): LogStatus {\n if (!state.online) return \"offline\"\n if (state.error) return \"error\"\n if (state.loading) return \"loading\"\n return \"idle\"\n}\n\n/** Use the full append-log store state and actions. */\nexport function useStarfishLog(store: StoreApi<StarfishLogStore>): StarfishLogStore {\n return useStore(store)\n}\n\n/** Use only the accumulated items, with an optional selector for fine-grained subscriptions. */\nexport function useStarfishLogItems<T = AppendElement[]>(\n store: StoreApi<StarfishLogStore>,\n selector?: (items: AppendElement[]) => T,\n): T {\n return useStore(store, (state) =>\n selector ? selector(state.items) : (state.items as unknown as T),\n )\n}\n\n/** Use the derived log status (idle | loading | error | offline). */\nexport function useLogStatus(store: StoreApi<StarfishLogStore>): LogStatus {\n return useStore(store, deriveLogStatus)\n}\n\n/** Subscribe to log status changes outside of React. Invoked immediately with the\n * current status, then on every change. Returns an unsubscribe function. */\nexport function subscribeLogStatus(\n store: StoreApi<StarfishLogStore>,\n callback: (status: LogStatus) => void,\n): () => void {\n let prev = deriveLogStatus(store.getState())\n callback(prev)\n return store.subscribe((state) => {\n const next = deriveLogStatus(state)\n if (next !== prev) {\n prev = next\n callback(next)\n }\n })\n}\n\n/** Binds browser online/offline events to the log store's setOnline action. Cleans up on unmount. */\nexport function useLogConnectivity(store: StoreApi<StarfishLogStore>): void {\n useEffect(() => {\n const handleOnline = () => store.getState().setOnline(true)\n const handleOffline = () => store.getState().setOnline(false)\n window.addEventListener(\"online\", handleOnline)\n window.addEventListener(\"offline\", handleOffline)\n return () => {\n window.removeEventListener(\"online\", handleOnline)\n window.removeEventListener(\"offline\", handleOffline)\n }\n }, [store])\n}\n", "const reduxImpl = (reducer, initial) => (set, _get, api) => {\n api.dispatch = (action) => {\n set((state) => reducer(state, action), false, action);\n return action;\n };\n api.dispatchFromDevtools = true;\n return { dispatch: (...args) => api.dispatch(...args), ...initial };\n};\nconst redux = reduxImpl;\n\nconst shouldDispatchFromDevtools = (api) => !!api.dispatchFromDevtools && typeof api.dispatch === \"function\";\nconst trackedConnections = /* @__PURE__ */ new Map();\nconst getTrackedConnectionState = (name) => {\n const api = trackedConnections.get(name);\n if (!api) return {};\n return Object.fromEntries(\n Object.entries(api.stores).map(([key, api2]) => [key, api2.getState()])\n );\n};\nconst extractConnectionInformation = (store, extensionConnector, options) => {\n if (store === void 0) {\n return {\n type: \"untracked\",\n connection: extensionConnector.connect(options)\n };\n }\n const existingConnection = trackedConnections.get(options.name);\n if (existingConnection) {\n return { type: \"tracked\", store, ...existingConnection };\n }\n const newConnection = {\n connection: extensionConnector.connect(options),\n stores: {}\n };\n trackedConnections.set(options.name, newConnection);\n return { type: \"tracked\", store, ...newConnection };\n};\nconst removeStoreFromTrackedConnections = (name, store) => {\n if (store === void 0) return;\n const connectionInfo = trackedConnections.get(name);\n if (!connectionInfo) return;\n delete connectionInfo.stores[store];\n if (Object.keys(connectionInfo.stores).length === 0) {\n trackedConnections.delete(name);\n }\n};\nconst findCallerName = (stack) => {\n var _a, _b;\n if (!stack) return void 0;\n const traceLines = stack.split(\"\\n\");\n const apiSetStateLineIndex = traceLines.findIndex(\n (traceLine) => traceLine.includes(\"api.setState\")\n );\n if (apiSetStateLineIndex < 0) return void 0;\n const callerLine = ((_a = traceLines[apiSetStateLineIndex + 1]) == null ? void 0 : _a.trim()) || \"\";\n return (_b = /.+ (.+) .+/.exec(callerLine)) == null ? void 0 : _b[1];\n};\nconst devtoolsImpl = (fn, devtoolsOptions = {}) => (set, get, api) => {\n const { enabled, anonymousActionType, store, ...options } = devtoolsOptions;\n let extensionConnector;\n try {\n extensionConnector = (enabled != null ? enabled : (import.meta.env ? import.meta.env.MODE : void 0) !== \"production\") && window.__REDUX_DEVTOOLS_EXTENSION__;\n } catch (e) {\n }\n if (!extensionConnector) {\n return fn(set, get, api);\n }\n const { connection, ...connectionInformation } = extractConnectionInformation(store, extensionConnector, options);\n let isRecording = true;\n api.setState = ((state, replace, nameOrAction) => {\n const r = set(state, replace);\n if (!isRecording) return r;\n const action = nameOrAction === void 0 ? {\n type: anonymousActionType || findCallerName(new Error().stack) || \"anonymous\"\n } : typeof nameOrAction === \"string\" ? { type: nameOrAction } : nameOrAction;\n if (store === void 0) {\n connection == null ? void 0 : connection.send(action, get());\n return r;\n }\n connection == null ? void 0 : connection.send(\n {\n ...action,\n type: `${store}/${action.type}`\n },\n {\n ...getTrackedConnectionState(options.name),\n [store]: api.getState()\n }\n );\n return r;\n });\n api.devtools = {\n cleanup: () => {\n if (connection && typeof connection.unsubscribe === \"function\") {\n connection.unsubscribe();\n }\n removeStoreFromTrackedConnections(options.name, store);\n }\n };\n const setStateFromDevtools = (...a) => {\n const originalIsRecording = isRecording;\n isRecording = false;\n set(...a);\n isRecording = originalIsRecording;\n };\n const initialState = fn(api.setState, get, api);\n if (connectionInformation.type === \"untracked\") {\n connection == null ? void 0 : connection.init(initialState);\n } else {\n connectionInformation.stores[connectionInformation.store] = api;\n connection == null ? void 0 : connection.init(\n Object.fromEntries(\n Object.entries(connectionInformation.stores).map(([key, store2]) => [\n key,\n key === connectionInformation.store ? initialState : store2.getState()\n ])\n )\n );\n }\n if (shouldDispatchFromDevtools(api)) {\n let didWarnAboutReservedActionType = false;\n const originalDispatch = api.dispatch;\n api.dispatch = (...args) => {\n if ((import.meta.env ? import.meta.env.MODE : void 0) !== \"production\" && args[0].type === \"__setState\" && !didWarnAboutReservedActionType) {\n console.warn(\n '[zustand devtools middleware] \"__setState\" action type is reserved to set state from the devtools. Avoid using it.'\n );\n didWarnAboutReservedActionType = true;\n }\n originalDispatch(...args);\n };\n }\n connection.subscribe((message) => {\n var _a;\n switch (message.type) {\n case \"ACTION\":\n if (typeof message.payload !== \"string\") {\n console.error(\n \"[zustand devtools middleware] Unsupported action format\"\n );\n return;\n }\n return parseJsonThen(\n message.payload,\n (action) => {\n if (action.type === \"__setState\") {\n if (store === void 0) {\n setStateFromDevtools(action.state);\n return;\n }\n if (Object.keys(action.state).length !== 1) {\n console.error(\n `\n [zustand devtools middleware] Unsupported __setState action format.\n When using 'store' option in devtools(), the 'state' should have only one key, which is a value of 'store' that was passed in devtools(),\n and value of this only key should be a state object. Example: { \"type\": \"__setState\", \"state\": { \"abc123Store\": { \"foo\": \"bar\" } } }\n `\n );\n }\n const stateFromDevtools = action.state[store];\n if (stateFromDevtools === void 0 || stateFromDevtools === null) {\n return;\n }\n if (JSON.stringify(api.getState()) !== JSON.stringify(stateFromDevtools)) {\n setStateFromDevtools(stateFromDevtools);\n }\n return;\n }\n if (shouldDispatchFromDevtools(api)) {\n api.dispatch(action);\n }\n }\n );\n case \"DISPATCH\":\n switch (message.payload.type) {\n case \"RESET\":\n setStateFromDevtools(initialState);\n if (store === void 0) {\n return connection == null ? void 0 : connection.init(api.getState());\n }\n return connection == null ? void 0 : connection.init(getTrackedConnectionState(options.name));\n case \"COMMIT\":\n if (store === void 0) {\n connection == null ? void 0 : connection.init(api.getState());\n return;\n }\n return connection == null ? void 0 : connection.init(getTrackedConnectionState(options.name));\n case \"ROLLBACK\":\n return parseJsonThen(message.state, (state) => {\n if (store === void 0) {\n setStateFromDevtools(state);\n connection == null ? void 0 : connection.init(api.getState());\n return;\n }\n setStateFromDevtools(state[store]);\n connection == null ? void 0 : connection.init(getTrackedConnectionState(options.name));\n });\n case \"JUMP_TO_STATE\":\n case \"JUMP_TO_ACTION\":\n return parseJsonThen(message.state, (state) => {\n if (store === void 0) {\n setStateFromDevtools(state);\n return;\n }\n if (JSON.stringify(api.getState()) !== JSON.stringify(state[store])) {\n setStateFromDevtools(state[store]);\n }\n });\n case \"IMPORT_STATE\": {\n const { nextLiftedState } = message.payload;\n const lastComputedState = (_a = nextLiftedState.computedStates.slice(-1)[0]) == null ? void 0 : _a.state;\n if (!lastComputedState) return;\n if (store === void 0) {\n setStateFromDevtools(lastComputedState);\n } else {\n setStateFromDevtools(lastComputedState[store]);\n }\n connection == null ? void 0 : connection.send(\n null,\n // FIXME no-any\n nextLiftedState\n );\n return;\n }\n case \"PAUSE_RECORDING\":\n return isRecording = !isRecording;\n }\n return;\n }\n });\n return initialState;\n};\nconst devtools = devtoolsImpl;\nconst parseJsonThen = (stringified, fn) => {\n let parsed;\n try {\n parsed = JSON.parse(stringified);\n } catch (e) {\n console.error(\n \"[zustand devtools middleware] Could not parse the received json\",\n e\n );\n }\n if (parsed !== void 0) fn(parsed);\n};\n\nconst subscribeWithSelectorImpl = (fn) => (set, get, api) => {\n const origSubscribe = api.subscribe;\n api.subscribe = ((selector, optListener, options) => {\n let listener = selector;\n if (optListener) {\n const equalityFn = (options == null ? void 0 : options.equalityFn) || Object.is;\n let currentSlice = selector(api.getState());\n listener = (state) => {\n const nextSlice = selector(state);\n if (!equalityFn(currentSlice, nextSlice)) {\n const previousSlice = currentSlice;\n optListener(currentSlice = nextSlice, previousSlice);\n }\n };\n if (options == null ? void 0 : options.fireImmediately) {\n optListener(currentSlice, currentSlice);\n }\n }\n return origSubscribe(listener);\n });\n const initialState = fn(set, get, api);\n return initialState;\n};\nconst subscribeWithSelector = subscribeWithSelectorImpl;\n\nfunction combine(initialState, create) {\n return (...args) => Object.assign({}, initialState, create(...args));\n}\n\nfunction createJSONStorage(getStorage, options) {\n let storage;\n try {\n storage = getStorage();\n } catch (e) {\n return;\n }\n const persistStorage = {\n getItem: (name) => {\n var _a;\n const parse = (str2) => {\n if (str2 === null) {\n return null;\n }\n return JSON.parse(str2, options == null ? void 0 : options.reviver);\n };\n const str = (_a = storage.getItem(name)) != null ? _a : null;\n if (str instanceof Promise) {\n return str.then(parse);\n }\n return parse(str);\n },\n setItem: (name, newValue) => storage.setItem(name, JSON.stringify(newValue, options == null ? void 0 : options.replacer)),\n removeItem: (name) => storage.removeItem(name)\n };\n return persistStorage;\n}\nconst toThenable = (fn) => (input) => {\n try {\n const result = fn(input);\n if (result instanceof Promise) {\n return result;\n }\n return {\n then(onFulfilled) {\n return toThenable(onFulfilled)(result);\n },\n catch(_onRejected) {\n return this;\n }\n };\n } catch (e) {\n return {\n then(_onFulfilled) {\n return this;\n },\n catch(onRejected) {\n return toThenable(onRejected)(e);\n }\n };\n }\n};\nconst persistImpl = (config, baseOptions) => (set, get, api) => {\n let options = {\n storage: createJSONStorage(() => window.localStorage),\n partialize: (state) => state,\n version: 0,\n merge: (persistedState, currentState) => ({\n ...currentState,\n ...persistedState\n }),\n ...baseOptions\n };\n let hasHydrated = false;\n let hydrationVersion = 0;\n const hydrationListeners = /* @__PURE__ */ new Set();\n const finishHydrationListeners = /* @__PURE__ */ new Set();\n let storage = options.storage;\n if (!storage) {\n return config(\n (...args) => {\n console.warn(\n `[zustand persist middleware] Unable to update item '${options.name}', the given storage is currently unavailable.`\n );\n set(...args);\n },\n get,\n api\n );\n }\n const setItem = () => {\n const state = options.partialize({ ...get() });\n return storage.setItem(options.name, {\n state,\n version: options.version\n });\n };\n const savedSetState = api.setState;\n api.setState = (state, replace) => {\n savedSetState(state, replace);\n return setItem();\n };\n const configResult = config(\n (...args) => {\n set(...args);\n return setItem();\n },\n get,\n api\n );\n api.getInitialState = () => configResult;\n let stateFromStorage;\n const hydrate = () => {\n var _a, _b;\n if (!storage) return;\n const currentVersion = ++hydrationVersion;\n hasHydrated = false;\n hydrationListeners.forEach((cb) => {\n var _a2;\n return cb((_a2 = get()) != null ? _a2 : configResult);\n });\n const postRehydrationCallback = ((_b = options.onRehydrateStorage) == null ? void 0 : _b.call(options, (_a = get()) != null ? _a : configResult)) || void 0;\n return toThenable(storage.getItem.bind(storage))(options.name).then((deserializedStorageValue) => {\n if (deserializedStorageValue) {\n if (typeof deserializedStorageValue.version === \"number\" && deserializedStorageValue.version !== options.version) {\n if (options.migrate) {\n const migration = options.migrate(\n deserializedStorageValue.state,\n deserializedStorageValue.version\n );\n if (migration instanceof Promise) {\n return migration.then((result) => [true, result]);\n }\n return [true, migration];\n }\n console.error(\n `State loaded from storage couldn't be migrated since no migrate function was provided`\n );\n } else {\n return [false, deserializedStorageValue.state];\n }\n }\n return [false, void 0];\n }).then((migrationResult) => {\n var _a2;\n if (currentVersion !== hydrationVersion) {\n return;\n }\n const [migrated, migratedState] = migrationResult;\n stateFromStorage = options.merge(\n migratedState,\n (_a2 = get()) != null ? _a2 : configResult\n );\n set(stateFromStorage, true);\n if (migrated) {\n return setItem();\n }\n }).then(() => {\n if (currentVersion !== hydrationVersion) {\n return;\n }\n postRehydrationCallback == null ? void 0 : postRehydrationCallback(stateFromStorage, void 0);\n stateFromStorage = get();\n hasHydrated = true;\n finishHydrationListeners.forEach((cb) => cb(stateFromStorage));\n }).catch((e) => {\n if (currentVersion !== hydrationVersion) {\n return;\n }\n postRehydrationCallback == null ? void 0 : postRehydrationCallback(void 0, e);\n });\n };\n api.persist = {\n setOptions: (newOptions) => {\n options = {\n ...options,\n ...newOptions\n };\n if (newOptions.storage) {\n storage = newOptions.storage;\n }\n },\n clearStorage: () => {\n storage == null ? void 0 : storage.removeItem(options.name);\n },\n getOptions: () => options,\n rehydrate: () => hydrate(),\n hasHydrated: () => hasHydrated,\n onHydrate: (cb) => {\n hydrationListeners.add(cb);\n return () => {\n hydrationListeners.delete(cb);\n };\n },\n onFinishHydration: (cb) => {\n finishHydrationListeners.add(cb);\n return () => {\n finishHydrationListeners.delete(cb);\n };\n }\n };\n if (!options.skipHydration) {\n hydrate();\n }\n return stateFromStorage || configResult;\n};\nconst persist = persistImpl;\n\nfunction ssrSafe(config, isSSR = typeof window === \"undefined\") {\n return (set, get, api) => {\n if (!isSSR) {\n return config(set, get, api);\n }\n const ssrSet = () => {\n throw new Error(\"Cannot set state of Zustand store in SSR\");\n };\n api.setState = ssrSet;\n return config(ssrSet, get, api);\n };\n}\n\nexport { combine, createJSONStorage, devtools, persist, redux, subscribeWithSelector, ssrSafe as unstable_ssrSafe };\n", "import type { PullResult, PushSuccess } from \"@drakkar.software/starfish-protocol\"\nimport {\n AUTHOR_PUBKEY_FIELD,\n AUTHOR_SIGNATURE_FIELD,\n DATA_FIELD,\n TS_FIELD,\n BASE_HASH_FIELD,\n PUSH_PATH_PREFIX,\n HEADER_AUTHORIZATION,\n HEADER_SIG,\n HEADER_TS,\n HEADER_NONCE,\n HEADER_PUB,\n HEADER_CONTENT_TYPE,\n HEADER_ACCEPT,\n signAppendAuthor,\n signRequest,\n stableStringify,\n type AppendAuthor,\n type SignableMethod,\n type SignableRequest,\n} from \"@drakkar.software/starfish-protocol\"\nimport type {\n StarfishClientOptions,\n StarfishCapProvider,\n PullCache,\n} from \"./types.js\"\nimport { AppendHttpError, ConflictError, StarfishHttpError } from \"./types.js\"\nimport { parseRetryAfterMs } from \"./fetch.js\"\n\nconst APPEND_DEFAULT_FIELD = \"items\"\nconst MAX_REVALIDATE_ATTEMPTS = 5\nconst REVALIDATE_INITIAL_DELAY_MS = 1_000\nconst REVALIDATE_MAX_DELAY_MS = 30_000\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/**\n * Shape persisted in a {@link PullCache} for one document: the raw server\n * `PullResult` fields. For E2E collections `data` is the sealed ciphertext.\n */\ninterface CachedPull {\n data: Record<string, unknown>\n hash: string\n timestamp: number\n /** Wall-clock ms when this snapshot was written \u2014 for {@link StarfishClientOptions.cacheMaxAgeMs} expiry. */\n cachedAt: number\n}\n\n/**\n * The cache key for a pull `pathAndQuery`: the path with any query string\n * dropped, so a checkpoint'd or `withKeyring` pull and a plain pull of the same\n * document share one stable key (the document identity, not the request shape).\n */\nfunction pullCacheKey(pathAndQuery: string): string {\n const q = pathAndQuery.indexOf(\"?\")\n return q === -1 ? pathAndQuery : pathAndQuery.slice(0, q)\n}\n\n/**\n * Whether a {@link PullResult} was served from the offline read-through cache\n * (the transport was unreachable) rather than a live server response. Used by\n * {@link SyncManager} to surface a `stale` flag to the UI without treating a\n * cache hit as proof the server is reachable.\n */\nexport function pullWasFromCache(result: PullResult): boolean {\n return (result as { fromCache?: boolean }).fromCache === true\n}\n\n/** The storage `documentKey` for a push `path`: the path with the `/push/`\n * action prefix stripped (the namespace lives only in the URL). The author\n * signature binds to this key. */\nexport function stripPushPrefix(path: string): string {\n return path.startsWith(PUSH_PATH_PREFIX) ? path.slice(PUSH_PATH_PREFIX.length) : path\n}\n\n/** Result of pulling a binary blob from the server. */\nexport interface BlobPullResult {\n data: ArrayBuffer\n /** Content hash from the ETag header. Null if the server didn't include an ETag. */\n hash: string | null\n contentType: string\n}\n\n/** Result of pushing a binary blob to the server. */\nexport interface BlobPushResult {\n hash: string\n}\n\n/** Options for append-only pull \u2014 extracts a single array field from the response. */\nexport interface AppendPullOptions {\n /** Array field name in `data`. Defaults to `\"items\"`. */\n appendField?: string\n /** Only return items appended after this timestamp (ms). Sent as `?checkpoint=`. */\n since?: number\n /** Return only the last K items (applied after `since` filter). Sent as `?last=`. */\n last?: number\n /** Return only the last K items. Alias of `last`; sent as `?limit=`. When both\n * are given, `limit` wins. */\n limit?: number\n /** Explicitly fetch the whole collection (sent as `?full=true`). Mutually\n * exclusive with `since`/`limit`/`last` \u2014 the server requires a pull to declare\n * exactly one of {checkpoint, limit/last, full}. */\n full?: boolean\n}\n\n/**\n * Options for a structured (non-append) pull.\n *\n * `withKeyring: true` appends `?withKeyring=1` so the server includes the\n * collection's sibling `<collection>/_keyring` document in the response,\n * saving a cold-start round-trip. The cap-cert scope MUST cover BOTH the\n * data path and `<collection>/_keyring` \u2014 `scopes.writer(collection)` denies\n * the keyring path and will produce a 403; use `scopes.readWrite()` or grant\n * the keyring path explicitly when opting in.\n */\nexport interface PullOptions {\n /** Server timestamp of the last successful pull (ms). Sent as `?checkpoint=`. */\n checkpoint?: number\n /** Include the sibling `_keyring` document in the response. Defaults to false. */\n withKeyring?: boolean\n /**\n * Serve the last-synced cached snapshot immediately (tagged via\n * {@link pullWasFromCache}) and revalidate in the background. Requires a\n * {@link StarfishClientOptions.cache} to be configured; without one the option\n * is a no-op and the pull goes to the network as usual.\n *\n * On a cache hit: returns the stale snapshot at once, kicks a background fetch,\n * and on success writes the fresh snapshot to cache and fires\n * {@link StarfishClientOptions.onRevalidated}. Uses the same dedup set as the\n * {@link StarfishClientOptions.cacheFallbackStatuses} revalidation path \u2014 a\n * concurrent error-triggered loop for the same document is not duplicated.\n *\n * On a cache miss: falls through to the normal network-first pull unchanged.\n */\n staleWhileRevalidate?: boolean\n}\n\n/** Per-collection result in a {@link BatchPullResult}: either the pulled\n * document (`data`/`hash`/`timestamp`) or a per-collection `error` string. */\nexport interface BatchPullEntry {\n data?: unknown\n hash?: string\n timestamp?: number\n error?: string\n}\n\n/** Response of {@link StarfishClient.batchPull}: a map of requested collection\n * name \u2192 an ARRAY of {@link BatchPullEntry}, one per requested param-set, in\n * request order. A collection read with no params yields a one-element array. */\nexport interface BatchPullResult {\n collections: Record<string, BatchPullEntry[]>\n}\n\n/** Options for {@link StarfishClient.batchPull}. */\nexport interface BatchPullOptions {\n /** Per-collection path params: collection name \u2192 an ARRAY of param-sets, one\n * per document to read from that collection, e.g.\n * `{ profile: [{ identity: \"a\" }, { identity: \"b\" }] }` reads two profiles in\n * one round-trip. Serialized to a URL-encoded JSON `params` query. The\n * `{identity}` param is auto-filled by the server from the authenticated\n * caller when a set omits it, so a single self-doc read can pass `[{}]` \u2014 or\n * omit the collection from `params` entirely (an unlisted collection reads one\n * auto-filled doc). Results come back under the same name in request order. */\n params?: Record<string, Record<string, string>[]>\n /**\n * Per-collection append options, index-aligned to `params`. Makes the batch\n * request **append/checkpoint-aware**: each entry returns the bounded tail of\n * that collection's append-only log rather than the full document.\n *\n * Serialized as URL-encoded JSON alongside `params`. Server ignores it for\n * collections that are not append-only (returns `{ error: \"append_params_not_supported\" }`\n * for those entries). `full` is disallowed in batch (`full_not_allowed` per entry).\n *\n * Example \u2014 read the last 5 events for two rooms and the newest item for a third:\n * ```ts\n * await client.batchPull([\"events\"], {\n * params: { events: [{ room: \"a\" }, { room: \"b\" }, { room: \"c\" }] },\n * appendParams: { events: [{ last: 5 }, { last: 5 }, { last: 1 }] },\n * })\n * ```\n * Each `data[appendField]` in the result is the filtered array for that entry.\n */\n appendParams?: Record<string, AppendPullOptions[]>\n}\n\n/**\n * Base64-encode the canonical stable-stringification of a cap-cert.\n *\n * Used as the value of the `Authorization: Cap <\u2026>` header in v3.0. We rely\n * on the host's `btoa` for browsers and fall back to `Buffer` in Node so the\n * client stays free of native dependencies.\n */\nfunction encodeCapAuth(cap: unknown): string {\n const json = stableStringify(cap as Record<string, unknown>)\n if (typeof btoa === \"function\") {\n return btoa(json)\n }\n const bufCtor = (globalThis as { Buffer?: { from: (s: string, enc: string) => { toString: (enc: string) => string } } }).Buffer\n if (bufCtor) return bufCtor.from(json, \"utf-8\").toString(\"base64\")\n throw new Error(\"No base64 encoder available\")\n}\n\n/**\n * Low-level HTTP client for the Starfish sync protocol.\n * Handles auth headers and response parsing.\n */\nexport class StarfishClient {\n private readonly baseUrl: string\n private readonly namespace?: string\n private readonly capProvider?: StarfishCapProvider\n private readonly fetch: typeof globalThis.fetch\n private readonly cache?: PullCache\n private readonly cacheMaxAgeMs?: number\n private readonly cacheFallbackStatuses?: ReadonlyArray<number>\n private readonly onRevalidated?: (path: string, result: PullResult) => void\n private readonly revalidating = new Set<string>()\n /**\n * In-memory mirror of the latest document timestamp written to each cache\n * key via {@link writeCache}. Updated synchronously so {@link revalidateLoop}\n * can guard against stale overwrites without an extra async cache read.\n */\n private readonly latestCacheTimestamp = new Map<string, number>()\n /**\n * Installed client-side plugins. Currently stored as inert data; no\n * hooks fire yet. Extensions can inspect this list if needed.\n */\n public readonly plugins: ReadonlyArray<import(\"./types.js\").ClientPlugin>\n\n constructor(options: StarfishClientOptions) {\n this.baseUrl = options.baseUrl.replace(/\\/$/, \"\")\n // Empty string \u21D2 no namespace (treat like unset), so a falsy env value\n // doesn't produce a malformed `/v1//\u2026` path.\n this.namespace = options.namespace || undefined\n this.capProvider = options.capProvider\n this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis)\n this.cache = options.cache\n this.cacheMaxAgeMs = options.cacheMaxAgeMs\n this.cacheFallbackStatuses = options.cacheFallbackStatuses\n this.onRevalidated = options.onRevalidated\n this.plugins = options.plugins ? [...options.plugins] : []\n }\n\n /**\n * Mark a `PullResult` as having been served from the offline read-through\n * cache (transport was unreachable). Non-enumerable so it doesn't leak into\n * JSON / equality / re-caching; read via {@link pullWasFromCache}.\n */\n private tagFromCache(result: PullResult): PullResult {\n Object.defineProperty(result, \"fromCache\", { value: true, enumerable: false })\n return result\n }\n\n /**\n * Resolve the host portion of the URL the client will send to. The host\n * is folded into the signed canonical input as the `h` field so the\n * server can refuse a signature that was minted against a different\n * Starfish host (replay-across-servers defence).\n *\n * When `baseUrl` is relative \u2014 e.g. the consumer passed a custom `fetch`\n * that resolves relative URLs in its own context \u2014 there is no parseable\n * host; we return `\"\"` so signing still proceeds. The server-side\n * verifier will also reconstruct host from its inbound URL, so the\n * empty-host case still verifies symmetrically when both sides agree.\n */\n private signingHost(): string {\n try {\n return new URL(this.baseUrl).host\n } catch {\n return \"\"\n }\n }\n\n /**\n * Rewrite a request path for the configured namespace. A no-op when no\n * namespace is set; otherwise `/{action}/\u2026` becomes `/v1/{namespace}/{action}/\u2026`\n * (the `/v1` protocol-version segment is part of the namespaced route, matching\n * the Python client and the server's namespace mount).\n *\n * Applied to the path used for BOTH the signature and the URL so the canonical\n * path the client signs equals the path the server reconstructs from the URL.\n * Covers SDK-helper-built paths too \u2014 that's the point: a namespace-unaware\n * helper passing `/push/spaces/x/_keyring` reaches `/v1/{ns}/push/spaces/x/_keyring`.\n */\n private applyNamespace(path: string): string {\n return this.namespace ? `/v1/${this.namespace}${path}` : path\n }\n\n /**\n * Build auth headers for a request. When a `capProvider` is set, signs the\n * request with the device's Ed25519 private key and returns the v3 header\n * set (`Authorization: Cap \u2026`, `X-Starfish-Sig`, `X-Starfish-Ts`,\n * `X-Starfish-Nonce`). Empty when no provider is configured (public reads).\n *\n * Body bytes signed MUST equal the bytes sent on the wire \u2014 callers pass\n * the already-serialized body string here so signing and transmission agree.\n * The host bound into the signature is derived from `baseUrl` once per call.\n */\n private async buildAuthHeaders(\n method: SignableMethod,\n pathAndQuery: string,\n body: string | undefined,\n ): Promise<Record<string, string>> {\n if (!this.capProvider) return {}\n const capCtx = await this.capProvider.getCap()\n return this.capRequestHeaders(capCtx, method, pathAndQuery, body)\n }\n\n /**\n * Build the request-signing headers from an ALREADY-fetched cap context. Split\n * out of {@link buildAuthHeaders} so {@link append} can fetch the cap once and\n * reuse it for BOTH the author signature (over the element data) and the\n * request signature (over the body), without redeeming the cap twice \u2014 a\n * second `getCap()` could rotate keys and break the `authorPubkey ===\n * presenter` bind the server checks.\n */\n private async capRequestHeaders(\n capCtx: Awaited<ReturnType<StarfishCapProvider[\"getCap\"]>>,\n method: SignableMethod,\n pathAndQuery: string,\n body: string | undefined,\n ): Promise<Record<string, string>> {\n const { cap, devEdPrivHex, pubHex } = capCtx\n const req: SignableRequest = {\n method,\n pathAndQuery,\n body,\n host: this.signingHost(),\n }\n const { sig, ts, nonce } = await signRequest(req, devEdPrivHex)\n const headers: Record<string, string> = {\n [HEADER_AUTHORIZATION]: `Cap ${encodeCapAuth(cap)}`,\n [HEADER_SIG]: sig,\n [HEADER_TS]: String(ts),\n [HEADER_NONCE]: nonce,\n }\n // Audience (public-link) caps bind no single subject, so the server needs\n // the presenter's pubkey to verify the signature and check the allow-list.\n if (pubHex !== undefined) headers[HEADER_PUB] = pubHex\n return headers\n }\n\n /**\n * Resolve the author public key to attach to a signed append: the redeemer's\n * `pubHex` for an audience cap, else the cert subject `cap.sub` for a\n * device/member cap. This is the SAME key that signs the request, so a server\n * enforcing author proof can bind the stored element to its writer. Returns\n * undefined only for a (malformed) cap with neither \u2014 the append then goes\n * unsigned and a server requiring signatures rejects it.\n */\n private appendAuthorKey(\n capCtx: Awaited<ReturnType<StarfishCapProvider[\"getCap\"]>>,\n ): { authorPubHex: string } | null {\n const { cap, pubHex } = capCtx\n const authorPubHex = pubHex ?? cap.sub\n if (authorPubHex === undefined) return null\n return { authorPubHex }\n }\n\n /** Pull synced data from the server. Returns the raw `PullResult`. */\n async pull(path: string, checkpoint?: number): Promise<PullResult>\n /** Pull synced data with structured options (e.g. `{withKeyring: true}`). */\n async pull(path: string, options: PullOptions): Promise<PullResult>\n /** Pull an append-only collection. Extracts and returns `data[appendField]` as `T[]`. */\n async pull<T = unknown>(path: string, options: AppendPullOptions): Promise<T[]>\n async pull<T = unknown>(\n path: string,\n checkpointOrOptions?: number | AppendPullOptions | PullOptions,\n ): Promise<PullResult | T[]> {\n let pathAndQuery = this.applyNamespace(path)\n let appendField: string | undefined\n let swr = false\n\n if (typeof checkpointOrOptions === \"number\") {\n if (checkpointOrOptions) pathAndQuery += `?checkpoint=${checkpointOrOptions}`\n } else if (checkpointOrOptions != null) {\n // Disambiguate AppendPullOptions vs PullOptions.\n //\n // PullOptions are identified by the presence of `withKeyring`, `checkpoint`,\n // or `staleWhileRevalidate` keys (which AppendPullOptions does not have \u2014\n // append uses `since`, not `checkpoint`). Anything else, including an empty\n // `{}` object, retains the historical behavior of AppendPullOptions\n // (extracts `data.items` with `?` query).\n const opts = checkpointOrOptions as AppendPullOptions & PullOptions\n const isPullOptions =\n opts.withKeyring !== undefined ||\n opts.checkpoint !== undefined ||\n opts.staleWhileRevalidate !== undefined\n const params = new URLSearchParams()\n\n if (isPullOptions) {\n if (opts.checkpoint != null && opts.checkpoint > 0) {\n params.set(\"checkpoint\", String(opts.checkpoint))\n }\n if (opts.withKeyring) {\n params.set(\"withKeyring\", \"1\")\n }\n swr = opts.staleWhileRevalidate === true\n } else {\n appendField = opts.appendField ?? APPEND_DEFAULT_FIELD\n // `full` means \"the whole collection\" \u2014 it cannot be combined with a bound.\n if (opts.full && (opts.since != null || opts.limit != null || opts.last != null)) {\n throw new Error(\"full cannot be combined with since, limit, or last\")\n }\n if (opts.since != null) {\n if (opts.since < 0) throw new Error(\"since must be non-negative\")\n params.set(\"checkpoint\", String(opts.since))\n }\n if (opts.limit != null) {\n if (opts.limit < 0) throw new Error(\"limit must be non-negative\")\n params.set(\"limit\", String(opts.limit))\n }\n if (opts.last != null) {\n if (opts.last < 0) throw new Error(\"last must be non-negative\")\n params.set(\"last\", String(opts.last))\n }\n if (opts.full) {\n params.set(\"full\", \"true\")\n }\n }\n if (params.size > 0) pathAndQuery += `?${params.toString()}`\n }\n\n const url = `${this.baseUrl}${pathAndQuery}`\n const authHeaders = await this.buildAuthHeaders(\"GET\", pathAndQuery, undefined)\n\n // Read-through cache: only for structured (non-append) pulls. Append\n // collections own their own warm-start persistence via AppendLogCursor.\n const cacheKey =\n this.cache && appendField === undefined ? pullCacheKey(pathAndQuery) : undefined\n\n // staleWhileRevalidate: serve the cached snapshot immediately (cache-first\n // paint without a zustand store), kick background revalidation, return stale.\n // Falls through to network-first when there is no cache hit (miss or expired).\n if (swr && cacheKey) {\n const cached = await this.readCache(cacheKey)\n if (cached) {\n this.scheduleRevalidate(cacheKey, pathAndQuery, null, /* immediate */ true)\n return cached\n }\n }\n\n let res: Response\n try {\n res = await this.fetch(url, {\n method: \"GET\",\n headers: { [HEADER_ACCEPT]: \"application/json\", ...authHeaders },\n })\n } catch (err) {\n // The TRANSPORT failed (offline / DNS / timeout) \u2014 fall back to the last\n // cached snapshot for this document if we have one, tagged so callers can\n // tell it's stale. A real HTTP error (below) is a genuine server answer\n // and never gets here; 403 and 404 always propagate. 429 and 5xx\n // propagate by default too, but can fall back to cache when\n // `cacheFallbackStatuses` is set \u2014 see the stale-while-revalidate branch.\n if (cacheKey) {\n const cached = await this.readCache(cacheKey)\n if (cached) return cached\n }\n throw err\n }\n if (!res.ok) {\n const status = res.status\n if (cacheKey && this.cacheFallbackStatuses?.includes(status)) {\n // Stale-while-revalidate: serve the last-synced snapshot immediately and\n // retry in the background. 403/404 are not in the configured set so they\n // still propagate as genuine answers.\n const retryAfterHeader = res.headers.get(\"Retry-After\")\n this.scheduleRevalidate(cacheKey, pathAndQuery, retryAfterHeader)\n const cached = await this.readCache(cacheKey)\n if (cached) {\n // Discard the response body so the underlying connection can be reused.\n void res.body?.cancel()\n return cached\n }\n }\n throw new StarfishHttpError(status, await res.text())\n }\n\n const result = await res.json() as PullResult\n if (appendField !== undefined) {\n const list = (result.data as Record<string, unknown> | null)?.[appendField]\n return (Array.isArray(list) ? list : []) as T[]\n }\n if (cacheKey) this.writeCache(cacheKey, result)\n return result\n }\n\n /**\n * Write a pull snapshot to the cache. Fire-and-forget; errors are swallowed\n * so a failing cache never blocks the caller. No-op when no cache is configured.\n */\n private writeCache(\n cacheKey: string,\n result: { data: Record<string, unknown>; hash: string; timestamp: number },\n ): void {\n if (!this.cache) return\n // Track the newest document timestamp written so revalidateLoop can check\n // staleness synchronously (without an async cache read adding extra ticks).\n if (result.timestamp > (this.latestCacheTimestamp.get(cacheKey) ?? -1)) {\n this.latestCacheTimestamp.set(cacheKey, result.timestamp)\n }\n const snapshot: CachedPull = {\n data: result.data,\n hash: result.hash,\n timestamp: result.timestamp,\n cachedAt: Date.now(),\n }\n void this.cache.set(cacheKey, JSON.stringify(snapshot)).catch(() => {})\n }\n\n /** Build the URL + auth headers for one revalidation GET. Shared between\n * {@link pull} and {@link revalidateLoop} to avoid duplicated fetch setup. */\n private async revalidateFetch(pathAndQuery: string): Promise<Response> {\n const url = `${this.baseUrl}${pathAndQuery}`\n const authHeaders = await this.buildAuthHeaders(\"GET\", pathAndQuery, undefined)\n return this.fetch(url, {\n method: \"GET\",\n headers: { [HEADER_ACCEPT]: \"application/json\", ...authHeaders },\n })\n }\n\n /**\n * Deduplicated fire-and-forget: starts one revalidation loop per cacheKey.\n * Used by both the {@link cacheFallbackStatuses} error path (delayed first\n * attempt, honoring `Retry-After`) and the {@link PullOptions.staleWhileRevalidate}\n * read path (`immediate: true` \u2014 no initial delay on the first attempt). The\n * `revalidating` set deduplicates across both triggers so a concurrent\n * error-triggered loop and an SWR-on-read loop for the same key collapse to one.\n */\n private scheduleRevalidate(\n cacheKey: string,\n pathAndQuery: string,\n retryAfterHeader: string | null,\n immediate = false,\n ): void {\n if (this.revalidating.has(cacheKey)) return\n this.revalidating.add(cacheKey)\n void this.revalidateLoop(cacheKey, pathAndQuery, retryAfterHeader, immediate).finally(() => {\n this.revalidating.delete(cacheKey)\n })\n }\n\n /**\n * Background revalidation loop shared by both {@link cacheFallbackStatuses}\n * hits and {@link PullOptions.staleWhileRevalidate} reads.\n *\n * Retries (honoring `Retry-After`) up to {@link MAX_REVALIDATE_ATTEMPTS} times.\n * When `immediate` is true the first attempt fires without any initial delay\n * (SWR-on-read path). On a live 2xx the fresh snapshot is written to cache and\n * {@link onRevalidated} fires. Stops early on a non-fallback status (403/404).\n */\n private async revalidateLoop(\n cacheKey: string,\n pathAndQuery: string,\n firstRetryAfter: string | null,\n immediate = false,\n ): Promise<void> {\n let retryAfterHeader = firstRetryAfter\n const fallbackSet = this.cacheFallbackStatuses ? new Set(this.cacheFallbackStatuses) : null\n for (let attempt = 0; attempt < MAX_REVALIDATE_ATTEMPTS; attempt++) {\n // Skip the initial delay for the first attempt when immediate mode is set\n // (staleWhileRevalidate path). Subsequent attempts always backoff normally.\n if (!immediate || attempt > 0) {\n const delay = parseRetryAfterMs(retryAfterHeader, {\n fallbackMs: Math.min(\n REVALIDATE_INITIAL_DELAY_MS * Math.pow(2, attempt),\n REVALIDATE_MAX_DELAY_MS,\n ),\n maxMs: REVALIDATE_MAX_DELAY_MS,\n })\n await sleep(delay)\n }\n\n try {\n const res = await this.revalidateFetch(pathAndQuery)\n\n if (res.ok) {\n const result = (await res.json()) as PullResult\n // Guard against stale overwrites: if push() wrote a newer snapshot\n // while this revalidation was in-flight, the in-memory tracker\n // reflects the current latest-written timestamp synchronously (no\n // extra async tick). We drop the revalidation result and leave the\n // cache \u2014 and onRevalidated \u2014 untouched so the pushed edit survives.\n const latestTs = this.latestCacheTimestamp.get(cacheKey) ?? -1\n if (result.timestamp >= latestTs) {\n this.writeCache(cacheKey, result)\n this.onRevalidated?.(pathAndQuery, result)\n }\n return\n }\n\n if (!fallbackSet?.has(res.status)) {\n // Genuine server answer (e.g. 403 or 404) \u2014 stop retrying.\n return\n }\n\n retryAfterHeader = res.headers.get(\"Retry-After\")\n } catch {\n // Transport failure \u2014 keep retrying with exponential backoff.\n retryAfterHeader = null\n }\n }\n }\n\n /**\n * Read the cached snapshot for a document `path` WITHOUT hitting the network \u2014\n * the basis for cache-first paint (seed the UI from the last-synced snapshot,\n * then revalidate with a live {@link pull}). Returns the tagged `PullResult`,\n * or null when no cache is configured / there's no entry. Namespacing matches\n * {@link pull}, so the key lines up with whatever `pull` wrote.\n */\n async peekCache(path: string): Promise<PullResult | null> {\n if (!this.cache) return null\n return this.readCache(pullCacheKey(this.applyNamespace(path)))\n }\n\n /** Read + parse a cached pull snapshot, tagged {@link tagFromCache}. Returns\n * null on a miss or an unparseable blob (never throws \u2014 a corrupt cache entry\n * must not break a pull, just miss). */\n private async readCache(cacheKey: string): Promise<PullResult | null> {\n try {\n const raw = await this.cache!.get(cacheKey)\n if (!raw) return null\n const parsed = JSON.parse(raw) as CachedPull\n if (!parsed || typeof parsed.hash !== \"string\") return null\n // Expiry: a snapshot older than the configured max age is a miss. Entries\n // written before this field existed (cachedAt missing) count as age 0 \u21D2\n // expired under any TTL, forcing a fresh pull once.\n if (this.cacheMaxAgeMs != null && Date.now() - (parsed.cachedAt ?? 0) > this.cacheMaxAgeMs) {\n return null\n }\n return this.tagFromCache({ data: parsed.data ?? {}, hash: parsed.hash, timestamp: parsed.timestamp ?? 0 })\n } catch {\n return null\n }\n }\n\n /**\n * Pull several documents in one round-trip via `/batch/pull`. `collections` is\n * the list of distinct collection names; `opts.params` supplies, per collection,\n * an ARRAY of path-param sets \u2014 one per document to read \u2014 so the SAME collection\n * can fan in many documents (e.g. many users' `profile`) in a single request.\n * The server auto-fills the `{identity}` param from the authenticated caller for\n * any set that omits it, so a self-doc collection needs no params. Returns a map\n * of collection name \u2192 an ARRAY of pulled documents (or per-document `{ error }`),\n * in request order. Honors the configured namespace.\n *\n * For the common \"many docs of one collection\" case prefer {@link batchPullMany}.\n *\n * Pass `appendParams` per entry for append-only bounded-tail reads (see {@link batchPullManyAppend}).\n */\n async batchPull(\n collections: string[],\n opts: BatchPullOptions = {},\n ): Promise<BatchPullResult> {\n const search = new URLSearchParams()\n search.set(\"collections\", collections.join(\",\"))\n if (opts.params && Object.keys(opts.params).length > 0) {\n search.set(\"params\", JSON.stringify(opts.params))\n }\n if (opts.appendParams && Object.keys(opts.appendParams).length > 0) {\n // Client-side guard: `full` is disallowed in batch (DoS risk). Apply the\n // same `full \u22A5 since/limit/last` mutual-exclusion check from pull() too.\n for (const [col, optsArr] of Object.entries(opts.appendParams)) {\n for (const ap of optsArr) {\n if (ap.full) {\n throw new Error(\n `batchPull: appendParams[\"${col}\"] contains full:true \u2014 full is not supported in batch pull`,\n )\n }\n // Validate since/last/limit are non-negative integers (floats are rejected\n // server-side; reject client-side for a faster, clearer error).\n if (ap.since != null && (!Number.isInteger(ap.since) || ap.since < 0)) {\n throw new Error(`batchPull: appendParams[\"${col}\"].since must be a non-negative integer`)\n }\n if (ap.last != null && (!Number.isInteger(ap.last) || ap.last < 0)) {\n throw new Error(`batchPull: appendParams[\"${col}\"].last must be a non-negative integer`)\n }\n if (ap.limit != null && (!Number.isInteger(ap.limit) || ap.limit < 0)) {\n throw new Error(`batchPull: appendParams[\"${col}\"].limit must be a non-negative integer`)\n }\n }\n }\n search.set(\"appendParams\", JSON.stringify(opts.appendParams))\n }\n const pathAndQuery = `${this.applyNamespace(\"/batch/pull\")}?${search.toString()}`\n const url = `${this.baseUrl}${pathAndQuery}`\n const authHeaders = await this.buildAuthHeaders(\"GET\", pathAndQuery, undefined)\n\n const res = await this.fetch(url, {\n method: \"GET\",\n headers: { [HEADER_ACCEPT]: \"application/json\", ...authHeaders },\n })\n if (!res.ok) {\n throw new StarfishHttpError(res.status, await res.text())\n }\n return await res.json() as BatchPullResult\n }\n\n /**\n * Convenience over {@link batchPull} for reading MANY documents of ONE\n * collection in a single round-trip: pass the per-document param-sets and get\n * back the {@link BatchPullEntry} array aligned to `paramsList` by index (each\n * entry is `{ data, hash, timestamp }` or `{ error }`). An empty `paramsList`\n * issues no request and returns `[]`.\n */\n async batchPullMany(\n collection: string,\n paramsList: Record<string, string>[],\n ): Promise<BatchPullEntry[]> {\n if (paramsList.length === 0) return []\n const res = await this.batchPull([collection], { params: { [collection]: paramsList } })\n return res.collections[collection] ?? []\n }\n\n /**\n * Convenience over {@link batchPull} for reading append-only bounded tails from\n * MANY entries of ONE collection in a single round-trip.\n *\n * Each request in `requests` carries optional `params` (path params) and\n * `options` (append bounds: `since`/`last`/`limit`/`appendField`). An empty\n * `requests` issues no request and returns `[]`.\n *\n * Returns an array aligned to `requests` by index. Each element is either:\n * - the filtered array `T[]` extracted from `entry.data[appendField]`, or\n * - `{ error: string }` if the server returned a per-entry error.\n *\n * The `appendField` used for extraction defaults to `\"items\"` and can be\n * overridden per request via `options.appendField`.\n *\n * The `appendField` option is client-side only (used for result extraction, not sent to the server).\n * It must match the collection's server-configured append field and defaults to `\"items\"`.\n *\n * Note: `full: true` is not supported in batch and is rejected client-side\n * before the request is sent.\n */\n async batchPullManyAppend<T = unknown>(\n collection: string,\n requests: { params?: Record<string, string>; options: AppendPullOptions }[],\n ): Promise<(T[] | { error: string })[]> {\n if (requests.length === 0) return []\n const paramsList = requests.map((r) => r.params ?? {})\n // Strip appendField from wire opts \u2014 server uses its configured field,\n // not the client-supplied one. We keep it locally for result extraction below.\n const appendParamsList = requests.map(({ options: { appendField: _af, ...wireOpts } }) => wireOpts)\n const res = await this.batchPull([collection], {\n params: { [collection]: paramsList },\n appendParams: { [collection]: appendParamsList },\n })\n const entries = res.collections[collection] ?? []\n return entries.map((entry, i) => {\n if (entry.error) return { error: entry.error }\n const appendField = requests[i]?.options.appendField ?? APPEND_DEFAULT_FIELD\n const data = entry.data as Record<string, unknown> | undefined\n const items = data?.[appendField]\n return Array.isArray(items) ? (items as T[]) : []\n })\n }\n\n /**\n * Push synced data to the server.\n * @param path - The push endpoint path (e.g. \"/push/users/abc/settings\")\n * @param data - The full document data to push\n * @param baseHash - Hash of the document this push is based on (null for first push)\n *\n * v3 author proof (`authorPubkey` + `authorSignature`) is passed via `author`\n * (produced by `SyncManager` when a `signer` is configured) and sent as\n * top-level body siblings of `data`, where the server verifies it.\n * @throws {ConflictError} if the server detects a hash mismatch (409)\n */\n async push(\n path: string,\n data: Record<string, unknown>,\n baseHash: string | null,\n author?: AppendAuthor,\n ): Promise<PushSuccess> {\n const body = JSON.stringify({\n [DATA_FIELD]: data,\n [BASE_HASH_FIELD]: baseHash,\n ...(author && {\n [AUTHOR_PUBKEY_FIELD]: author.authorPubkey,\n [AUTHOR_SIGNATURE_FIELD]: author.authorSignature,\n }),\n })\n\n const sendPath = this.applyNamespace(path)\n const authHeaders = await this.buildAuthHeaders(\"POST\", sendPath, body)\n\n const res = await this.fetch(`${this.baseUrl}${sendPath}`, {\n method: \"POST\",\n headers: {\n [HEADER_CONTENT_TYPE]: \"application/json\",\n [HEADER_ACCEPT]: \"application/json\",\n ...authHeaders,\n },\n body,\n })\n\n if (res.status === 409) {\n throw new ConflictError()\n }\n if (!res.ok) {\n throw new StarfishHttpError(res.status, await res.text())\n }\n const result = (await res.json()) as PushSuccess\n // Write-through: update the pull cache with the pushed data so an offline\n // restart reads the just-written state rather than the pre-push snapshot.\n // The push path is /push/X; the corresponding pull cache key is /pull/X.\n if (this.cache) {\n const pullPath = sendPath.replace(\"/push/\", \"/pull/\")\n this.writeCache(pullCacheKey(pullPath), { data, hash: result.hash, timestamp: result.timestamp })\n }\n return result\n }\n\n /**\n * Append an element to an appendOnly (`by_timestamp`) collection.\n *\n * Unlike {@link push}, appendOnly writes carry no hash/conflict check \u2014 an\n * authorized append is always accepted. Each element is stored server-side as\n * `{ts, data}` and pulls can filter by `ts` via `since`/`checkpoint`.\n *\n * @param path - the push endpoint (e.g. \"/push/events\")\n * @param data - the element payload. For a `delegated` collection, encrypt it\n * first (e.g. `createKeyringEncryptor(keyring, kem).encrypt(data)`); the\n * server stores it opaquely and never reads it.\n * @param opts.ts - optional client-supplied element timestamp (ms). Must be a\n * non-negative integer strictly greater than the latest stored element's ts\n * (else the server responds 409). Omit to let the server assign one.\n * @throws {StarfishHttpError} on a non-2xx response \u2014 e.g. 409\n * `{ error: \"non_monotonic_timestamp\" }` for a non-monotonic timestamp, or\n * `{ error: \"append_limit_exceeded\", limit }` if the collection's `maxItems`\n * cap is reached (partition by a path parameter for higher volume).\n */\n async append(\n path: string,\n data: Record<string, unknown>,\n opts: { ts?: number } = {},\n ): Promise<PushSuccess> {\n const sendPath = this.applyNamespace(path)\n const bodyObj: Record<string, unknown> = { [DATA_FIELD]: data }\n if (opts.ts !== undefined) bodyObj[TS_FIELD] = opts.ts\n\n // Author proof. Fetch the cap ONCE and reuse it for both the author\n // signature (over the element `data`) and the request signature (over the\n // final body) \u2014 see {@link capRequestHeaders}. The author fields are signed\n // with the same key that authenticates the request, so a collection with\n // `requireAuthorSignature` (the default) binds the stored element to its\n // writer. Without a cap provider the append is sent unsigned and such a\n // collection rejects it.\n const capCtx = this.capProvider ? await this.capProvider.getCap() : null\n if (capCtx) {\n const authorKey = this.appendAuthorKey(capCtx)\n if (authorKey) {\n // The signature binds the author to BOTH the element data AND the\n // document it is written to (the storage path = `path` minus the\n // `/push/` action prefix; the namespace lives only in the URL).\n const documentKey = stripPushPrefix(path)\n const { authorPubkey, authorSignature } = signAppendAuthor(\n documentKey,\n data,\n authorKey.authorPubHex,\n capCtx.devEdPrivHex,\n )\n bodyObj[AUTHOR_PUBKEY_FIELD] = authorPubkey\n bodyObj[AUTHOR_SIGNATURE_FIELD] = authorSignature\n }\n }\n\n const body = JSON.stringify(bodyObj)\n const authHeaders = capCtx\n ? await this.capRequestHeaders(capCtx, \"POST\", sendPath, body)\n : {}\n\n const res = await this.fetch(`${this.baseUrl}${sendPath}`, {\n method: \"POST\",\n headers: {\n [HEADER_CONTENT_TYPE]: \"application/json\",\n [HEADER_ACCEPT]: \"application/json\",\n ...authHeaders,\n },\n body,\n })\n\n if (!res.ok) {\n throw new StarfishHttpError(res.status, await res.text())\n }\n return res.json() as Promise<PushSuccess>\n }\n\n /**\n * Append one element to a **public-write** append-only collection with an\n * Ed25519 author proof but **no cap `Authorization` header**.\n *\n * Unlike {@link append}, which always attaches a cap-signed `Authorization`\n * header from the configured `capProvider`, this method signs only the\n * append-author proof (binding the element to the writer's Ed25519 key) and\n * sends the request without authentication headers. This is required for\n * collections with `writeRoles: [\"public\"]` \u2014 the server's cap-scope check\n * would reject a request carrying a cap whose scope does not cover the path.\n *\n * Typical use-case: writing a sealed invitation to another user's\n * public-write inbox collection without needing a cap scoped to the\n * recipient's namespace. The author proof is optional on the server side\n * (`requireAuthorSignature: false` for a public inbox), but signing anyway\n * binds the stored element to the sender's Ed25519 key for verification in\n * the receive path.\n *\n * The element is sent as `{ data, authorPubkey, authorSignature }`.\n *\n * @param path The push path, e.g. `/push/inbox/{userId}/{shard}`.\n * @param element The JSON element to append.\n * @param signer The sender's Ed25519 keypair (signs the author proof).\n *\n * @throws {AppendHttpError} on a non-2xx response.\n */\n async appendAnonymous(\n path: string,\n element: Record<string, unknown>,\n signer: { edPubHex: string; edPrivHex: string },\n ): Promise<void> {\n const sendPath = this.applyNamespace(path)\n const documentKey = stripPushPrefix(path)\n const { authorPubkey, authorSignature } = signAppendAuthor(\n documentKey,\n element,\n signer.edPubHex,\n signer.edPrivHex,\n )\n const body = JSON.stringify({\n [DATA_FIELD]: element,\n [AUTHOR_PUBKEY_FIELD]: authorPubkey,\n [AUTHOR_SIGNATURE_FIELD]: authorSignature,\n })\n const res = await this.fetch(`${this.baseUrl}${sendPath}`, {\n method: \"POST\",\n headers: {\n [HEADER_CONTENT_TYPE]: \"application/json\",\n [HEADER_ACCEPT]: \"application/json\",\n },\n body,\n })\n if (!res.ok) {\n const detail = await res.text().catch(() => \"\")\n throw new AppendHttpError(\n res.status,\n `anonymous append failed: HTTP ${res.status} ${detail}`.trim(),\n )\n }\n }\n\n /**\n * Pull binary data from a blob collection.\n * Returns raw bytes with the content hash from the ETag header.\n */\n async pullBlob(path: string): Promise<BlobPullResult> {\n const sendPath = this.applyNamespace(path)\n const authHeaders = await this.buildAuthHeaders(\"GET\", sendPath, undefined)\n\n const res = await this.fetch(`${this.baseUrl}${sendPath}`, {\n method: \"GET\",\n headers: { [HEADER_ACCEPT]: \"*/*\", ...authHeaders },\n })\n if (!res.ok) {\n throw new StarfishHttpError(res.status, await res.text())\n }\n\n const etag = res.headers.get(\"ETag\")?.replace(/\"/g, \"\") ?? null\n const contentType = res.headers.get(HEADER_CONTENT_TYPE) ?? \"application/octet-stream\"\n const data = await res.arrayBuffer()\n\n return { data, hash: etag, contentType }\n }\n\n /**\n * Push binary data to a blob collection.\n * Binary collections use last-write-wins (no conflict detection).\n */\n async pushBlob(\n path: string,\n data: ArrayBuffer | Uint8Array | Blob,\n contentType: string,\n ): Promise<BlobPushResult> {\n // Blobs are not JSON; we leave body undefined when signing \u2014 server-side\n // verification is expected to use a separate path for blob uploads.\n const sendPath = this.applyNamespace(path)\n const authHeaders = await this.buildAuthHeaders(\"POST\", sendPath, undefined)\n\n const res = await this.fetch(`${this.baseUrl}${sendPath}`, {\n method: \"POST\",\n headers: {\n [HEADER_CONTENT_TYPE]: contentType,\n [HEADER_ACCEPT]: \"application/json\",\n ...authHeaders,\n },\n body: data as BodyInit,\n })\n if (!res.ok) {\n throw new StarfishHttpError(res.status, await res.text())\n }\n return res.json() as Promise<BlobPushResult>\n }\n}\n", "import type { CapCert, PullResult } from \"@drakkar.software/starfish-protocol\"\n\n/** Push conflict error (HTTP 409). */\nexport class ConflictError extends Error {\n constructor() {\n super(\"hash_mismatch\")\n this.name = \"ConflictError\"\n }\n}\n\n/** HTTP error from the Starfish server. */\nexport class StarfishHttpError extends Error {\n constructor(\n public readonly status: number,\n public readonly body: string\n ) {\n super(`HTTP ${status}: ${body}`)\n this.name = \"StarfishHttpError\"\n }\n}\n\n/**\n * Non-2xx HTTP error from an anonymous (cap-less) append call.\n *\n * Distinct from {@link StarfishHttpError} so callers can distinguish \"the\n * anonymous write was rejected\" (e.g. auth required, payload too large) from\n * other client errors without pattern-matching on the error message.\n */\nexport class AppendHttpError extends Error {\n constructor(\n /** HTTP status returned by the server. */\n public readonly status: number,\n message: string,\n ) {\n super(message)\n this.name = \"AppendHttpError\"\n }\n}\n\n/**\n * v3.0 cap-cert provider for `StarfishClient`. Returns the device's cap-cert and\n * the matching Ed25519 private key (hex). The client calls `getCap()` once per\n * outgoing request; implementations are expected to cache so this is cheap.\n *\n * When set, the client signs every outgoing request: each call carries\n * `Authorization: Cap <base64(stableStringify(cap))>` plus `X-Starfish-Sig`,\n * `X-Starfish-Ts`, `X-Starfish-Nonce`.\n */\nexport interface StarfishCapProvider {\n /**\n * Returns the device's cap-cert and its Ed25519 private key (hex).\n * Implementations are expected to cache; the client may call this once per\n * authenticated request.\n *\n * For an `audience` (public-link) cap, which binds no single subject, also\n * return `pubHex` \u2014 the redeemer's own Ed25519 pubkey matching `devEdPrivHex`.\n * The client then sends it as `X-Starfish-Pub` so the server can verify the\n * request signature against it and check the cap's `aud` allow-list. Omit\n * `pubHex` for device/member caps (the server uses `cap.sub`).\n */\n getCap(): Promise<{\n cap: CapCert\n devEdPrivHex: string\n pubHex?: string\n }>\n}\n\n/**\n * A minimal async key-value store the client uses as a read-through cache for\n * {@link StarfishClient.pull} (offline-first reads). Host-provided so the SDK\n * stays storage-agnostic \u2014 back it by `localStorage`, `AsyncStorage`, a file,\n * etc. Shaped like a subset of zustand's `StateStorage` so an existing adapter\n * fits.\n *\n * IMPORTANT \u2014 what gets stored: the client caches the RAW server response only\n * (`data`/`hash`/`timestamp`). For E2E (`delegated`) collections that payload is\n * the SEALED ciphertext the server holds \u2014 never the decrypted form \u2014 so this\n * cache is ciphertext-at-rest by construction. Decryption always happens in\n * memory on read (see {@link SyncManager}). Public/plaintext collections cache\n * their plaintext, exactly as the server stores it.\n */\nexport interface PullCache {\n /** Return the previously-stored string for `key`, or null if absent. Must not throw. */\n get(key: string): Promise<string | null>\n /** Store `value` under `key`. Must not throw (failures are swallowed by the client). */\n set(key: string, value: string): Promise<void>\n}\n\n/** Options for creating a StarfishClient. */\nexport interface StarfishClientOptions {\n /** Base URL of the Starfish server (e.g. \"https://api.example.com/v1\"). */\n baseUrl: string\n /**\n * Optional namespace for a namespace-mounted server. When set, every request\n * path `/{action}/\u2026` is rewritten to `/v1/{namespace}/{action}/\u2026` for BOTH the\n * URL the client hits AND the canonical path it signs, so the signature the\n * server reconstructs from the namespaced URL verifies (no rewrite layer\n * needed). Mirrors the Python client's `namespace` parameter.\n *\n * Crucially this also rewrites the paths that namespace-unaware SDK helpers\n * build internally (e.g. `starfish-keyring`'s `addCollectionRecipient`, blob\n * uploads), so consumers no longer hand-prefix paths or wrap the client to\n * reach a namespaced deployment. Leave unset (default) for a root-mounted\n * server \u2014 paths pass through unchanged, byte-identical to before.\n *\n * Pass the bare namespace name (e.g. `\"octochat\"`); `baseUrl` then carries only\n * the origin (and any reverse-proxy mount the proxy strips), not the `/v1`\n * version segment. Must match `[A-Za-z0-9_-]+` and not be a reserved route name\n * (`pull`, `push`, `health`, `batch`).\n */\n namespace?: string\n /**\n * Cap-cert provider. When set, requests are signed with Ed25519 and carry\n * `Authorization: Cap <\u2026>`. Omit for unauthenticated public-read collections.\n */\n capProvider?: StarfishCapProvider\n /** Optional fetch implementation (defaults to global fetch). */\n fetch?: typeof fetch\n /**\n * Optional read-through cache for {@link StarfishClient.pull} \u2014 the basis for\n * offline-first reads. When set, every successful non-append pull is written\n * through to the cache (keyed by document path), and a pull that fails because\n * the TRANSPORT is unreachable (offline / DNS / timeout \u2014 `fetch` rejects)\n * falls back to the cached response, tagged so callers can tell it's stale.\n *\n * A real HTTP error (404/403/5xx) is a genuine server answer and always\n * propagates \u2014 the cache is NOT consulted \u2014 so \"no document yet\" and\n * \"access denied\" keep their meaning. Caches ciphertext for E2E collections\n * (the server only ever holds sealed payloads); never decrypted data.\n */\n cache?: PullCache\n /**\n * Optional max age (ms) for {@link cache} entries. An entry older than this is\n * treated as a cache MISS on every read \u2014 both cache-first paint and the\n * offline fallback \u2014 so a stale-beyond-policy snapshot is never served (the\n * pull then goes to the network, or rethrows the transport error offline).\n * Each cached snapshot records its write time; expiry is `now - cachedAt >\n * cacheMaxAgeMs`. Omit (default) for entries that never expire \u2014 recommended\n * for an offline-first app where any last-synced data beats none.\n */\n cacheMaxAgeMs?: number\n /**\n * HTTP status codes for which a structured `pull()` falls back to the\n * last-synced cached snapshot rather than throwing `StarfishHttpError` \u2014\n * a **stale-while-revalidate** strategy for transient server failures.\n *\n * When a pull returns one of these statuses AND a {@link cache} is configured\n * AND a cached snapshot exists for the document, `pull()` returns the cached\n * result immediately (tagged stale via `pullWasFromCache`) and spawns a\n * background revalidation loop that retries until it gets a live response.\n * On success the fresh snapshot is written through and {@link onRevalidated}\n * fires. When no cached snapshot exists the error propagates as usual.\n *\n * Applies only to structured (non-append) pulls. Do NOT include `403` or `404`\n * \u2014 they are genuine server answers (access denied / no document yet).\n *\n * Default `undefined` \u2014 every non-2xx status throws as before.\n *\n * Recommended set for offline-first apps: `[429, 500, 502, 503, 504]`.\n */\n cacheFallbackStatuses?: number[]\n /**\n * Called after a background revalidation delivers a fresh snapshot to the\n * cache. Fires for two revalidation paths:\n *\n * 1. **Error-triggered** ({@link cacheFallbackStatuses} hit): the server\n * returned a transient error (429/5xx), `pull()` served the stale cache,\n * and the background retry loop eventually got a live response.\n * 2. **SWR-on-read** ({@link PullOptions.staleWhileRevalidate}): `pull()`\n * served the cache immediately and the background fetch completed.\n *\n * In both cases `result` is the fresh `PullResult` just written to cache.\n * Use this to signal reachability recovery and/or push the fresh data into\n * any store that is showing the stale snapshot.\n *\n * `path` is the namespaced document path (namespace prefix + path + query\n * string, matching the cache key written by {@link StarfishClient.pull}).\n */\n onRevalidated?: (path: string, result: PullResult) => void\n /**\n * Optional list of client-side plugins. The list is stored on the client\n * instance but does not fire any hooks yet \u2014 the contract is plumbed so\n * extension packages (`starfish-identities`, `starfish-keyring`,\n * `starfish-sharing`, \u2026) can register against it later without a breaking\n * API change.\n *\n * The current set of hooks is purposely empty; extensions that need to\n * react to mint events or transport actions today can wrap the client\n * directly. Future hook additions will be additive.\n */\n plugins?: ClientPlugin[]\n}\n\n/**\n * Client-side plugin contract.\n *\n * A placeholder shape: the interface intentionally has no required hooks\n * yet; extensions declare a plugin object with `name` and opt into\n * specific lifecycle hooks once those exist. Apps wire plugins via\n * `new StarfishClient({ plugins: [...] })`.\n */\nexport interface ClientPlugin {\n /** Human-readable name. Used in error messages and audit output. */\n name: string\n /**\n * Reserved for future hook fields. Plugins typically declare only\n * `name`. Hook additions are additive \u2014 extensions implementing a\n * future hook will populate the relevant optional property without\n * affecting existing zero-hook plugins.\n */\n}\n\n/** Conflict resolver: given local and remote data, return merged result. */\nexport type ConflictResolver = (\n local: Record<string, unknown>,\n remote: Record<string, unknown>\n) => Record<string, unknown>\n", "/**\n * Parse a `Retry-After` header value into milliseconds.\n *\n * - Numeric string (`\"30\"`) \u2014 treated as seconds \u00D7 1000.\n * - HTTP-date string \u2014 delta from now in ms (floored to 0).\n * - `null`, empty, or unparseable \u2014 returns `opts.fallbackMs`.\n *\n * All results are clamped to `[0, opts.maxMs]`.\n */\nexport function parseRetryAfterMs(\n header: string | null | undefined,\n opts: { fallbackMs: number; maxMs: number },\n): number {\n const { fallbackMs, maxMs } = opts\n const trimmed = header?.trim()\n if (trimmed) {\n const seconds = Number(trimmed)\n if (!isNaN(seconds)) return Math.min(seconds * 1000, maxMs)\n const date = Date.parse(trimmed)\n if (!isNaN(date)) return Math.min(Math.max(date - Date.now(), 0), maxMs)\n }\n return Math.min(fallbackMs, maxMs)\n}\n\n/** Error category returned by classifyError. */\nexport type ErrorCategory =\n | \"network\"\n | \"auth\"\n | \"conflict\"\n | \"rate-limited\"\n | \"server\"\n | \"client\"\n | \"unknown\"\n\n/** Classify an error from a fetch response or network failure. */\nexport function classifyError(err: unknown): ErrorCategory {\n if (err instanceof Response || (err && typeof err === \"object\" && \"status\" in err)) {\n const status = (err as { status: unknown }).status\n if (typeof status !== \"number\" || isNaN(status)) return \"unknown\"\n if (status === 0) return \"network\"\n if (status === 401 || status === 403) return \"auth\"\n if (status === 409) return \"conflict\"\n if (status === 429) return \"rate-limited\"\n if (status >= 500) return \"server\"\n if (status >= 400) return \"client\"\n }\n if (err instanceof Error && /failed to fetch|fetch failed|network|load failed|ECONNREFUSED|ENOTFOUND/i.test(err.message)) return \"network\"\n return \"unknown\"\n}\n\nexport interface RetryOptions {\n /** Max number of retries (default: 3). */\n maxRetries?: number\n /** Initial delay in ms before first retry (default: 500). */\n initialDelayMs?: number\n /** Maximum delay in ms (default: 10000). */\n maxDelayMs?: number\n}\n\n/**\n * Wraps a fetch function with automatic retry for retriable errors\n * (network failures, 429, 5xx). Respects Retry-After headers.\n */\nexport function createRetryFetch(options?: RetryOptions): typeof globalThis.fetch {\n const maxRetries = Math.max(0, options?.maxRetries ?? 3)\n const initialDelay = options?.initialDelayMs ?? 500\n const maxDelay = options?.maxDelayMs ?? 10_000\n\n return async (input, init?) => {\n let attempt = 0\n while (true) {\n try {\n const res = await globalThis.fetch(input, init)\n if (res.ok || attempt >= maxRetries) return res\n\n const category = classifyError(res)\n if (category !== \"rate-limited\" && category !== \"server\") return res\n\n const retryAfterHeader = res.headers.get(\"Retry-After\")\n const exponentialDelay = Math.min(initialDelay * Math.pow(2, attempt), maxDelay)\n // When the header is present but unparseable, original falls back to\n // initialDelay (not exponential). Preserve that by checking presence first.\n const delay = parseRetryAfterMs(retryAfterHeader, {\n fallbackMs: retryAfterHeader?.trim() ? initialDelay : exponentialDelay,\n maxMs: maxDelay,\n })\n\n await new Promise<void>((r) => setTimeout(r, delay))\n attempt++\n } catch (err) {\n if (attempt >= maxRetries) throw err\n const category = classifyError(err)\n if (category !== \"network\") throw err\n\n const delay = Math.min(initialDelay * Math.pow(2, attempt), maxDelay)\n await new Promise<void>((r) => setTimeout(r, delay))\n attempt++\n }\n }\n }\n}\n\ntype BreakerState = \"closed\" | \"open\" | \"half-open\"\n\nexport interface CircuitBreakerOptions {\n /** Number of consecutive failures to open the circuit (default: 5). */\n threshold?: number\n /** Cooldown in ms before transitioning from open to half-open (default: 30000). */\n cooldownMs?: number\n}\n\n/** Circuit breaker that prevents requests when the backend is unavailable. */\nexport class CircuitBreaker {\n private state: BreakerState = \"closed\"\n private failures = 0\n private openedAt = 0\n private readonly threshold: number\n private readonly cooldownMs: number\n\n constructor(options?: CircuitBreakerOptions) {\n this.threshold = options?.threshold ?? 5\n this.cooldownMs = options?.cooldownMs ?? 30_000\n }\n\n getState(): BreakerState {\n this.maybeTransition()\n return this.state\n }\n\n isOpen(): boolean {\n return this.getState() === \"open\"\n }\n\n recordSuccess(): void {\n this.failures = 0\n this.state = \"closed\"\n }\n\n recordFailure(): void {\n this.failures++\n if (this.state === \"half-open\" || this.failures >= this.threshold) {\n this.state = \"open\"\n this.openedAt = Date.now()\n }\n }\n\n private maybeTransition(): void {\n if (this.state === \"open\" && Date.now() - this.openedAt >= this.cooldownMs) {\n this.state = \"half-open\"\n }\n }\n}\n\n/**\n * Wraps fetch to gzip-compress string request bodies using the CompressionStream API.\n * Adds Content-Encoding: gzip header. Non-string bodies (ArrayBuffer, Blob, etc.)\n * are passed through uncompressed. Requires CompressionStream (browsers, Node.js 18+, Deno).\n */\nexport function createCompressedFetch(inner?: typeof globalThis.fetch): typeof globalThis.fetch {\n const baseFetch = inner ?? globalThis.fetch.bind(globalThis)\n return async (input, init?) => {\n if (!init?.body || typeof CompressionStream === \"undefined\") {\n return baseFetch(input, init)\n }\n\n const bodyText = typeof init.body === \"string\" ? init.body : null\n if (!bodyText) return baseFetch(input, init)\n\n try {\n const stream = new Blob([bodyText]).stream().pipeThrough(new CompressionStream(\"gzip\"))\n const compressed = await new Response(stream).arrayBuffer()\n\n const normalized = Object.fromEntries(new Headers(init.headers as HeadersInit).entries())\n normalized[\"content-encoding\"] = \"gzip\"\n\n return baseFetch(input, {\n ...init,\n body: compressed,\n headers: normalized,\n })\n } catch {\n return baseFetch(input, init)\n }\n }\n}\n\n/**\n * Wrap `fetch` to bound the **connect / Time-to-First-Byte** phase with a\n * timeout. The timer is cleared as soon as the response HEADERS arrive (i.e.\n * the `fetch()` promise resolves), so a slow large-body download after a fast\n * connection is not interrupted. Only the initial \"will the server even\n * respond?\" window is bounded.\n *\n * The wrapper composes with the caller's `AbortSignal`: if the caller's signal\n * fires first the request is still aborted and the timeout timer is cleaned up.\n *\n * @param timeoutMs How long (in ms) to wait for the server to start\n * responding before aborting. Default `10 000`.\n * @param inner Optional underlying `fetch` to wrap (defaults to\n * `globalThis.fetch`).\n *\n * @example\n * ```ts\n * import { createTimeoutFetch, createResilientFetch } from \"@drakkar.software/starfish-client/fetch\"\n *\n * const { fetch: resilient } = createResilientFetch()\n * const client = new StarfishClient({\n * baseUrl: \"https://api.example.com\",\n * fetch: createTimeoutFetch(8_000, resilient),\n * })\n * ```\n */\nexport function createTimeoutFetch(\n timeoutMs = 10_000,\n inner?: typeof globalThis.fetch,\n): typeof globalThis.fetch {\n const baseFetch = inner ?? globalThis.fetch.bind(globalThis)\n return async (input, init?) => {\n const timeoutCtrl = new AbortController()\n const timer = setTimeout(() => timeoutCtrl.abort(new Error(`connect timeout after ${timeoutMs}ms`)), timeoutMs)\n\n // Compose with a caller-supplied AbortSignal if present.\n const callerSignal = init?.signal as AbortSignal | null | undefined\n let combinedSignal: AbortSignal\n\n if (callerSignal) {\n if (typeof AbortSignal.any === \"function\") {\n combinedSignal = AbortSignal.any([timeoutCtrl.signal, callerSignal])\n } else {\n // Polyfill for environments without AbortSignal.any.\n const combo = new AbortController()\n const onCallerAbort = () => combo.abort(callerSignal.reason)\n const onTimeout = () => combo.abort(timeoutCtrl.signal.reason)\n callerSignal.addEventListener(\"abort\", onCallerAbort, { once: true })\n timeoutCtrl.signal.addEventListener(\"abort\", onTimeout, { once: true })\n combinedSignal = combo.signal\n }\n } else {\n combinedSignal = timeoutCtrl.signal\n }\n\n try {\n const res = await baseFetch(input, { ...init, signal: combinedSignal })\n clearTimeout(timer) // Headers arrived \u2014 clear the connect timeout.\n return res\n } catch (err) {\n clearTimeout(timer)\n throw err\n }\n }\n}\n\n/**\n * Combines retry and circuit breaker into a single resilient fetch wrapper.\n * Rejects immediately when the circuit is open.\n */\nexport function createResilientFetch(\n retryOptions?: RetryOptions,\n breakerOptions?: CircuitBreakerOptions,\n): { fetch: typeof globalThis.fetch; breaker: CircuitBreaker } {\n const breaker = new CircuitBreaker(breakerOptions)\n const retryFetch = createRetryFetch(retryOptions)\n\n const resilientFetch: typeof globalThis.fetch = async (input, init?) => {\n if (breaker.isOpen()) {\n const cooldown = Math.ceil((breakerOptions?.cooldownMs ?? 30_000) / 1000)\n throw new Error(`Request blocked: too many consecutive failures. Retry in ${cooldown}s.`)\n }\n\n try {\n const res = await retryFetch(input, init)\n if (res.status >= 500) {\n breaker.recordFailure()\n } else {\n breaker.recordSuccess()\n }\n return res\n } catch (err) {\n breaker.recordFailure()\n throw err\n }\n }\n\n return { fetch: resilientFetch, breaker }\n}\n", "import type { PullResult } from \"@drakkar.software/starfish-protocol\"\nimport {\n AUTHOR_PUBKEY_FIELD,\n AUTHOR_SIGNATURE_FIELD,\n PUSH_PATH_PREFIX,\n deepMerge,\n docAuthorCanonicalInput,\n getBase64,\n type AppendAuthor,\n} from \"@drakkar.software/starfish-protocol\"\nimport type { ConflictResolver } from \"./types.js\"\nimport { ConflictError } from \"./types.js\"\nimport type { Encryptor } from \"@drakkar.software/starfish-protocol\"\nimport { StarfishClient, stripPushPrefix, pullWasFromCache } from \"./client.js\"\nimport type { SyncLogger } from \"./logger.js\"\nimport type { Validator } from \"./validate.js\"\nimport { ValidationError } from \"./validate.js\"\n\nexport class AbortError extends Error {\n constructor() {\n super(\"SyncManager was aborted\")\n this.name = \"AbortError\"\n }\n}\n\n/**\n * v3.0 author-signature plumbing for `SyncManager`.\n *\n * Returns the device's Ed25519 public key (hex) and a function that signs\n * arbitrary payload bytes. `SyncManager` calls `getSigner()` once per push\n * and uses the returned `sign` to produce a base64-encoded signature over\n * the canonical stringification of the encrypted payload (sans author fields).\n *\n * Implementations typically wrap the same Ed25519 private key used by\n * `StarfishCapProvider` so that `cap.sub === devEdPubHex`.\n */\nexport interface SyncSigner {\n /**\n * Returns the device's `cap.sub` (Ed25519 pubkey, hex) and a payload signer.\n * The `sign` function receives the canonical signing input bytes and must\n * return the raw 64-byte Ed25519 signature.\n */\n getSigner(): Promise<{ devEdPubHex: string; sign(payload: Uint8Array): Promise<Uint8Array> }>\n}\n\n\nexport interface SyncManagerOptions {\n client: StarfishClient\n pullPath: string\n pushPath: string\n /** Custom conflict resolver. Defaults to remote-wins deep merge. Arrays are atomic. */\n onConflict?: ConflictResolver\n /** Max conflict retry attempts (default: 3). */\n maxRetries?: number\n /**\n * Encryptor for client-side E2E encryption. For v3 `delegated` collections,\n * build it via `createKeyringEncryptor(keyring, deviceKemKeys)`.\n */\n encryptor?: Encryptor\n /**\n * v3 author-signature plumbing. When set, every push attaches\n * `authorPubkey` (= `cap.sub`) and `authorSignature` (= base64 Ed25519 over\n * stable-stringify of the encrypted payload minus author fields).\n */\n signer?: SyncSigner\n /** Structured logger for sync events. */\n logger?: SyncLogger\n /** Name passed to logger methods (default: derived from pullPath). */\n loggerName?: string\n /** Validate data before push. Throws ValidationError on failure. */\n validate?: Validator\n}\n\nexport class SyncManager {\n private readonly client: StarfishClient\n private readonly pullPath: string\n private readonly pushPath: string\n private readonly onConflict: ConflictResolver\n private readonly maxRetries: number\n private readonly encryptor: Encryptor | null\n private readonly signer?: SyncSigner\n private readonly logger?: SyncLogger\n private readonly loggerName: string\n private readonly validate?: Validator\n\n private lastHash: string | null = null\n private lastCheckpoint: number = 0\n private localData: Record<string, unknown> = {}\n private aborted: boolean = false\n private lastFromCache: boolean = false\n /** True once {@link seedFromCache} has successfully seeded localData from the cache. */\n private seeded: boolean = false\n\n constructor(options: SyncManagerOptions) {\n this.client = options.client\n this.pullPath = options.pullPath\n this.pushPath = options.pushPath\n this.onConflict = options.onConflict ?? deepMerge\n this.maxRetries = options.maxRetries ?? 3\n this.signer = options.signer\n this.logger = options.logger\n this.loggerName = options.loggerName ?? options.pullPath.split(\"/\").filter(Boolean).pop() ?? options.pullPath\n this.validate = options.validate\n this.encryptor = options.encryptor ?? null\n }\n\n abort(): void {\n this.aborted = true\n }\n\n get isAborted(): boolean {\n return this.aborted\n }\n\n getData(): Record<string, unknown> {\n return { ...this.localData }\n }\n\n /**\n * Returns true when `pull()` / `ingest()` should merge against the current\n * `localData` rather than replace it wholesale.\n *\n * Two situations establish a merge baseline:\n * - A successful prior pull/ingest advanced `lastCheckpoint` beyond 0 (the\n * normal steady-state case, unchanged since alpha.36).\n * - A cache seed painted `localData` via {@link seedFromCache} AND the store\n * uses a custom conflict resolver (i.e. NOT the default `deepMerge`). For a\n * union/custom resolver the seeded snapshot is a real baseline that must not\n * be clobbered by a short first live response (a cache-fallback on 429/5xx\n * or a momentarily-short concurrent server snapshot). For the default\n * `deepMerge` resolver we keep the pre-fix wholesale-replace behaviour so\n * non-union stores are byte-identical to alpha.36.\n */\n private hasMergeBaseline(): boolean {\n return this.lastCheckpoint > 0 || (this.seeded && this.onConflict !== deepMerge)\n }\n\n /**\n * Merge a remote snapshot with local (optimistic) data using this manager's\n * conflict resolver \u2014 the same resolver the push-conflict path uses. A plain\n * {@link pull} overwrites the store's data with the server snapshot, which\n * would drop un-pushed local writes (they live only in the store, never in\n * `localData` until a push succeeds). The zustand binding calls this on pull\n * while the store is dirty so those writes survive. `local` wins by the same\n * rules as a push conflict.\n */\n resolve(\n local: Record<string, unknown>,\n remote: Record<string, unknown>,\n ): Record<string, unknown> {\n return this.onConflict(local, remote)\n }\n\n getHash(): string | null {\n return this.lastHash\n }\n\n /** Set the last-known server hash. Used by persistence layers to restore state across restarts. */\n setHash(hash: string | null): void {\n this.lastHash = hash\n }\n\n /**\n * Whether the most recent {@link pull} (or {@link seedFromCache}) was served\n * from the client's offline read-through cache rather than a live server\n * response. The binding surfaces this as a `stale` flag so the UI can show an\n * offline indicator without treating a cache hit as \"reachable\". Reset to\n * false by the next successful network pull.\n */\n getLastPullFromCache(): boolean {\n return this.lastFromCache\n }\n\n /**\n * Cache-first paint: seed `localData` from the client's read-through cache\n * WITHOUT touching the network, decrypting in memory for E2E collections.\n * Returns whether anything was seeded (false on a miss, an expired entry, or\n * a decrypt failure \u2014 e.g. keyring skew). Call once on store creation before\n * the initial live {@link pull}.\n *\n * `lastCheckpoint` is intentionally left at 0 so the first live pull sends a\n * full (re)sync request to the server, not a delta. However, for stores with\n * a custom conflict resolver (e.g. `createUnionMerge`) the seeded snapshot is\n * treated as a merge baseline: {@link hasMergeBaseline} returns true, so the\n * first pull/ingest merges against the seed rather than replacing it wholesale.\n * This closes the bootstrap window where a short first-pull response (a cache-\n * fallback on 429/5xx or a momentarily-short concurrent snapshot) would\n * silently drop items the resolver was configured to preserve. For the default\n * `deepMerge` resolver the first pull still takes the snapshot wholesale \u2014\n * behaviour is byte-identical to alpha.36.\n *\n * Requires the client to have been built with a `cache`.\n */\n async seedFromCache(): Promise<boolean> {\n if (this.aborted) return false\n const cached = await this.client.peekCache(this.pullPath)\n if (!cached) return false\n let data: Record<string, unknown>\n try {\n data = this.encryptor ? await this.encryptor.decrypt(cached.data) : cached.data\n } catch {\n return false // undecryptable (keyring skew / foreign epoch) \u2014 seed nothing\n }\n if (this.aborted) return false\n this.localData = data\n this.lastHash = cached.hash\n // Mark the seed so hasMergeBaseline() can protect it for custom resolvers.\n // lastCheckpoint stays 0: the first live pull is a full resync (checkpoint=0\n // in the query), not a delta against a possibly-stale cache timestamp.\n this.seeded = true\n this.lastFromCache = true\n return true\n }\n\n getCheckpoint(): number {\n return this.lastCheckpoint\n }\n\n /**\n * Apply a freshly-fetched `PullResult` to this manager's state WITHOUT\n * firing a network request. Used by the zustand binding's `mergeResult`\n * action to absorb a background revalidation result (delivered via\n * {@link StarfishClientOptions.onRevalidated}) into the store.\n *\n * Like {@link pull}, `ingest` conflict-merges the snapshot against the\n * established baseline via `this.onConflict` when a merge baseline exists\n * ({@link hasMergeBaseline}) \u2014 so a union-merge store does not lose array\n * items when a revalidation result (e.g. a stale cache-fallback on 429/5xx)\n * is a shorter snapshot. The baseline is established by either a prior\n * pull/ingest that advanced `lastCheckpoint`, or by a successful\n * {@link seedFromCache} for a store with a custom resolver. The first ingest\n * without such a baseline takes the snapshot wholesale (default `deepMerge`\n * stores are byte-identical to alpha.36). Sets `lastFromCache = false` (a\n * revalidation is a live response) so the binding can clear its `stale` flag.\n *\n * **Staleness guard**: if a `push()` advanced `lastCheckpoint` between the\n * time the revalidation request was sent and the time it resolves, the\n * result is from an older document version. Ingesting it would clobber the\n * user's just-saved edit and reset `lastHash` to a stale server hash\n * (causing a spurious 409 on the next push). We silently drop the result in\n * that case \u2014 the store's post-push state is already correct.\n */\n async ingest(result: PullResult): Promise<void> {\n if (this.aborted) return\n // Drop a revalidation result that is older than our current local state.\n // `lastCheckpoint` is advanced by every successful push() and pull(); a\n // revalidation snapshot whose document timestamp is strictly less than the\n // current checkpoint is stale relative to a concurrent push.\n if (result.timestamp < this.lastCheckpoint) return\n let incoming: Record<string, unknown>\n if (this.encryptor) {\n incoming = await this.encryptor.decrypt(result.data)\n if (this.aborted) return\n } else {\n incoming = result.data\n }\n // Honor the configured conflict resolver against the established baseline\n // (same as pull()). The first ingest takes the snapshot wholesale unless a\n // prior cache seed established a baseline for a custom resolver.\n this.localData = this.hasMergeBaseline() ? this.onConflict(this.localData, incoming) : incoming\n this.lastHash = result.hash\n this.lastCheckpoint = result.timestamp\n this.lastFromCache = false\n }\n\n async pull(): Promise<PullResult> {\n if (this.aborted) throw new AbortError()\n this.logger?.pullStart(this.loggerName)\n const start = performance.now()\n try {\n // NOTE: `SyncManager.pull` does NOT auto-enable `withKeyring`. Clients\n // that drive the keyring helpers from `recipients.ts` and want to save\n // the cold-start round-trip should call `client.pull(path, {withKeyring: true})`\n // directly. We keep `SyncManager` keyring-agnostic so it stays usable\n // for collections that don't use delegated encryption.\n const result = await this.client.pull(this.pullPath, this.lastCheckpoint)\n if (this.aborted) throw new AbortError()\n // True when the client served this from its offline cache (transport was\n // unreachable); a live response clears it. Surfaced as `stale` by the binding.\n this.lastFromCache = pullWasFromCache(result)\n\n let incoming: Record<string, unknown>\n if (this.encryptor) {\n incoming = await this.encryptor.decrypt(result.data)\n if (this.aborted) throw new AbortError()\n } else {\n incoming = result.data\n }\n // Honor the configured conflict resolver against the established baseline \u2014\n // the same resolver the push-conflict path (push 409 / resolve()) already\n // uses. A union-merge store must not lose array items when a pull returns a\n // shorter/stale snapshot (cache-fallback on 429/5xx or a momentarily-short\n // concurrent write). hasMergeBaseline() returns true when either a prior\n // pull/ingest advanced lastCheckpoint OR a cache seed established a baseline\n // for a custom resolver. The first pull without a prior seed (or with the\n // default deepMerge) takes the snapshot wholesale \u2014 byte-identical to\n // alpha.36 for stores without a custom resolver.\n this.localData = this.hasMergeBaseline() ? this.onConflict(this.localData, incoming) : incoming\n result.data = this.localData\n\n this.lastHash = result.hash\n this.lastCheckpoint = result.timestamp\n this.logger?.pullSuccess(this.loggerName, Math.round(performance.now() - start))\n return result\n } catch (err) {\n this.logger?.pullError(this.loggerName, err instanceof Error ? err.message : String(err))\n throw err\n }\n }\n\n async push(data: Record<string, unknown>): Promise<{ hash: string; timestamp: number }> {\n if (this.aborted) throw new AbortError()\n if (this.validate) {\n const result = this.validate(data)\n if (result !== true) throw new ValidationError(result)\n }\n this.logger?.pushStart(this.loggerName)\n const start = performance.now()\n let attempt = 0\n let pendingData = data\n\n while (attempt <= this.maxRetries) {\n try {\n const sealed = this.encryptor\n ? await this.encryptor.encrypt(pendingData)\n : pendingData\n if (this.aborted) throw new AbortError()\n\n // v3.0 signer path: sign the document author proof over the doc-author\n // canonical input (domain-tagged, bound to documentKey) and pass it as\n // top-level body siblings of `data` (NOT inside `data`), where the server\n // verifies it and stores the raw author pubkey.\n let author: AppendAuthor | undefined\n if (this.signer) {\n const { devEdPubHex, sign } = await this.signer.getSigner()\n if (this.aborted) throw new AbortError()\n const documentKey = stripPushPrefix(this.pushPath)\n const canonical = docAuthorCanonicalInput(documentKey, sealed as Record<string, unknown>)\n const sigBytes = await sign(new TextEncoder().encode(canonical))\n if (this.aborted) throw new AbortError()\n author = {\n [AUTHOR_PUBKEY_FIELD]: devEdPubHex,\n [AUTHOR_SIGNATURE_FIELD]: getBase64().encode(sigBytes),\n }\n }\n\n const result = await this.client.push(\n this.pushPath,\n sealed as Record<string, unknown>,\n this.lastHash,\n author,\n )\n if (this.aborted) throw new AbortError()\n this.lastHash = result.hash\n this.lastCheckpoint = result.timestamp\n this.localData = pendingData\n this.logger?.pushSuccess(this.loggerName, Math.round(performance.now() - start))\n return result\n } catch (err) {\n if (err instanceof AbortError) throw err\n if (!(err instanceof ConflictError) || attempt >= this.maxRetries) {\n this.logger?.pushError(this.loggerName, err instanceof Error ? err.message : String(err))\n throw err\n }\n this.logger?.conflict(this.loggerName, attempt + 1)\n try {\n const remote = await this.client.pull(this.pullPath)\n if (this.aborted) throw new AbortError()\n const remoteData = this.encryptor\n ? await this.encryptor.decrypt(remote.data)\n : remote.data\n if (this.aborted) throw new AbortError()\n this.lastHash = remote.hash\n this.lastCheckpoint = remote.timestamp\n pendingData = this.onConflict(pendingData, remoteData)\n } catch (resolveErr) {\n if (resolveErr instanceof AbortError) throw resolveErr\n const msg = resolveErr instanceof Error ? resolveErr.message : String(resolveErr)\n this.logger?.pushError(this.loggerName, `Conflict resolution failed (attempt ${attempt + 1}): ${msg}`)\n throw resolveErr\n }\n await new Promise<void>(resolve => setTimeout(resolve, Math.min(100 * Math.pow(2, attempt), 2000) + Math.random() * 100))\n attempt++\n }\n }\n throw new ConflictError()\n }\n\n async update(\n modifier: (current: Record<string, unknown>) => Record<string, unknown>\n ): Promise<{ hash: string; timestamp: number }> {\n await this.pull()\n const updated = modifier(this.localData)\n return this.push(updated)\n }\n}\n", "/** Validation result: true if valid, or an array of error messages. */\nexport type ValidationResult = true | string[]\n\n/** A function that validates data before push. */\nexport type Validator = (data: Record<string, unknown>) => ValidationResult\n\n/** Error thrown when pre-push validation fails. */\nexport class ValidationError extends Error {\n constructor(public readonly errors: string[]) {\n super(`Validation failed: ${errors.join(\"; \")}`)\n this.name = \"ValidationError\"\n }\n}\n\n/**\n * Creates a validator from a JSON Schema object.\n * Requires an Ajv-compatible validate function.\n *\n * @example\n * ```ts\n * import Ajv from \"ajv\"\n * const ajv = new Ajv()\n * const validator = createSchemaValidator(ajv, mySchema)\n * ```\n */\nexport function createSchemaValidator(\n ajv: { compile: (schema: object) => { (data: unknown): boolean; errors?: unknown }; errorsText: (errors?: unknown) => string },\n schema: object,\n): Validator {\n const validate = ajv.compile(schema)\n return (data) => {\n if (validate(data)) return true\n return [ajv.errorsText(validate.errors)]\n }\n}\n", "/** Minimal store interface for cross-tab sync. Works with both Zustand and Legend bindings. */\nexport interface BroadcastableStore {\n getState(): { data: Record<string, unknown>; dirty: boolean }\n setState(partial: { data: Record<string, unknown>; dirty: boolean }): void\n subscribe(listener: (state: { data: Record<string, unknown>; dirty: boolean }, prev: { data: Record<string, unknown>; dirty: boolean }) => void): () => void\n}\n\ninterface BroadcastPayload {\n data: Record<string, unknown>\n dirty: boolean\n}\n\n/**\n * Syncs a Starfish store across browser tabs using BroadcastChannel.\n * Works with any store that has getState/setState/subscribe (Zustand, Legend adapters, etc.).\n * Returns a cleanup function that closes the channel.\n */\nexport function setupBroadcastSync(\n store: BroadcastableStore,\n name: string,\n): () => void {\n const channel = new BroadcastChannel(`starfish-${name}`)\n let lastReceivedData: Record<string, unknown> | null = null\n\n channel.onmessage = (event: MessageEvent<unknown>) => {\n const payload = event.data as BroadcastPayload | undefined\n if (!payload || typeof payload !== \"object\" || !payload.data || typeof payload.data !== \"object\") return\n lastReceivedData = payload.data\n store.setState({ data: payload.data, dirty: !!payload.dirty })\n }\n\n const unsub = store.subscribe((state, prev) => {\n if (state.data === lastReceivedData) return\n if (state.data !== prev.data || state.dirty !== prev.dirty) {\n try {\n channel.postMessage({ data: state.data, dirty: state.dirty } satisfies BroadcastPayload)\n } catch { /* non-serializable data \u2014 skip broadcast */ }\n }\n })\n\n return () => {\n unsub()\n channel.close()\n }\n}\n\n/**\n * Syncs a Starfish store across browser tabs using storage events.\n * Fallback for environments without BroadcastChannel.\n * Returns a cleanup function.\n */\nexport function setupStorageFallback(\n store: BroadcastableStore,\n name: string,\n): () => void {\n const storageKey = `starfish-broadcast-${name}`\n let lastReceivedData: Record<string, unknown> | null = null\n\n const onStorage = (e: StorageEvent) => {\n if (e.key !== storageKey || !e.newValue) return\n let payload: BroadcastPayload\n try {\n payload = JSON.parse(e.newValue)\n } catch {\n return\n }\n if (!payload || typeof payload !== \"object\" || !payload.data || typeof payload.data !== \"object\") return\n lastReceivedData = payload.data\n store.setState({ data: payload.data, dirty: !!payload.dirty })\n }\n\n globalThis.addEventListener(\"storage\", onStorage)\n\n const unsub = store.subscribe((state, prev) => {\n if (state.data === lastReceivedData) return\n if (state.data !== prev.data || state.dirty !== prev.dirty) {\n try {\n localStorage.setItem(\n storageKey,\n JSON.stringify({ data: state.data, dirty: state.dirty } satisfies BroadcastPayload),\n )\n } catch { /* quota exceeded or non-serializable \u2014 skip */ }\n }\n })\n\n return () => {\n unsub()\n globalThis.removeEventListener(\"storage\", onStorage)\n }\n}\n\n/**\n * Auto-detects the best cross-tab sync mechanism and sets it up.\n * Uses BroadcastChannel when available, falls back to storage events.\n * Returns a cleanup function.\n */\nexport function setupCrossTabSync(\n store: BroadcastableStore,\n name: string,\n): () => void {\n if (typeof BroadcastChannel !== \"undefined\") {\n return setupBroadcastSync(store, name)\n }\n if (typeof globalThis.addEventListener === \"function\" && typeof localStorage !== \"undefined\") {\n return setupStorageFallback(store, name)\n }\n return () => {}\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["AUTHOR_PUBKEY_FIELD", "AUTHOR_SIGNATURE_FIELD", "AUTHOR_PUBKEY_FIELD", "AUTHOR_SIGNATURE_FIELD"]
7
7
  }