local-replica 0.0.1 → 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.
@@ -1,18 +1,50 @@
1
+ /** Address of one persisted row: the resource's row key inside an identity scope. */
2
+ interface ReplicaRowKey {
3
+ queryKey: string;
4
+ scope: string;
5
+ }
6
+ interface ReplicaRow<T> {
7
+ data: T;
8
+ updatedAt: number;
9
+ }
1
10
  /**
2
- * First-class pagination for replicas.
3
- *
4
- * Vocabulary (direction-agnostic):
5
- * - HEAD page: the newest window, what a refresh re-fetches (`cursor` undefined).
6
- * - NEXT pages: further pages in the paging direction (older history).
7
- * - `forward`: the view is head-first, next pages append at the END
8
- * (sidebar lists: newest topic first, "load more" below).
9
- * - `backward`: the view is oldest-first, next pages PREPEND at the start
10
- * (chat transcripts: newest message last, "load older" on scroll-up).
11
- * - `offset` mode: the cursor is the page index (0 = head).
12
- * `cursor` mode: the server hands back an opaque `nextCursor`.
13
- *
14
- * Every function is pure; the store binding owns staleness and scope.
11
+ * Where a replica keeps its rows between sessions (IndexedDB, localStorage …).
12
+ * The server stays the durable source of truth, so `get` is best-effort: it
13
+ * swallows its own I/O failures and reports a miss. Writes are not: `set` and
14
+ * `remove` must reject when they did not land (see each method), so the
15
+ * engine can retry them instead of treating a dropped row as persisted.
15
16
  */
17
+ interface ReplicaStorage<T> {
18
+ get: (key: ReplicaRowKey) => Promise<ReplicaRow<T> | undefined>;
19
+ /**
20
+ * Strict read for destructive passes: like `get`, but **rejects** when the
21
+ * row could not be read (I/O error, unparsable value) instead of reporting a
22
+ * miss. The engine reads its per-scope index (and every row it patches)
23
+ * through it before `remove` / `clear` / an entity patch / an eviction, so an
24
+ * unreadable index fails the pass rather than passing for an empty one.
25
+ *
26
+ * Optional only for compatibility: a storage without it still hydrates and
27
+ * persists, but every destructive pass **fails** (`clear()` resolves `false`,
28
+ * a removal stays unpurged and is retried, an entity guard is kept) because
29
+ * the engine cannot tell a miss from a failed read through `get`.
30
+ */
31
+ getStrict?: (key: ReplicaRowKey) => Promise<ReplicaRow<T> | undefined>;
32
+ /**
33
+ * Delete a row. Must **reject** when the delete did not land (a closed
34
+ * IndexedDB, a quota or security error): the engine keeps the row indexed
35
+ * and the removal unpurged, so the next `remove` / `clear` retries it instead
36
+ * of letting a row the user deleted hydrate again. A missing row is a success.
37
+ */
38
+ remove: (key: ReplicaRowKey) => Promise<void>;
39
+ /**
40
+ * Write a row. Should **reject** when the write did not land: the engine only
41
+ * treats a row as holding a value after `set` resolved, so a rejected write is
42
+ * retried by the next persist rather than skipped as redundant. A storage that
43
+ * swallows failures still works, but a dropped write then survives until the
44
+ * value changes or the next session.
45
+ */
46
+ set: (key: ReplicaRowKey, row: ReplicaRow<T>) => Promise<void>;
47
+ }
16
48
  type ReplicaPagingMode = 'cursor' | 'offset';
17
49
  type ReplicaPagingDirection = 'backward' | 'forward';
18
50
  interface ReplicaPagingConfig<TItem, TCursor = any> {
@@ -109,115 +141,6 @@ interface ReplicaPagingContext<TItem> {
109
141
  */
110
142
  isClientOnly?: (item: TItem) => boolean;
111
143
  }
112
- /** Concatenate in view order: forward = head first, backward = head last. */
113
- declare const joinPages: <TItem>(head: TItem[], rest: TItem[], direction: ReplicaPagingDirection) => TItem[];
114
- /** The paging config one entry reads with: `directionOf` resolved for `key`. */
115
- declare const pagingFor: <TItem, TCursor>(config: ReplicaPagingConfig<TItem, TCursor>, key: string) => ReplicaPagingConfig<TItem, TCursor>;
116
- /**
117
- * Cursor to request the next page with; `null` → exhausted, `undefined` →
118
- * cannot page yet. Offset views without bookkeeping (seeded by legacy code or
119
- * an older persisted shape) derive it from `currentPage` / `hasMore`.
120
- */
121
- declare const getNextPageCursor: <TItem, TCursor>(data: ReplicaPagedData<TItem, TCursor> | undefined, config: Pick<ReplicaPagingConfig<TItem, TCursor>, 'deriveCursor' | 'direction' | 'isCursorable' | 'mode'>) => TCursor | null | undefined;
122
- /**
123
- * Fold a fresh HEAD page into the view.
124
- *
125
- * - First load, a query change (`reset`) or an unextended view → the head page.
126
- * - Extended view, `offset` mode → keep the loaded depth: fresh head first,
127
- * then previously loaded rows not in it (shifted offsets self-heal by id).
128
- * - Extended view, `cursor` mode → keep older pages only when the fresh head
129
- * still contains the join anchor and the next cursor is known; otherwise
130
- * collapse to the head page (never show a hidden gap).
131
- *
132
- * Client-only rows still in the view are kept on the head side.
133
- */
134
- declare const applyHeadPage: <TItem, TCursor>(current: ReplicaPagedData<TItem, TCursor> | undefined, page: ReplicaPageResult<TItem, TCursor>, options: {
135
- pageSize: number;
136
- reset?: boolean;
137
- }, config: ReplicaPagingConfig<TItem, TCursor>, ctx?: ReplicaPagingContext<TItem>) => ReplicaPagedData<TItem, TCursor>;
138
- /** Merge the NEXT page (fetched with `getNextPageCursor`) into the view. */
139
- declare const applyNextPage: <TItem, TCursor>(current: ReplicaPagedData<TItem, TCursor>, page: ReplicaPageResult<TItem, TCursor>, config: ReplicaPagingConfig<TItem, TCursor>, ctx?: ReplicaPagingContext<TItem>) => ReplicaPagedData<TItem, TCursor>;
140
- /**
141
- * Insert new rows on the HEAD side (a streamed message, a created topic).
142
- * Cursors point at the far end, so they stay valid; offsets shift and are
143
- * healed by id de-duplication on the next page merge.
144
- */
145
- declare const insertHeadItems: <TItem, TCursor>(current: ReplicaPagedData<TItem, TCursor>, items: TItem[], config: ReplicaPagingConfig<TItem, TCursor>) => ReplicaPagedData<TItem, TCursor>;
146
- /**
147
- * Patch (or remove, when `fn` returns `undefined`) one row by id. Returns the
148
- * same reference when the row is absent or unchanged.
149
- */
150
- declare const mapPagedItem: <TItem, TCursor, TData extends ReplicaPagedData<TItem, TCursor>>(current: TData, id: string, fn: (item: TItem) => TItem | undefined, config: Pick<ReplicaPagingConfig<TItem>, 'getId'>) => TData;
151
- /** Whether a paged view holds a row. */
152
- declare const hasPagedItem: <TItem>(data: ReplicaPagedData<TItem, unknown> | undefined, id: string, config: Pick<ReplicaPagingConfig<TItem>, 'getId'>) => boolean;
153
- /** Drop every loaded page but the head (e.g. after an edit inside older history). */
154
- declare const collapseToHead: <TItem, TCursor>(current: ReplicaPagedData<TItem, TCursor>, config: ReplicaPagingConfig<TItem, TCursor>) => ReplicaPagedData<TItem, TCursor>;
155
- /**
156
- * What survives a reload: whole pages from the head side, bounded by
157
- * `persist.pages` and `persist.maxItems`, client-only rows and transient
158
- * flags stripped. Offset views are cut at page-size boundaries so the
159
- * persisted `currentPage` stays a valid offset; a cut inside a cursor page
160
- * forgets the cursor (pagination resumes after the next server head page).
161
- */
162
- declare const toPersistedPage: <TItem, TCursor, TData extends ReplicaPagedData<TItem, TCursor>>(data: TData, config: ReplicaPagingConfig<TItem, TCursor>, ctx?: ReplicaPagingContext<TItem>) => TData;
163
- /** Address of one persisted row: the resource's row key inside an identity scope. */
164
- interface ReplicaRowKey {
165
- queryKey: string;
166
- scope: string;
167
- }
168
- interface ReplicaRow<T> {
169
- data: T;
170
- updatedAt: number;
171
- }
172
- /**
173
- * Where a replica keeps its rows between sessions (IndexedDB, localStorage …).
174
- * The server stays the durable source of truth, so `get` is best-effort: it
175
- * swallows its own I/O failures and reports a miss. Writes are not: `set` and
176
- * `remove` must reject when they did not land (see each method), so the
177
- * engine can retry them instead of treating a dropped row as persisted.
178
- */
179
- interface ReplicaStorage<T> {
180
- get: (key: ReplicaRowKey) => Promise<ReplicaRow<T> | undefined>;
181
- /**
182
- * Strict read for destructive passes: like `get`, but **rejects** when the
183
- * row could not be read (I/O error, unparsable value) instead of reporting a
184
- * miss. The engine reads its per-scope index (and every row it patches)
185
- * through it before `remove` / `clear` / an entity patch / an eviction, so an
186
- * unreadable index fails the pass rather than passing for an empty one.
187
- *
188
- * Optional only for compatibility: a storage without it still hydrates and
189
- * persists, but every destructive pass **fails** (`clear()` resolves `false`,
190
- * a removal stays unpurged and is retried, an entity guard is kept) because
191
- * the engine cannot tell a miss from a failed read through `get`.
192
- */
193
- getStrict?: (key: ReplicaRowKey) => Promise<ReplicaRow<T> | undefined>;
194
- /**
195
- * Delete a row. Must **reject** when the delete did not land (a closed
196
- * IndexedDB, a quota or security error): the engine keeps the row indexed
197
- * and the removal unpurged, so the next `remove` / `clear` retries it instead
198
- * of letting a row the user deleted hydrate again. A missing row is a success.
199
- */
200
- remove: (key: ReplicaRowKey) => Promise<void>;
201
- /**
202
- * Write a row. Should **reject** when the write did not land: the engine only
203
- * treats a row as holding a value after `set` resolved, so a rejected write is
204
- * retried by the next persist rather than skipped as redundant. A storage that
205
- * swallows failures still works, but a dropped write then survives until the
206
- * value changes or the next session.
207
- */
208
- set: (key: ReplicaRowKey, row: ReplicaRow<T>) => Promise<void>;
209
- }
210
- /**
211
- * Where the confirmed value of an entry came from.
212
- * - `storage`: hydrated from the persisted projection (may be stale)
213
- * - `server`: confirmed by a network response
214
- * - `local`: written locally before any hydrate/replace landed
215
- * - `seed`: a provisional local row (e.g. a list row standing in for a detail so
216
- * an object stays resolvable). Unlike `local` it is *not* authoritative: a
217
- * persisted `storage` value may replace it on hydrate, and it never persists —
218
- * so it cannot overwrite the detail it is only standing in for.
219
- */
220
- type ReplicaSource = 'local' | 'seed' | 'server' | 'storage';
221
144
  /**
222
145
  * Identity partition of the persisted projection (user + workspace by default).
223
146
  * `use` feeds the sync hook, `get` imperative actions, `canPersist` gates writes
@@ -236,60 +159,6 @@ interface ReplicaScope {
236
159
  get: () => string;
237
160
  use: () => string;
238
161
  }
239
- interface ReplicaPendingMutation<T> {
240
- apply: (data: T) => T;
241
- id: number;
242
- /**
243
- * Query the overlay was started under. An entry key can be reused for another
244
- * query (a navigation), and an overlay only ever describes the query that
245
- * created it — carrying it over would apply (and later persist) one query's
246
- * row inside another.
247
- */
248
- query?: string;
249
- }
250
- interface ReplicaEntryMeta<T> {
251
- /**
252
- * Confirmed snapshot. Only kept while optimistic mutations are in flight —
253
- * otherwise the view itself is the confirmed value.
254
- */
255
- base?: T;
256
- /** Params of the last hydrate/replace — what `loadMore` pages with. */
257
- params?: unknown;
258
- pending: ReplicaPendingMutation<T>[];
259
- /** Stable query identity beyond the key (filters, page size). */
260
- query?: string;
261
- /**
262
- * Every write made while the entry is still a `seed`, in order — the seed
263
- * writes themselves and the confirmed writes made over them. A hydrate that
264
- * replaces the seed replays them onto the stored value: the seed's fresher
265
- * fields (a list row's renamed title) and a confirmed edit made before
266
- * storage answered both survive, instead of storage painting older values.
267
- */
268
- seedPatches?: ReplicaSeedPatch<T>[];
269
- source: ReplicaSource;
270
- updatedAt: number;
271
- }
272
- interface ReplicaSeedPatch<T> {
273
- /**
274
- * Replayed onto the stored value, so a seed write must merge the fields it
275
- * owns into its input rather than ignore it (its first run sees `undefined`).
276
- */
277
- apply: (data: T | undefined) => T | undefined;
278
- persist: boolean;
279
- /** A provisional seed write: replayed, but never makes the result confirmed. */
280
- seed?: boolean;
281
- /**
282
- * Source of a seed write. A later seed of the same tag supersedes this one
283
- * (since the last confirmed write), so it must carry every field this one set.
284
- */
285
- tag?: string;
286
- }
287
- /** Bookkeeping slot a replica keeps inside its domain store. */
288
- interface ReplicaState<T> {
289
- entries: Record<string, ReplicaEntryMeta<T>>;
290
- /** The scope every entry in memory belongs to. */
291
- scope?: string;
292
- }
293
162
  interface ReplicaResource<TParams, TData, TFetched = TData, TCursor = any> {
294
163
  /** Paged resources receive the page cursor (`undefined` = head page). */
295
164
  fetcher?: (params: TParams, cursor?: TCursor) => Promise<TFetched>;
@@ -319,6 +188,89 @@ interface ReplicaResource<TParams, TData, TFetched = TData, TCursor = any> {
319
188
  syncKey?: (params: TParams) => readonly unknown[];
320
189
  version: number;
321
190
  }
191
+ /** Builds the storage of one resource from its namespace (`replica:<name>:v<version>`). */
192
+ type ReplicaStorageFactory<TData> = (namespace: string) => ReplicaStorage<TData> | undefined;
193
+ interface DefineReplicaOptions<TParams, TData, TFetched = TData, TCursor = any> {
194
+ /** Default network fetcher; a store binding may override it. */
195
+ fetcher?: (params: TParams, cursor?: TCursor) => Promise<TFetched>;
196
+ /** Entry identity inside one scope (memory bucket + persisted row). */
197
+ key: (params: TParams) => string;
198
+ name: string;
199
+ paging?: ReplicaPagingConfig<any, TCursor>;
200
+ /**
201
+ * Bound what the resource keeps in storage. `maxEntries` caps the persisted
202
+ * rows (storage keys: one per entry key and query) per scope; past it the
203
+ * least recently used rows are evicted — their persisted row, index entry
204
+ * and, when nothing holds it, their memory entry. Use rows (a hydrate or a
205
+ * persist, including a skipped unchanged one) count as recent, and the order
206
+ * lives in the persisted index, so it survives reloads. A row on screen (a
207
+ * mounted `useSync`, see `retain`), with pending optimistic writes or a
208
+ * `loadMore` in flight is never evicted. Unbounded by default.
209
+ */
210
+ persist?: {
211
+ maxEntries?: number;
212
+ };
213
+ /**
214
+ * Entries that never touch storage (no hydrate, no persist) — e.g. scoped
215
+ * buckets whose rows must not land under an ordinary entry. Defaults to all.
216
+ */
217
+ persistKey?: (key: string) => boolean;
218
+ /**
219
+ * Query identity beyond `key` (filters, sort, page size) — anything that
220
+ * changes the rows without changing the key. Persisted rows are kept per
221
+ * query, so a projection taken under other filters never hydrates; in memory
222
+ * a query change resets loaded pages.
223
+ */
224
+ query?: (params: TParams) => unknown;
225
+ /** Identity partition of memory and persisted rows. Defaults to one static scope. */
226
+ scope?: ReplicaScope;
227
+ /** Persisted copy of the replica. Omit for a memory-only replica (no first-frame paint). */
228
+ storage?: ReplicaStorage<TData> | ReplicaStorageFactory<TData>;
229
+ /**
230
+ * Query-cache key of the network sync, for a resource adopting an existing
231
+ * key that other code already revalidates by. Defaults to `replicaKeys.sync`.
232
+ * With a custom key, revalidate through that key — `slice.revalidate` only
233
+ * matches default keys.
234
+ */
235
+ syncKey?: (params: TParams) => readonly unknown[];
236
+ /** Bump to invalidate every persisted row written by older shapes. */
237
+ version: number;
238
+ }
239
+ declare const defineReplica: <TParams, TData, TFetched = TData, TCursor = any>(options: DefineReplicaOptions<TParams, TData, TFetched, TCursor>) => ReplicaResource<TParams, TData, TFetched, TCursor>;
240
+ interface DefinePagedReplicaOptions<TParams, TItem, TCursor> extends Omit<DefineReplicaOptions<TParams, ReplicaPagedData<TItem, TCursor>, ReplicaPageResult<TItem, TCursor>>, 'fetcher' | 'paging' | 'storage'> {
241
+ /** `cursor` is `undefined` for the head page. */
242
+ fetchPage?: (params: TParams, cursor: TCursor | undefined) => Promise<ReplicaPageResult<TItem, TCursor>>;
243
+ paging: ReplicaPagingConfig<TItem, TCursor>;
244
+ storage?: ReplicaStorage<any> | ReplicaStorageFactory<any>;
245
+ }
246
+ /**
247
+ * A resource whose value is a paged list ({@link ReplicaPagedData}). The
248
+ * store view type may extend the paged shape with domain fields.
249
+ */
250
+ declare const definePagedReplica: <TParams, TItem, TCursor = number, TData extends ReplicaPagedData<TItem, TCursor> = ReplicaPagedData<TItem, TCursor>>(options: DefinePagedReplicaOptions<TParams, TItem, TCursor>) => ReplicaResource<TParams, TData, ReplicaPageResult<TItem, TCursor>, TCursor>;
251
+ /**
252
+ * Keys of the queries a fetch adapter runs for a replica. Hosts that sweep
253
+ * their query cache by prefix (e.g. "refetch everything after a restore")
254
+ * match on {@link REPLICA_KEY_PREFIX}.
255
+ */
256
+ declare const REPLICA_KEY_PREFIX = "replica:";
257
+ declare const replicaKeys: {
258
+ /** One-shot read of the persisted row for `storageKey`. */
259
+ hydrate: (name: string, version: number, scope: string, storageKey: string) => readonly ['replica:hydrate', string, number, string, string];
260
+ /** Network sync of the head of one entry. */
261
+ sync: (name: string, version: number, scope: string, key: string, params: unknown) => readonly ['replica:sync', string, number, string, string, unknown];
262
+ };
263
+ /** Match a resource's sync keys, optionally narrowed to one scope and/or entry key. */
264
+ declare const isReplicaSyncKey: (queryKey: unknown, name: string, filter?: {
265
+ key?: string;
266
+ scope?: string;
267
+ }) => boolean;
268
+ /**
269
+ * A fixed identity partition. The default for apps without accounts; an app
270
+ * with sign-in passes a scope derived from the signed-in user / workspace so
271
+ * one browser profile never mixes two identities' rows.
272
+ */
273
+ declare const staticReplicaScope: (scope?: string) => ReplicaScope;
322
274
  /** Persisted row key: one row per entry key and query. */
323
275
  declare const replicaStorageKey: (key: string, query?: string) => string;
324
276
  /**
@@ -336,71 +288,70 @@ declare const isReplicaStorageKeyOf: (storageKey: string, key: string) => boolea
336
288
  /** Deterministic JSON: sorted keys, `undefined` dropped. */
337
289
  declare const stableQueryKey: (value: unknown) => string;
338
290
  /**
339
- * A fixed identity partition. The default for apps without accounts; an app
340
- * with sign-in passes a scope derived from the signed-in user / workspace so
341
- * one browser profile never mixes two identities' rows.
291
+ * Where the confirmed value of an entry came from.
292
+ * - `storage`: hydrated from the persisted projection (may be stale)
293
+ * - `server`: confirmed by a network response
294
+ * - `local`: written locally before any hydrate/replace landed
295
+ * - `seed`: a provisional local row (e.g. a list row standing in for a detail so
296
+ * an object stays resolvable). Unlike `local` it is *not* authoritative: a
297
+ * persisted `storage` value may replace it on hydrate, and it never persists —
298
+ * so it cannot overwrite the detail it is only standing in for.
342
299
  */
343
- declare const staticReplicaScope: (scope?: string) => ReplicaScope;
344
- /** Builds the storage of one resource from its namespace (`replica:<name>:v<version>`). */
345
- type ReplicaStorageFactory<TData> = (namespace: string) => ReplicaStorage<TData> | undefined;
346
- interface DefineReplicaOptions<TParams, TData, TFetched = TData, TCursor = any> {
347
- /** Default network fetcher; a store binding may override it. */
348
- fetcher?: (params: TParams, cursor?: TCursor) => Promise<TFetched>;
349
- /** Entry identity inside one scope (memory bucket + persisted row). */
350
- key: (params: TParams) => string;
351
- name: string;
352
- paging?: ReplicaPagingConfig<any, TCursor>;
300
+ type ReplicaSource = 'local' | 'seed' | 'server' | 'storage';
301
+ interface ReplicaPendingMutation<T> {
302
+ apply: (data: T) => T;
303
+ id: number;
353
304
  /**
354
- * Bound what the resource keeps in storage. `maxEntries` caps the persisted
355
- * rows (storage keys: one per entry key and query) per scope; past it the
356
- * least recently used rows are evicted — their persisted row, index entry
357
- * and, when nothing holds it, their memory entry. Use rows (a hydrate or a
358
- * persist, including a skipped unchanged one) count as recent, and the order
359
- * lives in the persisted index, so it survives reloads. A row on screen (a
360
- * mounted `useSync`, see `retain`), with pending optimistic writes or a
361
- * `loadMore` in flight is never evicted. Unbounded by default.
305
+ * Query the overlay was started under. An entry key can be reused for another
306
+ * query (a navigation), and an overlay only ever describes the query that
307
+ * created it — carrying it over would apply (and later persist) one query's
308
+ * row inside another.
309
+ */
310
+ query?: string;
311
+ }
312
+ interface ReplicaEntryMeta<T> {
313
+ /**
314
+ * Confirmed snapshot. Only kept while optimistic mutations are in flight —
315
+ * otherwise the view itself is the confirmed value.
362
316
  */
363
- persist?: {
364
- maxEntries?: number;
365
- };
317
+ base?: T;
318
+ /** Params of the last hydrate/replace — what `loadMore` pages with. */
319
+ params?: unknown;
320
+ pending: ReplicaPendingMutation<T>[];
321
+ /** Stable query identity beyond the key (filters, page size). */
322
+ query?: string;
366
323
  /**
367
- * Entries that never touch storage (no hydrate, no persist) — e.g. scoped
368
- * buckets whose rows must not land under an ordinary entry. Defaults to all.
324
+ * Every write made while the entry is still a `seed`, in order — the seed
325
+ * writes themselves and the confirmed writes made over them. A hydrate that
326
+ * replaces the seed replays them onto the stored value: the seed's fresher
327
+ * fields (a list row's renamed title) and a confirmed edit made before
328
+ * storage answered both survive, instead of storage painting older values.
369
329
  */
370
- persistKey?: (key: string) => boolean;
330
+ seedPatches?: ReplicaSeedPatch<T>[];
331
+ source: ReplicaSource;
332
+ updatedAt: number;
333
+ }
334
+ interface ReplicaSeedPatch<T> {
371
335
  /**
372
- * Query identity beyond `key` (filters, sort, page size) — anything that
373
- * changes the rows without changing the key. Persisted rows are kept per
374
- * query, so a projection taken under other filters never hydrates; in memory
375
- * a query change resets loaded pages.
336
+ * Replayed onto the stored value, so a seed write must merge the fields it
337
+ * owns into its input rather than ignore it (its first run sees `undefined`).
376
338
  */
377
- query?: (params: TParams) => unknown;
378
- /** Identity partition of memory and persisted rows. Defaults to one static scope. */
379
- scope?: ReplicaScope;
380
- /** Persisted copy of the replica. Omit for a memory-only replica (no first-frame paint). */
381
- storage?: ReplicaStorage<TData> | ReplicaStorageFactory<TData>;
339
+ apply: (data: T | undefined) => T | undefined;
340
+ persist: boolean;
341
+ /** A provisional seed write: replayed, but never makes the result confirmed. */
342
+ seed?: boolean;
382
343
  /**
383
- * Query-cache key of the network sync, for a resource adopting an existing
384
- * key that other code already revalidates by. Defaults to `replicaKeys.sync`.
385
- * With a custom key, revalidate through that key — `slice.revalidate` only
386
- * matches default keys.
344
+ * Source of a seed write. A later seed of the same tag supersedes this one
345
+ * (since the last confirmed write), so it must carry every field this one set.
387
346
  */
388
- syncKey?: (params: TParams) => readonly unknown[];
389
- /** Bump to invalidate every persisted row written by older shapes. */
390
- version: number;
347
+ tag?: string;
391
348
  }
392
- declare const defineReplica: <TParams, TData, TFetched = TData, TCursor = any>(options: DefineReplicaOptions<TParams, TData, TFetched, TCursor>) => ReplicaResource<TParams, TData, TFetched, TCursor>;
393
- interface DefinePagedReplicaOptions<TParams, TItem, TCursor> extends Omit<DefineReplicaOptions<TParams, ReplicaPagedData<TItem, TCursor>, ReplicaPageResult<TItem, TCursor>>, 'fetcher' | 'paging' | 'storage'> {
394
- /** `cursor` is `undefined` for the head page. */
395
- fetchPage?: (params: TParams, cursor: TCursor | undefined) => Promise<ReplicaPageResult<TItem, TCursor>>;
396
- paging: ReplicaPagingConfig<TItem, TCursor>;
397
- storage?: ReplicaStorage<any> | ReplicaStorageFactory<any>;
349
+ /** Bookkeeping slot a replica keeps inside its domain store. */
350
+ interface ReplicaState<T> {
351
+ entries: Record<string, ReplicaEntryMeta<T>>;
352
+ /** The scope every entry in memory belongs to. */
353
+ scope?: string;
398
354
  }
399
- /**
400
- * A resource whose value is a paged list ({@link ReplicaPagedData}). The
401
- * store view type may extend the paged shape with domain fields.
402
- */
403
- declare const definePagedReplica: <TParams, TItem, TCursor = number, TData extends ReplicaPagedData<TItem, TCursor> = ReplicaPagedData<TItem, TCursor>>(options: DefinePagedReplicaOptions<TParams, TItem, TCursor>) => ReplicaResource<TParams, TData, ReplicaPageResult<TItem, TCursor>, TCursor>;
404
355
  /**
405
356
  * Pure transition core of a replica. It never touches the store
406
357
  * or storage: it returns the next bookkeeping slot, the view writes the store
@@ -500,41 +451,21 @@ interface ReplicaTransition<T> {
500
451
  declare const createReplicaState: <T>() => ReplicaState<T>;
501
452
  declare const replicaReducer: <T>(state: ReplicaState<T>, action: ReplicaAction<T>, readView: (key: string) => T | undefined, now?: number) => ReplicaTransition<T>;
502
453
  /**
503
- * Strict read of one row (see `ReplicaStorage.getStrict`). Rejects when the
504
- * storage has no strict read: its `get` swallows I/O failures into a miss, so a
505
- * destructive pass reading through it could take an unreadable row (the index)
506
- * for an empty one and report success for rows it never saw.
507
- */
508
- declare const readReplicaRowStrict: <T>(storage: ReplicaStorage<T>, key: ReplicaRowKey) => Promise<ReplicaRow<T> | undefined>;
509
- /**
510
- * Serializes writes for one projection key. A later snapshot can never be overwritten by an
511
- * earlier, slower IndexedDB write.
454
+ * How entities (e.g. topics) appear inside a non-paged resource value. Paged
455
+ * resources get this for free from `paging.getId`. Build one with
456
+ * `singleEntity` (a detail value), `arrayEntity` (a plain list) or by hand for
457
+ * nested shapes (grouped lists).
512
458
  */
513
- declare class ReplicaWriteQueue<T> {
514
- #private;
515
- constructor(storage: ReplicaStorage<T>);
516
- /** Resolves `false` when storage rejected the delete (see `ReplicaStorage.remove`). */
517
- remove: (key: ReplicaRowKey) => Promise<boolean>;
518
- set: (key: ReplicaRowKey, value: ReplicaRow<T>) => Promise<boolean>;
459
+ interface ReplicaEntityAdapter<TData, TItem> {
460
+ /** Whether `data` holds the entity. */
461
+ has: (data: TData, id: string) => boolean;
519
462
  /**
520
- * Serialized read-modify-write: `fn` sees the row as of every earlier queued
521
- * write for this key. Return a projection to write it, `null` to remove the
522
- * row, or `undefined` to leave it untouched (a missing row stays missing).
523
- *
524
- * `read` picks how the row is read:
525
- * - `'get'` (default): best-effort `get`, a failed read passes for a miss.
526
- * - `'prefer-strict'`: `getStrict` when the storage has one, so a read failure
527
- * fails the update (resolves `false`) instead of passing for a missing row
528
- * that `fn` would then overwrite; `get` otherwise.
529
- * - `'strict'`: `getStrict` only; a storage without it fails the update.
530
- * Required for destructive passes (see `readReplicaRowStrict`).
463
+ * Map the entity inside `data` (`fn` returning `undefined` deletes it).
464
+ * Return `data` itself when nothing changed, or `undefined` when the whole
465
+ * value goes away with the entity (a detail value).
531
466
  */
532
- update: (key: ReplicaRowKey, fn: (current: ReplicaRow<T> | undefined) => ReplicaRow<T> | null | undefined, { read }?: {
533
- read?: 'get' | 'prefer-strict' | 'strict';
534
- }) => Promise<boolean>;
467
+ map: (data: TData, id: string, fn: (item: TItem) => TItem | undefined) => TData | undefined;
535
468
  }
536
- /** Reserved storage key of the per-scope index of persisted rows. */
537
- declare const REPLICA_INDEX_KEY = "__replica:index";
538
469
  /**
539
470
  * The engine's view of the host store. The engine never owns the rendered
540
471
  * value — the host does (a Zustand slice, a signal, a plain object) — it only
@@ -553,22 +484,6 @@ interface ReplicaStorePort<TData> {
553
484
  /** Materialized value of one entry, as the UI sees it. */
554
485
  read: (key: string) => TData | undefined;
555
486
  }
556
- /**
557
- * How entities (e.g. topics) appear inside a non-paged resource value. Paged
558
- * resources get this for free from `paging.getId`. Build one with
559
- * `singleEntity` (a detail value), `arrayEntity` (a plain list) or by hand for
560
- * nested shapes (grouped lists).
561
- */
562
- interface ReplicaEntityAdapter<TData, TItem> {
563
- /** Whether `data` holds the entity. */
564
- has: (data: TData, id: string) => boolean;
565
- /**
566
- * Map the entity inside `data` (`fn` returning `undefined` deletes it).
567
- * Return `data` itself when nothing changed, or `undefined` when the whole
568
- * value goes away with the entity (a detail value).
569
- */
570
- map: (data: TData, id: string, fn: (item: TItem) => TItem | undefined) => TData | undefined;
571
- }
572
487
  interface ReplicaEngineOptions<TParams, TData, TFetched> {
573
488
  /** Label prefix of host updates (devtools action names). Defaults to the resource name. */
574
489
  actionPrefix?: string;
@@ -664,7 +579,7 @@ declare const createReplicaEngine: <TParams, TData, TFetched = TData>(resource:
664
579
  persist?: boolean | undefined;
665
580
  }) => boolean;
666
581
  isMissingHead: (incoming: TFetched) => boolean;
667
- loadMore: (key: string, fallbackParams?: TParams) => Promise<void>;
582
+ loadMore: (key: string, fallbackParams?: TParams | undefined) => Promise<void>;
668
583
  optimistic: <TResult>(key: string, apply: (data: TData) => TData, serverCall: () => Promise<TResult>, mutationOptions?: OptimisticMutationOptions<TData, TResult>) => Promise<TResult>;
669
584
  patchStoredEntity: <TItem>(id: string, fn: (item: TItem) => TItem | undefined) => Promise<void>;
670
585
  patchStoredRows: (fn: (data: TData) => TData | null | undefined) => Promise<void>;
@@ -697,6 +612,67 @@ declare const createReplicaEngine: <TParams, TData, TFetched = TData>(resource:
697
612
  }) => boolean;
698
613
  };
699
614
  type ReplicaEngine<TParams, TData, TFetched = TData> = ReturnType<typeof createReplicaEngine<TParams, TData, TFetched>>;
615
+ /** Concatenate in view order: forward = head first, backward = head last. */
616
+ declare const joinPages: <TItem>(head: TItem[], rest: TItem[], direction: ReplicaPagingDirection) => TItem[];
617
+ /** The paging config one entry reads with: `directionOf` resolved for `key`. */
618
+ declare const pagingFor: <TItem, TCursor>(config: ReplicaPagingConfig<TItem, TCursor>, key: string) => ReplicaPagingConfig<TItem, TCursor>;
619
+ /**
620
+ * Cursor to request the next page with; `null` → exhausted, `undefined` →
621
+ * cannot page yet. Offset views without bookkeeping (seeded by legacy code or
622
+ * an older persisted shape) derive it from `currentPage` / `hasMore`.
623
+ */
624
+ declare const getNextPageCursor: <TItem, TCursor>(data: ReplicaPagedData<TItem, TCursor> | undefined, config: Pick<ReplicaPagingConfig<TItem, TCursor>, 'deriveCursor' | 'direction' | 'isCursorable' | 'mode'>) => TCursor | null | undefined;
625
+ /**
626
+ * Fold a fresh HEAD page into the view.
627
+ *
628
+ * - First load, a query change (`reset`) or an unextended view → the head page.
629
+ * - Extended view, `offset` mode → keep the loaded depth: fresh head first,
630
+ * then previously loaded rows not in it (shifted offsets self-heal by id).
631
+ * - Extended view, `cursor` mode → keep older pages only when the fresh head
632
+ * still contains the join anchor and the next cursor is known; otherwise
633
+ * collapse to the head page (never show a hidden gap).
634
+ *
635
+ * Client-only rows still in the view are kept on the head side.
636
+ */
637
+ declare const applyHeadPage: <TItem, TCursor>(current: ReplicaPagedData<TItem, TCursor> | undefined, page: ReplicaPageResult<TItem, TCursor>, options: {
638
+ pageSize: number;
639
+ reset?: boolean;
640
+ }, config: ReplicaPagingConfig<TItem, TCursor>, ctx?: ReplicaPagingContext<TItem>) => ReplicaPagedData<TItem, TCursor>;
641
+ /** Merge the NEXT page (fetched with `getNextPageCursor`) into the view. */
642
+ declare const applyNextPage: <TItem, TCursor>(current: ReplicaPagedData<TItem, TCursor>, page: ReplicaPageResult<TItem, TCursor>, config: ReplicaPagingConfig<TItem, TCursor>, ctx?: ReplicaPagingContext<TItem>) => ReplicaPagedData<TItem, TCursor>;
643
+ /**
644
+ * Insert new rows on the HEAD side (a streamed message, a created topic).
645
+ * Cursors point at the far end, so they stay valid; offsets shift and are
646
+ * healed by id de-duplication on the next page merge.
647
+ */
648
+ declare const insertHeadItems: <TItem, TCursor>(current: ReplicaPagedData<TItem, TCursor>, items: TItem[], config: ReplicaPagingConfig<TItem, TCursor>) => ReplicaPagedData<TItem, TCursor>;
649
+ /**
650
+ * Patch (or remove, when `fn` returns `undefined`) one row by id. Returns the
651
+ * same reference when the row is absent or unchanged.
652
+ */
653
+ declare const mapPagedItem: <TItem, TCursor, TData extends ReplicaPagedData<TItem, TCursor>>(current: TData, id: string, fn: (item: TItem) => TItem | undefined, config: Pick<ReplicaPagingConfig<TItem>, 'getId'>) => TData;
654
+ /** Whether a paged view holds a row. */
655
+ declare const hasPagedItem: <TItem>(data: ReplicaPagedData<TItem, unknown> | undefined, id: string, config: Pick<ReplicaPagingConfig<TItem>, 'getId'>) => boolean;
656
+ /** Drop every loaded page but the head (e.g. after an edit inside older history). */
657
+ declare const collapseToHead: <TItem, TCursor>(current: ReplicaPagedData<TItem, TCursor>, config: ReplicaPagingConfig<TItem, TCursor>) => ReplicaPagedData<TItem, TCursor>;
658
+ /**
659
+ * What survives a reload: whole pages from the head side, bounded by
660
+ * `persist.pages` and `persist.maxItems`, client-only rows and transient
661
+ * flags stripped. Offset views are cut at page-size boundaries so the
662
+ * persisted `currentPage` stays a valid offset; a cut inside a cursor page
663
+ * forgets the cursor (pagination resumes after the next server head page).
664
+ */
665
+ declare const toPersistedPage: <TItem, TCursor, TData extends ReplicaPagedData<TItem, TCursor>>(data: TData, config: ReplicaPagingConfig<TItem, TCursor>, ctx?: ReplicaPagingContext<TItem>) => TData;
666
+ /**
667
+ * The value IS one entity, or wraps one (`{ project, members }`). Deleting the
668
+ * entity drops the whole value.
669
+ */
670
+ declare const singleEntity: <TData, TItem = TData>(getId: (item: TItem) => string, lens?: {
671
+ get: (data: TData) => TItem;
672
+ set: (data: TData, item: TItem) => TData;
673
+ }) => ReplicaEntityAdapter<TData, TItem>;
674
+ /** The value is a plain list of entities; deleting one filters it out. */
675
+ declare const arrayEntity: <TItem>(getId: (item: TItem) => string) => ReplicaEntityAdapter<TItem[], TItem>;
700
676
  /** The part of a slice entity propagation needs (structural, so any slice fits). */
701
677
  interface ReplicaEntityTarget {
702
678
  beginEntityOptimistic: <TItem>(id: string, fn: (item: TItem) => TItem | undefined) => ReplicaOptimisticToken<any>[];
@@ -739,40 +715,41 @@ declare const linkReplicaEntity: <TItem>(targets: ReplicaEntityTarget[]) => {
739
715
  }) => void;
740
716
  };
741
717
  /**
742
- * The value IS one entity, or wraps one (`{ project, members }`). Deleting the
743
- * entity drops the whole value.
744
- */
745
- declare const singleEntity: <TData, TItem = TData>(getId: (item: TItem) => string, lens?: {
746
- get: (data: TData) => TItem;
747
- set: (data: TData, item: TItem) => TData;
748
- }) => ReplicaEntityAdapter<TData, TItem>;
749
- /** The value is a plain list of entities; deleting one filters it out. */
750
- declare const arrayEntity: <TItem>(getId: (item: TItem) => string) => ReplicaEntityAdapter<TItem[], TItem>;
751
- /**
752
- * Keys of the queries a fetch adapter runs for a replica. Hosts that sweep
753
- * their query cache by prefix (e.g. "refetch everything after a restore")
754
- * match on {@link REPLICA_KEY_PREFIX}.
718
+ * Strict read of one row (see `ReplicaStorage.getStrict`). Rejects when the
719
+ * storage has no strict read: its `get` swallows I/O failures into a miss, so a
720
+ * destructive pass reading through it could take an unreadable row (the index)
721
+ * for an empty one and report success for rows it never saw.
755
722
  */
756
- declare const REPLICA_KEY_PREFIX = "replica:";
757
- declare const replicaKeys: {
758
- /** One-shot read of the persisted row for `storageKey`. */
759
- hydrate: (name: string, version: number, scope: string, storageKey: string) => readonly ['replica:hydrate', string, number, string, string];
760
- /** Network sync of the head of one entry. */
761
- sync: (name: string, version: number, scope: string, key: string, params: unknown) => readonly ['replica:sync', string, number, string, string, unknown];
762
- };
763
- /** Match a resource's sync keys, optionally narrowed to one scope and/or entry key. */
764
- declare const isReplicaSyncKey: (queryKey: unknown, name: string, filter?: {
765
- key?: string;
766
- scope?: string;
767
- }) => boolean;
723
+ declare const readReplicaRowStrict: <T>(storage: ReplicaStorage<T>, key: ReplicaRowKey) => Promise<ReplicaRow<T> | undefined>;
768
724
  /**
769
- * Resolve once `promise` settles or `ms` elapses — never rejects, never hangs.
770
- *
771
- * Bounds a pre-paint hydrate in a route loader: the persisted read is usually
772
- * sub-millisecond, and the deadline only fires on a pathological storage stall,
773
- * so a route transition never waits longer than `ms` for it.
725
+ * Serializes writes for one projection key. A later snapshot can never be overwritten by an
726
+ * earlier, slower IndexedDB write.
774
727
  */
775
- declare const settleWithin: (promise: Promise<unknown>, ms: number) => Promise<void>;
728
+ declare class ReplicaWriteQueue<T> {
729
+ #private;
730
+ constructor(storage: ReplicaStorage<T>);
731
+ /** Resolves `false` when storage rejected the delete (see `ReplicaStorage.remove`). */
732
+ remove: (key: ReplicaRowKey) => Promise<boolean>;
733
+ set: (key: ReplicaRowKey, value: ReplicaRow<T>) => Promise<boolean>;
734
+ /**
735
+ * Serialized read-modify-write: `fn` sees the row as of every earlier queued
736
+ * write for this key. Return a projection to write it, `null` to remove the
737
+ * row, or `undefined` to leave it untouched (a missing row stays missing).
738
+ *
739
+ * `read` picks how the row is read:
740
+ * - `'get'` (default): best-effort `get`, a failed read passes for a miss.
741
+ * - `'prefer-strict'`: `getStrict` when the storage has one, so a read failure
742
+ * fails the update (resolves `false`) instead of passing for a missing row
743
+ * that `fn` would then overwrite; `get` otherwise.
744
+ * - `'strict'`: `getStrict` only; a storage without it fails the update.
745
+ * Required for destructive passes (see `readReplicaRowStrict`).
746
+ */
747
+ update: (key: ReplicaRowKey, fn: (current: ReplicaRow<T> | undefined) => ReplicaRow<T> | null | undefined, { read }?: {
748
+ read?: 'get' | 'prefer-strict' | 'strict';
749
+ }) => Promise<boolean>;
750
+ }
751
+ /** Reserved storage key of the per-scope index of persisted rows. */
752
+ declare const REPLICA_INDEX_KEY = "__replica:index";
776
753
  /**
777
754
  * The entry storage keys this resource has a persisted row for in the active
778
755
  * scope, oldest first.
@@ -788,6 +765,14 @@ declare const settleWithin: (promise: Promise<unknown>, ms: number) => Promise<v
788
765
  * `query` unset.
789
766
  */
790
767
  declare const readReplicaStoredKeys: (resource: Pick<ReplicaResource<any, any, any>, 'scope' | 'storage'>) => Promise<string[]>;
768
+ /**
769
+ * Resolve once `promise` settles or `ms` elapses — never rejects, never hangs.
770
+ *
771
+ * Bounds a pre-paint hydrate in a route loader: the persisted read is usually
772
+ * sub-millisecond, and the deadline only fires on a pathological storage stall,
773
+ * so a route transition never waits longer than `ms` for it.
774
+ */
775
+ declare const settleWithin: (promise: Promise<unknown>, ms: number) => Promise<void>;
791
776
  /**
792
777
  * An async key-value store a replica can persist into: an IndexedDB object
793
778
  * store, a SQLite table behind IPC, a `Map` in tests. Values are stored as
@@ -867,5 +852,5 @@ declare class LocalStorageReplicaStorage<T> implements ReplicaStorage<T> {
867
852
  */
868
853
  set: (key: ReplicaRowKey, row: ReplicaRow<T>) => Promise<void>;
869
854
  }
870
- export { ReplicaPageInfo as $, ReplicaTransition as A, replicaStorageKey as B, ReplicaStorePort as C, readReplicaRowStrict as D, ReplicaWriteQueue as E, DefineReplicaOptions as F, ReplicaResource as G, staticReplicaScope as H, ReplicaStorageFactory as I, ReplicaScope as J, ReplicaRow as K, definePagedReplica as L, createReplicaState as M, replicaReducer as N, ReplicaAction as O, DefinePagedReplicaOptions as P, ReplicaStorage as Q, defineReplica as R, ReplicaOptimisticToken as S, REPLICA_INDEX_KEY as T, ReplicaEntryMeta as U, stableQueryKey as V, ReplicaPendingMutation as W, ReplicaSource as X, ReplicaSeedPatch as Y, ReplicaState as Z, OptimisticMutationOptions as _, KeyValueReplicaStorageOptions as a, ReplicaPagingMode as at, ReplicaEntityAdapter as b, readReplicaStoredKeys as c, collapseToHead as ct, isReplicaSyncKey as d, insertHeadItems as dt, ReplicaPageResult as et, replicaKeys as f, joinPages as ft, linkReplicaEntity as g, ReplicaEntityTarget as h, toPersistedPage as ht, KeyValueReplicaStorage as i, ReplicaPagingDirection as it, ReplicaViewWrite as j, ReplicaEffect as k, settleWithin as l, getNextPageCursor as lt, singleEntity as m, pagingFor as mt, LocalStorageReplicaStorageOptions as n, ReplicaPagingConfig as nt, ReplicaKeyValueStore as o, applyHeadPage as ot, arrayEntity as p, mapPagedItem as pt, ReplicaRowKey as q, ReplicaRowCodec as r, ReplicaPagingContext as rt, replicaRowStoreKey as s, applyNextPage as st, LocalStorageReplicaStorage as t, ReplicaPagedData as tt, REPLICA_KEY_PREFIX as u, hasPagedItem as ut, ReplicaEngine as v, createReplicaEngine as w, ReplicaHeadOutcome as x, ReplicaEngineOptions as y, isReplicaStorageKeyOf as z };
871
- //# sourceMappingURL=index-CFbCHM7Q.d.ts.map
855
+ export { DefinePagedReplicaOptions as $, ReplicaEngineOptions as A, replicaReducer as B, joinPages as C, ReplicaEngine as D, toPersistedPage as E, ReplicaAction as F, ReplicaState as G, ReplicaPendingMutation as H, ReplicaEffect as I, stableQueryKey as J, isReplicaStorageKeyOf as K, ReplicaTransition as L, ReplicaOptimisticToken as M, ReplicaStorePort as N, createReplicaEngine as O, ReplicaEntityAdapter as P, replicaKeys as Q, ReplicaViewWrite as R, insertHeadItems as S, pagingFor as T, ReplicaSeedPatch as U, ReplicaEntryMeta as V, ReplicaSource as W, REPLICA_KEY_PREFIX as X, staticReplicaScope as Y, isReplicaSyncKey as Z, applyHeadPage as _, KeyValueReplicaStorageOptions as a, ReplicaScope as at, getNextPageCursor as b, settleWithin as c, ReplicaPagedData as ct, ReplicaWriteQueue as d, ReplicaPagingDirection as dt, DefineReplicaOptions as et, readReplicaRowStrict as f, ReplicaPagingMode as ft, singleEntity as g, arrayEntity as h, ReplicaStorage as ht, KeyValueReplicaStorage as i, ReplicaResource as it, ReplicaHeadOutcome as j, OptimisticMutationOptions as k, readReplicaStoredKeys as l, ReplicaPagingConfig as lt, linkReplicaEntity as m, ReplicaRowKey as mt, LocalStorageReplicaStorageOptions as n, definePagedReplica as nt, ReplicaKeyValueStore as o, ReplicaPageInfo as ot, ReplicaEntityTarget as p, ReplicaRow as pt, replicaStorageKey as q, ReplicaRowCodec as r, defineReplica as rt, replicaRowStoreKey as s, ReplicaPageResult as st, LocalStorageReplicaStorage as t, ReplicaStorageFactory as tt, REPLICA_INDEX_KEY as u, ReplicaPagingContext as ut, applyNextPage as v, mapPagedItem as w, hasPagedItem as x, collapseToHead as y, createReplicaState as z };
856
+ //# sourceMappingURL=index-DsElitHD.d.ts.map