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.
- package/README.md +152 -2
- package/dist/index-CFbCHM7Q.d.ts +871 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +239 -0
- package/dist/index.js.map +1 -0
- package/dist/keys-BTsqwfEa.js +1986 -0
- package/dist/keys-BTsqwfEa.js.map +1 -0
- package/dist/zustand/index.d.ts +362 -1
- package/dist/zustand/index.js +580 -0
- package/dist/zustand/index.js.map +1 -0
- package/package.json +9 -1
|
@@ -0,0 +1,871 @@
|
|
|
1
|
+
/**
|
|
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.
|
|
15
|
+
*/
|
|
16
|
+
type ReplicaPagingMode = 'cursor' | 'offset';
|
|
17
|
+
type ReplicaPagingDirection = 'backward' | 'forward';
|
|
18
|
+
interface ReplicaPagingConfig<TItem, TCursor = any> {
|
|
19
|
+
/**
|
|
20
|
+
* Cursor mode: cursor to the page past `item`, the last cursor-eligible row
|
|
21
|
+
* on the far side, for a server that pages by row but reports no
|
|
22
|
+
* `nextCursor` (a plain list). Only safe when the row carries a lossless
|
|
23
|
+
* position; omit it when the server owns the cursor, and paging then waits
|
|
24
|
+
* for one.
|
|
25
|
+
*/
|
|
26
|
+
deriveCursor?: (item: TItem) => TCursor;
|
|
27
|
+
direction: ReplicaPagingDirection;
|
|
28
|
+
/**
|
|
29
|
+
* Per-entry direction, for a resource whose entries read differently (a
|
|
30
|
+
* transcript read from its first round). Defaults to `direction`. Persisting
|
|
31
|
+
* an entry always uses `direction`, so keep such entries in memory.
|
|
32
|
+
*/
|
|
33
|
+
directionOf?: (key: string) => ReplicaPagingDirection;
|
|
34
|
+
getId: (item: TItem) => string;
|
|
35
|
+
/**
|
|
36
|
+
* Rows that may anchor a join or derive a cursor. Synthetic rows the query
|
|
37
|
+
* injects (group / summary nodes) have no position of their own on the
|
|
38
|
+
* server, so they are skipped. Defaults to every row.
|
|
39
|
+
*/
|
|
40
|
+
isCursorable?: (item: TItem) => boolean;
|
|
41
|
+
mode: ReplicaPagingMode;
|
|
42
|
+
/** Which part survives a reload. Defaults to the head page only. */
|
|
43
|
+
persist?: {
|
|
44
|
+
/** Hard cap on persisted items (taken from the head side). */
|
|
45
|
+
maxItems?: number;
|
|
46
|
+
/** Number of pages from the head side. Default 1. */
|
|
47
|
+
pages?: number;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Optional total order re-applied after every merge (e.g. messages by
|
|
51
|
+
* createdAt). The sort is stable: rows the comparator ties keep the order
|
|
52
|
+
* the server sent them in, so compare positions only — an id tie-break
|
|
53
|
+
* would reorder rows created within the same tick.
|
|
54
|
+
*/
|
|
55
|
+
sort?: (a: TItem, b: TItem) => number;
|
|
56
|
+
}
|
|
57
|
+
interface ReplicaPageResult<TItem, TCursor> {
|
|
58
|
+
items: TItem[];
|
|
59
|
+
/**
|
|
60
|
+
* Cursor mode: next page cursor; `null` = exhausted, `undefined` = unknown.
|
|
61
|
+
* Offset mode: the server's own verdict — `null` = exhausted, any other
|
|
62
|
+
* value = more pages (the engine still advances by page index); `undefined`
|
|
63
|
+
* falls back to `total` / a full page. A server that filters rows after
|
|
64
|
+
* paging returns short pages that are not the end, so it must report this.
|
|
65
|
+
*/
|
|
66
|
+
nextCursor?: TCursor | null;
|
|
67
|
+
total?: number;
|
|
68
|
+
}
|
|
69
|
+
interface ReplicaPageInfo<TCursor> {
|
|
70
|
+
/** Items this page contributed after de-duplication. */
|
|
71
|
+
count: number;
|
|
72
|
+
/** Cursor of the page after this one; `null` = exhausted, `undefined` = unknown. */
|
|
73
|
+
next?: TCursor | null;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The paged view a resource keeps in the store. Domain views extend it, so
|
|
77
|
+
* selectors read `items` / `hasMore` / `isLoadingMore` as before.
|
|
78
|
+
*/
|
|
79
|
+
interface ReplicaPagedData<TItem, TCursor = number> {
|
|
80
|
+
/**
|
|
81
|
+
* Boundary row of the head page once more pages are loaded (forward: its
|
|
82
|
+
* last row, backward: its first row). A refreshed head page that no longer
|
|
83
|
+
* contains it has slid past the loaded pages — merging would hide a gap.
|
|
84
|
+
*/
|
|
85
|
+
anchorId?: string;
|
|
86
|
+
/** Index of the last merged page (0 = head only). */
|
|
87
|
+
currentPage: number;
|
|
88
|
+
hasMore: boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Synthetic (non-cursorable) rows of the head page. A sort can place them
|
|
91
|
+
* among older pages, so a head refresh drops them by id rather than by
|
|
92
|
+
* position — the fresh head brings its own.
|
|
93
|
+
*/
|
|
94
|
+
headSyntheticIds?: string[];
|
|
95
|
+
isLoadingMore?: boolean;
|
|
96
|
+
items: TItem[];
|
|
97
|
+
loadMoreError?: unknown;
|
|
98
|
+
/** Cursor for the next page (offset: page index). `null` = exhausted. */
|
|
99
|
+
nextCursor?: TCursor | null;
|
|
100
|
+
/** Per-page bookkeeping, head first. Missing on legacy data = one page. */
|
|
101
|
+
pages?: ReplicaPageInfo<TCursor>[];
|
|
102
|
+
pageSize: number;
|
|
103
|
+
total?: number;
|
|
104
|
+
}
|
|
105
|
+
interface ReplicaPagingContext<TItem> {
|
|
106
|
+
/**
|
|
107
|
+
* Client-only rows (optimistic inserts whose server row may not exist yet).
|
|
108
|
+
* They survive head refreshes and are never persisted.
|
|
109
|
+
*/
|
|
110
|
+
isClientOnly?: (item: TItem) => boolean;
|
|
111
|
+
}
|
|
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
|
+
/**
|
|
222
|
+
* Identity partition of the persisted projection (user + workspace by default).
|
|
223
|
+
* `use` feeds the sync hook, `get` imperative actions, `canPersist` gates writes
|
|
224
|
+
* while the scope is still an optimistic guess (identity not resolved yet).
|
|
225
|
+
*/
|
|
226
|
+
interface ReplicaScope {
|
|
227
|
+
/**
|
|
228
|
+
* Whether this runtime may read the persisted projection back into memory.
|
|
229
|
+
* Defaults to `true`. A surface that never resolves an identity (e.g. a
|
|
230
|
+
* public embed that does not mount the user store) opts out: its scope is
|
|
231
|
+
* only the last-known guess, so hydrating it could paint a different — or a
|
|
232
|
+
* since-invalidated — session's private rows before authorization is checked.
|
|
233
|
+
*/
|
|
234
|
+
canHydrate?: () => boolean;
|
|
235
|
+
canPersist: () => boolean;
|
|
236
|
+
get: () => string;
|
|
237
|
+
use: () => string;
|
|
238
|
+
}
|
|
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
|
+
interface ReplicaResource<TParams, TData, TFetched = TData, TCursor = any> {
|
|
294
|
+
/** Paged resources receive the page cursor (`undefined` = head page). */
|
|
295
|
+
fetcher?: (params: TParams, cursor?: TCursor) => Promise<TFetched>;
|
|
296
|
+
key: (params: TParams) => string;
|
|
297
|
+
/** Cap of persisted rows per scope (LRU eviction, see `DefineReplicaOptions.persist`). */
|
|
298
|
+
maxPersistedEntries?: number;
|
|
299
|
+
name: string;
|
|
300
|
+
/** Storage namespace — `name` + `version`, so a version bump orphans old rows. */
|
|
301
|
+
namespace: string;
|
|
302
|
+
/** Present on paged resources (`definePagedReplica`). */
|
|
303
|
+
paging?: ReplicaPagingConfig<any, TCursor>;
|
|
304
|
+
/** Whether the resource survives a reload (it has a storage). */
|
|
305
|
+
persisted: boolean;
|
|
306
|
+
/** Whether an entry key may be hydrated and persisted (see `persistKey`). */
|
|
307
|
+
persistKey: (key: string) => boolean;
|
|
308
|
+
/**
|
|
309
|
+
* Query identity beyond `key` (filters, page size). Persisted rows are
|
|
310
|
+
* stored per query, so a different query never hydrates; in memory a query
|
|
311
|
+
* change resets loaded pages.
|
|
312
|
+
*/
|
|
313
|
+
query: (params: TParams) => string | undefined;
|
|
314
|
+
scope: ReplicaScope;
|
|
315
|
+
storage?: ReplicaStorage<TData>;
|
|
316
|
+
/** Row key in `storage` for these params (`key`, plus `?query` when set). */
|
|
317
|
+
storageKey: (params: TParams) => string;
|
|
318
|
+
/** Custom query-cache key of the network sync (see `DefineReplicaOptions.syncKey`). */
|
|
319
|
+
syncKey?: (params: TParams) => readonly unknown[];
|
|
320
|
+
version: number;
|
|
321
|
+
}
|
|
322
|
+
/** Persisted row key: one row per entry key and query. */
|
|
323
|
+
declare const replicaStorageKey: (key: string, query?: string) => string;
|
|
324
|
+
/**
|
|
325
|
+
* Whether `storageKey` is a persisted row of entry `key`, under any query.
|
|
326
|
+
*
|
|
327
|
+
* Entry keys are free-form, so a `?` prefix alone cannot tell `foo` + a query
|
|
328
|
+
* from a plain entry `foo?bar`. A row only counts as a variant when it is
|
|
329
|
+
* exactly `replicaStorageKey(key, query)` for a query this module could have
|
|
330
|
+
* written: the rest after `key?` must be a canonical `stableQueryKey` (it
|
|
331
|
+
* parses as JSON and serializes back to itself). Row keys and the index keep
|
|
332
|
+
* their existing flat format, so rows already persisted on devices match the
|
|
333
|
+
* same way.
|
|
334
|
+
*/
|
|
335
|
+
declare const isReplicaStorageKeyOf: (storageKey: string, key: string) => boolean;
|
|
336
|
+
/** Deterministic JSON: sorted keys, `undefined` dropped. */
|
|
337
|
+
declare const stableQueryKey: (value: unknown) => string;
|
|
338
|
+
/**
|
|
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.
|
|
342
|
+
*/
|
|
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>;
|
|
353
|
+
/**
|
|
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.
|
|
362
|
+
*/
|
|
363
|
+
persist?: {
|
|
364
|
+
maxEntries?: number;
|
|
365
|
+
};
|
|
366
|
+
/**
|
|
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.
|
|
369
|
+
*/
|
|
370
|
+
persistKey?: (key: string) => boolean;
|
|
371
|
+
/**
|
|
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.
|
|
376
|
+
*/
|
|
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>;
|
|
382
|
+
/**
|
|
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.
|
|
387
|
+
*/
|
|
388
|
+
syncKey?: (params: TParams) => readonly unknown[];
|
|
389
|
+
/** Bump to invalidate every persisted row written by older shapes. */
|
|
390
|
+
version: number;
|
|
391
|
+
}
|
|
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>;
|
|
398
|
+
}
|
|
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
|
+
/**
|
|
405
|
+
* Pure transition core of a replica. It never touches the store
|
|
406
|
+
* or storage: it returns the next bookkeeping slot, the view writes the store
|
|
407
|
+
* binding must apply, and the persistence effects it must run.
|
|
408
|
+
*
|
|
409
|
+
* The VIEW (what selectors read) lives in the domain store, wherever the
|
|
410
|
+
* binding's lens puts it. The reducer only reads it through `readView`, so a
|
|
411
|
+
* domain can keep its existing state shape (e.g. `topicDataMap`).
|
|
412
|
+
*/
|
|
413
|
+
type ReplicaAction<T> = {
|
|
414
|
+
data: T;
|
|
415
|
+
key: string;
|
|
416
|
+
/**
|
|
417
|
+
* Repaint a slot that answers a *different* query from the persisted page
|
|
418
|
+
* (a navigation). Only the caller that knows the entry has moved on
|
|
419
|
+
* (the requested query changed) may ask for it; by default a hydrate only
|
|
420
|
+
* fills an empty slot.
|
|
421
|
+
*/
|
|
422
|
+
overwrite?: boolean;
|
|
423
|
+
params?: unknown;
|
|
424
|
+
query?: string;
|
|
425
|
+
scope: string;
|
|
426
|
+
type: 'hydrate';
|
|
427
|
+
updatedAt?: number;
|
|
428
|
+
} | {
|
|
429
|
+
/** Receives the confirmed value (base or view); `undefined` keeps it. */
|
|
430
|
+
data: (confirmed: T | undefined) => T | undefined;
|
|
431
|
+
key: string;
|
|
432
|
+
params?: unknown;
|
|
433
|
+
query?: string;
|
|
434
|
+
scope: string;
|
|
435
|
+
type: 'replace';
|
|
436
|
+
} | {
|
|
437
|
+
apply: (data: T | undefined) => T | undefined;
|
|
438
|
+
key: string;
|
|
439
|
+
persist: boolean;
|
|
440
|
+
scope: string;
|
|
441
|
+
/** Marks a provisional write (`'seed'`); defaults to the entry's own source. */
|
|
442
|
+
source?: ReplicaSource;
|
|
443
|
+
/**
|
|
444
|
+
* Seed writes of one source (a list refresh, a session refresh). A later
|
|
445
|
+
* seed write supersedes an earlier one of the same tag since the last
|
|
446
|
+
* confirmed write, so a remembered seed history stays bounded.
|
|
447
|
+
*/
|
|
448
|
+
seedTag?: string;
|
|
449
|
+
type: 'update';
|
|
450
|
+
} | {
|
|
451
|
+
apply: (data: T) => T;
|
|
452
|
+
id: number;
|
|
453
|
+
key: string;
|
|
454
|
+
/** Query the overlay is started under (see `ReplicaPendingMutation`). */
|
|
455
|
+
query?: string;
|
|
456
|
+
scope: string;
|
|
457
|
+
type: 'optimistic';
|
|
458
|
+
} | {
|
|
459
|
+
confirm?: (data: T) => T;
|
|
460
|
+
id: number;
|
|
461
|
+
key: string;
|
|
462
|
+
scope: string;
|
|
463
|
+
type: 'commit';
|
|
464
|
+
} | {
|
|
465
|
+
id: number;
|
|
466
|
+
key: string;
|
|
467
|
+
scope: string;
|
|
468
|
+
type: 'rollback';
|
|
469
|
+
} | {
|
|
470
|
+
key: string;
|
|
471
|
+
scope: string;
|
|
472
|
+
type: 'remove';
|
|
473
|
+
} | {
|
|
474
|
+
scope: string;
|
|
475
|
+
type: 'resetScope';
|
|
476
|
+
};
|
|
477
|
+
type ReplicaEffect<T> = {
|
|
478
|
+
data: T;
|
|
479
|
+
key: string;
|
|
480
|
+
query?: string;
|
|
481
|
+
scope: string;
|
|
482
|
+
type: 'persist';
|
|
483
|
+
} | {
|
|
484
|
+
key: string;
|
|
485
|
+
query?: string;
|
|
486
|
+
scope: string;
|
|
487
|
+
type: 'remove';
|
|
488
|
+
};
|
|
489
|
+
type ReplicaViewWrite<T> = {
|
|
490
|
+
data: T | undefined;
|
|
491
|
+
key: string;
|
|
492
|
+
} | {
|
|
493
|
+
type: 'clear';
|
|
494
|
+
};
|
|
495
|
+
interface ReplicaTransition<T> {
|
|
496
|
+
effects: ReplicaEffect<T>[];
|
|
497
|
+
state: ReplicaState<T>;
|
|
498
|
+
writes: ReplicaViewWrite<T>[];
|
|
499
|
+
}
|
|
500
|
+
declare const createReplicaState: <T>() => ReplicaState<T>;
|
|
501
|
+
declare const replicaReducer: <T>(state: ReplicaState<T>, action: ReplicaAction<T>, readView: (key: string) => T | undefined, now?: number) => ReplicaTransition<T>;
|
|
502
|
+
/**
|
|
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.
|
|
512
|
+
*/
|
|
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>;
|
|
519
|
+
/**
|
|
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`).
|
|
531
|
+
*/
|
|
532
|
+
update: (key: ReplicaRowKey, fn: (current: ReplicaRow<T> | undefined) => ReplicaRow<T> | null | undefined, { read }?: {
|
|
533
|
+
read?: 'get' | 'prefer-strict' | 'strict';
|
|
534
|
+
}) => Promise<boolean>;
|
|
535
|
+
}
|
|
536
|
+
/** Reserved storage key of the per-scope index of persisted rows. */
|
|
537
|
+
declare const REPLICA_INDEX_KEY = "__replica:index";
|
|
538
|
+
/**
|
|
539
|
+
* The engine's view of the host store. The engine never owns the rendered
|
|
540
|
+
* value — the host does (a Zustand slice, a signal, a plain object) — it only
|
|
541
|
+
* reads entries through `read` and hands every transition to `commit`.
|
|
542
|
+
*/
|
|
543
|
+
interface ReplicaStorePort<TData> {
|
|
544
|
+
/**
|
|
545
|
+
* Apply the view writes and the next bookkeeping slot as ONE host update, so
|
|
546
|
+
* subscribers never observe a view out of step with its bookkeeping.
|
|
547
|
+
*/
|
|
548
|
+
commit: (writes: ReplicaViewWrite<TData>[], state: ReplicaState<TData>, label: string) => void;
|
|
549
|
+
/** Current bookkeeping slot. */
|
|
550
|
+
getState: () => ReplicaState<TData>;
|
|
551
|
+
/** Enumerate loaded keys (entity propagation). Defaults to the bookkeeping entries. */
|
|
552
|
+
keys?: () => string[];
|
|
553
|
+
/** Materialized value of one entry, as the UI sees it. */
|
|
554
|
+
read: (key: string) => TData | undefined;
|
|
555
|
+
}
|
|
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
|
+
interface ReplicaEngineOptions<TParams, TData, TFetched> {
|
|
573
|
+
/** Label prefix of host updates (devtools action names). Defaults to the resource name. */
|
|
574
|
+
actionPrefix?: string;
|
|
575
|
+
/** Non-paged resources: how entities sit in the value (see `linkReplicaEntity`). */
|
|
576
|
+
entity?: ReplicaEntityAdapter<TData, any>;
|
|
577
|
+
/** Overrides `resource.fetcher` when the fetch needs store context. */
|
|
578
|
+
fetcher?: (params: TParams, cursor?: any) => Promise<TFetched>;
|
|
579
|
+
/** Paged: rows that only exist client-side (kept across refreshes, never persisted). */
|
|
580
|
+
isClientOnly?: (item: any) => boolean;
|
|
581
|
+
/** Reject a persisted value that cannot serve these params. Rarely needed: rows are stored per query. */
|
|
582
|
+
isHydratable?: (cached: TData, params: TParams) => boolean;
|
|
583
|
+
/**
|
|
584
|
+
* Non-paged: whether a head response means the entry no longer exists. Such a
|
|
585
|
+
* response is applied as an explicit removal (memory + persisted row) instead
|
|
586
|
+
* of through `merge` — otherwise a `null` would fall back to the confirmed
|
|
587
|
+
* value (the reducer resolves `action.data(confirmed) ?? confirmed`), so a
|
|
588
|
+
* deleted aggregate would be retained and re-persisted.
|
|
589
|
+
*/
|
|
590
|
+
isMissing?: (incoming: TFetched) => boolean;
|
|
591
|
+
/**
|
|
592
|
+
* Non-paged: fold a server response into the confirmed value. Return
|
|
593
|
+
* `undefined` to keep the current value. Defaults to "the response is the value".
|
|
594
|
+
*/
|
|
595
|
+
merge?: (incoming: TFetched, confirmed: TData | undefined, params: TParams) => TData | undefined;
|
|
596
|
+
/** Where the engine reads and commits the view. */
|
|
597
|
+
port: ReplicaStorePort<TData>;
|
|
598
|
+
/**
|
|
599
|
+
* Reconcile a server response with the value currently shown before it is
|
|
600
|
+
* folded in — e.g. keep a row the client knows is newer, or ignore a
|
|
601
|
+
* response that raced a newer local state. Return `undefined` to drop the
|
|
602
|
+
* response entirely.
|
|
603
|
+
*/
|
|
604
|
+
prepareHead?: (incoming: TFetched, current: TData | undefined, params: TParams) => TFetched | undefined;
|
|
605
|
+
/** Re-run the network sync of one entry (or all); wired by the fetch adapter. */
|
|
606
|
+
revalidate?: (key?: string) => Promise<unknown>;
|
|
607
|
+
/**
|
|
608
|
+
* Strip transient / client-only parts before persisting.
|
|
609
|
+
*
|
|
610
|
+
* - a value → persisted
|
|
611
|
+
* - `undefined` → skip this write (keep whatever is already stored)
|
|
612
|
+
* - `null` → the entry is a server-confirmed absence: drop any prior
|
|
613
|
+
* persisted projection, so a later hydrate cannot paint a value the server
|
|
614
|
+
* no longer has (the view keeps the confirmed value).
|
|
615
|
+
*/
|
|
616
|
+
toPersisted?: (data: TData) => TData | null | undefined;
|
|
617
|
+
/** Paged: domain fields derived from params, written with every head page. */
|
|
618
|
+
viewFields?: (params: TParams) => Partial<TData>;
|
|
619
|
+
}
|
|
620
|
+
/** What a head response did once folded in (see `applyHead`). */
|
|
621
|
+
type ReplicaHeadOutcome = 'dropped' | 'missing' | 'replaced';
|
|
622
|
+
interface OptimisticMutationOptions<TData, TResult> {
|
|
623
|
+
/** Turn the server result into the confirmed value; defaults to re-applying `apply`. */
|
|
624
|
+
confirm?: (result: TResult) => (data: TData) => TData;
|
|
625
|
+
/** Revalidate the entry after the server call settles. */
|
|
626
|
+
revalidate?: boolean;
|
|
627
|
+
}
|
|
628
|
+
/** Handle of an optimistic overlay that is settled later (see `beginOptimistic`). */
|
|
629
|
+
interface ReplicaOptimisticToken<TData> {
|
|
630
|
+
commit: (confirm?: (data: TData) => TData) => void;
|
|
631
|
+
rollback: () => void;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* The replica engine: every transition of one resource, independent of any
|
|
635
|
+
* UI framework or state library.
|
|
636
|
+
*
|
|
637
|
+
* It owns hydrate-if-empty, server replace, pagination, optimistic overlays
|
|
638
|
+
* with commit/rollback, entity propagation, scope isolation and serialized
|
|
639
|
+
* persistence. The host store keeps the rendered value (see
|
|
640
|
+
* {@link ReplicaStorePort}); fetch scheduling (when to sync, dedupe, focus
|
|
641
|
+
* revalidation) belongs to an adapter such as `local-replica/zustand`.
|
|
642
|
+
*/
|
|
643
|
+
declare const createReplicaEngine: <TParams, TData, TFetched = TData>(resource: ReplicaResource<TParams, TData, TFetched>, options: ReplicaEngineOptions<TParams, TData, TFetched>) => {
|
|
644
|
+
applyHead: (params: TParams, fetched: TFetched, scope?: string) => {
|
|
645
|
+
changed: boolean;
|
|
646
|
+
outcome: ReplicaHeadOutcome;
|
|
647
|
+
};
|
|
648
|
+
beginEntityOptimistic: <TItem>(id: string, fn: (item: TItem) => TItem | undefined) => ReplicaOptimisticToken<TData>[];
|
|
649
|
+
beginOptimistic: (key: string, apply: (data: TData) => TData, query?: string) => ReplicaOptimisticToken<TData>;
|
|
650
|
+
clear: () => Promise<boolean>;
|
|
651
|
+
clearGeneration: (scope: string) => number;
|
|
652
|
+
clearScope: (scope: string) => Promise<boolean>;
|
|
653
|
+
removalGeneration: (scope: string, key: string) => number;
|
|
654
|
+
collapse: (key: string) => boolean;
|
|
655
|
+
dispatch: (action: ReplicaAction<TData>) => boolean;
|
|
656
|
+
ensureScope: (scope: string) => void;
|
|
657
|
+
entityKeys: (id: string) => string[];
|
|
658
|
+
fetcher: ((params: TParams, cursor?: any) => Promise<TFetched>) | undefined;
|
|
659
|
+
getConfirmed: (key: string) => TData | undefined;
|
|
660
|
+
hydrate: (params: TParams, scope?: string, { overwrite }?: {
|
|
661
|
+
overwrite?: boolean;
|
|
662
|
+
}) => Promise<boolean>;
|
|
663
|
+
insertHead: <TItem>(key: string, items: TItem[], { persist }?: {
|
|
664
|
+
persist?: boolean | undefined;
|
|
665
|
+
}) => boolean;
|
|
666
|
+
isMissingHead: (incoming: TFetched) => boolean;
|
|
667
|
+
loadMore: (key: string, fallbackParams?: TParams) => Promise<void>;
|
|
668
|
+
optimistic: <TResult>(key: string, apply: (data: TData) => TData, serverCall: () => Promise<TResult>, mutationOptions?: OptimisticMutationOptions<TData, TResult>) => Promise<TResult>;
|
|
669
|
+
patchStoredEntity: <TItem>(id: string, fn: (item: TItem) => TItem | undefined) => Promise<void>;
|
|
670
|
+
patchStoredRows: (fn: (data: TData) => TData | null | undefined) => Promise<void>;
|
|
671
|
+
persist: (key: string) => void;
|
|
672
|
+
remove: (key: string, scope?: string) => boolean;
|
|
673
|
+
removeEntity: (id: string, options?: {
|
|
674
|
+
persist?: boolean;
|
|
675
|
+
scope?: string;
|
|
676
|
+
} | undefined) => void;
|
|
677
|
+
replace: (params: TParams, fetched: TFetched, scope?: string) => boolean;
|
|
678
|
+
resource: ReplicaResource<TParams, TData, TFetched, any>;
|
|
679
|
+
retain: (key: string, queryKey?: string) => () => void;
|
|
680
|
+
revalidate: (key?: string) => Promise<unknown>;
|
|
681
|
+
update: (key: string, apply: (data: TData | undefined) => TData | undefined, { persist, seedTag, source }?: {
|
|
682
|
+
persist?: boolean;
|
|
683
|
+
seedTag?: string;
|
|
684
|
+
source?: ReplicaSource;
|
|
685
|
+
}) => boolean;
|
|
686
|
+
updateEntity: <TItem>(id: string, fn: (item: TItem) => TItem | undefined, options?: {
|
|
687
|
+
persist?: boolean;
|
|
688
|
+
scope?: string;
|
|
689
|
+
} | undefined) => void;
|
|
690
|
+
updateMany: (entries: {
|
|
691
|
+
apply: (data: TData | undefined) => TData | undefined;
|
|
692
|
+
key: string;
|
|
693
|
+
}[], { persist, seedTag, source }?: {
|
|
694
|
+
persist?: boolean;
|
|
695
|
+
seedTag?: string;
|
|
696
|
+
source?: ReplicaSource;
|
|
697
|
+
}) => boolean;
|
|
698
|
+
};
|
|
699
|
+
type ReplicaEngine<TParams, TData, TFetched = TData> = ReturnType<typeof createReplicaEngine<TParams, TData, TFetched>>;
|
|
700
|
+
/** The part of a slice entity propagation needs (structural, so any slice fits). */
|
|
701
|
+
interface ReplicaEntityTarget {
|
|
702
|
+
beginEntityOptimistic: <TItem>(id: string, fn: (item: TItem) => TItem | undefined) => ReplicaOptimisticToken<any>[];
|
|
703
|
+
/** Wipe the resource in the active scope (memory + every persisted row). */
|
|
704
|
+
clear?: () => Promise<boolean>;
|
|
705
|
+
/** Patch persisted rows whose entry is not loaded in memory. */
|
|
706
|
+
patchStoredEntity: <TItem>(id: string, fn: (item: TItem) => TItem | undefined) => Promise<void>;
|
|
707
|
+
/**
|
|
708
|
+
* Explicit entity removal that also guards later hydrates. Optional for
|
|
709
|
+
* custom targets; `linkReplicaEntity` falls back to `updateEntity`.
|
|
710
|
+
*/
|
|
711
|
+
removeEntity?: (id: string, options?: {
|
|
712
|
+
persist?: boolean;
|
|
713
|
+
scope?: string;
|
|
714
|
+
}) => void;
|
|
715
|
+
revalidate: (key?: string) => Promise<unknown>;
|
|
716
|
+
updateEntity: <TItem>(id: string, fn: (item: TItem) => TItem | undefined, options?: {
|
|
717
|
+
persist?: boolean;
|
|
718
|
+
}) => void;
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Link resources that hold copies of the same entity (e.g. a topic in the
|
|
722
|
+
* sidebar list, the management page and the detail cache).
|
|
723
|
+
*
|
|
724
|
+
* Each resource keeps exactly one store location and decides how the entity
|
|
725
|
+
* maps into its own shape (paged resources by `getId`, single-entity ones by
|
|
726
|
+
* their entity adapter). The link only fans an entity-level change out to the
|
|
727
|
+
* resources that currently hold it — no global event bus, no shared owner.
|
|
728
|
+
*/
|
|
729
|
+
declare const linkReplicaEntity: <TItem>(targets: ReplicaEntityTarget[]) => {
|
|
730
|
+
clear: () => Promise<boolean>;
|
|
731
|
+
optimistic: <TResult>(id: string, fn: ((item: TItem) => TItem) | 'remove', serverCall: () => Promise<TResult>) => Promise<TResult>;
|
|
732
|
+
remove: (id: string, options?: {
|
|
733
|
+
persist?: boolean;
|
|
734
|
+
scope?: string;
|
|
735
|
+
}) => void;
|
|
736
|
+
revalidate: () => Promise<unknown[]>;
|
|
737
|
+
update: (id: string, fn: (item: TItem) => TItem, options?: {
|
|
738
|
+
persist?: boolean;
|
|
739
|
+
}) => void;
|
|
740
|
+
};
|
|
741
|
+
/**
|
|
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}.
|
|
755
|
+
*/
|
|
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;
|
|
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>;
|
|
776
|
+
/**
|
|
777
|
+
* The entry storage keys this resource has a persisted row for in the active
|
|
778
|
+
* scope, oldest first.
|
|
779
|
+
*
|
|
780
|
+
* Storage has no key listing of its own; the engine keeps one index row per
|
|
781
|
+
* scope (`REPLICA_INDEX_KEY`) as it persists and drops entries, and this reads
|
|
782
|
+
* it. That is how code outside the owning store reaches rows an *earlier
|
|
783
|
+
* session* wrote — bounding how many survive a reload, or invalidating a row
|
|
784
|
+
* whose entry is not loaded in memory.
|
|
785
|
+
*
|
|
786
|
+
* The keys are storage keys (`key`, plus `?query` when the resource sets one),
|
|
787
|
+
* so a caller only matches them against entry keys when the resource leaves
|
|
788
|
+
* `query` unset.
|
|
789
|
+
*/
|
|
790
|
+
declare const readReplicaStoredKeys: (resource: Pick<ReplicaResource<any, any, any>, 'scope' | 'storage'>) => Promise<string[]>;
|
|
791
|
+
/**
|
|
792
|
+
* An async key-value store a replica can persist into: an IndexedDB object
|
|
793
|
+
* store, a SQLite table behind IPC, a `Map` in tests. Values are stored as
|
|
794
|
+
* given (structured clone), so the store decides what it can hold.
|
|
795
|
+
*/
|
|
796
|
+
interface ReplicaKeyValueStore {
|
|
797
|
+
/** Rejects when the delete did not land. A missing key is a success. */
|
|
798
|
+
delete: (key: string) => Promise<void>;
|
|
799
|
+
/** Rejects when the read failed; resolves `undefined` for a missing key. */
|
|
800
|
+
get: (key: string) => Promise<unknown>;
|
|
801
|
+
/** Opens the store; awaited before every operation. */
|
|
802
|
+
initialize?: () => Promise<void>;
|
|
803
|
+
/** Rejects when the write did not land. */
|
|
804
|
+
set: (key: string, value: unknown) => Promise<void>;
|
|
805
|
+
}
|
|
806
|
+
interface KeyValueReplicaStorageOptions {
|
|
807
|
+
/** Prefix of every row of this storage, usually the resource name. */
|
|
808
|
+
namespace: string;
|
|
809
|
+
store: ReplicaKeyValueStore;
|
|
810
|
+
}
|
|
811
|
+
/** Store key of one row: `<namespace>:<scope>:<queryKey>`, each part URI-encoded but the namespace. */
|
|
812
|
+
declare const replicaRowStoreKey: (namespace: string, { queryKey, scope }: ReplicaRowKey) => string;
|
|
813
|
+
/** {@link ReplicaStorage} over an async key-value store, one record per row. */
|
|
814
|
+
declare class KeyValueReplicaStorage<T> implements ReplicaStorage<T> {
|
|
815
|
+
#private;
|
|
816
|
+
constructor({ namespace, store }: KeyValueReplicaStorageOptions);
|
|
817
|
+
get: (key: ReplicaRowKey) => Promise<ReplicaRow<T> | undefined>;
|
|
818
|
+
/** Like `get`, but rejects when the row could not be read (see `ReplicaStorage.getStrict`). */
|
|
819
|
+
getStrict: (key: ReplicaRowKey) => Promise<ReplicaRow<T> | undefined>;
|
|
820
|
+
/**
|
|
821
|
+
* Rejects when the delete did not land, unlike reads: a caller that removes a
|
|
822
|
+
* row the user deleted must be able to retry it.
|
|
823
|
+
*/
|
|
824
|
+
remove: (key: ReplicaRowKey) => Promise<void>;
|
|
825
|
+
/**
|
|
826
|
+
* Rejects when the write did not land (see `ReplicaStorage.set`): the replica
|
|
827
|
+
* engine only treats a row as holding a value after a confirmed write, so a
|
|
828
|
+
* dropped write is retried by the next persist instead of being skipped as
|
|
829
|
+
* redundant.
|
|
830
|
+
*/
|
|
831
|
+
set: (key: ReplicaRowKey, row: ReplicaRow<T>) => Promise<void>;
|
|
832
|
+
}
|
|
833
|
+
/** Turns a row into the string `localStorage` holds, and back. */
|
|
834
|
+
interface ReplicaRowCodec<T> {
|
|
835
|
+
parse: (value: string) => ReplicaRow<T>;
|
|
836
|
+
stringify: (row: ReplicaRow<T>) => string;
|
|
837
|
+
}
|
|
838
|
+
interface LocalStorageReplicaStorageOptions<T> {
|
|
839
|
+
/** Defaults to JSON. Pass a richer codec (e.g. superjson) for `Date`, `Map` … */
|
|
840
|
+
codec?: ReplicaRowCodec<T>;
|
|
841
|
+
/** Prefix of every row of this storage, usually the resource name. */
|
|
842
|
+
namespace: string;
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* {@link ReplicaStorage} over `window.localStorage`. Small values only (the
|
|
846
|
+
* origin shares a ~5 MB quota), but the read is synchronous: `getSync` restores
|
|
847
|
+
* a row inside pre-paint initialization without yielding a frame. Outside a
|
|
848
|
+
* browser every operation is a no-op miss.
|
|
849
|
+
*/
|
|
850
|
+
declare class LocalStorageReplicaStorage<T> implements ReplicaStorage<T> {
|
|
851
|
+
#private;
|
|
852
|
+
constructor({ codec, namespace }: LocalStorageReplicaStorageOptions<T>);
|
|
853
|
+
get: (key: ReplicaRowKey) => Promise<ReplicaRow<T> | undefined>;
|
|
854
|
+
/** Restore a row inside pre-paint initialization without yielding a frame. */
|
|
855
|
+
getSync: (key: ReplicaRowKey) => ReplicaRow<T> | undefined;
|
|
856
|
+
/** Like `get`, but rejects when the row could not be read or parsed (see `ReplicaStorage.getStrict`). */
|
|
857
|
+
getStrict: (key: ReplicaRowKey) => Promise<ReplicaRow<T> | undefined>;
|
|
858
|
+
/**
|
|
859
|
+
* Rejects when the delete did not land (e.g. storage access denied), unlike
|
|
860
|
+
* reads: a caller removing a row the user deleted must be able to retry it.
|
|
861
|
+
*/
|
|
862
|
+
remove: (key: ReplicaRowKey) => Promise<void>;
|
|
863
|
+
/**
|
|
864
|
+
* Rejects when the write did not land (a quota error, see `ReplicaStorage.set`):
|
|
865
|
+
* the replica engine only treats a row as holding a value after a confirmed
|
|
866
|
+
* write.
|
|
867
|
+
*/
|
|
868
|
+
set: (key: ReplicaRowKey, row: ReplicaRow<T>) => Promise<void>;
|
|
869
|
+
}
|
|
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
|