local-replica 0.0.0 → 0.0.3

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/zustand/errors.ts","../../src/zustand/lens.ts","../../src/zustand/requests.ts","../../src/zustand/useSync.ts","../../src/zustand/createReplicaSlice.ts","../../src/zustand/driver.ts"],"sourcesContent":["/** `slice.fetch` was asked to fetch for a scope that is no longer active. */\nexport class ReplicaScopeChangedError extends Error {\n constructor(name: string, scope: string) {\n super(`[replica] ${name}: scope ${scope} is no longer active`);\n this.name = 'ReplicaScopeChangedError';\n }\n}\n","import type { ReplicaPagedData } from '../core/paging/types';\nimport type { ReplicaViewWrite } from '../core/state/reducer';\n\n/**\n * One `key` write in a batched lens update (see {@link ReplicaLens.setMany}).\n */\nexport interface ReplicaViewSet<TData> {\n data: TData | undefined;\n key: string;\n}\n\n/**\n * Where the materialized value lives in the domain store. Selectors keep\n * reading this location; the slice is the only writer.\n */\nexport interface ReplicaLens<TStore, TData> {\n clear: (state: TStore) => Partial<TStore>;\n get: (state: TStore, key: string) => TData | undefined;\n /** Enumerate loaded keys (needed for entity propagation). */\n keys?: (state: TStore) => string[];\n set: (state: TStore, key: string, data: TData | undefined) => Partial<TStore>;\n /**\n * Apply several key writes in one pass — one clone of the backing field\n * instead of one per key. Optional: the slice falls back to repeated `set`\n * when a lens does not implement it.\n */\n setMany?: (state: TStore, entries: ReplicaViewSet<TData>[]) => Partial<TStore>;\n}\n\n/**\n * Apply every view write as ONE host update, so subscribers never observe a\n * view out of step with its bookkeeping. Consecutive key writes go through\n * the lens' `setMany` when it has one, so seeding N rows clones the backing\n * field once instead of N times.\n */\nexport const applyLensWrites = <TStore, TData>(\n view: ReplicaLens<TStore, TData>,\n state: TStore,\n writes: ReplicaViewWrite<TData>[],\n): Partial<TStore> => {\n let patch: Partial<TStore> = {};\n let current = state;\n let pending: ReplicaViewSet<TData>[] = [];\n\n const flush = () => {\n if (pending.length === 0) return;\n const entries = pending;\n pending = [];\n if (view.setMany && entries.length > 1) {\n const next = view.setMany(current, entries);\n patch = { ...patch, ...next };\n current = { ...current, ...next };\n return;\n }\n for (const entry of entries) {\n const next = view.set(current, entry.key, entry.data);\n patch = { ...patch, ...next };\n current = { ...current, ...next };\n }\n };\n\n for (const write of writes) {\n if ('type' in write) {\n flush();\n const next = view.clear(current);\n patch = { ...patch, ...next };\n current = { ...current, ...next };\n } else {\n pending.push({ data: write.data, key: write.key });\n }\n }\n flush();\n return patch;\n};\n\n/** Lens for the common case: a `Record<key, TData>` field on the store. */\nexport const recordLens = <TStore, TData>(\n field: keyof TStore & string,\n): ReplicaLens<TStore, TData> => ({\n clear: () => ({ [field]: {} }) as Partial<TStore>,\n get: (state, key) => (state[field] as Record<string, TData> | undefined)?.[key],\n keys: (state) => Object.keys((state[field] as Record<string, TData> | undefined) ?? {}),\n set: (state, key, data) => {\n const next = { ...(state[field] as Record<string, TData> | undefined) };\n if (data === undefined) delete next[key];\n else next[key] = data;\n return { [field]: next } as Partial<TStore>;\n },\n setMany: (state, entries) => {\n // One clone of the record, then every key — a list refresh seeds N groups\n // in a single copy instead of N.\n const next = { ...(state[field] as Record<string, TData> | undefined) };\n for (const { key, data } of entries) {\n if (data === undefined) delete next[key];\n else next[key] = data;\n }\n return { [field]: next } as Partial<TStore>;\n },\n});\n\n/**\n * Lens for a paged view split across store fields: the rows stay where\n * existing readers expect them (`itemsField[key]`, a plain array), the paging\n * bookkeeping lives in `metaField[key]`, and `derive` recomputes fields built\n * from the rows (e.g. a parsed display list) in the same commit — so a derived\n * field is never one frame behind its rows.\n *\n * Rows seeded without bookkeeping (by code outside the replica) read as a\n * single head page.\n */\nexport const splitPagedLens = <TStore, TItem, TCursor = unknown>({\n clearDerived,\n derive,\n itemsField,\n metaField,\n}: {\n /** Derived fields to reset on a scope change. */\n clearDerived?: () => Partial<TStore>;\n /** Fields computed from an entry's rows (`undefined` = the entry is gone). */\n derive?: (state: TStore, key: string, items: TItem[] | undefined) => Partial<TStore>;\n itemsField: keyof TStore & string;\n metaField: keyof TStore & string;\n}): ReplicaLens<TStore, ReplicaPagedData<TItem, TCursor>> => {\n type Meta = Omit<ReplicaPagedData<TItem, TCursor>, 'items'>;\n const NO_META = {} as Meta;\n // Same rows + same bookkeeping → the same view object, so the engine's\n // reference checks see \"unchanged\".\n const views = new WeakMap<TItem[], WeakMap<Meta, ReplicaPagedData<TItem, TCursor>>>();\n const remember = (items: TItem[], meta: Meta, view: ReplicaPagedData<TItem, TCursor>) => {\n let byMeta = views.get(items);\n if (!byMeta) views.set(items, (byMeta = new WeakMap()));\n byMeta.set(meta, view);\n return view;\n };\n const rowsOf = (state: TStore) => (state[itemsField] ?? {}) as Record<string, TItem[]>;\n const metaOf = (state: TStore) => (state[metaField] ?? {}) as Record<string, Meta>;\n\n return {\n clear: () => ({ [itemsField]: {}, [metaField]: {}, ...clearDerived?.() }) as Partial<TStore>,\n get: (state, key) => {\n const items = rowsOf(state)[key];\n if (!items) return undefined;\n const meta = metaOf(state)[key] ?? NO_META;\n const cached = views.get(items)?.get(meta);\n if (cached) return cached;\n const view =\n meta === NO_META\n ? { currentPage: 0, hasMore: true, items, pageSize: items.length }\n : { ...meta, items };\n return remember(items, meta, view);\n },\n keys: (state) => Object.keys(rowsOf(state)),\n set: (state, key, data) => {\n const rows = { ...rowsOf(state) };\n const metas = { ...metaOf(state) };\n if (data === undefined) {\n delete rows[key];\n delete metas[key];\n } else {\n const { items, ...meta } = data;\n rows[key] = items;\n metas[key] = meta;\n remember(items, meta, data);\n }\n const next = { ...state, [itemsField]: rows, [metaField]: metas } as TStore;\n return {\n [itemsField]: rows,\n [metaField]: metas,\n ...derive?.(next, key, data?.items),\n } as Partial<TStore>;\n },\n };\n};\n","import { replicaKeys } from '../core/resource/queryKeys';\nimport { stableQueryKey } from '../core/resource/storageKey';\nimport type { ReplicaResource } from '../core/resource/types';\n\n/** One head request, from `begin` to `settle`. */\nexport interface ReplicaRequest<TParams, TFetched> {\n /** `clear()` generation of `scope` when the request started. */\n clearGeneration: number;\n key: string;\n params: TParams;\n /** Settles the request; set once it is sent. */\n promise?: Promise<TFetched>;\n /** Row key of the query it asks for (`headQuery` guard). */\n queryKey: string;\n /** `remove` generation of `scope` + `key` when the request started. */\n removalGeneration: number;\n /** Scope + sync key: requests of the same id dedupe and supersede each other. */\n requestId: string;\n /** The cache scope it was captured under. */\n scope: string;\n /** Start order across the slice. */\n seq: number;\n /** What it settled to; a request is settled once (a joined fetch and its sync share it). */\n settled?: ReplicaSettled;\n /** The response (or failure) it settled with. */\n settledWith?: unknown;\n source: 'fetch' | 'sync';\n syncKey: readonly unknown[];\n}\n\nexport interface ReplicaSettled {\n /** Settled into the active scope (folded in or removed as missing). */\n applied: boolean;\n /** Passed every guard; `isActive` says whether its scope is still on screen. */\n current: boolean;\n isActive: boolean;\n missing: boolean;\n}\n\nexport type ReplicaOutcome<TFetched> = { data: TFetched } | { error: unknown };\n\nexport interface ReplicaRequestsDeps<TParams, TFetched> {\n clearGeneration: (scope: string) => number;\n fetcher?: (params: TParams, cursor?: any) => Promise<TFetched>;\n /** Fold a request's outcome into the replica (guards, `applyHead`, effects); runs once. */\n fold: (\n request: ReplicaRequest<TParams, TFetched>,\n outcome: ReplicaOutcome<TFetched>,\n ) => ReplicaSettled;\n removalGeneration: (scope: string, key: string) => number;\n resource: ReplicaResource<TParams, any, TFetched>;\n}\n\n/** Request id of a sync key in a scope: requests of the same id dedupe and supersede. */\nexport const replicaRequestId = (scope: string, syncKey: unknown) =>\n `${scope}\\u0000${stableQueryKey(syncKey)}`;\n\n/**\n * Lifecycle bookkeeping of head requests: capture (`begin`), send and track in\n * flight, retire older requests once a newer response applied, and settle each\n * request exactly once. What settling *does* is the slice's `fold`; this only\n * guarantees every path — a mounted `useSync`, or `fetch` with no view mounted\n * — goes through it the same way.\n */\nexport const createRequests = <TParams, TFetched>({\n clearGeneration,\n fetcher,\n fold,\n removalGeneration,\n resource,\n}: ReplicaRequestsDeps<TParams, TFetched>) => {\n let requestSeq = 0;\n\n /**\n * Requests whose start order is at or below this number are retired, per\n * request id: a newer response already applied, or a `fresh` fetch started\n * after them. Their late response would repaint an older value.\n *\n * An entry only matters while a request it retires is still unsettled:\n * every request started later has a higher sequence, so a missing entry\n * (read as 0) admits exactly the same responses. Entries are pruned as soon\n * as no sent request at or below them is pending, so the map holds only the\n * ids with an older request in flight.\n */\n const retiredThrough = new Map<string, number>();\n /** Sequences of the sent requests that have not settled yet, per request id. */\n const unsettled = new Map<string, Set<number>>();\n\n /** Drop the retirement of `requestId` once no request it retires is pending. */\n const pruneRetired = (requestId: string) => {\n const through = retiredThrough.get(requestId);\n if (through === undefined) return;\n for (const seq of unsettled.get(requestId) ?? []) if (seq <= through) return;\n retiredThrough.delete(requestId);\n };\n\n const retire = (requestId: string, seq: number) => {\n if ((retiredThrough.get(requestId) ?? 0) < seq) retiredThrough.set(requestId, seq);\n // Every entry is pruned the moment it stops retiring a pending request\n // (here, and as each request settles), so the map stays bounded by the ids\n // with an older request in flight — no size backstop needed. Evicting an\n // entry that still retires one would let its stale response win.\n pruneRetired(requestId);\n };\n\n /** A newer response already applied, or a `fresh` fetch started after it. */\n const isRetired = ({ requestId, seq }: ReplicaRequest<TParams, TFetched>) =>\n seq <= (retiredThrough.get(requestId) ?? 0);\n\n const trackUnsettled = ({ requestId, seq }: ReplicaRequest<TParams, TFetched>) => {\n const pending = unsettled.get(requestId) ?? new Set<number>();\n unsettled.set(requestId, pending);\n pending.add(seq);\n };\n\n const untrackUnsettled = ({ requestId, seq }: ReplicaRequest<TParams, TFetched>) => {\n const pending = unsettled.get(requestId);\n if (!pending?.delete(seq)) return;\n if (pending.size === 0) unsettled.delete(requestId);\n pruneRetired(requestId);\n };\n\n /** The latest request in flight per id, which a non-`fresh` `fetch` joins. */\n const inflight = new Map<string, ReplicaRequest<TParams, TFetched>>();\n /** `useSync` requests in flight per id (`fetch` seeds the driver only while none is). */\n const syncInflight = new Map<string, number>();\n\n /**\n * `useSync` request each sync id last started, settled by the driver's\n * callback. The driver settles one request per key at a time (a newer\n * request supersedes the older one), so the latest start is the one that\n * settles.\n */\n const syncRequests = new Map<string, ReplicaRequest<TParams, TFetched>>();\n\n const syncKeyOf = (params: TParams, scope: string, key: string) =>\n resource.syncKey?.(params) ??\n replicaKeys.sync(resource.name, resource.version, scope, key, params);\n\n /** Capture what a head request for `params` started under. */\n const begin = (\n params: TParams,\n scope: string,\n source: 'fetch' | 'sync',\n ): ReplicaRequest<TParams, TFetched> => {\n const key = resource.key(params);\n const syncKey = syncKeyOf(params, scope, key);\n return {\n clearGeneration: clearGeneration(scope),\n key,\n params,\n queryKey: resource.storageKey(params),\n removalGeneration: removalGeneration(scope, key),\n requestId: replicaRequestId(scope, syncKey),\n scope,\n seq: ++requestSeq,\n source,\n syncKey,\n };\n };\n\n /** Send the request and track it in flight until it settles. */\n const send = (request: ReplicaRequest<TParams, TFetched>) => {\n const { requestId } = request;\n const sent = fetcher!(request.params, undefined);\n // Tolerate a fetcher (a test double) that does not return a promise: the\n // driver still gets exactly what it returned.\n const promise = Promise.resolve(sent);\n request.promise = promise;\n // Until it settles, a retirement that covers it must be kept.\n trackUnsettled(request);\n inflight.set(requestId, request);\n if (request.source === 'sync')\n syncInflight.set(requestId, (syncInflight.get(requestId) ?? 0) + 1);\n const done = () => {\n if (inflight.get(requestId) === request) inflight.delete(requestId);\n if (request.source !== 'sync') return;\n // Incremented before this `done` was attached, so the count is present.\n const count = syncInflight.get(requestId)! - 1;\n if (count > 0) syncInflight.set(requestId, count);\n else syncInflight.delete(requestId);\n };\n promise.then(done, done);\n // A `useSync` request settles on its own response, not only through the\n // driver's callback: a driver skips that callback once the hook moved to\n // another key, unmounted, or a newer request superseded it — and the\n // resource effects (a terminal absence, a purge) must not depend on which\n // view is still mounted. The callback then reads the memoized result.\n if (request.source === 'sync')\n promise.then(\n (data) => settle(request, { data }),\n (error: unknown) => settle(request, { error }),\n );\n return sent;\n };\n\n /**\n * Fold a settled head request into the replica (see the slice's `fold`). A\n * request is settled once: a `fetch` that joined a `useSync` request shares\n * its result.\n */\n const settle = (\n request: ReplicaRequest<TParams, TFetched>,\n outcome: ReplicaOutcome<TFetched>,\n ): ReplicaSettled => {\n if (request.settled) return request.settled;\n request.settledWith = 'error' in outcome ? outcome.error : outcome.data;\n request.settled = fold(request, outcome);\n untrackUnsettled(request);\n if (request.source === 'sync') releaseSyncRequest(request);\n return request.settled;\n };\n\n /**\n * Drop a settled `useSync` request's record once its driver callback had its\n * chance to read it. The record is normally taken by that callback, but the\n * driver skips it when the hook changed key, unmounted or was superseded —\n * the request still settles on its own promise, and its record (holding the\n * response) must not stay behind. A driver runs the callback in the same\n * turn the response resolves (SWR does, in microtasks), so a macrotask later\n * the record is only a leak.\n */\n const releaseSyncRequest = (request: ReplicaRequest<TParams, TFetched>) => {\n setTimeout(() => {\n if (syncRequests.get(request.requestId) === request) syncRequests.delete(request.requestId);\n }, 0);\n };\n\n /** Start a `useSync` request and record it for the driver's callback. */\n const startSync = (params: TParams, scope: string) => {\n const request = begin(params, scope, 'sync');\n syncRequests.set(request.requestId, request);\n return send(request);\n };\n\n /**\n * The `useSync` request settling now: the one `requestId` last started. A\n * response the driver settles without having run our fetcher (a\n * cache-shared key), or one the recorded request did not produce, is\n * captured now: nothing it started under is known.\n */\n const takeSyncRequest = (\n requestId: string,\n response: unknown,\n params: TParams,\n scope: string,\n ): ReplicaRequest<TParams, TFetched> => {\n const request = syncRequests.get(requestId);\n syncRequests.delete(requestId);\n // The request already settled on its own response, and the driver now\n // reports that same response: read the memoized result.\n if (request && (!request.settled || request.settledWith === response)) return request;\n return begin(params, scope, 'sync');\n };\n\n return {\n begin,\n /** Records of `useSync` requests not yet released (leak checks in tests). */\n debugPendingSyncRequests: () => syncRequests.size,\n /** Retirement records still held (leak checks in tests). */\n debugRetiredRequests: () => retiredThrough.size,\n /** The request in flight for `requestId`, which a non-`fresh` `fetch` joins. */\n inflight: (requestId: string) => inflight.get(requestId),\n isRetired,\n /** Whether a `useSync` request of `requestId` is in flight. */\n isSyncInflight: (requestId: string) => syncInflight.has(requestId),\n retire,\n send,\n settle,\n startSync,\n syncKeyOf,\n takeSyncRequest,\n };\n};\n\nexport type ReplicaRequests<TParams, TFetched> = ReturnType<\n typeof createRequests<TParams, TFetched>\n>;\n","import { useEffect, useLayoutEffect, useRef } from 'react';\n\nimport type { ReplicaEngine } from '../core/engine/createReplicaEngine';\nimport { replicaKeys } from '../core/resource/queryKeys';\nimport type { ReplicaResource } from '../core/resource/types';\nimport type { ReplicaSyncDriver } from './driver';\nimport type { ReplicaLens } from './lens';\nimport { replicaRequestId, type ReplicaRequests } from './requests';\nimport type { ReplicaSyncOptions, ReplicaSyncResult } from './types';\n\nexport interface ReplicaUseSyncDeps<TStore, TParams, TData, TFetched> {\n driver: ReplicaSyncDriver;\n engine: Pick<\n ReplicaEngine<TParams, TData, TFetched>,\n 'ensureScope' | 'fetcher' | 'hydrate' | 'retain'\n >;\n get: () => TStore;\n /** Row key of the head query each entry last asked for (see the slice's `headQuery`). */\n headQuery: Map<string, string>;\n requests: ReplicaRequests<TParams, TFetched>;\n resource: ReplicaResource<TParams, TData, TFetched>;\n view: ReplicaLens<TStore, TData>;\n}\n\n/**\n * Hydrates the persisted row once per scope/key/query, then lets the driver\n * fetch and revalidate the head. Read the data from the store.\n */\nexport const createUseSync = <TStore, TParams, TData, TFetched>({\n driver,\n engine: { ensureScope, fetcher, hydrate, retain },\n get,\n headQuery,\n requests,\n resource,\n view,\n}: ReplicaUseSyncDeps<TStore, TParams, TData, TFetched>) => {\n /**\n * Entries (`scope` + key) whose one-shot hydration actually painted a\n * persisted row. The hydration query settles `true` on a cache miss too, so\n * a reactivated key may only replay `onHydrated` when its row really came from\n * storage — not off a provisional seed or another source that happens to sit\n * in the view.\n */\n const hydratedEntries = new Set<string>();\n const hydratedEntryId = (scope: string, key: string) => `${scope}\\u0000${key}`;\n\n const useSync = (\n params: TParams | null | undefined,\n {\n enabled = true,\n onError,\n onHydrated,\n onSuccess,\n ...schedule\n }: ReplicaSyncOptions<TFetched, TData> = {},\n ): ReplicaSyncResult => {\n const scope = resource.scope.use();\n const key = params ? resource.key(params) : undefined;\n const active = enabled && !!params && key !== undefined;\n const queryKey = params ? resource.storageKey(params) : undefined;\n\n // Layout effect: runs before paint, so a scope switch never shows a frame\n // of the previous identity's data.\n useLayoutEffect(() => {\n if (active) ensureScope(scope);\n }, [active, scope]);\n\n // Remember the query this entry now asks for (see `headQuery`).\n useLayoutEffect(() => {\n if (active && key !== undefined && queryKey !== undefined) headQuery.set(key, queryKey);\n }, [active, key, queryKey]);\n\n // Pin the entry on screen against eviction (`persist.maxEntries`).\n useLayoutEffect(() => {\n if (active && key !== undefined && queryKey !== undefined) return retain(key, queryKey);\n }, [active, key, queryKey]);\n\n // The entry this hook currently asks for. A hydrate that resolves after the\n // hook moved to another key still fills its own cache entry (harmless), but\n // it must not replay `onHydrated`: those side effects act on the *current*\n // view, so they would re-adopt a superseded object (e.g. navigate g1 → g2\n // while g1's storage read is slow, then g1's hydrate resets the active group\n // back to g1's supervisor).\n const activeKey = useRef<string | undefined>(undefined);\n // The entry (scope + key) whose hydration side effects already ran in its\n // current activation. Reset on every key or scope change, so an entry that\n // comes back (g1 → g2 → g1, or scope A → B → A with the same key) gets them\n // replayed even though its `once` hydration is not re-run.\n const hydratedKey = useRef<string | undefined>(undefined);\n useLayoutEffect(() => {\n activeKey.current = active ? key : undefined;\n hydratedKey.current = undefined;\n }, [active, key, scope]);\n\n const hydration = driver.useQuery<boolean>(\n active && resource.persisted && resource.persistKey(key!)\n ? replicaKeys.hydrate(resource.name, resource.version, scope, resource.storageKey(params!))\n : null,\n async () => {\n const didHydrate = await hydrate(params!, scope);\n if (didHydrate) hydratedEntries.add(hydratedEntryId(scope, key!));\n // A superseded key (the hook moved on) runs no side effects at all.\n if (activeKey.current !== key) return true;\n hydratedKey.current = hydratedEntryId(scope, key!);\n // A persisted row is a successful value with no network response: replay\n // the success side effects off it, so an offline / slow first paint still\n // adopts the roster and settles the flags the response path would.\n if (didHydrate) {\n const hydrated = view.get(get(), key!);\n if (hydrated !== undefined) onHydrated?.(hydrated);\n }\n return true;\n },\n { once: true },\n );\n\n // Reactivating a key whose hydration already settled: the driver reuses the\n // `once` result and the fetcher above does not run, so replay the side\n // effects off the value in memory. Without it, returning to g1 offline\n // leaves them acting on g2 until a response lands (or forever, offline).\n useEffect(() => {\n if (!active || key === undefined || hydration.data !== true) return;\n const entry = hydratedEntryId(scope, key);\n if (hydratedKey.current === entry) return;\n hydratedKey.current = entry;\n const current = view.get(get(), key);\n if (current !== undefined) {\n // A lookup that missed storage hydrated nothing to replay.\n if (hydratedEntries.has(entry)) onHydrated?.(current);\n return;\n }\n // Back on a scope whose view `ensureScope` cleared (A → B → A): the cached\n // `once` result means the fetcher will not read storage again, so read it\n // here — otherwise a row still in A's partition stays off screen while\n // the network is slow or offline.\n void hydrate(params!, scope).then((didHydrate) => {\n if (!didHydrate) return;\n hydratedEntries.add(entry);\n if (activeKey.current !== key || resource.scope.get() !== scope) return;\n const hydrated = view.get(get(), key);\n if (hydrated !== undefined) onHydrated?.(hydrated);\n });\n }, [active, key, scope, hydration.data]);\n\n const syncKey = active && fetcher ? requests.syncKeyOf(params!, scope, key!) : null;\n /** The request settling now: the one this sync id last started. */\n const takeRequest = (response: unknown) =>\n requests.takeSyncRequest(replicaRequestId(scope, syncKey), response, params!, scope);\n\n const sync = driver.useQuery<TFetched>(\n syncKey,\n () => requests.startSync(params!, scope),\n {\n ...schedule,\n // Always installed, so a failed request is settled (and its capture\n // dropped) even when the caller has no error side effects. A failure\n // of a query the entry has moved past (or of a request from before a\n // clear) never surfaces on the query now on screen; an off-scope one is\n // still forwarded with the captured key/scope (see `settle`).\n onError: (error: unknown) => {\n const settled = requests.settle(takeRequest(error), { error });\n if (settled.current) onError?.(error, { isActive: settled.isActive, key, scope });\n },\n onSuccess: (data) => {\n const settled = requests.settle(takeRequest(data), { data });\n if (!settled.current || !settled.isActive) return;\n // A key the hook has moved past (g1 → g2 while g1's request is in\n // flight): the response still fills g1's own entry, but this view's\n // side effects would re-adopt g1 over the g2 view.\n if (activeKey.current !== key) return;\n onSuccess?.(data);\n },\n },\n );\n\n return {\n error: sync.error,\n // A read with no key has nothing to hydrate, so it counts as hydrated: a\n // conditional consumer must not show \"loading\" for an absent id (the\n // former SWR disabled-key behaviour). A read that is disabled but *has* a\n // key still waits for its persisted row.\n isHydrated:\n key === undefined ||\n !resource.persisted ||\n !resource.persistKey(key) ||\n hydration.data === true,\n isValidating: sync.isValidating,\n revalidate: () => sync.mutate(),\n };\n };\n\n return useSync;\n};\n","import { createReplicaEngine } from '../core/engine/createReplicaEngine';\nimport { isReplicaSyncKey } from '../core/resource/queryKeys';\nimport type { ReplicaResource } from '../core/resource/types';\nimport type { ReplicaState } from '../core/state/types';\nimport { ReplicaScopeChangedError } from './errors';\nimport { applyLensWrites } from './lens';\nimport {\n createRequests,\n type ReplicaOutcome,\n type ReplicaRequest,\n type ReplicaSettled,\n} from './requests';\nimport type {\n CreateReplicaSliceOptions,\n ReplicaFetchOptions,\n ReplicaFetchResult,\n ReplicaSettleContext,\n} from './types';\nimport { createUseSync } from './useSync';\n\n/**\n * Bind a replica to a domain Zustand store.\n *\n * The domain store stays the only UI source of truth: components read the\n * `view` location through their usual selectors. The engine owns every\n * transition of that location; this slice adds the Zustand port and a\n * `useSync` hook that only orchestrates fetching — data never flows through\n * its return value.\n */\nexport const createReplicaSlice = <TStore, TParams, TData, TFetched = TData>(\n resource: ReplicaResource<TParams, TData, TFetched>,\n {\n driver,\n effects,\n get,\n set,\n stateKey,\n view,\n ...options\n }: CreateReplicaSliceOptions<TStore, TParams, TData, TFetched>,\n) => {\n const revalidate = (key?: string) =>\n driver.revalidate((queryKey) =>\n isReplicaSyncKey(queryKey, resource.name, { key, scope: resource.scope.get() }),\n );\n\n const engine = createReplicaEngine(resource, {\n ...options,\n port: {\n // One host update for the view writes and the bookkeeping slot together.\n commit: (writes, state, label) =>\n set(\n { ...applyLensWrites(view, get(), writes), [stateKey]: state } as Partial<TStore>,\n false,\n label,\n ),\n getState: () => get()[stateKey] as unknown as ReplicaState<TData>,\n keys: view.keys && (() => view.keys!(get())),\n read: (key) => view.get(get(), key),\n },\n revalidate,\n });\n const { applyHead, clearGeneration, fetcher, isMissingHead, removalGeneration } = engine;\n\n /**\n * Row key of the head query each entry last asked for. One entry backs a\n * single view, so a response that belongs to a query the entry has already\n * moved past must not repaint it — it would show a superseded query, and a\n * view gated on the current query would then wait forever. Making this an\n * invariant of the slice (not of one driver) keeps it true for drivers that\n * leave a request's own callback live after the query changed.\n */\n const headQuery = new Map<string, string>();\n\n // ---- one settle pipeline ---------------------------------------------\n // Every head response — a mounted `useSync`, or `fetch` with no view mounted\n // — is captured by `begin` when its request starts and folded in by `fold`\n // when it settles (see `requests.ts`), so every path applies the same guards\n // and the same resource effects.\n\n const requests = createRequests<TParams, TFetched>({\n clearGeneration,\n fetcher,\n fold: (request, outcome) => fold(request, outcome),\n removalGeneration,\n resource,\n });\n\n /**\n * Whether the request still answers the query its entry shows. A `useSync`\n * request must match the query the entry last asked for (see `headQuery`). A\n * `fetch` may also land with no view mounted, but never over a view that\n * shows another query of the key.\n */\n const isActiveQuery = ({ key, queryKey, source }: ReplicaRequest<TParams, TFetched>) => {\n const head = headQuery.get(key);\n return source === 'sync' ? head === queryKey : head === undefined || head === queryKey;\n };\n\n /**\n * Fold a settled head request into the replica: apply every guard, then\n * `replace` / remove a terminal absence (in the captured scope) / purge one\n * captured off-scope, then run the resource effects.\n */\n const fold = (\n request: ReplicaRequest<TParams, TFetched>,\n outcome: ReplicaOutcome<TFetched>,\n ): ReplicaSettled => {\n const { key, params, scope } = request;\n /**\n * Whether the entry is still on the scope the request was captured under.\n *\n * A response can settle after an identity/workspace switch. `replace` drops\n * it — the engine refuses off-scope actions — but no side effect may run\n * either: it carries the previous identity's state (onboarding redirect,\n * display snapshot, analytics identify, settings writes). The captured\n * key/scope is not enough on its own, because a switch to a scope that\n * reads the same key/query satisfies the other guards.\n */\n const isActive = resource.scope.get() === scope;\n const dropped: ReplicaSettled = { applied: false, current: false, isActive, missing: false };\n\n // Discard a response that started before a `clear()` of its scope or a\n // `remove` of its entry (it holds wiped data), one for a query the entry\n // has moved past (the newer query owns the view, see `headQuery`), and one\n // older than a response already applied or a `fresh` fetch.\n if (\n request.clearGeneration !== clearGeneration(scope) ||\n request.removalGeneration !== removalGeneration(scope, key) ||\n requests.isRetired(request) ||\n !isActiveQuery(request)\n )\n return dropped;\n\n if ('error' in outcome) {\n // Forwarded even off-scope: the captured `key`/`scope` lets a consumer\n // purge that scope's row (a late NOT_FOUND / FORBIDDEN) without touching\n // the scope on screen; `isActive` tells it whether the failure also\n // belongs to the view on screen.\n effects?.onError?.(outcome.error, params, { isActive, key, scope });\n return { ...dropped, current: true };\n }\n\n if (!isActive) {\n // Off-scope: never fold a value into the scope on screen. A terminal\n // absence still purges its own scope's row (`remove` routes it there), so\n // a switch back cannot hydrate a value the server no longer has.\n const missing = isMissingHead(outcome.data);\n if (missing) {\n remove(key, scope);\n effects?.onPurge?.(params, { key, scope, source: request.source });\n }\n return { ...dropped, current: true, missing };\n }\n\n requests.retire(request.requestId, request.seq - 1);\n const { outcome: folded } = applyHead(params, outcome.data, scope);\n const context: ReplicaSettleContext = { key, scope, source: request.source };\n if (folded === 'replaced') effects?.onApplied?.(outcome.data, params, context);\n else if (folded === 'missing') effects?.onMissing?.(params, context);\n return {\n applied: folded !== 'dropped',\n current: true,\n isActive,\n missing: folded === 'missing',\n };\n };\n\n /**\n * Fetch the head of an entry now and settle it like a `useSync` response —\n * for stores that need the data while no view is mounted (a hover prefetch,\n * a refresh after a mutation on a route without the list). `revalidate` only\n * re-runs mounted queries.\n *\n * Joins a request in flight for the same entry unless `fresh`. Resolves with\n * what was fetched and whether it was applied; rejects when the request\n * fails, after routing the failure through `effects.onError`.\n */\n const fetch = async (\n params: TParams,\n { fresh = false, scope = resource.scope.get() }: ReplicaFetchOptions = {},\n ): Promise<ReplicaFetchResult<TFetched>> => {\n if (!fetcher) throw new Error(`[replica] ${resource.name} has no fetcher`);\n if (scope !== resource.scope.get()) throw new ReplicaScopeChangedError(resource.name, scope);\n let request = requests.begin(params, scope, 'fetch');\n const joined = fresh ? undefined : requests.inflight(request.requestId);\n if (joined) request = joined;\n else {\n if (fresh) requests.retire(request.requestId, request.seq - 1);\n requests.send(request);\n }\n\n let data: TFetched;\n try {\n data = await request.promise!;\n } catch (error) {\n requests.settle(request, { error });\n throw error;\n }\n const settled = requests.settle(request, { data });\n // Seed the driver's cache with what this fetch applied, unless a `useSync`\n // request of the key is in flight: it fills the cache itself, and a write\n // overtaking it could make the driver discard its newer response.\n if (\n settled.applied &&\n request.source === 'fetch' &&\n !requests.isSyncInflight(request.requestId)\n )\n driver.seed?.(request.syncKey, data);\n return { applied: settled.applied, data, missing: settled.missing };\n };\n\n /**\n * Forget the key's head query as well as the entry. A `useSync` request\n * already in flight for it would otherwise pass the settle guards (the\n * marker still matches), `replace` the entry and persist pre-mutation rows\n * back over an invalidation that has already completed. (The engine's\n * removal generation drops a `fetch` captured before the removal.)\n */\n const remove = (key: string, scope?: string) => {\n // Only the active scope's marker belongs to the entry on screen; an\n // off-scope removal (see `engine.remove`) must not drop it.\n if (scope === undefined || scope === resource.scope.get()) headQuery.delete(key);\n return engine.remove(key, scope);\n };\n\n const useSync = createUseSync<TStore, TParams, TData, TFetched>({\n driver,\n engine,\n get,\n headQuery,\n requests,\n resource,\n view,\n });\n\n return {\n ...engine,\n /** @internal Records of `useSync` requests not yet released (leak checks in tests). */\n debugPendingSyncRequests: requests.debugPendingSyncRequests,\n /** @internal Retirement records still held (leak checks in tests). */\n debugRetiredRequests: requests.debugRetiredRequests,\n fetch,\n remove,\n useSync,\n };\n};\n\nexport type ReplicaSlice<TStore, TParams, TData, TFetched = TData> = ReturnType<\n typeof createReplicaSlice<TStore, TParams, TData, TFetched>\n>;\n","/**\n * Scheduling knobs a sync can ask its driver for. Named after SWR's options;\n * a driver maps them onto its own cache (or ignores what it cannot honor).\n */\nexport interface ReplicaSyncSchedule {\n /** Collapse identical requests within this window (ms). */\n dedupingInterval?: number;\n /** Skip focus revalidation while the last one is younger than this (ms). */\n focusThrottleInterval?: number;\n /**\n * Hold every network request (mount, stale, focus, revalidate) while it\n * returns `true`, for a read whose input outside the sync key is not ready\n * yet. Unlike `revalidateOnMount: false` it also covers a later key change.\n * Read on each request, so it may close over a ref.\n */\n isPaused?: () => boolean;\n /** Poll every N ms while mounted (0 = off); a function decides per tick from the latest data. */\n refreshInterval?: number | ((latest: any) => number);\n /** Keep polling while the page is hidden. */\n refreshWhenHidden?: boolean;\n /** Keep polling while the browser is offline. */\n refreshWhenOffline?: boolean;\n revalidateIfStale?: boolean;\n revalidateOnFocus?: boolean;\n /**\n * Whether the first mount fetches. `false` leaves the first request to an\n * explicit `revalidate`, for a read whose input outside the sync key is not\n * ready yet. SWR only honors it on the first mount: a later key change still\n * fetches when the new key has no cached data.\n */\n revalidateOnMount?: boolean;\n revalidateOnReconnect?: boolean;\n}\n\nexport interface ReplicaQueryOptions<T> extends ReplicaSyncSchedule {\n /** Run once per key: no refetch on focus, reconnect or remount (hydration reads). */\n once?: boolean;\n onError?: (error: unknown) => void;\n onSuccess?: (data: T) => void;\n}\n\nexport interface ReplicaQueryResult<T> {\n data?: T;\n error?: unknown;\n isValidating: boolean;\n /** Re-run this query. */\n mutate: () => Promise<unknown>;\n}\n\n/**\n * The query cache that schedules a replica's fetches: dedupe, focus /\n * reconnect revalidation, retries. The replica decides what a response means;\n * the driver only decides when to ask. `createSWRDriver` adapts SWR.\n */\nexport interface ReplicaSyncDriver {\n /** Re-run every mounted query whose key matches. */\n revalidate: (match: (key: unknown) => boolean) => Promise<unknown>;\n /**\n * Write a head response that `slice.fetch` requested outside `useQuery` into\n * the query cache under its sync key, without re-running the query — so a\n * `useQuery` of that key sees the latest data (a `revalidateIfStale: false`\n * mount does not refetch it). Optional: a driver without a cache of its own\n * leaves it out; requests still dedupe in the slice.\n *\n * The slice calls it only while no `useQuery` request of the key is in\n * flight. A cache that discards in-flight requests a write overtakes (SWR\n * does) would otherwise drop a newer response than the one written.\n */\n seed?: (key: readonly unknown[], data: unknown) => void;\n useQuery: <T>(\n key: readonly unknown[] | null,\n fetcher: () => Promise<T>,\n options: ReplicaQueryOptions<T>,\n ) => ReplicaQueryResult<T>;\n}\n\ntype SWRLikeHook = (\n key: any,\n fetcher: any,\n config?: any,\n) => { data?: any; error?: unknown; isValidating: boolean; mutate: () => Promise<any> };\n\n/**\n * Driver over SWR. Pass the app's own hook and scoped `mutate` when it wraps\n * SWR (custom cache provider, key augmentation, retry policy).\n */\nexport const createSWRDriver = ({\n mutate,\n useSWR,\n}: {\n /**\n * SWR's (scoped) `mutate`. Called as `mutate(match)` to revalidate, and as\n * `mutate(key, data, { revalidate: false })` to seed — pass the arguments\n * through unchanged: SWR tells a revalidation from a write by their count.\n */\n mutate: (\n ...args: [match: (key: any) => boolean] | [key: unknown, data: unknown, options: object]\n ) => Promise<unknown>;\n useSWR: SWRLikeHook;\n}): ReplicaSyncDriver => ({\n revalidate: (match) => mutate(match),\n // Populate the cache only: re-running the query would refetch what was just fetched.\n seed: (key, data) => {\n void mutate(key, data, { revalidate: false }).catch(() => undefined);\n },\n useQuery: (key, fetcher, { once, onError, onSuccess, ...schedule }) =>\n useSWR(key, fetcher, {\n ...Object.fromEntries(Object.entries(schedule).filter(([, value]) => value !== undefined)),\n ...(once && {\n revalidateIfStale: false,\n revalidateOnFocus: false,\n revalidateOnReconnect: false,\n }),\n ...(onError && { onError: (error: unknown) => onError(error) }),\n ...(onSuccess && { onSuccess: (data: unknown) => onSuccess(data as never) }),\n }),\n});\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-replica",
3
- "version": "0.0.0",
3
+ "version": "0.0.3",
4
4
  "description": "Local-first replicas: paint the last known state on the first frame, reconcile with the server in the background",
5
5
  "keywords": [
6
6
  "local-first",
@@ -42,7 +42,9 @@
42
42
  ],
43
43
  "scripts": {
44
44
  "build": "tsdown",
45
+ "prepare": "husky",
45
46
  "prepublishOnly": "bun run build",
47
+ "release": "semantic-release",
46
48
  "test": "vitest run",
47
49
  "test:coverage": "vitest run --coverage",
48
50
  "type-check": "tsc --noEmit"
@@ -51,10 +53,17 @@
51
53
  "fast-deep-equal": "^3.1.3"
52
54
  },
53
55
  "devDependencies": {
56
+ "@commitlint/cli": "^21.2.4",
57
+ "@lobehub/commitlint-config": "^2.1.7",
58
+ "@lobehub/semantic-release-config": "^2.1.7",
54
59
  "@testing-library/react": "^16.3.3",
55
60
  "@types/react": "^19.3.0",
61
+ "@vitest/coverage-v8": "^5.0.5",
62
+ "conventional-changelog-gitmoji-config": "^1.5.2",
56
63
  "happy-dom": "^21.0.4",
64
+ "husky": "^9.1.7",
57
65
  "react": "^19.3.1",
66
+ "semantic-release": "^25.0.9",
58
67
  "swr": "^2.5.1",
59
68
  "tsdown": "^0.23.1",
60
69
  "typescript": "^7.0.2",