local-replica 0.0.0 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1 +1,2 @@
1
- export {}
1
+ import { $ as DefinePagedReplicaOptions, A as ReplicaEngineOptions, B as replicaReducer, C as joinPages, D as ReplicaEngine, E as toPersistedPage, F as ReplicaAction, G as ReplicaState, H as ReplicaPendingMutation, I as ReplicaEffect, J as stableQueryKey, K as isReplicaStorageKeyOf, L as ReplicaTransition, M as ReplicaOptimisticToken, N as ReplicaStorePort, O as createReplicaEngine, P as ReplicaEntityAdapter, Q as replicaKeys, R as ReplicaViewWrite, S as insertHeadItems, T as pagingFor, U as ReplicaSeedPatch, V as ReplicaEntryMeta, W as ReplicaSource, X as REPLICA_KEY_PREFIX, Y as staticReplicaScope, Z as isReplicaSyncKey, _ as applyHeadPage, a as KeyValueReplicaStorageOptions, at as ReplicaScope, b as getNextPageCursor, c as settleWithin, ct as ReplicaPagedData, d as ReplicaWriteQueue, dt as ReplicaPagingDirection, et as DefineReplicaOptions, f as readReplicaRowStrict, ft as ReplicaPagingMode, g as singleEntity, h as arrayEntity, ht as ReplicaStorage, i as KeyValueReplicaStorage, it as ReplicaResource, j as ReplicaHeadOutcome, k as OptimisticMutationOptions, l as readReplicaStoredKeys, lt as ReplicaPagingConfig, m as linkReplicaEntity, mt as ReplicaRowKey, n as LocalStorageReplicaStorageOptions, nt as definePagedReplica, o as ReplicaKeyValueStore, ot as ReplicaPageInfo, p as ReplicaEntityTarget, pt as ReplicaRow, q as replicaStorageKey, r as ReplicaRowCodec, rt as defineReplica, s as replicaRowStoreKey, st as ReplicaPageResult, t as LocalStorageReplicaStorage, tt as ReplicaStorageFactory, u as REPLICA_INDEX_KEY, ut as ReplicaPagingContext, v as applyNextPage, w as mapPagedItem, x as hasPagedItem, y as collapseToHead, z as createReplicaState } from "./index-DsElitHD.js";
2
+ export { DefinePagedReplicaOptions, DefineReplicaOptions, KeyValueReplicaStorage, KeyValueReplicaStorageOptions, LocalStorageReplicaStorage, LocalStorageReplicaStorageOptions, type OptimisticMutationOptions, REPLICA_INDEX_KEY, REPLICA_KEY_PREFIX, ReplicaAction, ReplicaEffect, ReplicaEngine, type ReplicaEngineOptions, type ReplicaEntityAdapter, ReplicaEntityTarget, type ReplicaEntryMeta, type ReplicaHeadOutcome, ReplicaKeyValueStore, type ReplicaOptimisticToken, type ReplicaPageInfo, type ReplicaPageResult, type ReplicaPagedData, type ReplicaPagingConfig, type ReplicaPagingContext, type ReplicaPagingDirection, type ReplicaPagingMode, type ReplicaPendingMutation, type ReplicaResource, type ReplicaRow, ReplicaRowCodec, type ReplicaRowKey, type ReplicaScope, type ReplicaSeedPatch, type ReplicaSource, type ReplicaState, type ReplicaStorage, ReplicaStorageFactory, type ReplicaStorePort, ReplicaTransition, ReplicaViewWrite, ReplicaWriteQueue, applyHeadPage, applyNextPage, arrayEntity, collapseToHead, createReplicaEngine, createReplicaState, definePagedReplica, defineReplica, getNextPageCursor, hasPagedItem, insertHeadItems, isReplicaStorageKeyOf, isReplicaSyncKey, joinPages, linkReplicaEntity, mapPagedItem, pagingFor, readReplicaRowStrict, readReplicaStoredKeys, replicaKeys, replicaReducer, replicaRowStoreKey, replicaStorageKey, settleWithin, singleEntity, stableQueryKey, staticReplicaScope, toPersistedPage };
package/dist/index.js CHANGED
@@ -0,0 +1,281 @@
1
+ import { S as stableQueryKey, _ as REPLICA_KEY_PREFIX, a as ReplicaWriteQueue, b as isReplicaStorageKeyOf, c as applyNextPage, d as hasPagedItem, f as insertHeadItems, g as toPersistedPage, h as pagingFor, i as REPLICA_INDEX_KEY, l as collapseToHead, m as mapPagedItem, n as createReplicaState, o as readReplicaRowStrict, p as joinPages, r as replicaReducer, s as applyHeadPage, t as createReplicaEngine, u as getNextPageCursor, v as isReplicaSyncKey, x as replicaStorageKey, y as replicaKeys } from "./createReplicaEngine-CdR-URHo.js";
2
+ // src/core/resource/scope.ts
3
+ /**
4
+ * A fixed identity partition. The default for apps without accounts; an app
5
+ * with sign-in passes a scope derived from the signed-in user / workspace so
6
+ * one browser profile never mixes two identities' rows.
7
+ */
8
+ const staticReplicaScope = (scope = "default") => ({
9
+ canPersist: () => true,
10
+ get: () => scope,
11
+ use: () => scope
12
+ });
13
+ // src/core/resource/defineReplica.ts
14
+ const DEFAULT_SCOPE = staticReplicaScope();
15
+ const defineReplica = (options) => {
16
+ const namespace = `replica:${options.name}:v${options.version}`;
17
+ const storage = typeof options.storage === "function" ? options.storage(namespace) : options.storage;
18
+ return {
19
+ fetcher: options.fetcher,
20
+ key: options.key,
21
+ maxPersistedEntries: options.persist?.maxEntries,
22
+ name: options.name,
23
+ namespace,
24
+ paging: options.paging,
25
+ persistKey: options.persistKey ?? (() => true),
26
+ persisted: !!storage,
27
+ query: (params) => options.query ? stableQueryKey(options.query(params)) : void 0,
28
+ storageKey: (params) => replicaStorageKey(options.key(params), options.query ? stableQueryKey(options.query(params)) : void 0),
29
+ scope: options.scope ?? DEFAULT_SCOPE,
30
+ syncKey: options.syncKey,
31
+ storage,
32
+ version: options.version
33
+ };
34
+ };
35
+ /**
36
+ * A resource whose value is a paged list ({@link ReplicaPagedData}). The
37
+ * store view type may extend the paged shape with domain fields.
38
+ */
39
+ const definePagedReplica = (options) => defineReplica({
40
+ ...options,
41
+ fetcher: options.fetchPage,
42
+ storage: options.storage
43
+ });
44
+ // src/core/entity/adapters.ts
45
+ /**
46
+ * The value IS one entity, or wraps one (`{ project, members }`). Deleting the
47
+ * entity drops the whole value.
48
+ */
49
+ const singleEntity = (getId, lens) => {
50
+ const pick = (data) => lens ? lens.get(data) : data;
51
+ return {
52
+ has: (data, id) => getId(pick(data)) === id,
53
+ map: (data, _id, fn) => {
54
+ const current = pick(data);
55
+ const next = fn(current);
56
+ if (next === void 0) return void 0;
57
+ if (next === current) return data;
58
+ return lens ? lens.set(data, next) : next;
59
+ }
60
+ };
61
+ };
62
+ /** The value is a plain list of entities; deleting one filters it out. */
63
+ const arrayEntity = (getId) => ({
64
+ has: (data, id) => data.some((item) => getId(item) === id),
65
+ map: (data, id, fn) => {
66
+ let changed = false;
67
+ const next = [];
68
+ for (const item of data) {
69
+ if (getId(item) !== id) {
70
+ next.push(item);
71
+ continue;
72
+ }
73
+ const mapped = fn(item);
74
+ if (mapped !== item) changed = true;
75
+ if (mapped !== void 0) next.push(mapped);
76
+ }
77
+ return changed ? next : data;
78
+ }
79
+ });
80
+ // src/core/entity/link.ts
81
+ /**
82
+ * Link resources that hold copies of the same entity (e.g. a topic in the
83
+ * sidebar list, the management page and the detail cache).
84
+ *
85
+ * Each resource keeps exactly one store location and decides how the entity
86
+ * maps into its own shape (paged resources by `getId`, single-entity ones by
87
+ * their entity adapter). The link only fans an entity-level change out to the
88
+ * resources that currently hold it — no global event bus, no shared owner.
89
+ */
90
+ const linkReplicaEntity = (targets) => {
91
+ /**
92
+ * Confirmed patch everywhere the entity is loaded, and (when persisted, the
93
+ * default) in persisted rows of entries that are not loaded.
94
+ */
95
+ const update = (id, fn, options) => {
96
+ for (const target of targets) target.updateEntity(id, fn, options);
97
+ };
98
+ /**
99
+ * Confirmed removal everywhere (list rows dropped, single-entity values
100
+ * removed). A value hydrated later — even one whose read was already in
101
+ * flight — comes back without the entity. With `scope` set to a scope left
102
+ * behind, only that scope's persisted rows lose the entity; the view on
103
+ * screen belongs to another scope and is left alone.
104
+ */
105
+ const remove = (id, options) => {
106
+ for (const target of targets) if (target.removeEntity) target.removeEntity(id, options);
107
+ else if (!options?.scope) target.updateEntity(id, () => void 0, options);
108
+ };
109
+ /**
110
+ * Wipe every linked resource in the active scope (e.g. "delete all"): memory,
111
+ * every persisted row and reads in flight. Resolves `true` once every
112
+ * persisted row is gone.
113
+ */
114
+ const clear = async () => {
115
+ return (await Promise.all(targets.map((target) => target.clear?.() ?? true))).every(Boolean);
116
+ };
117
+ /**
118
+ * One server call, one overlay per resource holding the entity: all commit
119
+ * together or all roll back together (then rethrow). Pass `'remove'` for an
120
+ * optimistic delete.
121
+ */
122
+ const optimistic = async (id, fn, serverCall) => {
123
+ const apply = fn === "remove" ? () => void 0 : fn;
124
+ const tokens = targets.flatMap((target) => target.beginEntityOptimistic(id, apply));
125
+ try {
126
+ const result = await serverCall();
127
+ for (const token of tokens) token.commit();
128
+ if (fn === "remove") remove(id);
129
+ else for (const target of targets) target.patchStoredEntity(id, fn);
130
+ return result;
131
+ } catch (error) {
132
+ for (const token of tokens) token.rollback();
133
+ throw error;
134
+ }
135
+ };
136
+ const revalidate = () => Promise.all(targets.map((target) => target.revalidate()));
137
+ return {
138
+ clear,
139
+ optimistic,
140
+ remove,
141
+ revalidate,
142
+ update
143
+ };
144
+ };
145
+ // src/core/persistence/storedKeys.ts
146
+ /**
147
+ * The entry storage keys this resource has a persisted row for in the active
148
+ * scope, oldest first.
149
+ *
150
+ * Storage has no key listing of its own; the engine keeps one index row per
151
+ * scope (`REPLICA_INDEX_KEY`) as it persists and drops entries, and this reads
152
+ * it. That is how code outside the owning store reaches rows an *earlier
153
+ * session* wrote — bounding how many survive a reload, or invalidating a row
154
+ * whose entry is not loaded in memory.
155
+ *
156
+ * The keys are storage keys (`key`, plus `?query` when the resource sets one),
157
+ * so a caller only matches them against entry keys when the resource leaves
158
+ * `query` unset.
159
+ */
160
+ const readReplicaStoredKeys = async (resource) => {
161
+ return (await resource.storage?.get({
162
+ queryKey: "__replica:index",
163
+ scope: resource.scope.get()
164
+ }))?.data ?? [];
165
+ };
166
+ // src/core/utils/settleWithin.ts
167
+ /**
168
+ * Resolve once `promise` settles or `ms` elapses — never rejects, never hangs.
169
+ *
170
+ * Bounds a pre-paint hydrate in a route loader: the persisted read is usually
171
+ * sub-millisecond, and the deadline only fires on a pathological storage stall,
172
+ * so a route transition never waits longer than `ms` for it.
173
+ */
174
+ const settleWithin = (promise, ms) => new Promise((resolve) => {
175
+ const finish = () => {
176
+ clearTimeout(timer);
177
+ resolve();
178
+ };
179
+ const timer = setTimeout(finish, ms);
180
+ Promise.resolve(promise).then(finish, finish);
181
+ });
182
+ // src/storage/keyValue.ts
183
+ /** Store key of one row: `<namespace>:<scope>:<queryKey>`, each part URI-encoded but the namespace. */
184
+ const replicaRowStoreKey = (namespace, { queryKey, scope }) => `${namespace}:${encodeURIComponent(scope)}:${encodeURIComponent(queryKey)}`;
185
+ /** {@link ReplicaStorage} over an async key-value store, one record per row. */
186
+ var KeyValueReplicaStorage = class {
187
+ #namespace;
188
+ #store;
189
+ constructor({ namespace, store }) {
190
+ this.#namespace = namespace;
191
+ this.#store = store;
192
+ }
193
+ #key = (key) => replicaRowStoreKey(this.#namespace, key);
194
+ get = async (key) => {
195
+ try {
196
+ return await this.getStrict(key);
197
+ } catch {
198
+ return;
199
+ }
200
+ };
201
+ /** Like `get`, but rejects when the row could not be read (see `ReplicaStorage.getStrict`). */
202
+ getStrict = async (key) => {
203
+ await this.#store.initialize?.();
204
+ return await this.#store.get(this.#key(key));
205
+ };
206
+ /**
207
+ * Rejects when the delete did not land, unlike reads: a caller that removes a
208
+ * row the user deleted must be able to retry it.
209
+ */
210
+ remove = async (key) => {
211
+ await this.#store.initialize?.();
212
+ await this.#store.delete(this.#key(key));
213
+ };
214
+ /**
215
+ * Rejects when the write did not land (see `ReplicaStorage.set`): the replica
216
+ * engine only treats a row as holding a value after a confirmed write, so a
217
+ * dropped write is retried by the next persist instead of being skipped as
218
+ * redundant.
219
+ */
220
+ set = async (key, row) => {
221
+ await this.#store.initialize?.();
222
+ await this.#store.set(this.#key(key), row);
223
+ };
224
+ };
225
+ // src/storage/localStorage.ts
226
+ const jsonCodec = {
227
+ parse: JSON.parse,
228
+ stringify: JSON.stringify
229
+ };
230
+ /**
231
+ * {@link ReplicaStorage} over `window.localStorage`. Small values only (the
232
+ * origin shares a ~5 MB quota), but the read is synchronous: `getSync` restores
233
+ * a row inside pre-paint initialization without yielding a frame. Outside a
234
+ * browser every operation is a no-op miss.
235
+ */
236
+ var LocalStorageReplicaStorage = class {
237
+ #codec;
238
+ #namespace;
239
+ constructor({ codec = jsonCodec, namespace }) {
240
+ this.#codec = codec;
241
+ this.#namespace = namespace;
242
+ }
243
+ #key = (key) => replicaRowStoreKey(this.#namespace, key);
244
+ get = async (key) => this.getSync(key);
245
+ /** Restore a row inside pre-paint initialization without yielding a frame. */
246
+ getSync = (key) => {
247
+ if (typeof window === "undefined") return void 0;
248
+ try {
249
+ const value = localStorage.getItem(this.#key(key));
250
+ return value ? this.#codec.parse(value) : void 0;
251
+ } catch {
252
+ return;
253
+ }
254
+ };
255
+ /** Like `get`, but rejects when the row could not be read or parsed (see `ReplicaStorage.getStrict`). */
256
+ getStrict = async (key) => {
257
+ if (typeof window === "undefined") return void 0;
258
+ const value = localStorage.getItem(this.#key(key));
259
+ return value ? this.#codec.parse(value) : void 0;
260
+ };
261
+ /**
262
+ * Rejects when the delete did not land (e.g. storage access denied), unlike
263
+ * reads: a caller removing a row the user deleted must be able to retry it.
264
+ */
265
+ remove = async (key) => {
266
+ if (typeof window === "undefined") return;
267
+ localStorage.removeItem(this.#key(key));
268
+ };
269
+ /**
270
+ * Rejects when the write did not land (a quota error, see `ReplicaStorage.set`):
271
+ * the replica engine only treats a row as holding a value after a confirmed
272
+ * write.
273
+ */
274
+ set = async (key, row) => {
275
+ if (typeof window === "undefined") return;
276
+ localStorage.setItem(this.#key(key), this.#codec.stringify(row));
277
+ };
278
+ };
279
+ export { KeyValueReplicaStorage, LocalStorageReplicaStorage, REPLICA_INDEX_KEY, REPLICA_KEY_PREFIX, ReplicaWriteQueue, applyHeadPage, applyNextPage, arrayEntity, collapseToHead, createReplicaEngine, createReplicaState, definePagedReplica, defineReplica, getNextPageCursor, hasPagedItem, insertHeadItems, isReplicaStorageKeyOf, isReplicaSyncKey, joinPages, linkReplicaEntity, mapPagedItem, pagingFor, readReplicaRowStrict, readReplicaStoredKeys, replicaKeys, replicaReducer, replicaRowStoreKey, replicaStorageKey, settleWithin, singleEntity, stableQueryKey, staticReplicaScope, toPersistedPage };
280
+
281
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/core/resource/scope.ts","../src/core/resource/defineReplica.ts","../src/core/entity/adapters.ts","../src/core/entity/link.ts","../src/core/persistence/storedKeys.ts","../src/core/utils/settleWithin.ts","../src/storage/keyValue.ts","../src/storage/localStorage.ts"],"sourcesContent":["import type { ReplicaScope } from './types';\n\n/**\n * A fixed identity partition. The default for apps without accounts; an app\n * with sign-in passes a scope derived from the signed-in user / workspace so\n * one browser profile never mixes two identities' rows.\n */\nexport const staticReplicaScope = (scope = 'default'): ReplicaScope => ({\n canPersist: () => true,\n get: () => scope,\n use: () => scope,\n});\n","import type { ReplicaStorage } from '../../storage/types';\nimport type { ReplicaPagedData, ReplicaPageResult, ReplicaPagingConfig } from '../paging/types';\nimport { staticReplicaScope } from './scope';\nimport { replicaStorageKey, stableQueryKey } from './storageKey';\nimport type { ReplicaResource, ReplicaScope } from './types';\n\nconst DEFAULT_SCOPE = staticReplicaScope();\n\n/** Builds the storage of one resource from its namespace (`replica:<name>:v<version>`). */\nexport type ReplicaStorageFactory<TData> = (namespace: string) => ReplicaStorage<TData> | undefined;\n\nexport interface DefineReplicaOptions<TParams, TData, TFetched = TData, TCursor = any> {\n /** Default network fetcher; a store binding may override it. */\n fetcher?: (params: TParams, cursor?: TCursor) => Promise<TFetched>;\n /** Entry identity inside one scope (memory bucket + persisted row). */\n key: (params: TParams) => string;\n name: string;\n paging?: ReplicaPagingConfig<any, TCursor>;\n /**\n * Bound what the resource keeps in storage. `maxEntries` caps the persisted\n * rows (storage keys: one per entry key and query) per scope; past it the\n * least recently used rows are evicted — their persisted row, index entry\n * and, when nothing holds it, their memory entry. Use rows (a hydrate or a\n * persist, including a skipped unchanged one) count as recent, and the order\n * lives in the persisted index, so it survives reloads. A row on screen (a\n * mounted `useSync`, see `retain`), with pending optimistic writes or a\n * `loadMore` in flight is never evicted. Unbounded by default.\n */\n persist?: { maxEntries?: number };\n /**\n * Entries that never touch storage (no hydrate, no persist) — e.g. scoped\n * buckets whose rows must not land under an ordinary entry. Defaults to all.\n */\n persistKey?: (key: string) => boolean;\n /**\n * Query identity beyond `key` (filters, sort, page size) — anything that\n * changes the rows without changing the key. Persisted rows are kept per\n * query, so a projection taken under other filters never hydrates; in memory\n * a query change resets loaded pages.\n */\n query?: (params: TParams) => unknown;\n /** Identity partition of memory and persisted rows. Defaults to one static scope. */\n scope?: ReplicaScope;\n /** Persisted copy of the replica. Omit for a memory-only replica (no first-frame paint). */\n storage?: ReplicaStorage<TData> | ReplicaStorageFactory<TData>;\n /**\n * Query-cache key of the network sync, for a resource adopting an existing\n * key that other code already revalidates by. Defaults to `replicaKeys.sync`.\n * With a custom key, revalidate through that key — `slice.revalidate` only\n * matches default keys.\n */\n syncKey?: (params: TParams) => readonly unknown[];\n /** Bump to invalidate every persisted row written by older shapes. */\n version: number;\n}\n\nexport const defineReplica = <TParams, TData, TFetched = TData, TCursor = any>(\n options: DefineReplicaOptions<TParams, TData, TFetched, TCursor>,\n): ReplicaResource<TParams, TData, TFetched, TCursor> => {\n const namespace = `replica:${options.name}:v${options.version}`;\n const storage =\n typeof options.storage === 'function' ? options.storage(namespace) : options.storage;\n\n return {\n fetcher: options.fetcher,\n key: options.key,\n maxPersistedEntries: options.persist?.maxEntries,\n name: options.name,\n namespace,\n paging: options.paging,\n persistKey: options.persistKey ?? (() => true),\n persisted: !!storage,\n query: (params) => (options.query ? stableQueryKey(options.query(params)) : undefined),\n storageKey: (params) =>\n replicaStorageKey(\n options.key(params),\n options.query ? stableQueryKey(options.query(params)) : undefined,\n ),\n scope: options.scope ?? DEFAULT_SCOPE,\n syncKey: options.syncKey,\n storage,\n version: options.version,\n };\n};\n\nexport interface DefinePagedReplicaOptions<TParams, TItem, TCursor> extends Omit<\n DefineReplicaOptions<\n TParams,\n ReplicaPagedData<TItem, TCursor>,\n ReplicaPageResult<TItem, TCursor>\n >,\n 'fetcher' | 'paging' | 'storage'\n> {\n /** `cursor` is `undefined` for the head page. */\n fetchPage?: (\n params: TParams,\n cursor: TCursor | undefined,\n ) => Promise<ReplicaPageResult<TItem, TCursor>>;\n paging: ReplicaPagingConfig<TItem, TCursor>;\n storage?: ReplicaStorage<any> | ReplicaStorageFactory<any>;\n}\n\n/**\n * A resource whose value is a paged list ({@link ReplicaPagedData}). The\n * store view type may extend the paged shape with domain fields.\n */\nexport const definePagedReplica = <\n TParams,\n TItem,\n TCursor = number,\n TData extends ReplicaPagedData<TItem, TCursor> = ReplicaPagedData<TItem, TCursor>,\n>(\n options: DefinePagedReplicaOptions<TParams, TItem, TCursor>,\n): ReplicaResource<TParams, TData, ReplicaPageResult<TItem, TCursor>, TCursor> =>\n defineReplica<TParams, TData, ReplicaPageResult<TItem, TCursor>, TCursor>({\n ...options,\n fetcher: options.fetchPage,\n storage: options.storage as ReplicaStorage<TData> | ReplicaStorageFactory<TData> | undefined,\n });\n","import type { ReplicaEntityAdapter } from './types';\n\n/**\n * The value IS one entity, or wraps one (`{ project, members }`). Deleting the\n * entity drops the whole value.\n */\nexport const singleEntity = <TData, TItem = TData>(\n getId: (item: TItem) => string,\n lens?: { get: (data: TData) => TItem; set: (data: TData, item: TItem) => TData },\n): ReplicaEntityAdapter<TData, TItem> => {\n const pick = (data: TData) => (lens ? lens.get(data) : (data as unknown as TItem));\n return {\n has: (data, id) => getId(pick(data)) === id,\n map: (data, _id, fn) => {\n const current = pick(data);\n const next = fn(current);\n if (next === undefined) return undefined;\n if (next === current) return data;\n return lens ? lens.set(data, next) : (next as unknown as TData);\n },\n };\n};\n\n/** The value is a plain list of entities; deleting one filters it out. */\nexport const arrayEntity = <TItem>(\n getId: (item: TItem) => string,\n): ReplicaEntityAdapter<TItem[], TItem> => ({\n has: (data, id) => data.some((item) => getId(item) === id),\n map: (data, id, fn) => {\n let changed = false;\n const next: TItem[] = [];\n for (const item of data) {\n if (getId(item) !== id) {\n next.push(item);\n continue;\n }\n const mapped = fn(item);\n if (mapped !== item) changed = true;\n if (mapped !== undefined) next.push(mapped);\n }\n return changed ? next : data;\n },\n});\n","import type { ReplicaOptimisticToken } from '../engine/types';\n\n/** The part of a slice entity propagation needs (structural, so any slice fits). */\nexport interface ReplicaEntityTarget {\n beginEntityOptimistic: <TItem>(\n id: string,\n fn: (item: TItem) => TItem | undefined,\n ) => ReplicaOptimisticToken<any>[];\n /** Wipe the resource in the active scope (memory + every persisted row). */\n clear?: () => Promise<boolean>;\n /** Patch persisted rows whose entry is not loaded in memory. */\n patchStoredEntity: <TItem>(id: string, fn: (item: TItem) => TItem | undefined) => Promise<void>;\n /**\n * Explicit entity removal that also guards later hydrates. Optional for\n * custom targets; `linkReplicaEntity` falls back to `updateEntity`.\n */\n removeEntity?: (id: string, options?: { persist?: boolean; scope?: string }) => void;\n revalidate: (key?: string) => Promise<unknown>;\n updateEntity: <TItem>(\n id: string,\n fn: (item: TItem) => TItem | undefined,\n options?: { persist?: boolean },\n ) => void;\n}\n\n/**\n * Link resources that hold copies of the same entity (e.g. a topic in the\n * sidebar list, the management page and the detail cache).\n *\n * Each resource keeps exactly one store location and decides how the entity\n * maps into its own shape (paged resources by `getId`, single-entity ones by\n * their entity adapter). The link only fans an entity-level change out to the\n * resources that currently hold it — no global event bus, no shared owner.\n */\nexport const linkReplicaEntity = <TItem>(targets: ReplicaEntityTarget[]) => {\n /**\n * Confirmed patch everywhere the entity is loaded, and (when persisted, the\n * default) in persisted rows of entries that are not loaded.\n */\n const update = (\n id: string,\n fn: (item: TItem) => TItem,\n options?: { persist?: boolean },\n ): void => {\n for (const target of targets) target.updateEntity<TItem>(id, fn, options);\n };\n\n /**\n * Confirmed removal everywhere (list rows dropped, single-entity values\n * removed). A value hydrated later — even one whose read was already in\n * flight — comes back without the entity. With `scope` set to a scope left\n * behind, only that scope's persisted rows lose the entity; the view on\n * screen belongs to another scope and is left alone.\n */\n const remove = (id: string, options?: { persist?: boolean; scope?: string }): void => {\n for (const target of targets) {\n if (target.removeEntity) target.removeEntity(id, options);\n else if (!options?.scope) target.updateEntity<TItem>(id, () => undefined, options);\n }\n };\n\n /**\n * Wipe every linked resource in the active scope (e.g. \"delete all\"): memory,\n * every persisted row and reads in flight. Resolves `true` once every\n * persisted row is gone.\n */\n const clear = async (): Promise<boolean> => {\n const purged = await Promise.all(targets.map((target) => target.clear?.() ?? true));\n return purged.every(Boolean);\n };\n\n /**\n * One server call, one overlay per resource holding the entity: all commit\n * together or all roll back together (then rethrow). Pass `'remove'` for an\n * optimistic delete.\n */\n const optimistic = async <TResult>(\n id: string,\n fn: ((item: TItem) => TItem) | 'remove',\n serverCall: () => Promise<TResult>,\n ): Promise<TResult> => {\n const apply = fn === 'remove' ? () => undefined : fn;\n const tokens = targets.flatMap((target) => target.beginEntityOptimistic<TItem>(id, apply));\n try {\n const result = await serverCall();\n for (const token of tokens) token.commit();\n // `remove` / `update` also patch persisted rows that are not loaded.\n if (fn === 'remove') remove(id);\n else for (const target of targets) void target.patchStoredEntity<TItem>(id, fn);\n return result;\n } catch (error) {\n for (const token of tokens) token.rollback();\n throw error;\n }\n };\n\n const revalidate = () => Promise.all(targets.map((target) => target.revalidate()));\n\n return { clear, optimistic, remove, revalidate, update };\n};\n","import type { ReplicaResource } from '../resource/types';\nimport { REPLICA_INDEX_KEY } from './persistedRows';\n\n/**\n * The entry storage keys this resource has a persisted row for in the active\n * scope, oldest first.\n *\n * Storage has no key listing of its own; the engine keeps one index row per\n * scope (`REPLICA_INDEX_KEY`) as it persists and drops entries, and this reads\n * it. That is how code outside the owning store reaches rows an *earlier\n * session* wrote — bounding how many survive a reload, or invalidating a row\n * whose entry is not loaded in memory.\n *\n * The keys are storage keys (`key`, plus `?query` when the resource sets one),\n * so a caller only matches them against entry keys when the resource leaves\n * `query` unset.\n */\nexport const readReplicaStoredKeys = async (\n resource: Pick<ReplicaResource<any, any, any>, 'scope' | 'storage'>,\n): Promise<string[]> => {\n const row = await resource.storage?.get({\n queryKey: REPLICA_INDEX_KEY,\n scope: resource.scope.get(),\n });\n return (row?.data as unknown as string[] | undefined) ?? [];\n};\n","/**\n * Resolve once `promise` settles or `ms` elapses — never rejects, never hangs.\n *\n * Bounds a pre-paint hydrate in a route loader: the persisted read is usually\n * sub-millisecond, and the deadline only fires on a pathological storage stall,\n * so a route transition never waits longer than `ms` for it.\n */\nexport const settleWithin = (promise: Promise<unknown>, ms: number): Promise<void> =>\n new Promise((resolve) => {\n const finish = () => {\n clearTimeout(timer);\n resolve();\n };\n const timer = setTimeout(finish, ms);\n void Promise.resolve(promise).then(finish, finish);\n });\n","import type { ReplicaRow, ReplicaRowKey, ReplicaStorage } from './types';\n\n/**\n * An async key-value store a replica can persist into: an IndexedDB object\n * store, a SQLite table behind IPC, a `Map` in tests. Values are stored as\n * given (structured clone), so the store decides what it can hold.\n */\nexport interface ReplicaKeyValueStore {\n /** Rejects when the delete did not land. A missing key is a success. */\n delete: (key: string) => Promise<void>;\n /** Rejects when the read failed; resolves `undefined` for a missing key. */\n get: (key: string) => Promise<unknown>;\n /** Opens the store; awaited before every operation. */\n initialize?: () => Promise<void>;\n /** Rejects when the write did not land. */\n set: (key: string, value: unknown) => Promise<void>;\n}\n\nexport interface KeyValueReplicaStorageOptions {\n /** Prefix of every row of this storage, usually the resource name. */\n namespace: string;\n store: ReplicaKeyValueStore;\n}\n\n/** Store key of one row: `<namespace>:<scope>:<queryKey>`, each part URI-encoded but the namespace. */\nexport const replicaRowStoreKey = (namespace: string, { queryKey, scope }: ReplicaRowKey) =>\n `${namespace}:${encodeURIComponent(scope)}:${encodeURIComponent(queryKey)}`;\n\n/** {@link ReplicaStorage} over an async key-value store, one record per row. */\nexport class KeyValueReplicaStorage<T> implements ReplicaStorage<T> {\n readonly #namespace: string;\n readonly #store: ReplicaKeyValueStore;\n\n constructor({ namespace, store }: KeyValueReplicaStorageOptions) {\n this.#namespace = namespace;\n this.#store = store;\n }\n\n #key = (key: ReplicaRowKey) => replicaRowStoreKey(this.#namespace, key);\n\n get = async (key: ReplicaRowKey): Promise<ReplicaRow<T> | undefined> => {\n try {\n return await this.getStrict(key);\n } catch {\n return undefined;\n }\n };\n\n /** Like `get`, but rejects when the row could not be read (see `ReplicaStorage.getStrict`). */\n getStrict = async (key: ReplicaRowKey): Promise<ReplicaRow<T> | undefined> => {\n await this.#store.initialize?.();\n return (await this.#store.get(this.#key(key))) as ReplicaRow<T> | undefined;\n };\n\n /**\n * Rejects when the delete did not land, unlike reads: a caller that removes a\n * row the user deleted must be able to retry it.\n */\n remove = async (key: ReplicaRowKey): Promise<void> => {\n await this.#store.initialize?.();\n await this.#store.delete(this.#key(key));\n };\n\n /**\n * Rejects when the write did not land (see `ReplicaStorage.set`): the replica\n * engine only treats a row as holding a value after a confirmed write, so a\n * dropped write is retried by the next persist instead of being skipped as\n * redundant.\n */\n set = async (key: ReplicaRowKey, row: ReplicaRow<T>): Promise<void> => {\n await this.#store.initialize?.();\n await this.#store.set(this.#key(key), row);\n };\n}\n","import { replicaRowStoreKey } from './keyValue';\nimport type { ReplicaRow, ReplicaRowKey, ReplicaStorage } from './types';\n\n/** Turns a row into the string `localStorage` holds, and back. */\nexport interface ReplicaRowCodec<T> {\n parse: (value: string) => ReplicaRow<T>;\n stringify: (row: ReplicaRow<T>) => string;\n}\n\nexport interface LocalStorageReplicaStorageOptions<T> {\n /** Defaults to JSON. Pass a richer codec (e.g. superjson) for `Date`, `Map` … */\n codec?: ReplicaRowCodec<T>;\n /** Prefix of every row of this storage, usually the resource name. */\n namespace: string;\n}\n\nconst jsonCodec: ReplicaRowCodec<any> = { parse: JSON.parse, stringify: JSON.stringify };\n\n/**\n * {@link ReplicaStorage} over `window.localStorage`. Small values only (the\n * origin shares a ~5 MB quota), but the read is synchronous: `getSync` restores\n * a row inside pre-paint initialization without yielding a frame. Outside a\n * browser every operation is a no-op miss.\n */\nexport class LocalStorageReplicaStorage<T> implements ReplicaStorage<T> {\n readonly #codec: ReplicaRowCodec<T>;\n readonly #namespace: string;\n\n constructor({ codec = jsonCodec, namespace }: LocalStorageReplicaStorageOptions<T>) {\n this.#codec = codec;\n this.#namespace = namespace;\n }\n\n #key = (key: ReplicaRowKey) => replicaRowStoreKey(this.#namespace, key);\n\n get = async (key: ReplicaRowKey): Promise<ReplicaRow<T> | undefined> => this.getSync(key);\n\n /** Restore a row inside pre-paint initialization without yielding a frame. */\n getSync = (key: ReplicaRowKey): ReplicaRow<T> | undefined => {\n if (typeof window === 'undefined') return undefined;\n\n try {\n const value = localStorage.getItem(this.#key(key));\n return value ? this.#codec.parse(value) : undefined;\n } catch {\n return undefined;\n }\n };\n\n /** Like `get`, but rejects when the row could not be read or parsed (see `ReplicaStorage.getStrict`). */\n getStrict = async (key: ReplicaRowKey): Promise<ReplicaRow<T> | undefined> => {\n if (typeof window === 'undefined') return undefined;\n\n const value = localStorage.getItem(this.#key(key));\n return value ? this.#codec.parse(value) : undefined;\n };\n\n /**\n * Rejects when the delete did not land (e.g. storage access denied), unlike\n * reads: a caller removing a row the user deleted must be able to retry it.\n */\n remove = async (key: ReplicaRowKey): Promise<void> => {\n if (typeof window === 'undefined') return;\n\n localStorage.removeItem(this.#key(key));\n };\n\n /**\n * Rejects when the write did not land (a quota error, see `ReplicaStorage.set`):\n * the replica engine only treats a row as holding a value after a confirmed\n * write.\n */\n set = async (key: ReplicaRowKey, row: ReplicaRow<T>): Promise<void> => {\n if (typeof window === 'undefined') return;\n\n localStorage.setItem(this.#key(key), this.#codec.stringify(row));\n };\n}\n"],"mappings":";;;;;;;AAOA,MAAa,sBAAsB,QAAQ,eAA6B;CACtE,kBAAkB;CAClB,WAAW;CACX,WAAW;AACb;;ACLA,MAAM,gBAAgB,mBAAmB;AAkDzC,MAAa,iBACX,YACuD;CACvD,MAAM,YAAY,WAAW,QAAQ,KAAK,IAAI,QAAQ;CACtD,MAAM,UACJ,OAAO,QAAQ,YAAY,aAAa,QAAQ,QAAQ,SAAS,IAAI,QAAQ;CAE/E,OAAO;EACL,SAAS,QAAQ;EACjB,KAAK,QAAQ;EACb,qBAAqB,QAAQ,SAAS;EACtC,MAAM,QAAQ;EACd;EACA,QAAQ,QAAQ;EAChB,YAAY,QAAQ,qBAAqB;EACzC,WAAW,CAAC,CAAC;EACb,QAAQ,WAAY,QAAQ,QAAQ,eAAe,QAAQ,MAAM,MAAM,CAAC,IAAI,KAAA;EAC5E,aAAa,WACX,kBACE,QAAQ,IAAI,MAAM,GAClB,QAAQ,QAAQ,eAAe,QAAQ,MAAM,MAAM,CAAC,IAAI,KAAA,CAC1D;EACF,OAAO,QAAQ,SAAS;EACxB,SAAS,QAAQ;EACjB;EACA,SAAS,QAAQ;CACnB;AACF;;;;;AAuBA,MAAa,sBAMX,YAEA,cAA0E;CACxE,GAAG;CACH,SAAS,QAAQ;CACjB,SAAS,QAAQ;AACnB,CAAC;;;;;;AChHH,MAAa,gBACX,OACA,SACuC;CACvC,MAAM,QAAQ,SAAiB,OAAO,KAAK,IAAI,IAAI,IAAK;CACxD,OAAO;EACL,MAAM,MAAM,OAAO,MAAM,KAAK,IAAI,CAAC,MAAM;EACzC,MAAM,MAAM,KAAK,OAAO;GACtB,MAAM,UAAU,KAAK,IAAI;GACzB,MAAM,OAAO,GAAG,OAAO;GACvB,IAAI,SAAS,KAAA,GAAW,OAAO,KAAA;GAC/B,IAAI,SAAS,SAAS,OAAO;GAC7B,OAAO,OAAO,KAAK,IAAI,MAAM,IAAI,IAAK;EACxC;CACF;AACF;;AAGA,MAAa,eACX,WAC0C;CAC1C,MAAM,MAAM,OAAO,KAAK,MAAM,SAAS,MAAM,IAAI,MAAM,EAAE;CACzD,MAAM,MAAM,IAAI,OAAO;EACrB,IAAI,UAAU;EACd,MAAM,OAAgB,CAAC;EACvB,KAAK,MAAM,QAAQ,MAAM;GACvB,IAAI,MAAM,IAAI,MAAM,IAAI;IACtB,KAAK,KAAK,IAAI;IACd;GACF;GACA,MAAM,SAAS,GAAG,IAAI;GACtB,IAAI,WAAW,MAAM,UAAU;GAC/B,IAAI,WAAW,KAAA,GAAW,KAAK,KAAK,MAAM;EAC5C;EACA,OAAO,UAAU,OAAO;CAC1B;AACF;;;;;;;;;;;ACRA,MAAa,qBAA4B,YAAmC;;;;;CAK1E,MAAM,UACJ,IACA,IACA,YACS;EACT,KAAK,MAAM,UAAU,SAAS,OAAO,aAAoB,IAAI,IAAI,OAAO;CAC1E;;;;;;;;CASA,MAAM,UAAU,IAAY,YAA0D;EACpF,KAAK,MAAM,UAAU,SACnB,IAAI,OAAO,cAAc,OAAO,aAAa,IAAI,OAAO;OACnD,IAAI,CAAC,SAAS,OAAO,OAAO,aAAoB,UAAU,KAAA,GAAW,OAAO;CAErF;;;;;;CAOA,MAAM,QAAQ,YAA8B;EAE1C,QAAO,MADc,QAAQ,IAAI,QAAQ,KAAK,WAAW,OAAO,QAAQ,KAAK,IAAI,CAAC,EAAA,CACpE,MAAM,OAAO;CAC7B;;;;;;CAOA,MAAM,aAAa,OACjB,IACA,IACA,eACqB;EACrB,MAAM,QAAQ,OAAO,iBAAiB,KAAA,IAAY;EAClD,MAAM,SAAS,QAAQ,SAAS,WAAW,OAAO,sBAA6B,IAAI,KAAK,CAAC;EACzF,IAAI;GACF,MAAM,SAAS,MAAM,WAAW;GAChC,KAAK,MAAM,SAAS,QAAQ,MAAM,OAAO;GAEzC,IAAI,OAAO,UAAU,OAAO,EAAE;QACzB,KAAK,MAAM,UAAU,SAAS,OAAY,kBAAyB,IAAI,EAAE;GAC9E,OAAO;EACT,SAAS,OAAO;GACd,KAAK,MAAM,SAAS,QAAQ,MAAM,SAAS;GAC3C,MAAM;EACR;CACF;CAEA,MAAM,mBAAmB,QAAQ,IAAI,QAAQ,KAAK,WAAW,OAAO,WAAW,CAAC,CAAC;CAEjF,OAAO;EAAE;EAAO;EAAY;EAAQ;EAAY;CAAO;AACzD;;;;;;;;;;;;;;;;AClFA,MAAa,wBAAwB,OACnC,aACsB;CAKtB,QAAQ,MAJU,SAAS,SAAS,IAAI;EACtC,UAAA;EACA,OAAO,SAAS,MAAM,IAAI;CAC5B,CAAC,EAAA,EACY,QAA4C,CAAC;AAC5D;;;;;;;;;AClBA,MAAa,gBAAgB,SAA2B,OACtD,IAAI,SAAS,YAAY;CACvB,MAAM,eAAe;EACnB,aAAa,KAAK;EAClB,QAAQ;CACV;CACA,MAAM,QAAQ,WAAW,QAAQ,EAAE;CACnC,QAAa,QAAQ,OAAO,CAAC,CAAC,KAAK,QAAQ,MAAM;AACnD,CAAC;;;ACUH,MAAa,sBAAsB,WAAmB,EAAE,UAAU,YAChE,GAAG,UAAU,GAAG,mBAAmB,KAAK,EAAE,GAAG,mBAAmB,QAAQ;;AAG1E,IAAa,yBAAb,MAAoE;CAClE;CACA;CAEA,YAAY,EAAE,WAAW,SAAwC;EAC/D,KAAK,aAAa;EAClB,KAAK,SAAS;CAChB;CAEA,QAAQ,QAAuB,mBAAmB,KAAK,YAAY,GAAG;CAEtE,MAAM,OAAO,QAA2D;EACtE,IAAI;GACF,OAAO,MAAM,KAAK,UAAU,GAAG;EACjC,QAAQ;GACN;EACF;CACF;;CAGA,YAAY,OAAO,QAA2D;EAC5E,MAAM,KAAK,OAAO,aAAa;EAC/B,OAAQ,MAAM,KAAK,OAAO,IAAI,KAAK,KAAK,GAAG,CAAC;CAC9C;;;;;CAMA,SAAS,OAAO,QAAsC;EACpD,MAAM,KAAK,OAAO,aAAa;EAC/B,MAAM,KAAK,OAAO,OAAO,KAAK,KAAK,GAAG,CAAC;CACzC;;;;;;;CAQA,MAAM,OAAO,KAAoB,QAAsC;EACrE,MAAM,KAAK,OAAO,aAAa;EAC/B,MAAM,KAAK,OAAO,IAAI,KAAK,KAAK,GAAG,GAAG,GAAG;CAC3C;AACF;;ACzDA,MAAM,YAAkC;CAAE,OAAO,KAAK;CAAO,WAAW,KAAK;AAAU;;;;;;;AAQvF,IAAa,6BAAb,MAAwE;CACtE;CACA;CAEA,YAAY,EAAE,QAAQ,WAAW,aAAmD;EAClF,KAAK,SAAS;EACd,KAAK,aAAa;CACpB;CAEA,QAAQ,QAAuB,mBAAmB,KAAK,YAAY,GAAG;CAEtE,MAAM,OAAO,QAA2D,KAAK,QAAQ,GAAG;;CAGxF,WAAW,QAAkD;EAC3D,IAAI,OAAO,WAAW,aAAa,OAAO,KAAA;EAE1C,IAAI;GACF,MAAM,QAAQ,aAAa,QAAQ,KAAK,KAAK,GAAG,CAAC;GACjD,OAAO,QAAQ,KAAK,OAAO,MAAM,KAAK,IAAI,KAAA;EAC5C,QAAQ;GACN;EACF;CACF;;CAGA,YAAY,OAAO,QAA2D;EAC5E,IAAI,OAAO,WAAW,aAAa,OAAO,KAAA;EAE1C,MAAM,QAAQ,aAAa,QAAQ,KAAK,KAAK,GAAG,CAAC;EACjD,OAAO,QAAQ,KAAK,OAAO,MAAM,KAAK,IAAI,KAAA;CAC5C;;;;;CAMA,SAAS,OAAO,QAAsC;EACpD,IAAI,OAAO,WAAW,aAAa;EAEnC,aAAa,WAAW,KAAK,KAAK,GAAG,CAAC;CACxC;;;;;;CAOA,MAAM,OAAO,KAAoB,QAAsC;EACrE,IAAI,OAAO,WAAW,aAAa;EAEnC,aAAa,QAAQ,KAAK,KAAK,GAAG,GAAG,KAAK,OAAO,UAAU,GAAG,CAAC;CACjE;AACF"}