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/dist/index.d.ts CHANGED
@@ -1 +1,2 @@
1
- export {}
1
+ import { $ as ReplicaPageInfo, A as ReplicaTransition, B as replicaStorageKey, C as ReplicaStorePort, D as readReplicaRowStrict, E as ReplicaWriteQueue, F as DefineReplicaOptions, G as ReplicaResource, H as staticReplicaScope, I as ReplicaStorageFactory, J as ReplicaScope, K as ReplicaRow, L as definePagedReplica, M as createReplicaState, N as replicaReducer, O as ReplicaAction, P as DefinePagedReplicaOptions, Q as ReplicaStorage, R as defineReplica, S as ReplicaOptimisticToken, T as REPLICA_INDEX_KEY, U as ReplicaEntryMeta, V as stableQueryKey, W as ReplicaPendingMutation, X as ReplicaSource, Y as ReplicaSeedPatch, Z as ReplicaState, _ as OptimisticMutationOptions, a as KeyValueReplicaStorageOptions, at as ReplicaPagingMode, b as ReplicaEntityAdapter, c as readReplicaStoredKeys, ct as collapseToHead, d as isReplicaSyncKey, dt as insertHeadItems, et as ReplicaPageResult, f as replicaKeys, ft as joinPages, g as linkReplicaEntity, h as ReplicaEntityTarget, ht as toPersistedPage, i as KeyValueReplicaStorage, it as ReplicaPagingDirection, j as ReplicaViewWrite, k as ReplicaEffect, l as settleWithin, lt as getNextPageCursor, m as singleEntity, mt as pagingFor, n as LocalStorageReplicaStorageOptions, nt as ReplicaPagingConfig, o as ReplicaKeyValueStore, ot as applyHeadPage, p as arrayEntity, pt as mapPagedItem, q as ReplicaRowKey, r as ReplicaRowCodec, rt as ReplicaPagingContext, s as replicaRowStoreKey, st as applyNextPage, t as LocalStorageReplicaStorage, tt as ReplicaPagedData, u as REPLICA_KEY_PREFIX, ut as hasPagedItem, v as ReplicaEngine, w as createReplicaEngine, x as ReplicaHeadOutcome, y as ReplicaEngineOptions, z as isReplicaStorageKeyOf } from "./index-CFbCHM7Q.js";
2
+ export { DefinePagedReplicaOptions, DefineReplicaOptions, KeyValueReplicaStorage, KeyValueReplicaStorageOptions, LocalStorageReplicaStorage, LocalStorageReplicaStorageOptions, OptimisticMutationOptions, REPLICA_INDEX_KEY, REPLICA_KEY_PREFIX, ReplicaAction, ReplicaEffect, ReplicaEngine, ReplicaEngineOptions, ReplicaEntityAdapter, ReplicaEntityTarget, type ReplicaEntryMeta, ReplicaHeadOutcome, ReplicaKeyValueStore, ReplicaOptimisticToken, ReplicaPageInfo, ReplicaPageResult, ReplicaPagedData, ReplicaPagingConfig, ReplicaPagingContext, ReplicaPagingDirection, ReplicaPagingMode, type ReplicaPendingMutation, type ReplicaResource, type ReplicaRow, ReplicaRowCodec, type ReplicaRowKey, type ReplicaScope, type ReplicaSeedPatch, type ReplicaSource, type ReplicaState, type ReplicaStorage, ReplicaStorageFactory, 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,239 @@
1
+ import { C as isReplicaStorageKeyOf, E as staticReplicaScope, S as defineReplica, T as stableQueryKey, _ as joinPages, b as toPersistedPage, c as REPLICA_INDEX_KEY, d as applyHeadPage, f as applyNextPage, g as insertHeadItems, h as hasPagedItem, i as createReplicaEngine, l as ReplicaWriteQueue, m as getNextPageCursor, n as isReplicaSyncKey, o as createReplicaState, p as collapseToHead, r as replicaKeys, s as replicaReducer, t as REPLICA_KEY_PREFIX, u as readReplicaRowStrict, v as mapPagedItem, w as replicaStorageKey, x as definePagedReplica, y as pagingFor } from "./keys-BTsqwfEa.js";
2
+ // src/core/entity.ts
3
+ /**
4
+ * Link resources that hold copies of the same entity (e.g. a topic in the
5
+ * sidebar list, the management page and the detail cache).
6
+ *
7
+ * Each resource keeps exactly one store location and decides how the entity
8
+ * maps into its own shape (paged resources by `getId`, single-entity ones by
9
+ * their entity adapter). The link only fans an entity-level change out to the
10
+ * resources that currently hold it — no global event bus, no shared owner.
11
+ */
12
+ const linkReplicaEntity = (targets) => {
13
+ /**
14
+ * Confirmed patch everywhere the entity is loaded, and (when persisted, the
15
+ * default) in persisted rows of entries that are not loaded.
16
+ */
17
+ const update = (id, fn, options) => {
18
+ for (const target of targets) target.updateEntity(id, fn, options);
19
+ };
20
+ /**
21
+ * Confirmed removal everywhere (list rows dropped, single-entity values
22
+ * removed). A value hydrated later — even one whose read was already in
23
+ * flight — comes back without the entity. With `scope` set to a scope left
24
+ * behind, only that scope's persisted rows lose the entity; the view on
25
+ * screen belongs to another scope and is left alone.
26
+ */
27
+ const remove = (id, options) => {
28
+ for (const target of targets) if (target.removeEntity) target.removeEntity(id, options);
29
+ else if (!options?.scope) target.updateEntity(id, () => void 0, options);
30
+ };
31
+ /**
32
+ * Wipe every linked resource in the active scope (e.g. "delete all"): memory,
33
+ * every persisted row and reads in flight. Resolves `true` once every
34
+ * persisted row is gone.
35
+ */
36
+ const clear = async () => {
37
+ return (await Promise.all(targets.map((target) => target.clear?.() ?? true))).every(Boolean);
38
+ };
39
+ /**
40
+ * One server call, one overlay per resource holding the entity: all commit
41
+ * together or all roll back together (then rethrow). Pass `'remove'` for an
42
+ * optimistic delete.
43
+ */
44
+ const optimistic = async (id, fn, serverCall) => {
45
+ const apply = fn === "remove" ? () => void 0 : fn;
46
+ const tokens = targets.flatMap((target) => target.beginEntityOptimistic(id, apply));
47
+ try {
48
+ const result = await serverCall();
49
+ for (const token of tokens) token.commit();
50
+ if (fn === "remove") remove(id);
51
+ else for (const target of targets) target.patchStoredEntity(id, fn);
52
+ return result;
53
+ } catch (error) {
54
+ for (const token of tokens) token.rollback();
55
+ throw error;
56
+ }
57
+ };
58
+ const revalidate = () => Promise.all(targets.map((target) => target.revalidate()));
59
+ return {
60
+ clear,
61
+ optimistic,
62
+ remove,
63
+ revalidate,
64
+ update
65
+ };
66
+ };
67
+ // src/core/entityAdapters.ts
68
+ /**
69
+ * The value IS one entity, or wraps one (`{ project, members }`). Deleting the
70
+ * entity drops the whole value.
71
+ */
72
+ const singleEntity = (getId, lens) => {
73
+ const pick = (data) => lens ? lens.get(data) : data;
74
+ return {
75
+ has: (data, id) => getId(pick(data)) === id,
76
+ map: (data, _id, fn) => {
77
+ const current = pick(data);
78
+ const next = fn(current);
79
+ if (next === void 0) return void 0;
80
+ if (next === current) return data;
81
+ return lens ? lens.set(data, next) : next;
82
+ }
83
+ };
84
+ };
85
+ /** The value is a plain list of entities; deleting one filters it out. */
86
+ const arrayEntity = (getId) => ({
87
+ has: (data, id) => data.some((item) => getId(item) === id),
88
+ map: (data, id, fn) => {
89
+ let changed = false;
90
+ const next = [];
91
+ for (const item of data) {
92
+ if (getId(item) !== id) {
93
+ next.push(item);
94
+ continue;
95
+ }
96
+ const mapped = fn(item);
97
+ if (mapped !== item) changed = true;
98
+ if (mapped !== void 0) next.push(mapped);
99
+ }
100
+ return changed ? next : data;
101
+ }
102
+ });
103
+ // src/core/settle.ts
104
+ /**
105
+ * Resolve once `promise` settles or `ms` elapses — never rejects, never hangs.
106
+ *
107
+ * Bounds a pre-paint hydrate in a route loader: the persisted read is usually
108
+ * sub-millisecond, and the deadline only fires on a pathological storage stall,
109
+ * so a route transition never waits longer than `ms` for it.
110
+ */
111
+ const settleWithin = (promise, ms) => new Promise((resolve) => {
112
+ const finish = () => {
113
+ clearTimeout(timer);
114
+ resolve();
115
+ };
116
+ const timer = setTimeout(finish, ms);
117
+ Promise.resolve(promise).then(finish, finish);
118
+ });
119
+ // src/core/storedKeys.ts
120
+ /**
121
+ * The entry storage keys this resource has a persisted row for in the active
122
+ * scope, oldest first.
123
+ *
124
+ * Storage has no key listing of its own; the engine keeps one index row per
125
+ * scope (`REPLICA_INDEX_KEY`) as it persists and drops entries, and this reads
126
+ * it. That is how code outside the owning store reaches rows an *earlier
127
+ * session* wrote — bounding how many survive a reload, or invalidating a row
128
+ * whose entry is not loaded in memory.
129
+ *
130
+ * The keys are storage keys (`key`, plus `?query` when the resource sets one),
131
+ * so a caller only matches them against entry keys when the resource leaves
132
+ * `query` unset.
133
+ */
134
+ const readReplicaStoredKeys = async (resource) => {
135
+ return (await resource.storage?.get({
136
+ queryKey: "__replica:index",
137
+ scope: resource.scope.get()
138
+ }))?.data ?? [];
139
+ };
140
+ // src/storage/keyValue.ts
141
+ /** Store key of one row: `<namespace>:<scope>:<queryKey>`, each part URI-encoded but the namespace. */
142
+ const replicaRowStoreKey = (namespace, { queryKey, scope }) => `${namespace}:${encodeURIComponent(scope)}:${encodeURIComponent(queryKey)}`;
143
+ /** {@link ReplicaStorage} over an async key-value store, one record per row. */
144
+ var KeyValueReplicaStorage = class {
145
+ #namespace;
146
+ #store;
147
+ constructor({ namespace, store }) {
148
+ this.#namespace = namespace;
149
+ this.#store = store;
150
+ }
151
+ #key = (key) => replicaRowStoreKey(this.#namespace, key);
152
+ get = async (key) => {
153
+ try {
154
+ return await this.getStrict(key);
155
+ } catch {
156
+ return;
157
+ }
158
+ };
159
+ /** Like `get`, but rejects when the row could not be read (see `ReplicaStorage.getStrict`). */
160
+ getStrict = async (key) => {
161
+ await this.#store.initialize?.();
162
+ return await this.#store.get(this.#key(key));
163
+ };
164
+ /**
165
+ * Rejects when the delete did not land, unlike reads: a caller that removes a
166
+ * row the user deleted must be able to retry it.
167
+ */
168
+ remove = async (key) => {
169
+ await this.#store.initialize?.();
170
+ await this.#store.delete(this.#key(key));
171
+ };
172
+ /**
173
+ * Rejects when the write did not land (see `ReplicaStorage.set`): the replica
174
+ * engine only treats a row as holding a value after a confirmed write, so a
175
+ * dropped write is retried by the next persist instead of being skipped as
176
+ * redundant.
177
+ */
178
+ set = async (key, row) => {
179
+ await this.#store.initialize?.();
180
+ await this.#store.set(this.#key(key), row);
181
+ };
182
+ };
183
+ // src/storage/localStorage.ts
184
+ const jsonCodec = {
185
+ parse: JSON.parse,
186
+ stringify: JSON.stringify
187
+ };
188
+ /**
189
+ * {@link ReplicaStorage} over `window.localStorage`. Small values only (the
190
+ * origin shares a ~5 MB quota), but the read is synchronous: `getSync` restores
191
+ * a row inside pre-paint initialization without yielding a frame. Outside a
192
+ * browser every operation is a no-op miss.
193
+ */
194
+ var LocalStorageReplicaStorage = class {
195
+ #codec;
196
+ #namespace;
197
+ constructor({ codec = jsonCodec, namespace }) {
198
+ this.#codec = codec;
199
+ this.#namespace = namespace;
200
+ }
201
+ #key = (key) => replicaRowStoreKey(this.#namespace, key);
202
+ get = async (key) => this.getSync(key);
203
+ /** Restore a row inside pre-paint initialization without yielding a frame. */
204
+ getSync = (key) => {
205
+ if (typeof window === "undefined") return void 0;
206
+ try {
207
+ const value = localStorage.getItem(this.#key(key));
208
+ return value ? this.#codec.parse(value) : void 0;
209
+ } catch {
210
+ return;
211
+ }
212
+ };
213
+ /** Like `get`, but rejects when the row could not be read or parsed (see `ReplicaStorage.getStrict`). */
214
+ getStrict = async (key) => {
215
+ if (typeof window === "undefined") return void 0;
216
+ const value = localStorage.getItem(this.#key(key));
217
+ return value ? this.#codec.parse(value) : void 0;
218
+ };
219
+ /**
220
+ * Rejects when the delete did not land (e.g. storage access denied), unlike
221
+ * reads: a caller removing a row the user deleted must be able to retry it.
222
+ */
223
+ remove = async (key) => {
224
+ if (typeof window === "undefined") return;
225
+ localStorage.removeItem(this.#key(key));
226
+ };
227
+ /**
228
+ * Rejects when the write did not land (a quota error, see `ReplicaStorage.set`):
229
+ * the replica engine only treats a row as holding a value after a confirmed
230
+ * write.
231
+ */
232
+ set = async (key, row) => {
233
+ if (typeof window === "undefined") return;
234
+ localStorage.setItem(this.#key(key), this.#codec.stringify(row));
235
+ };
236
+ };
237
+ 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 };
238
+
239
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/core/entity.ts","../src/core/entityAdapters.ts","../src/core/settle.ts","../src/core/storedKeys.ts","../src/storage/keyValue.ts","../src/storage/localStorage.ts"],"sourcesContent":["import type { ReplicaOptimisticToken } from './engine';\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 { ReplicaEntityAdapter } from './engine';\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","/**\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 { REPLICA_INDEX_KEY } from './engine';\nimport type { ReplicaResource } from './types';\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","import type { ReplicaRow, ReplicaRowKey, ReplicaStorage } from '../core/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 type { ReplicaRow, ReplicaRowKey, ReplicaStorage } from '../core/types';\nimport { replicaRowStoreKey } from './keyValue';\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":"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"}