local-replica 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{keys-BTsqwfEa.js → createReplicaEngine-CdR-URHo.js} +275 -243
- package/dist/createReplicaEngine-CdR-URHo.js.map +1 -0
- package/dist/{index-CFbCHM7Q.d.ts → index-DsElitHD.d.ts} +296 -311
- package/dist/index.d.ts +2 -2
- package/dist/index.js +97 -55
- package/dist/index.js.map +1 -1
- package/dist/zustand/index.d.ts +38 -47
- package/dist/zustand/index.js +383 -322
- package/dist/zustand/index.js.map +1 -1
- package/package.json +2 -1
- package/dist/keys-BTsqwfEa.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import isEqual from "fast-deep-equal";
|
|
2
|
-
// src/core/
|
|
2
|
+
// src/core/resource/storageKey.ts
|
|
3
3
|
/** Persisted row key: one row per entry key and query. */
|
|
4
4
|
const replicaStorageKey = (key, query) => query ? `${key}?${query}` : key;
|
|
5
5
|
/**
|
|
@@ -25,47 +25,50 @@ const isReplicaStorageKeyOf = (storageKey, key) => {
|
|
|
25
25
|
};
|
|
26
26
|
/** Deterministic JSON: sorted keys, `undefined` dropped. */
|
|
27
27
|
const stableQueryKey = (value) => JSON.stringify(value, (_key, item) => item && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(Object.keys(item).sort().filter((key) => item[key] !== void 0).map((key) => [key, item[key]])) : item) ?? "";
|
|
28
|
+
// src/core/resource/queryKeys.ts
|
|
28
29
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
30
|
+
* Keys of the queries a fetch adapter runs for a replica. Hosts that sweep
|
|
31
|
+
* their query cache by prefix (e.g. "refetch everything after a restore")
|
|
32
|
+
* match on {@link REPLICA_KEY_PREFIX}.
|
|
32
33
|
*/
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
name
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
storageKey: (params) => replicaStorageKey(options.key(params), options.query ? stableQueryKey(options.query(params)) : void 0),
|
|
53
|
-
scope: options.scope ?? DEFAULT_SCOPE,
|
|
54
|
-
syncKey: options.syncKey,
|
|
55
|
-
storage,
|
|
56
|
-
version: options.version
|
|
57
|
-
};
|
|
34
|
+
const REPLICA_KEY_PREFIX = "replica:";
|
|
35
|
+
const replicaKeys = {
|
|
36
|
+
/** One-shot read of the persisted row for `storageKey`. */
|
|
37
|
+
hydrate: (name, version, scope, storageKey) => [
|
|
38
|
+
"replica:hydrate",
|
|
39
|
+
name,
|
|
40
|
+
version,
|
|
41
|
+
scope,
|
|
42
|
+
storageKey
|
|
43
|
+
],
|
|
44
|
+
/** Network sync of the head of one entry. */
|
|
45
|
+
sync: (name, version, scope, key, params) => [
|
|
46
|
+
"replica:sync",
|
|
47
|
+
name,
|
|
48
|
+
version,
|
|
49
|
+
scope,
|
|
50
|
+
key,
|
|
51
|
+
params
|
|
52
|
+
]
|
|
58
53
|
};
|
|
54
|
+
/** Match a resource's sync keys, optionally narrowed to one scope and/or entry key. */
|
|
55
|
+
const isReplicaSyncKey = (queryKey, name, filter = {}) => Array.isArray(queryKey) && queryKey[0] === "replica:sync" && queryKey[1] === name && (filter.scope === void 0 || queryKey[3] === filter.scope) && (filter.key === void 0 || queryKey[4] === filter.key);
|
|
56
|
+
// src/core/paging/operations.ts
|
|
59
57
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
58
|
+
* First-class pagination for replicas.
|
|
59
|
+
*
|
|
60
|
+
* Vocabulary (direction-agnostic):
|
|
61
|
+
* - HEAD page: the newest window, what a refresh re-fetches (`cursor` undefined).
|
|
62
|
+
* - NEXT pages: further pages in the paging direction (older history).
|
|
63
|
+
* - `forward`: the view is head-first, next pages append at the END
|
|
64
|
+
* (sidebar lists: newest topic first, "load more" below).
|
|
65
|
+
* - `backward`: the view is oldest-first, next pages PREPEND at the start
|
|
66
|
+
* (chat transcripts: newest message last, "load older" on scroll-up).
|
|
67
|
+
* - `offset` mode: the cursor is the page index (0 = head).
|
|
68
|
+
* `cursor` mode: the server hands back an opaque `nextCursor`.
|
|
69
|
+
*
|
|
70
|
+
* Every function is pure; the store binding owns staleness and scope.
|
|
62
71
|
*/
|
|
63
|
-
const definePagedReplica = (options) => defineReplica({
|
|
64
|
-
...options,
|
|
65
|
-
fetcher: options.fetchPage,
|
|
66
|
-
storage: options.storage
|
|
67
|
-
});
|
|
68
|
-
// src/core/paging.ts
|
|
69
72
|
const dedupe = (items, getId, seen = /* @__PURE__ */ new Set()) => {
|
|
70
73
|
const out = [];
|
|
71
74
|
for (const item of items) {
|
|
@@ -395,7 +398,7 @@ const toPersistedPage = (data, config, ctx = {}) => {
|
|
|
395
398
|
}]
|
|
396
399
|
};
|
|
397
400
|
};
|
|
398
|
-
// src/core/writeQueue.ts
|
|
401
|
+
// src/core/persistence/writeQueue.ts
|
|
399
402
|
/**
|
|
400
403
|
* Strict read of one row (see `ReplicaStorage.getStrict`). Rejects when the
|
|
401
404
|
* storage has no strict read: its `get` swallows I/O failures into a miss, so a
|
|
@@ -454,7 +457,7 @@ var ReplicaWriteQueue = class {
|
|
|
454
457
|
return settled;
|
|
455
458
|
};
|
|
456
459
|
};
|
|
457
|
-
// src/core/persistedRows.ts
|
|
460
|
+
// src/core/persistence/persistedRows.ts
|
|
458
461
|
/** Reserved storage key of the per-scope index of persisted rows. */
|
|
459
462
|
const REPLICA_INDEX_KEY = "__replica:index";
|
|
460
463
|
/** Returned by a `patchRow` transform to delete the row (`null` stays a value). */
|
|
@@ -549,8 +552,11 @@ const createPersistedRows = ({ commit, getSlot, isRemoved, loadingMore, resource
|
|
|
549
552
|
for (const [key, entry] of Object.entries(slot.entries)) rows.set(replicaStorageKey(key, entry.query), key);
|
|
550
553
|
return rows;
|
|
551
554
|
};
|
|
552
|
-
/**
|
|
553
|
-
|
|
555
|
+
/**
|
|
556
|
+
* A row on screen, or whose entry has writes / a page load in flight.
|
|
557
|
+
* `entryKey` comes from `loadedRows`, so its entry is in the slot.
|
|
558
|
+
*/
|
|
559
|
+
const isPinned = (queryKey, entryKey) => retainedRows.has(queryKey) || entryKey !== void 0 && (retainedKeys.has(entryKey) || getSlot().entries[entryKey].pending.length > 0 || loadingMore.has(entryKey));
|
|
554
560
|
/** The oldest unpinned rows past the cap (`keys` is oldest first). */
|
|
555
561
|
const pickEvictions = (scope, keys, touched) => {
|
|
556
562
|
const going = evicting.get(scope);
|
|
@@ -621,9 +627,8 @@ const createPersistedRows = ({ commit, getSlot, isRemoved, loadingMore, resource
|
|
|
621
627
|
if (picked.length > 0) evictRows(scope, picked);
|
|
622
628
|
});
|
|
623
629
|
};
|
|
624
|
-
/** Record a persisted row in the index (queued after the row write). */
|
|
630
|
+
/** Record a persisted row in the index (queued after the row write; see `writeRow`). */
|
|
625
631
|
const trackStorageKey = (scope, queryKey) => {
|
|
626
|
-
if (!writeQueue) return;
|
|
627
632
|
noteWrite(scope, queryKey);
|
|
628
633
|
if (maxEntries !== void 0) return touchStorageKey(scope, queryKey);
|
|
629
634
|
const known = indexed.get(scope) ?? /* @__PURE__ */ new Set();
|
|
@@ -813,7 +818,7 @@ const createPersistedRows = ({ commit, getSlot, isRemoved, loadingMore, resource
|
|
|
813
818
|
writeRow
|
|
814
819
|
};
|
|
815
820
|
};
|
|
816
|
-
// src/core/
|
|
821
|
+
// src/core/entity/propagation.ts
|
|
817
822
|
/**
|
|
818
823
|
* Entity propagation: a change to one entity (e.g. a topic) applied to every
|
|
819
824
|
* entry that holds it — in memory, in persisted rows memory does not hold,
|
|
@@ -982,7 +987,7 @@ const createEntityPropagation = ({ beginOptimistic, entity, entityGuards, getSlo
|
|
|
982
987
|
withoutRemovedEntities
|
|
983
988
|
};
|
|
984
989
|
};
|
|
985
|
-
// src/core/reducer.ts
|
|
990
|
+
// src/core/state/reducer.ts
|
|
986
991
|
const createReplicaState = () => ({ entries: {} });
|
|
987
992
|
const materialize = (base, pending) => base === void 0 ? void 0 : pending.reduce((data, mutation) => mutation.apply(data), base);
|
|
988
993
|
const noop = (state) => ({
|
|
@@ -1197,7 +1202,124 @@ const replicaReducer = (state, action, readView, now = Date.now()) => {
|
|
|
1197
1202
|
};
|
|
1198
1203
|
}
|
|
1199
1204
|
};
|
|
1200
|
-
// src/core/
|
|
1205
|
+
// src/core/engine/pagination.ts
|
|
1206
|
+
/**
|
|
1207
|
+
* The engine's paged-resource transitions: folding a head page into the loaded
|
|
1208
|
+
* pages, fetching the next page, head inserts and collapsing back to the head.
|
|
1209
|
+
* The pure page math lives in `paging/operations.ts`; this adds the entry,
|
|
1210
|
+
* scope and in-flight bookkeeping around it. Every method is a no-op (or a
|
|
1211
|
+
* pass-through) for a non-paged resource.
|
|
1212
|
+
*/
|
|
1213
|
+
const createPagination = ({ fetcher, getSlot, loadingMore, options, port, resource, update }) => {
|
|
1214
|
+
const paging = resource.paging;
|
|
1215
|
+
const at = (key) => pagingFor(paging, key);
|
|
1216
|
+
const pagingCtx = { isClientOnly: options.isClientOnly };
|
|
1217
|
+
/** The persistable part of a paged value (the value itself when not paged). */
|
|
1218
|
+
const toPersisted = (data) => paging ? toPersistedPage(data, paging, pagingCtx) : data;
|
|
1219
|
+
/**
|
|
1220
|
+
* An overwrite hydrate repaints the entry from another query's persisted page.
|
|
1221
|
+
* Client-only rows (an in-flight upload / create) have no server row yet, so
|
|
1222
|
+
* that page cannot contain them — keep the ones still in the view, so a
|
|
1223
|
+
* same-pool query change (a re-sort, a view-mode switch) never drops them.
|
|
1224
|
+
* A cross-pool navigate is not "client-only" for the new params and is dropped.
|
|
1225
|
+
*/
|
|
1226
|
+
const keepClientOnlyRows = (incoming, current, key) => {
|
|
1227
|
+
if (!paging || !options.isClientOnly || !current) return incoming;
|
|
1228
|
+
const page = incoming;
|
|
1229
|
+
const currentItems = current.items ?? [];
|
|
1230
|
+
const incomingIds = new Set(page.items.map((item) => paging.getId(item)));
|
|
1231
|
+
const survivors = currentItems.filter((item) => options.isClientOnly(item) && !incomingIds.has(paging.getId(item)));
|
|
1232
|
+
if (survivors.length === 0) return incoming;
|
|
1233
|
+
const items = joinPages(survivors, page.items, at(key).direction);
|
|
1234
|
+
return {
|
|
1235
|
+
...incoming,
|
|
1236
|
+
items
|
|
1237
|
+
};
|
|
1238
|
+
};
|
|
1239
|
+
const viewMatchesFields = (current, params) => {
|
|
1240
|
+
const fields = options.viewFields?.(params);
|
|
1241
|
+
if (!current || !fields) return true;
|
|
1242
|
+
const norm = (value) => stableQueryKey(value || null);
|
|
1243
|
+
return Object.entries(fields).every(([field, value]) => norm(value) === norm(current[field]));
|
|
1244
|
+
};
|
|
1245
|
+
const mergeHead = (key, incoming, confirmed, params, reset) => {
|
|
1246
|
+
const page = incoming;
|
|
1247
|
+
const pageSize = params.pageSize ?? page.items.length;
|
|
1248
|
+
const merged = {
|
|
1249
|
+
...applyHeadPage(confirmed, page, {
|
|
1250
|
+
pageSize,
|
|
1251
|
+
reset
|
|
1252
|
+
}, at(key), pagingCtx),
|
|
1253
|
+
...options.viewFields?.(params),
|
|
1254
|
+
isLoadingMore: loadingMore.has(key)
|
|
1255
|
+
};
|
|
1256
|
+
const next = {
|
|
1257
|
+
...confirmed,
|
|
1258
|
+
...merged
|
|
1259
|
+
};
|
|
1260
|
+
return confirmed !== void 0 && isEqual(next, confirmed) ? void 0 : next;
|
|
1261
|
+
};
|
|
1262
|
+
/**
|
|
1263
|
+
* Fetch and merge the next page with the params of the loaded head page
|
|
1264
|
+
* (`fallbackParams` covers views seeded outside `useSync`). In `cursor` mode
|
|
1265
|
+
* paging only starts from a server-confirmed head page: a hydrated cursor
|
|
1266
|
+
* may be stale. A result is dropped when the scope, the query or the loaded
|
|
1267
|
+
* depth changed while it was in flight.
|
|
1268
|
+
*/
|
|
1269
|
+
const loadMore = async (key, fallbackParams) => {
|
|
1270
|
+
if (!paging || !fetcher) return;
|
|
1271
|
+
const entry = getSlot().entries[key];
|
|
1272
|
+
const current = port.read(key);
|
|
1273
|
+
if (!current || loadingMore.has(key)) return;
|
|
1274
|
+
if (paging.mode === "cursor" && entry?.source === "storage") return;
|
|
1275
|
+
const params = entry?.params ?? fallbackParams;
|
|
1276
|
+
if (params === void 0) return;
|
|
1277
|
+
const cursor = getNextPageCursor(current, at(key));
|
|
1278
|
+
if (cursor === null || cursor === void 0) return;
|
|
1279
|
+
const query = entry?.query;
|
|
1280
|
+
const scope = resource.scope.get();
|
|
1281
|
+
const depth = current.currentPage;
|
|
1282
|
+
const setLoading = (patch) => update(key, (data) => data && {
|
|
1283
|
+
...data,
|
|
1284
|
+
...patch
|
|
1285
|
+
}, { persist: false });
|
|
1286
|
+
loadingMore.add(key);
|
|
1287
|
+
setLoading({
|
|
1288
|
+
isLoadingMore: true,
|
|
1289
|
+
loadMoreError: void 0
|
|
1290
|
+
});
|
|
1291
|
+
const isCurrent = () => {
|
|
1292
|
+
const latest = port.read(key);
|
|
1293
|
+
return resource.scope.get() === scope && getSlot().entries[key]?.query === query && latest?.currentPage === depth;
|
|
1294
|
+
};
|
|
1295
|
+
try {
|
|
1296
|
+
const page = await fetcher(params, cursor);
|
|
1297
|
+
if (!isCurrent()) return void setLoading({ isLoadingMore: false });
|
|
1298
|
+
update(key, (data) => data && applyNextPage(data, page, at(key), pagingCtx), { persist: (paging.persist?.pages ?? 1) > 1 });
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
setLoading({
|
|
1301
|
+
isLoadingMore: false,
|
|
1302
|
+
loadMoreError: isCurrent() ? error : void 0
|
|
1303
|
+
});
|
|
1304
|
+
} finally {
|
|
1305
|
+
loadingMore.delete(key);
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
/** Insert rows at the head (new / streamed items). */
|
|
1309
|
+
const insertHead = (key, items, { persist = false } = {}) => paging ? update(key, (data) => data && insertHeadItems(data, items, at(key)), { persist }) : false;
|
|
1310
|
+
/** Drop loaded pages, keeping the head (e.g. after an edit inside older pages). */
|
|
1311
|
+
const collapse = (key) => paging ? update(key, (data) => data && collapseToHead(data, at(key)), { persist: false }) : false;
|
|
1312
|
+
return {
|
|
1313
|
+
collapse,
|
|
1314
|
+
insertHead,
|
|
1315
|
+
keepClientOnlyRows,
|
|
1316
|
+
loadMore,
|
|
1317
|
+
mergeHead,
|
|
1318
|
+
toPersisted,
|
|
1319
|
+
viewMatchesFields
|
|
1320
|
+
};
|
|
1321
|
+
};
|
|
1322
|
+
// src/core/engine/tombstones.ts
|
|
1201
1323
|
/**
|
|
1202
1324
|
* How many removal tombstones one engine remembers. The guards exist to win a
|
|
1203
1325
|
* race with a hydrate that started around a removal, so a modest window is
|
|
@@ -1249,7 +1371,7 @@ const createTombstones = (limit = DEFAULT_REMOVAL_TOMBSTONE_LIMIT) => {
|
|
|
1249
1371
|
size: () => tracked
|
|
1250
1372
|
};
|
|
1251
1373
|
};
|
|
1252
|
-
// src/core/removalGuards.ts
|
|
1374
|
+
// src/core/engine/removalGuards.ts
|
|
1253
1375
|
/**
|
|
1254
1376
|
* Removal guard of one engine.
|
|
1255
1377
|
*
|
|
@@ -1377,7 +1499,87 @@ const createEntityRemovalGuards = () => {
|
|
|
1377
1499
|
startHydrate
|
|
1378
1500
|
};
|
|
1379
1501
|
};
|
|
1380
|
-
// src/core/engine.ts
|
|
1502
|
+
// src/core/engine/scopeClear.ts
|
|
1503
|
+
/**
|
|
1504
|
+
* Resource-wide clear of one scope: memory, every persisted row and reads in
|
|
1505
|
+
* flight. A clear bumps the scope's generation so reads started before it are
|
|
1506
|
+
* dropped, and records its row purge so a later hydrate waits for it.
|
|
1507
|
+
*/
|
|
1508
|
+
const createScopeClear = ({ port, prefix, resource, rows, writeQueue }) => {
|
|
1509
|
+
const { deleteRows, readIndex } = rows;
|
|
1510
|
+
/** Bumped by every `clear()` of a scope; reads started before it are stale. */
|
|
1511
|
+
const clearGenerations = /* @__PURE__ */ new Map();
|
|
1512
|
+
const clearGeneration = (scope) => clearGenerations.get(scope) ?? 0;
|
|
1513
|
+
/**
|
|
1514
|
+
* The persisted-row purge of the last `clear()` per scope: `true` once every
|
|
1515
|
+
* row delete landed. A hydrate waits for it, and refuses to read while the
|
|
1516
|
+
* purge failed (an untrusted scope, a storage error) so a wiped resource can
|
|
1517
|
+
* never paint a row that survived it.
|
|
1518
|
+
*/
|
|
1519
|
+
const scopePurges = /* @__PURE__ */ new Map();
|
|
1520
|
+
const lastPurge = (scope) => scopePurges.get(scope);
|
|
1521
|
+
/**
|
|
1522
|
+
* Wipe the resource in the active scope: every entry leaves memory in one
|
|
1523
|
+
* host commit, every persisted row (all keys, all query variants) and the
|
|
1524
|
+
* index are deleted, and reads already in flight are dropped — a hydrate
|
|
1525
|
+
* started before the clear never dispatches, and `useSync` discards head
|
|
1526
|
+
* responses whose request started before it. Requests started afterwards
|
|
1527
|
+
* apply normally. Resolves `true` once the persisted rows are gone.
|
|
1528
|
+
*/
|
|
1529
|
+
const clear = () => {
|
|
1530
|
+
const scope = resource.scope.get();
|
|
1531
|
+
clearGenerations.set(scope, clearGeneration(scope) + 1);
|
|
1532
|
+
port.commit([{ type: "clear" }], {
|
|
1533
|
+
entries: {},
|
|
1534
|
+
scope
|
|
1535
|
+
}, `${prefix}/clear`);
|
|
1536
|
+
rows.forgetScope(scope);
|
|
1537
|
+
const purge = purgeScope(scope);
|
|
1538
|
+
scopePurges.set(scope, purge);
|
|
1539
|
+
return purge;
|
|
1540
|
+
};
|
|
1541
|
+
/**
|
|
1542
|
+
* `clear()` for any scope, including one that is not active (a user who
|
|
1543
|
+
* switched away while their data was being deleted): every persisted row of
|
|
1544
|
+
* `scope` and its index go, and for the active scope memory is wiped too
|
|
1545
|
+
* (exactly `clear()`). An inactive scope's memory is not on screen and is
|
|
1546
|
+
* left alone. The row deletes go through the write queue, so they land after
|
|
1547
|
+
* any write this session already queued for that scope — a row (or index
|
|
1548
|
+
* entry) written just before is purged rather than landing after the purge.
|
|
1549
|
+
* A later hydrate of `scope` waits for the purge. Resolves `true` once the
|
|
1550
|
+
* persisted rows are gone.
|
|
1551
|
+
*/
|
|
1552
|
+
const clearScope = (scope) => {
|
|
1553
|
+
if (scope === resource.scope.get()) return clear();
|
|
1554
|
+
clearGenerations.set(scope, clearGeneration(scope) + 1);
|
|
1555
|
+
const purge = purgeScope(scope);
|
|
1556
|
+
scopePurges.set(scope, purge);
|
|
1557
|
+
return purge;
|
|
1558
|
+
};
|
|
1559
|
+
/** Delete every persisted row of `scope` (see `clear`). */
|
|
1560
|
+
const purgeScope = async (scope) => {
|
|
1561
|
+
if (!writeQueue) return true;
|
|
1562
|
+
if (!resource.scope.canPersist()) return false;
|
|
1563
|
+
const queued = deleteRows(scope, [...rows.indexedKeys(scope) ?? []]);
|
|
1564
|
+
let persisted;
|
|
1565
|
+
try {
|
|
1566
|
+
persisted = await readIndex(scope);
|
|
1567
|
+
} catch {
|
|
1568
|
+
return false;
|
|
1569
|
+
}
|
|
1570
|
+
const known = rows.indexedKeys(scope);
|
|
1571
|
+
const stale = persisted.filter((queryKey) => !known?.has(queryKey));
|
|
1572
|
+
const [first, rest] = await Promise.all([queued, deleteRows(scope, stale)]);
|
|
1573
|
+
return first && rest;
|
|
1574
|
+
};
|
|
1575
|
+
return {
|
|
1576
|
+
clear,
|
|
1577
|
+
clearGeneration,
|
|
1578
|
+
clearScope,
|
|
1579
|
+
lastPurge
|
|
1580
|
+
};
|
|
1581
|
+
};
|
|
1582
|
+
// src/core/engine/createReplicaEngine.ts
|
|
1381
1583
|
/**
|
|
1382
1584
|
* The replica engine: every transition of one resource, independent of any
|
|
1383
1585
|
* UI framework or state library.
|
|
@@ -1391,8 +1593,6 @@ const createEntityRemovalGuards = () => {
|
|
|
1391
1593
|
const createReplicaEngine = (resource, options) => {
|
|
1392
1594
|
const { port } = options;
|
|
1393
1595
|
const paging = resource.paging;
|
|
1394
|
-
const at = (key) => pagingFor(paging, key);
|
|
1395
|
-
const pagingCtx = { isClientOnly: options.isClientOnly };
|
|
1396
1596
|
const prefix = options.actionPrefix ?? resource.name;
|
|
1397
1597
|
const writeQueue = resource.storage ? new ReplicaWriteQueue(resource.storage) : void 0;
|
|
1398
1598
|
const fetcher = options.fetcher ?? resource.fetcher;
|
|
@@ -1401,20 +1601,10 @@ const createReplicaEngine = (resource, options) => {
|
|
|
1401
1601
|
const loadingMore = /* @__PURE__ */ new Set();
|
|
1402
1602
|
const { clearRemoved, isPurged, isRemoved, markPurged, markRemoved, removalGeneration } = createRemovalGuards();
|
|
1403
1603
|
const entityGuards = createEntityRemovalGuards();
|
|
1404
|
-
/** Bumped by every `clear()` of a scope; reads started before it are stale. */
|
|
1405
|
-
const clearGenerations = /* @__PURE__ */ new Map();
|
|
1406
|
-
const clearGeneration = (scope) => clearGenerations.get(scope) ?? 0;
|
|
1407
|
-
/**
|
|
1408
|
-
* The persisted-row purge of the last `clear()` per scope: `true` once every
|
|
1409
|
-
* row delete landed. A hydrate waits for it, and refuses to read while the
|
|
1410
|
-
* purge failed (an untrusted scope, a storage error) so a wiped resource can
|
|
1411
|
-
* never paint a row that survived it.
|
|
1412
|
-
*/
|
|
1413
|
-
const scopePurges = /* @__PURE__ */ new Map();
|
|
1414
1604
|
const getSlot = port.getState;
|
|
1415
1605
|
const storageKey = (key, query) => ({ queryKey: replicaStorageKey(key, query) });
|
|
1416
1606
|
const toPersisted = (data) => {
|
|
1417
|
-
const paged =
|
|
1607
|
+
const paged = pagination.toPersisted(data);
|
|
1418
1608
|
return options.toPersisted ? options.toPersisted(paged) : paged;
|
|
1419
1609
|
};
|
|
1420
1610
|
const rows = createPersistedRows({
|
|
@@ -1428,7 +1618,14 @@ const createReplicaEngine = (resource, options) => {
|
|
|
1428
1618
|
resource,
|
|
1429
1619
|
writeQueue
|
|
1430
1620
|
});
|
|
1431
|
-
const { deleteRows,
|
|
1621
|
+
const { deleteRows, removeStoredVariants, retain, touchStorageKey } = rows;
|
|
1622
|
+
const { clear, clearGeneration, clearScope, lastPurge } = createScopeClear({
|
|
1623
|
+
port,
|
|
1624
|
+
prefix,
|
|
1625
|
+
resource,
|
|
1626
|
+
rows,
|
|
1627
|
+
writeQueue
|
|
1628
|
+
});
|
|
1432
1629
|
const runEffects = (effects) => {
|
|
1433
1630
|
if (!writeQueue || effects.length === 0) return;
|
|
1434
1631
|
if (!resource.scope.canPersist()) return;
|
|
@@ -1522,26 +1719,6 @@ const createReplicaEngine = (resource, options) => {
|
|
|
1522
1719
|
const entry = getSlot().entries[key];
|
|
1523
1720
|
return entry?.pending.length ? entry.base : port.read(key);
|
|
1524
1721
|
};
|
|
1525
|
-
/**
|
|
1526
|
-
* An overwrite hydrate repaints the entry from another query's persisted page.
|
|
1527
|
-
* Client-only rows (an in-flight upload / create) have no server row yet, so
|
|
1528
|
-
* that page cannot contain them — keep the ones still in the view, so a
|
|
1529
|
-
* same-pool query change (a re-sort, a view-mode switch) never drops them.
|
|
1530
|
-
* A cross-pool navigate is not "client-only" for the new params and is dropped.
|
|
1531
|
-
*/
|
|
1532
|
-
const keepClientOnlyRows = (incoming, current, key) => {
|
|
1533
|
-
if (!paging || !options.isClientOnly || !current) return incoming;
|
|
1534
|
-
const page = incoming;
|
|
1535
|
-
const currentItems = current.items ?? [];
|
|
1536
|
-
const incomingIds = new Set(page.items.map((item) => paging.getId(item)));
|
|
1537
|
-
const survivors = currentItems.filter((item) => options.isClientOnly(item) && !incomingIds.has(paging.getId(item)));
|
|
1538
|
-
if (survivors.length === 0) return incoming;
|
|
1539
|
-
const items = joinPages(survivors, page.items, at(key).direction);
|
|
1540
|
-
return {
|
|
1541
|
-
...incoming,
|
|
1542
|
-
items
|
|
1543
|
-
};
|
|
1544
|
-
};
|
|
1545
1722
|
const hydrate = async (params, scope = resource.scope.get(), { overwrite = false } = {}) => {
|
|
1546
1723
|
if (!resource.storage) return false;
|
|
1547
1724
|
if (resource.scope.canHydrate && !resource.scope.canHydrate()) return false;
|
|
@@ -1558,7 +1735,7 @@ const createReplicaEngine = (resource, options) => {
|
|
|
1558
1735
|
const readAndHydrate = async (params, key, scope, overwrite) => {
|
|
1559
1736
|
const storage = resource.storage;
|
|
1560
1737
|
const generation = clearGeneration(scope);
|
|
1561
|
-
const purge =
|
|
1738
|
+
const purge = lastPurge(scope);
|
|
1562
1739
|
if (purge && !await purge) return false;
|
|
1563
1740
|
const query = resource.query(params);
|
|
1564
1741
|
const { queryKey } = storageKey(key, query);
|
|
@@ -1573,7 +1750,7 @@ const createReplicaEngine = (resource, options) => {
|
|
|
1573
1750
|
if (options.isHydratable && !options.isHydratable(cached.data, params)) return false;
|
|
1574
1751
|
const stored = withoutRemovedEntities(scope, cached.data);
|
|
1575
1752
|
if (stored === void 0) return false;
|
|
1576
|
-
const data = overwrite ? keepClientOnlyRows(stored, port.read(key), key) : stored;
|
|
1753
|
+
const data = overwrite ? pagination.keepClientOnlyRows(stored, port.read(key), key) : stored;
|
|
1577
1754
|
const applied = dispatch({
|
|
1578
1755
|
data,
|
|
1579
1756
|
key,
|
|
@@ -1587,31 +1764,8 @@ const createReplicaEngine = (resource, options) => {
|
|
|
1587
1764
|
if (applied && resource.scope.canPersist()) touchStorageKey(scope, queryKey);
|
|
1588
1765
|
return applied;
|
|
1589
1766
|
};
|
|
1590
|
-
const viewMatchesFields = (current, params) => {
|
|
1591
|
-
const fields = options.viewFields?.(params);
|
|
1592
|
-
if (!current || !fields) return true;
|
|
1593
|
-
const norm = (value) => stableQueryKey(value || null);
|
|
1594
|
-
return Object.entries(fields).every(([field, value]) => norm(value) === norm(current[field]));
|
|
1595
|
-
};
|
|
1596
1767
|
/** Non-paged: whether a (prepared) head response is a terminal absence (`isMissing`). */
|
|
1597
1768
|
const isMissingHead = (incoming) => !paging && !!options.isMissing?.(incoming);
|
|
1598
|
-
const mergeHead = (key, incoming, confirmed, params, reset) => {
|
|
1599
|
-
const page = incoming;
|
|
1600
|
-
const pageSize = params.pageSize ?? page.items.length;
|
|
1601
|
-
const merged = {
|
|
1602
|
-
...applyHeadPage(confirmed, page, {
|
|
1603
|
-
pageSize,
|
|
1604
|
-
reset
|
|
1605
|
-
}, at(key), pagingCtx),
|
|
1606
|
-
...options.viewFields?.(params),
|
|
1607
|
-
isLoadingMore: loadingMore.has(key)
|
|
1608
|
-
};
|
|
1609
|
-
const next = {
|
|
1610
|
-
...confirmed,
|
|
1611
|
-
...merged
|
|
1612
|
-
};
|
|
1613
|
-
return confirmed !== void 0 && isEqual(next, confirmed) ? void 0 : next;
|
|
1614
|
-
};
|
|
1615
1769
|
/**
|
|
1616
1770
|
* Fold a head response in and report what it was: dropped by `prepareHead`,
|
|
1617
1771
|
* a terminal absence (`isMissing`, applied as a removal) or a replacement.
|
|
@@ -1631,9 +1785,9 @@ const createReplicaEngine = (resource, options) => {
|
|
|
1631
1785
|
};
|
|
1632
1786
|
const query = resource.query(params);
|
|
1633
1787
|
const entry = getSlot().entries[key];
|
|
1634
|
-
const reset = entry ? entry.query !== query : paging !== void 0 && !viewMatchesFields(port.read(key), params);
|
|
1788
|
+
const reset = entry ? entry.query !== query : paging !== void 0 && !pagination.viewMatchesFields(port.read(key), params);
|
|
1635
1789
|
const applied = dispatch({
|
|
1636
|
-
data: (confirmed) => paging ? mergeHead(key, incoming, confirmed, params, reset) : options.merge ? options.merge(incoming, confirmed, params) : incoming,
|
|
1790
|
+
data: (confirmed) => paging ? pagination.mergeHead(key, incoming, confirmed, params, reset) : options.merge ? options.merge(incoming, confirmed, params) : incoming,
|
|
1637
1791
|
key,
|
|
1638
1792
|
params,
|
|
1639
1793
|
query,
|
|
@@ -1747,60 +1901,6 @@ const createReplicaEngine = (resource, options) => {
|
|
|
1747
1901
|
type: "persist"
|
|
1748
1902
|
}]);
|
|
1749
1903
|
};
|
|
1750
|
-
/**
|
|
1751
|
-
* Wipe the resource in the active scope: every entry leaves memory in one
|
|
1752
|
-
* host commit, every persisted row (all keys, all query variants) and the
|
|
1753
|
-
* index are deleted, and reads already in flight are dropped — a hydrate
|
|
1754
|
-
* started before the clear never dispatches, and `useSync` discards head
|
|
1755
|
-
* responses whose request started before it. Requests started afterwards
|
|
1756
|
-
* apply normally. Resolves `true` once the persisted rows are gone.
|
|
1757
|
-
*/
|
|
1758
|
-
const clear = () => {
|
|
1759
|
-
const scope = resource.scope.get();
|
|
1760
|
-
clearGenerations.set(scope, clearGeneration(scope) + 1);
|
|
1761
|
-
port.commit([{ type: "clear" }], {
|
|
1762
|
-
entries: {},
|
|
1763
|
-
scope
|
|
1764
|
-
}, `${prefix}/clear`);
|
|
1765
|
-
rows.forgetScope(scope);
|
|
1766
|
-
const purge = purgeScope(scope);
|
|
1767
|
-
scopePurges.set(scope, purge);
|
|
1768
|
-
return purge;
|
|
1769
|
-
};
|
|
1770
|
-
/**
|
|
1771
|
-
* `clear()` for any scope, including one that is not active (a user who
|
|
1772
|
-
* switched away while their data was being deleted): every persisted row of
|
|
1773
|
-
* `scope` and its index go, and for the active scope memory is wiped too
|
|
1774
|
-
* (exactly `clear()`). An inactive scope's memory is not on screen and is
|
|
1775
|
-
* left alone. The row deletes go through the write queue, so they land after
|
|
1776
|
-
* any write this session already queued for that scope — a row (or index
|
|
1777
|
-
* entry) written just before is purged rather than landing after the purge.
|
|
1778
|
-
* A later hydrate of `scope` waits for the purge. Resolves `true` once the
|
|
1779
|
-
* persisted rows are gone.
|
|
1780
|
-
*/
|
|
1781
|
-
const clearScope = (scope) => {
|
|
1782
|
-
if (scope === resource.scope.get()) return clear();
|
|
1783
|
-
clearGenerations.set(scope, clearGeneration(scope) + 1);
|
|
1784
|
-
const purge = purgeScope(scope);
|
|
1785
|
-
scopePurges.set(scope, purge);
|
|
1786
|
-
return purge;
|
|
1787
|
-
};
|
|
1788
|
-
/** Delete every persisted row of `scope` (see `clear`). */
|
|
1789
|
-
const purgeScope = async (scope) => {
|
|
1790
|
-
if (!writeQueue) return true;
|
|
1791
|
-
if (!resource.scope.canPersist()) return false;
|
|
1792
|
-
const queued = deleteRows(scope, [...rows.indexedKeys(scope) ?? []]);
|
|
1793
|
-
let persisted;
|
|
1794
|
-
try {
|
|
1795
|
-
persisted = await readIndex(scope);
|
|
1796
|
-
} catch {
|
|
1797
|
-
return false;
|
|
1798
|
-
}
|
|
1799
|
-
const known = rows.indexedKeys(scope);
|
|
1800
|
-
const stale = persisted.filter((queryKey) => !known?.has(queryKey));
|
|
1801
|
-
const [first, rest] = await Promise.all([queued, deleteRows(scope, stale)]);
|
|
1802
|
-
return first && rest;
|
|
1803
|
-
};
|
|
1804
1904
|
const revalidate = (key) => options.revalidate ? options.revalidate(key) : Promise.resolve();
|
|
1805
1905
|
/**
|
|
1806
1906
|
* Start an optimistic overlay now and settle it later (multi-resource flows).
|
|
@@ -1857,56 +1957,16 @@ const createReplicaEngine = (resource, options) => {
|
|
|
1857
1957
|
if (mutationOptions.revalidate) revalidate(key);
|
|
1858
1958
|
}
|
|
1859
1959
|
};
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
const current = port.read(key);
|
|
1871
|
-
if (!current || loadingMore.has(key)) return;
|
|
1872
|
-
if (paging.mode === "cursor" && entry?.source === "storage") return;
|
|
1873
|
-
const params = entry?.params ?? fallbackParams;
|
|
1874
|
-
if (params === void 0) return;
|
|
1875
|
-
const cursor = getNextPageCursor(current, at(key));
|
|
1876
|
-
if (cursor === null || cursor === void 0) return;
|
|
1877
|
-
const query = entry?.query;
|
|
1878
|
-
const scope = resource.scope.get();
|
|
1879
|
-
const depth = current.currentPage;
|
|
1880
|
-
const setLoading = (patch) => update(key, (data) => data && {
|
|
1881
|
-
...data,
|
|
1882
|
-
...patch
|
|
1883
|
-
}, { persist: false });
|
|
1884
|
-
loadingMore.add(key);
|
|
1885
|
-
setLoading({
|
|
1886
|
-
isLoadingMore: true,
|
|
1887
|
-
loadMoreError: void 0
|
|
1888
|
-
});
|
|
1889
|
-
const isCurrent = () => {
|
|
1890
|
-
const latest = port.read(key);
|
|
1891
|
-
return resource.scope.get() === scope && getSlot().entries[key]?.query === query && latest?.currentPage === depth;
|
|
1892
|
-
};
|
|
1893
|
-
try {
|
|
1894
|
-
const page = await fetcher(params, cursor);
|
|
1895
|
-
if (!isCurrent()) return void setLoading({ isLoadingMore: false });
|
|
1896
|
-
update(key, (data) => data && applyNextPage(data, page, at(key), pagingCtx), { persist: (paging.persist?.pages ?? 1) > 1 });
|
|
1897
|
-
} catch (error) {
|
|
1898
|
-
setLoading({
|
|
1899
|
-
isLoadingMore: false,
|
|
1900
|
-
loadMoreError: isCurrent() ? error : void 0
|
|
1901
|
-
});
|
|
1902
|
-
} finally {
|
|
1903
|
-
loadingMore.delete(key);
|
|
1904
|
-
}
|
|
1905
|
-
};
|
|
1906
|
-
/** Insert rows at the head (new / streamed items). */
|
|
1907
|
-
const insertHead = (key, items, { persist = false } = {}) => paging ? update(key, (data) => data && insertHeadItems(data, items, at(key)), { persist }) : false;
|
|
1908
|
-
/** Drop loaded pages, keeping the head (e.g. after an edit inside older pages). */
|
|
1909
|
-
const collapse = (key) => paging ? update(key, (data) => data && collapseToHead(data, at(key)), { persist: false }) : false;
|
|
1960
|
+
const pagination = createPagination({
|
|
1961
|
+
fetcher,
|
|
1962
|
+
getSlot,
|
|
1963
|
+
loadingMore,
|
|
1964
|
+
options,
|
|
1965
|
+
port,
|
|
1966
|
+
resource,
|
|
1967
|
+
update
|
|
1968
|
+
});
|
|
1969
|
+
const { collapse, insertHead, loadMore } = pagination;
|
|
1910
1970
|
const { beginEntityOptimistic, entityKeys, forgetRestoredEntities, patchStoredEntity, removeEntity, updateEntity, withoutRemovedEntities } = createEntityPropagation({
|
|
1911
1971
|
beginOptimistic,
|
|
1912
1972
|
entity: options.entity,
|
|
@@ -1953,34 +2013,6 @@ const createReplicaEngine = (resource, options) => {
|
|
|
1953
2013
|
updateMany
|
|
1954
2014
|
};
|
|
1955
2015
|
};
|
|
1956
|
-
|
|
1957
|
-
/**
|
|
1958
|
-
* Keys of the queries a fetch adapter runs for a replica. Hosts that sweep
|
|
1959
|
-
* their query cache by prefix (e.g. "refetch everything after a restore")
|
|
1960
|
-
* match on {@link REPLICA_KEY_PREFIX}.
|
|
1961
|
-
*/
|
|
1962
|
-
const REPLICA_KEY_PREFIX = "replica:";
|
|
1963
|
-
const replicaKeys = {
|
|
1964
|
-
/** One-shot read of the persisted row for `storageKey`. */
|
|
1965
|
-
hydrate: (name, version, scope, storageKey) => [
|
|
1966
|
-
"replica:hydrate",
|
|
1967
|
-
name,
|
|
1968
|
-
version,
|
|
1969
|
-
scope,
|
|
1970
|
-
storageKey
|
|
1971
|
-
],
|
|
1972
|
-
/** Network sync of the head of one entry. */
|
|
1973
|
-
sync: (name, version, scope, key, params) => [
|
|
1974
|
-
"replica:sync",
|
|
1975
|
-
name,
|
|
1976
|
-
version,
|
|
1977
|
-
scope,
|
|
1978
|
-
key,
|
|
1979
|
-
params
|
|
1980
|
-
]
|
|
1981
|
-
};
|
|
1982
|
-
/** Match a resource's sync keys, optionally narrowed to one scope and/or entry key. */
|
|
1983
|
-
const isReplicaSyncKey = (queryKey, name, filter = {}) => Array.isArray(queryKey) && queryKey[0] === "replica:sync" && queryKey[1] === name && (filter.scope === void 0 || queryKey[3] === filter.scope) && (filter.key === void 0 || queryKey[4] === filter.key);
|
|
1984
|
-
export { isReplicaStorageKeyOf as C, staticReplicaScope as E, defineReplica as S, stableQueryKey as T, joinPages as _, DEFAULT_REMOVAL_TOMBSTONE_LIMIT as a, toPersistedPage as b, REPLICA_INDEX_KEY as c, applyHeadPage as d, applyNextPage as f, insertHeadItems as g, hasPagedItem as h, createReplicaEngine as i, ReplicaWriteQueue as l, getNextPageCursor as m, isReplicaSyncKey as n, createReplicaState as o, collapseToHead as p, replicaKeys as r, replicaReducer as s, REPLICA_KEY_PREFIX as t, readReplicaRowStrict as u, mapPagedItem as v, replicaStorageKey as w, definePagedReplica as x, pagingFor as y };
|
|
2016
|
+
export { stableQueryKey as S, REPLICA_KEY_PREFIX as _, ReplicaWriteQueue as a, isReplicaStorageKeyOf as b, applyNextPage as c, hasPagedItem as d, insertHeadItems as f, toPersistedPage as g, pagingFor as h, REPLICA_INDEX_KEY as i, collapseToHead as l, mapPagedItem as m, createReplicaState as n, readReplicaRowStrict as o, joinPages as p, replicaReducer as r, applyHeadPage as s, createReplicaEngine as t, getNextPageCursor as u, isReplicaSyncKey as v, replicaStorageKey as x, replicaKeys as y };
|
|
1985
2017
|
|
|
1986
|
-
//# sourceMappingURL=
|
|
2018
|
+
//# sourceMappingURL=createReplicaEngine-CdR-URHo.js.map
|