local-replica 0.0.0 → 0.0.1

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 +1,362 @@
1
- export {}
1
+ import { G as ReplicaResource, O as ReplicaAction, S as ReplicaOptimisticToken, X as ReplicaSource, _ as OptimisticMutationOptions, tt as ReplicaPagedData, x as ReplicaHeadOutcome, y as ReplicaEngineOptions } from "../index-CFbCHM7Q.js";
2
+ /**
3
+ * Scheduling knobs a sync can ask its driver for. Named after SWR's options;
4
+ * a driver maps them onto its own cache (or ignores what it cannot honor).
5
+ */
6
+ export interface ReplicaSyncSchedule {
7
+ /** Collapse identical requests within this window (ms). */
8
+ dedupingInterval?: number;
9
+ /** Skip focus revalidation while the last one is younger than this (ms). */
10
+ focusThrottleInterval?: number;
11
+ /**
12
+ * Hold every network request (mount, stale, focus, revalidate) while it
13
+ * returns `true`, for a read whose input outside the sync key is not ready
14
+ * yet. Unlike `revalidateOnMount: false` it also covers a later key change.
15
+ * Read on each request, so it may close over a ref.
16
+ */
17
+ isPaused?: () => boolean;
18
+ /** Poll every N ms while mounted (0 = off); a function decides per tick from the latest data. */
19
+ refreshInterval?: number | ((latest: any) => number);
20
+ /** Keep polling while the page is hidden. */
21
+ refreshWhenHidden?: boolean;
22
+ /** Keep polling while the browser is offline. */
23
+ refreshWhenOffline?: boolean;
24
+ revalidateIfStale?: boolean;
25
+ revalidateOnFocus?: boolean;
26
+ /**
27
+ * Whether the first mount fetches. `false` leaves the first request to an
28
+ * explicit `revalidate`, for a read whose input outside the sync key is not
29
+ * ready yet. SWR only honors it on the first mount: a later key change still
30
+ * fetches when the new key has no cached data.
31
+ */
32
+ revalidateOnMount?: boolean;
33
+ revalidateOnReconnect?: boolean;
34
+ }
35
+ export interface ReplicaQueryOptions<T> extends ReplicaSyncSchedule {
36
+ /** Run once per key: no refetch on focus, reconnect or remount (hydration reads). */
37
+ once?: boolean;
38
+ onError?: (error: unknown) => void;
39
+ onSuccess?: (data: T) => void;
40
+ }
41
+ export interface ReplicaQueryResult<T> {
42
+ data?: T;
43
+ error?: unknown;
44
+ isValidating: boolean;
45
+ /** Re-run this query. */
46
+ mutate: () => Promise<unknown>;
47
+ }
48
+ /**
49
+ * The query cache that schedules a replica's fetches: dedupe, focus /
50
+ * reconnect revalidation, retries. The replica decides what a response means;
51
+ * the driver only decides when to ask. `createSWRDriver` adapts SWR.
52
+ */
53
+ export interface ReplicaSyncDriver {
54
+ /** Re-run every mounted query whose key matches. */
55
+ revalidate: (match: (key: unknown) => boolean) => Promise<unknown>;
56
+ /**
57
+ * Write a head response that `slice.fetch` requested outside `useQuery` into
58
+ * the query cache under its sync key, without re-running the query — so a
59
+ * `useQuery` of that key sees the latest data (a `revalidateIfStale: false`
60
+ * mount does not refetch it). Optional: a driver without a cache of its own
61
+ * leaves it out; requests still dedupe in the slice.
62
+ *
63
+ * The slice calls it only while no `useQuery` request of the key is in
64
+ * flight. A cache that discards in-flight requests a write overtakes (SWR
65
+ * does) would otherwise drop a newer response than the one written.
66
+ */
67
+ seed?: (key: readonly unknown[], data: unknown) => void;
68
+ useQuery: <T>(key: readonly unknown[] | null, fetcher: () => Promise<T>, options: ReplicaQueryOptions<T>) => ReplicaQueryResult<T>;
69
+ }
70
+ type SWRLikeHook = (key: any, fetcher: any, config?: any) => {
71
+ data?: any;
72
+ error?: unknown;
73
+ isValidating: boolean;
74
+ mutate: () => Promise<any>;
75
+ };
76
+ /**
77
+ * Driver over SWR. Pass the app's own hook and scoped `mutate` when it wraps
78
+ * SWR (custom cache provider, key augmentation, retry policy).
79
+ */
80
+ export declare const createSWRDriver: ({ mutate, useSWR }: {
81
+ /**
82
+ * SWR's (scoped) `mutate`. Called as `mutate(match)` to revalidate, and as
83
+ * `mutate(key, data, { revalidate: false })` to seed — pass the arguments
84
+ * through unchanged: SWR tells a revalidation from a write by their count.
85
+ */
86
+ mutate: (...args: [match: (key: any) => boolean] | [key: unknown, data: unknown, options: object]) => Promise<unknown>;
87
+ useSWR: SWRLikeHook;
88
+ }) => ReplicaSyncDriver;
89
+ /** Zustand `setState`, with the devtools action label. */
90
+ type Setter<TStore> = (partial: Partial<TStore>, replace?: false, action?: any) => void;
91
+ /**
92
+ * One `key` write in a batched lens update (see {@link ReplicaLens.setMany}).
93
+ */
94
+ export interface ReplicaViewSet<TData> {
95
+ data: TData | undefined;
96
+ key: string;
97
+ }
98
+ /**
99
+ * Where the materialized value lives in the domain store. Selectors keep
100
+ * reading this location; the slice is the only writer.
101
+ */
102
+ export interface ReplicaLens<TStore, TData> {
103
+ clear: (state: TStore) => Partial<TStore>;
104
+ get: (state: TStore, key: string) => TData | undefined;
105
+ /** Enumerate loaded keys (needed for entity propagation). */
106
+ keys?: (state: TStore) => string[];
107
+ set: (state: TStore, key: string, data: TData | undefined) => Partial<TStore>;
108
+ /**
109
+ * Apply several key writes in one pass — one clone of the backing field
110
+ * instead of one per key. Optional: the slice falls back to repeated `set`
111
+ * when a lens does not implement it.
112
+ */
113
+ setMany?: (state: TStore, entries: ReplicaViewSet<TData>[]) => Partial<TStore>;
114
+ }
115
+ export interface CreateReplicaSliceOptions<TStore, TParams, TData, TFetched> extends Omit<ReplicaEngineOptions<TParams, TData, TFetched>, 'port' | 'revalidate'> {
116
+ /** Query cache that schedules fetches (see `createSWRDriver`). */
117
+ driver: ReplicaSyncDriver;
118
+ /**
119
+ * Side effects of the resource itself, run by every head response — a
120
+ * mounted `useSync` and an imperative `fetch` alike — after the response is
121
+ * folded in (see {@link ReplicaEffects}).
122
+ */
123
+ effects?: ReplicaEffects<TParams, TFetched>;
124
+ get: () => TStore;
125
+ set: Setter<TStore>;
126
+ /** Store field holding the {@link ReplicaState} bookkeeping slot. */
127
+ stateKey: keyof TStore & string;
128
+ /** Where the view lives in the store; `recordLens(field)` covers `Record<key, TData>`. */
129
+ view: ReplicaLens<TStore, TData>;
130
+ }
131
+ /**
132
+ * What a failed sync read was captured under. `scope` is the cache scope active
133
+ * when the request was issued, which may differ from the scope active when the
134
+ * callback runs (an identity / workspace switch in between).
135
+ */
136
+ export interface ReplicaSyncErrorContext {
137
+ /**
138
+ * Whether the captured `scope` is still the one the store is on. A failure
139
+ * with `isActive: false` belongs to an identity / workspace the store has
140
+ * left: surface it on the *current* view only when this is true, while still
141
+ * using the captured `key` / `scope` to clean up the scope it came from.
142
+ */
143
+ isActive: boolean;
144
+ /** The entry key the read was for; `undefined` for a disabled read. */
145
+ key: string | undefined;
146
+ /** The cache scope the read was captured under. */
147
+ scope: string;
148
+ }
149
+ /** Where a settled head response was captured, for {@link ReplicaEffects}. */
150
+ export interface ReplicaSettleContext {
151
+ key: string;
152
+ /** The cache scope the request was captured under (the active one, except for `onPurge`). */
153
+ scope: string;
154
+ /** Which path requested it: a mounted `useSync`, or `slice.fetch`. */
155
+ source: 'fetch' | 'sync';
156
+ }
157
+ /**
158
+ * Resource-level side effects of head responses. Unlike `useSync`'s per-call
159
+ * `onSuccess` / `onError` (UI callbacks of one mounted view), these run for
160
+ * every response the resource settles, whichever path requested it — so a
161
+ * store that fetches while no view is mounted keeps the same side effects.
162
+ *
163
+ * They run only for a response that passed every guard (clear, removal, active
164
+ * query, a newer response already applied) and, for `onApplied` / `onMissing`,
165
+ * only when its captured scope is still the active one.
166
+ */
167
+ export interface ReplicaEffects<TParams, TFetched> {
168
+ /**
169
+ * A response was folded into the active scope (`prepareHead` kept it and it
170
+ * is not a terminal absence). Runs even when the value did not change.
171
+ */
172
+ onApplied?: (data: TFetched, params: TParams, context: ReplicaSettleContext) => void;
173
+ /**
174
+ * A failed head read. Like `useSync`'s `onError`, it is forwarded off-scope
175
+ * too, with `isActive: false`: use the captured key / scope to clean up that
176
+ * scope, and touch the view on screen only when `isActive`.
177
+ */
178
+ onError?: (error: unknown, params: TParams, context: ReplicaSyncErrorContext) => void;
179
+ /**
180
+ * The response was a terminal absence (`isMissing`) and the entry was
181
+ * removed from the active scope. An absence captured under a scope the store
182
+ * has left purges that scope's row and runs `onPurge` instead.
183
+ */
184
+ onMissing?: (params: TParams, context: ReplicaSettleContext) => void;
185
+ /**
186
+ * A terminal absence captured under a scope the store has since left. The
187
+ * entry's row in that scope is already purged; purge what other resources
188
+ * keep of it there (a linked list row). Never touch the view on screen.
189
+ */
190
+ onPurge?: (params: TParams, context: ReplicaSettleContext) => void;
191
+ }
192
+ export interface ReplicaFetchOptions {
193
+ /**
194
+ * Start a new request even when one for the same entry is in flight, and
195
+ * retire every request started before it: a late response of an older one
196
+ * is dropped instead of applied. Use it for refreshes after a mutation,
197
+ * where an in-flight request may hold the pre-mutation value. By default
198
+ * `fetch` joins the request in flight (a `useSync` one or another `fetch`).
199
+ */
200
+ fresh?: boolean;
201
+ /**
202
+ * The cache scope the fetch belongs to, captured by a caller that started a
203
+ * multi-step flow earlier (a warmup fanning out from a parent response). The
204
+ * response settles into that scope under the same off-scope rules as any
205
+ * other. When the scope is no longer active the fetch rejects without a
206
+ * request: it would read the new identity's data under the old one's label.
207
+ * Defaults to the active scope.
208
+ */
209
+ scope?: string;
210
+ }
211
+ export interface ReplicaFetchResult<TFetched> {
212
+ /**
213
+ * Whether the response was settled into the active scope: folded in, or
214
+ * applied as a terminal absence. `false` when a guard dropped it (scope
215
+ * switch, `remove` / `clear` mid-flight, superseded) or `prepareHead` did.
216
+ */
217
+ applied: boolean;
218
+ /** The response as fetched, also when it was not applied. */
219
+ data: TFetched;
220
+ /** The response was a terminal absence (`isMissing`), in whichever scope. */
221
+ missing: boolean;
222
+ }
223
+ export interface ReplicaSyncOptions<TFetched = unknown, TData = unknown> extends ReplicaSyncSchedule {
224
+ enabled?: boolean;
225
+ /**
226
+ * Side effects of a failed fetch (error side-maps); the store view is left as is.
227
+ * Receives the captured {@link ReplicaSyncErrorContext} so a terminal removal
228
+ * can target the scope the request belonged to rather than the one that is
229
+ * active once the callback runs.
230
+ */
231
+ onError?: (error: unknown, context: ReplicaSyncErrorContext) => void;
232
+ /**
233
+ * Side effects of a hydrated persisted value, run after it is folded into the
234
+ * replica. A cold start with no network (offline / slow) has no response for
235
+ * `onSuccess` to run on, so the same "success" handling must also run off the
236
+ * persisted value it paints.
237
+ */
238
+ onHydrated?: (data: TData) => void;
239
+ /**
240
+ * Side effects of a response, run after it is folded into the replica
241
+ * (e.g. adopting an active id). UI callbacks of this view only: effects every
242
+ * response of the resource needs belong in the slice's `effects`, which
243
+ * `fetch` runs too.
244
+ */
245
+ onSuccess?: (data: TFetched) => void;
246
+ }
247
+ export interface ReplicaSyncResult {
248
+ error: unknown;
249
+ /** The persisted row has been read (or there is nothing to read). */
250
+ isHydrated: boolean;
251
+ /** A network request is in flight. Never a reason to hide store data. */
252
+ isValidating: boolean;
253
+ /** Re-run the network sync for this entry. */
254
+ revalidate: () => Promise<unknown>;
255
+ }
256
+ /** `slice.fetch` was asked to fetch for a scope that is no longer active. */
257
+ export declare class ReplicaScopeChangedError extends Error {
258
+ constructor(name: string, scope: string);
259
+ }
260
+ export interface ReplicaSyncResult {
261
+ error: unknown;
262
+ /** The persisted row has been read (or there is nothing to read). */
263
+ isHydrated: boolean;
264
+ /** A network request is in flight. Never a reason to hide store data. */
265
+ isValidating: boolean;
266
+ /** Re-run the network sync for this entry. */
267
+ revalidate: () => Promise<unknown>;
268
+ }
269
+ /**
270
+ * Bind a replica to a domain Zustand store.
271
+ *
272
+ * The domain store stays the only UI source of truth: components read the
273
+ * `view` location through their usual selectors. The engine owns every
274
+ * transition of that location; this slice adds the Zustand port and a
275
+ * `useSync` hook that only orchestrates fetching — data never flows through
276
+ * its return value.
277
+ */
278
+ export declare const createReplicaSlice: <TStore, TParams, TData, TFetched = TData>(resource: ReplicaResource<TParams, TData, TFetched>, { driver, effects, get, set, stateKey, view, ...options }: CreateReplicaSliceOptions<TStore, TParams, TData, TFetched>) => {
279
+ applyHead: (params: TParams, fetched: TFetched, scope?: string) => {
280
+ changed: boolean;
281
+ outcome: ReplicaHeadOutcome;
282
+ };
283
+ beginEntityOptimistic: <TItem>(id: string, fn: (item: TItem) => TItem | undefined) => ReplicaOptimisticToken<TData>[];
284
+ beginOptimistic: (key: string, apply: (data: TData) => TData, query?: string) => ReplicaOptimisticToken<TData>;
285
+ clear: () => Promise<boolean>;
286
+ clearGeneration: (scope: string) => number;
287
+ clearScope: (scope: string) => Promise<boolean>;
288
+ removalGeneration: (scope: string, key: string) => number;
289
+ collapse: (key: string) => boolean;
290
+ dispatch: (action: ReplicaAction<TData>) => boolean;
291
+ ensureScope: (scope: string) => void;
292
+ entityKeys: (id: string) => string[];
293
+ fetcher: ((params: TParams, cursor?: any) => Promise<TFetched>) | undefined;
294
+ getConfirmed: (key: string) => TData | undefined;
295
+ hydrate: (params: TParams, scope?: string, { overwrite }?: {
296
+ overwrite?: boolean;
297
+ }) => Promise<boolean>;
298
+ insertHead: <TItem>(key: string, items: TItem[], { persist }?: {
299
+ persist?: boolean | undefined;
300
+ }) => boolean;
301
+ isMissingHead: (incoming: TFetched) => boolean;
302
+ loadMore: (key: string, fallbackParams?: TParams | undefined) => Promise<void>;
303
+ optimistic: <TResult>(key: string, apply: (data: TData) => TData, serverCall: () => Promise<TResult>, mutationOptions?: OptimisticMutationOptions<TData, TResult>) => Promise<TResult>;
304
+ patchStoredEntity: <TItem>(id: string, fn: (item: TItem) => TItem | undefined) => Promise<void>;
305
+ patchStoredRows: (fn: (data: TData) => TData | null | undefined) => Promise<void>;
306
+ persist: (key: string) => void;
307
+ removeEntity: (id: string, options?: {
308
+ persist?: boolean;
309
+ scope?: string;
310
+ } | undefined) => void;
311
+ replace: (params: TParams, fetched: TFetched, scope?: string) => boolean;
312
+ resource: ReplicaResource<TParams, TData, TFetched, any>;
313
+ retain: (key: string, queryKey?: string) => () => void;
314
+ revalidate: (key?: string) => Promise<unknown>;
315
+ update: (key: string, apply: (data: TData | undefined) => TData | undefined, { persist, seedTag, source }?: {
316
+ persist?: boolean;
317
+ seedTag?: string;
318
+ source?: ReplicaSource;
319
+ }) => boolean;
320
+ updateEntity: <TItem>(id: string, fn: (item: TItem) => TItem | undefined, options?: {
321
+ persist?: boolean;
322
+ scope?: string;
323
+ } | undefined) => void;
324
+ updateMany: (entries: {
325
+ apply: (data: TData | undefined) => TData | undefined;
326
+ key: string;
327
+ }[], { persist, seedTag, source }?: {
328
+ persist?: boolean;
329
+ seedTag?: string;
330
+ source?: ReplicaSource;
331
+ }) => boolean;
332
+ /** @internal Records of `useSync` requests not yet released (leak checks in tests). */
333
+ debugPendingSyncRequests: () => number;
334
+ /** @internal Retirement records still held (leak checks in tests). */
335
+ debugRetiredRequests: () => number;
336
+ fetch: (params: TParams, { fresh, scope }?: ReplicaFetchOptions) => Promise<ReplicaFetchResult<TFetched>>;
337
+ remove: (key: string, scope?: string) => boolean;
338
+ useSync: (params: TParams | null | undefined, { enabled, onError, onHydrated, onSuccess, ...schedule }?: ReplicaSyncOptions<TFetched, TData>) => ReplicaSyncResult;
339
+ };
340
+ export type ReplicaSlice<TStore, TParams, TData, TFetched = TData> = ReturnType<typeof createReplicaSlice<TStore, TParams, TData, TFetched>>;
341
+ /** Lens for the common case: a `Record<key, TData>` field on the store. */
342
+ export declare const recordLens: <TStore, TData>(field: keyof TStore & string) => ReplicaLens<TStore, TData>;
343
+ /**
344
+ * Lens for a paged view split across store fields: the rows stay where
345
+ * existing readers expect them (`itemsField[key]`, a plain array), the paging
346
+ * bookkeeping lives in `metaField[key]`, and `derive` recomputes fields built
347
+ * from the rows (e.g. a parsed display list) in the same commit — so a derived
348
+ * field is never one frame behind its rows.
349
+ *
350
+ * Rows seeded without bookkeeping (by code outside the replica) read as a
351
+ * single head page.
352
+ */
353
+ export declare const splitPagedLens: <TStore, TItem, TCursor = unknown>({ clearDerived, derive, itemsField, metaField }: {
354
+ /** Derived fields to reset on a scope change. */
355
+ clearDerived?: () => Partial<TStore>;
356
+ /** Fields computed from an entry's rows (`undefined` = the entry is gone). */
357
+ derive?: (state: TStore, key: string, items: TItem[] | undefined) => Partial<TStore>;
358
+ itemsField: keyof TStore & string;
359
+ metaField: keyof TStore & string;
360
+ }) => ReplicaLens<TStore, ReplicaPagedData<TItem, TCursor>>;
361
+ export {};
362
+ //# sourceMappingURL=index.d.ts.map