@drakkar.software/starfish-client 3.0.0-alpha.31 → 3.0.0-alpha.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/bindings/zustand.d.ts +13 -1
- package/dist/bindings/zustand.js +134 -73
- package/dist/bindings/zustand.js.map +2 -2
- package/dist/client.d.ts +38 -6
- package/dist/index.js +115 -73
- package/dist/index.js.map +2 -2
- package/dist/sync.d.ts +12 -0
- package/dist/types.d.ts +13 -7
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -211,7 +211,8 @@ Pass a `cache` (a `PullCache`: `{ get(k): Promise<string|null>; set(k, v): Promi
|
|
|
211
211
|
- **Write-through:** a successful pull stores the raw `{data, hash, timestamp}` keyed by document path.
|
|
212
212
|
- **Offline fallback (without a persist-backed store):** a pull that fails because the **transport** is unreachable returns the last cached snapshot, tagged so callers can tell it's stale (`pullWasFromCache(result)`).
|
|
213
213
|
- **Real HTTP errors propagate:** 404/403 are genuine server answers — the cache is *not* consulted, so "no document yet" and "access denied" keep their meaning. 429 and 5xx can optionally be caught via `cacheFallbackStatuses` (see below).
|
|
214
|
-
- **
|
|
214
|
+
- **Error-triggered stale-while-revalidate:** set `cacheFallbackStatuses: [429, 500, 502, 503, 504]` to make transient server failures serve the last-synced snapshot immediately and retry in the background (honoring `Retry-After`). When the live response arrives, the cache is updated and `onRevalidated` fires. No snapshot → the error propagates as usual. Do NOT include 403/404 — they are genuine answers, not transient failures.
|
|
215
|
+
- **Proactive stale-while-revalidate (`staleWhileRevalidate` pull option):** pass `{ staleWhileRevalidate: true }` to `client.pull(path, { staleWhileRevalidate: true })` to serve the cached snapshot immediately and revalidate in the background on every read (not just on errors). On a cache hit the cached result is returned at once (tagged via `pullWasFromCache`) and the background fetch fires immediately (no initial delay). On a miss it falls through to a normal network-first pull. `onRevalidated` fires on success with the fresh `PullResult`. Both SWR paths share the same dedup loop — a concurrent error-triggered loop and an SWR-on-read loop for the same document collapse to one.
|
|
215
216
|
- **`cacheMaxAgeMs`:** an entry older than this is treated as a miss (both cache-first paint and offline fallback); omit for entries that never expire (recommended for offline-first, where any last-synced data beats none).
|
|
216
217
|
- **Ciphertext-at-rest by construction:** the cache stores the raw server payload, which for E2E (`delegated`) collections is the sealed ciphertext the server holds — never the decrypted form. Decryption happens in memory on read (see `SyncManager.seedFromCache`).
|
|
217
218
|
- **`client.peekCache(path)`** reads the cached snapshot *without* a network round-trip — the basis for cache-first paint.
|
|
@@ -233,6 +234,9 @@ new SyncManager({
|
|
|
233
234
|
- `encryptor` is the only encryption option — the v2 single-secret `encryptionSecret`/`encryptionSalt` shorthand was removed in v3.
|
|
234
235
|
- `onConflict` resolves write conflicts on push *and* reconciles a pull against un-pushed local writes. On a zustand-bound store, a `pull()` while the store is `dirty` merges the fetched snapshot with the local data through this resolver (rather than overwriting it), so an optimistic write isn't lost when a pull races a `set()`. Use a union/CRDT-style resolver (`createUnionMerge`) for append-style collections so both the local and remote writes survive; `SyncManager.resolve(local, remote)` exposes the same merge for callers that need it.
|
|
235
236
|
- **Offline-first:** Zustand stores backed by `storage` are offline-first without any client `cache` — a transport failure during `pull()` preserves the persisted data and sets `stale: true` on the store. When a client `cache` is also configured, `seedFromCache()` additionally populates `localData` from the ciphertext cache without a network round-trip (decrypting in memory for E2E collections); `getLastPullFromCache()` reports whether the latest `pull()`/seed came from that cache. On a zustand store, the `stale` flag tracks both sources: it is set by an offline `pull()` (no cache needed) and by `seed()` (cache-first paint before the initial live pull).
|
|
237
|
+
- **`SyncManager.ingest(result: PullResult)`** applies an externally-delivered `PullResult` to the manager's state (decrypting for E2E, updating `localData`/`lastHash`/`lastCheckpoint`, clearing `lastFromCache`) without a network call. Used by the zustand binding's `mergeResult` action to absorb background revalidation results.
|
|
238
|
+
|
|
239
|
+
**Auto-merge on revalidation:** when `cacheFallbackStatuses` or `staleWhileRevalidate` triggers a background revalidation, `useSyncInit` and `acquireSyncStore` automatically push the fresh `PullResult` into the bound store via `mergeResult` — the store repaints without waiting for the next explicit `pull()`. The consumer's own `onRevalidated` callback is still called after the store update. The `mergeResult` store action is also available for manual use when a caller holds a fresh `PullResult` it wants to push into the store without an extra round-trip.
|
|
236
240
|
|
|
237
241
|
## `AppendLogCursor`
|
|
238
242
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type StoreApi } from "zustand/vanilla";
|
|
2
2
|
import { type StateStorage } from "zustand/middleware";
|
|
3
3
|
import type { DevtoolsOptions } from "zustand/middleware";
|
|
4
|
-
import type { Encryptor } from "@drakkar.software/starfish-protocol";
|
|
4
|
+
import type { Encryptor, PullResult } from "@drakkar.software/starfish-protocol";
|
|
5
5
|
import { SyncManager } from "../sync.js";
|
|
6
6
|
import { AppendLogCursor, type AppendElement } from "../append-log.js";
|
|
7
7
|
import type { StarfishClientOptions, StarfishCapProvider, ConflictResolver, PullCache } from "../types.js";
|
|
@@ -39,6 +39,18 @@ export interface StarfishActions {
|
|
|
39
39
|
* pull; the live pull then supersedes the seeded snapshot.
|
|
40
40
|
*/
|
|
41
41
|
seed: () => Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Apply a freshly-fetched `PullResult` to the store WITHOUT firing a network
|
|
44
|
+
* request. Decrypts in memory for E2E collections, conflict-merges against
|
|
45
|
+
* any local optimistic writes (same logic as a live pull), and clears `stale`.
|
|
46
|
+
*
|
|
47
|
+
* Primarily called automatically by the binding when
|
|
48
|
+
* {@link StarfishClientOptions.onRevalidated} fires (background revalidation
|
|
49
|
+
* delivered a fresh snapshot after a 429/5xx hit or an SWR-on-read). Also
|
|
50
|
+
* available for manual use when a caller holds a fresh `PullResult` it wants
|
|
51
|
+
* to push into the store without a second network round-trip.
|
|
52
|
+
*/
|
|
53
|
+
mergeResult: (result: PullResult) => Promise<void>;
|
|
42
54
|
}
|
|
43
55
|
export type StarfishStore = StarfishState & StarfishActions;
|
|
44
56
|
export interface CreateStarfishStoreOptions {
|
package/dist/bindings/zustand.js
CHANGED
|
@@ -451,11 +451,12 @@ var StarfishClient = class {
|
|
|
451
451
|
async pull(path, checkpointOrOptions) {
|
|
452
452
|
let pathAndQuery = this.applyNamespace(path);
|
|
453
453
|
let appendField;
|
|
454
|
+
let swr = false;
|
|
454
455
|
if (typeof checkpointOrOptions === "number") {
|
|
455
456
|
if (checkpointOrOptions) pathAndQuery += `?checkpoint=${checkpointOrOptions}`;
|
|
456
457
|
} else if (checkpointOrOptions != null) {
|
|
457
458
|
const opts = checkpointOrOptions;
|
|
458
|
-
const isPullOptions = opts.withKeyring !== void 0 || opts.checkpoint !== void 0;
|
|
459
|
+
const isPullOptions = opts.withKeyring !== void 0 || opts.checkpoint !== void 0 || opts.staleWhileRevalidate !== void 0;
|
|
459
460
|
const params = new URLSearchParams();
|
|
460
461
|
if (isPullOptions) {
|
|
461
462
|
if (opts.checkpoint != null && opts.checkpoint > 0) {
|
|
@@ -464,6 +465,7 @@ var StarfishClient = class {
|
|
|
464
465
|
if (opts.withKeyring) {
|
|
465
466
|
params.set("withKeyring", "1");
|
|
466
467
|
}
|
|
468
|
+
swr = opts.staleWhileRevalidate === true;
|
|
467
469
|
} else {
|
|
468
470
|
appendField = opts.appendField ?? APPEND_DEFAULT_FIELD;
|
|
469
471
|
if (opts.full && (opts.since != null || opts.limit != null || opts.last != null)) {
|
|
@@ -490,6 +492,19 @@ var StarfishClient = class {
|
|
|
490
492
|
const url = `${this.baseUrl}${pathAndQuery}`;
|
|
491
493
|
const authHeaders = await this.buildAuthHeaders("GET", pathAndQuery, void 0);
|
|
492
494
|
const cacheKey = this.cache && appendField === void 0 ? pullCacheKey(pathAndQuery) : void 0;
|
|
495
|
+
if (swr && cacheKey) {
|
|
496
|
+
const cached = await this.readCache(cacheKey);
|
|
497
|
+
if (cached) {
|
|
498
|
+
this.scheduleRevalidate(
|
|
499
|
+
cacheKey,
|
|
500
|
+
pathAndQuery,
|
|
501
|
+
null,
|
|
502
|
+
/* immediate */
|
|
503
|
+
true
|
|
504
|
+
);
|
|
505
|
+
return cached;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
493
508
|
let res;
|
|
494
509
|
try {
|
|
495
510
|
res = await this.fetch(url, {
|
|
@@ -521,67 +536,81 @@ var StarfishClient = class {
|
|
|
521
536
|
const list = result.data?.[appendField];
|
|
522
537
|
return Array.isArray(list) ? list : [];
|
|
523
538
|
}
|
|
524
|
-
if (cacheKey)
|
|
525
|
-
const snapshot = {
|
|
526
|
-
data: result.data,
|
|
527
|
-
hash: result.hash,
|
|
528
|
-
timestamp: result.timestamp,
|
|
529
|
-
cachedAt: Date.now()
|
|
530
|
-
};
|
|
531
|
-
void this.cache.set(cacheKey, JSON.stringify(snapshot)).catch(() => {
|
|
532
|
-
});
|
|
533
|
-
}
|
|
539
|
+
if (cacheKey) this.writeCache(cacheKey, result);
|
|
534
540
|
return result;
|
|
535
541
|
}
|
|
536
|
-
/**
|
|
537
|
-
|
|
542
|
+
/**
|
|
543
|
+
* Write a pull snapshot to the cache. Fire-and-forget; errors are swallowed
|
|
544
|
+
* so a failing cache never blocks the caller. No-op when no cache is configured.
|
|
545
|
+
*/
|
|
546
|
+
writeCache(cacheKey, result) {
|
|
547
|
+
if (!this.cache) return;
|
|
548
|
+
const snapshot = {
|
|
549
|
+
data: result.data,
|
|
550
|
+
hash: result.hash,
|
|
551
|
+
timestamp: result.timestamp,
|
|
552
|
+
cachedAt: Date.now()
|
|
553
|
+
};
|
|
554
|
+
void this.cache.set(cacheKey, JSON.stringify(snapshot)).catch(() => {
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
/** Build the URL + auth headers for one revalidation GET. Shared between
|
|
558
|
+
* {@link pull} and {@link revalidateLoop} to avoid duplicated fetch setup. */
|
|
559
|
+
async revalidateFetch(pathAndQuery) {
|
|
560
|
+
const url = `${this.baseUrl}${pathAndQuery}`;
|
|
561
|
+
const authHeaders = await this.buildAuthHeaders("GET", pathAndQuery, void 0);
|
|
562
|
+
return this.fetch(url, {
|
|
563
|
+
method: "GET",
|
|
564
|
+
headers: { [HEADER_ACCEPT]: "application/json", ...authHeaders }
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Deduplicated fire-and-forget: starts one revalidation loop per cacheKey.
|
|
569
|
+
* Used by both the {@link cacheFallbackStatuses} error path (delayed first
|
|
570
|
+
* attempt, honoring `Retry-After`) and the {@link PullOptions.staleWhileRevalidate}
|
|
571
|
+
* read path (`immediate: true` — no initial delay on the first attempt). The
|
|
572
|
+
* `revalidating` set deduplicates across both triggers so a concurrent
|
|
573
|
+
* error-triggered loop and an SWR-on-read loop for the same key collapse to one.
|
|
574
|
+
*/
|
|
575
|
+
scheduleRevalidate(cacheKey, pathAndQuery, retryAfterHeader, immediate = false) {
|
|
538
576
|
if (this.revalidating.has(cacheKey)) return;
|
|
539
577
|
this.revalidating.add(cacheKey);
|
|
540
|
-
void this.revalidateLoop(cacheKey, pathAndQuery, retryAfterHeader).finally(() => {
|
|
578
|
+
void this.revalidateLoop(cacheKey, pathAndQuery, retryAfterHeader, immediate).finally(() => {
|
|
541
579
|
this.revalidating.delete(cacheKey);
|
|
542
580
|
});
|
|
543
581
|
}
|
|
544
582
|
/**
|
|
545
|
-
* Background revalidation loop
|
|
546
|
-
*
|
|
547
|
-
*
|
|
548
|
-
*
|
|
549
|
-
*
|
|
583
|
+
* Background revalidation loop shared by both {@link cacheFallbackStatuses}
|
|
584
|
+
* hits and {@link PullOptions.staleWhileRevalidate} reads.
|
|
585
|
+
*
|
|
586
|
+
* Retries (honoring `Retry-After`) up to {@link MAX_REVALIDATE_ATTEMPTS} times.
|
|
587
|
+
* When `immediate` is true the first attempt fires without any initial delay
|
|
588
|
+
* (SWR-on-read path). On a live 2xx the fresh snapshot is written to cache and
|
|
589
|
+
* {@link onRevalidated} fires. Stops early on a non-fallback status (403/404).
|
|
550
590
|
*/
|
|
551
|
-
async revalidateLoop(cacheKey, pathAndQuery, firstRetryAfter) {
|
|
591
|
+
async revalidateLoop(cacheKey, pathAndQuery, firstRetryAfter, immediate = false) {
|
|
552
592
|
let retryAfterHeader = firstRetryAfter;
|
|
593
|
+
const fallbackSet = this.cacheFallbackStatuses ? new Set(this.cacheFallbackStatuses) : null;
|
|
553
594
|
for (let attempt = 0; attempt < MAX_REVALIDATE_ATTEMPTS; attempt++) {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
await sleep(delay);
|
|
562
|
-
try {
|
|
563
|
-
const url = `${this.baseUrl}${pathAndQuery}`;
|
|
564
|
-
const authHeaders = await this.buildAuthHeaders("GET", pathAndQuery, void 0);
|
|
565
|
-
const res = await this.fetch(url, {
|
|
566
|
-
method: "GET",
|
|
567
|
-
headers: { [HEADER_ACCEPT]: "application/json", ...authHeaders }
|
|
595
|
+
if (!immediate || attempt > 0) {
|
|
596
|
+
const delay = parseRetryAfterMs(retryAfterHeader, {
|
|
597
|
+
fallbackMs: Math.min(
|
|
598
|
+
REVALIDATE_INITIAL_DELAY_MS * Math.pow(2, attempt),
|
|
599
|
+
REVALIDATE_MAX_DELAY_MS
|
|
600
|
+
),
|
|
601
|
+
maxMs: REVALIDATE_MAX_DELAY_MS
|
|
568
602
|
});
|
|
603
|
+
await sleep(delay);
|
|
604
|
+
}
|
|
605
|
+
try {
|
|
606
|
+
const res = await this.revalidateFetch(pathAndQuery);
|
|
569
607
|
if (res.ok) {
|
|
570
608
|
const result = await res.json();
|
|
571
|
-
|
|
572
|
-
const snapshot = {
|
|
573
|
-
data: result.data,
|
|
574
|
-
hash: result.hash,
|
|
575
|
-
timestamp: result.timestamp,
|
|
576
|
-
cachedAt: Date.now()
|
|
577
|
-
};
|
|
578
|
-
void this.cache.set(cacheKey, JSON.stringify(snapshot)).catch(() => {
|
|
579
|
-
});
|
|
580
|
-
}
|
|
609
|
+
this.writeCache(cacheKey, result);
|
|
581
610
|
this.onRevalidated?.(pathAndQuery, result);
|
|
582
611
|
return;
|
|
583
612
|
}
|
|
584
|
-
if (!
|
|
613
|
+
if (!fallbackSet?.has(res.status)) {
|
|
585
614
|
return;
|
|
586
615
|
}
|
|
587
616
|
retryAfterHeader = res.headers.get("Retry-After");
|
|
@@ -703,15 +732,7 @@ var StarfishClient = class {
|
|
|
703
732
|
const result = await res.json();
|
|
704
733
|
if (this.cache) {
|
|
705
734
|
const pullPath = sendPath.replace("/push/", "/pull/");
|
|
706
|
-
|
|
707
|
-
const snapshot = {
|
|
708
|
-
data,
|
|
709
|
-
hash: result.hash,
|
|
710
|
-
timestamp: result.timestamp,
|
|
711
|
-
cachedAt: Date.now()
|
|
712
|
-
};
|
|
713
|
-
void this.cache.set(cacheKey, JSON.stringify(snapshot)).catch(() => {
|
|
714
|
-
});
|
|
735
|
+
this.writeCache(pullCacheKey(pullPath), { data, hash: result.hash, timestamp: result.timestamp });
|
|
715
736
|
}
|
|
716
737
|
return result;
|
|
717
738
|
}
|
|
@@ -985,6 +1006,30 @@ var SyncManager = class {
|
|
|
985
1006
|
getCheckpoint() {
|
|
986
1007
|
return this.lastCheckpoint;
|
|
987
1008
|
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Apply a freshly-fetched `PullResult` to this manager's state WITHOUT
|
|
1011
|
+
* firing a network request. Used by the zustand binding's `mergeResult`
|
|
1012
|
+
* action to absorb a background revalidation result (delivered via
|
|
1013
|
+
* {@link StarfishClientOptions.onRevalidated}) into the store.
|
|
1014
|
+
*
|
|
1015
|
+
* Unlike {@link pull}, `ingest` never does a deep-merge with the previous
|
|
1016
|
+
* checkpoint — the revalidated result is always a full fresh snapshot. It
|
|
1017
|
+
* sets `lastFromCache = false` (a revalidation is a live response) so the
|
|
1018
|
+
* binding can clear its `stale` flag.
|
|
1019
|
+
*/
|
|
1020
|
+
async ingest(result) {
|
|
1021
|
+
if (this.aborted) return;
|
|
1022
|
+
if (this.encryptor) {
|
|
1023
|
+
const decrypted = await this.encryptor.decrypt(result.data);
|
|
1024
|
+
if (this.aborted) return;
|
|
1025
|
+
this.localData = decrypted;
|
|
1026
|
+
} else {
|
|
1027
|
+
this.localData = result.data;
|
|
1028
|
+
}
|
|
1029
|
+
this.lastHash = result.hash;
|
|
1030
|
+
this.lastCheckpoint = result.timestamp;
|
|
1031
|
+
this.lastFromCache = false;
|
|
1032
|
+
}
|
|
988
1033
|
async pull() {
|
|
989
1034
|
if (this.aborted) throw new AbortError();
|
|
990
1035
|
this.logger?.pullStart(this.loggerName);
|
|
@@ -1183,6 +1228,14 @@ function createStarfishStore(options) {
|
|
|
1183
1228
|
retryTimer = void 0;
|
|
1184
1229
|
retryAttempt = 0;
|
|
1185
1230
|
};
|
|
1231
|
+
const commitRemote = (label) => {
|
|
1232
|
+
const remote = syncManager.getData();
|
|
1233
|
+
const newData = get().dirty ? syncManager.resolve(get().data, remote) : remote;
|
|
1234
|
+
set({ data: newData, syncing: false, hash: syncManager.getHash(), stale: syncManager.getLastPullFromCache() }, false, label);
|
|
1235
|
+
if (get().online && get().dirty) get().flush().catch(() => {
|
|
1236
|
+
});
|
|
1237
|
+
options.onRemoteUpdate?.(newData);
|
|
1238
|
+
};
|
|
1186
1239
|
return {
|
|
1187
1240
|
data: {},
|
|
1188
1241
|
syncing: false,
|
|
@@ -1201,15 +1254,10 @@ function createStarfishStore(options) {
|
|
|
1201
1254
|
}
|
|
1202
1255
|
},
|
|
1203
1256
|
pull: async () => {
|
|
1204
|
-
set({ syncing: true, error: null }, false, "pull/start");
|
|
1257
|
+
set(get().stale ? { error: null } : { syncing: true, error: null }, false, "pull/start");
|
|
1205
1258
|
try {
|
|
1206
1259
|
await syncManager.pull();
|
|
1207
|
-
|
|
1208
|
-
const newData = get().dirty ? syncManager.resolve(get().data, remote) : remote;
|
|
1209
|
-
set({ data: newData, syncing: false, hash: syncManager.getHash(), stale: syncManager.getLastPullFromCache() }, false, "pull/success");
|
|
1210
|
-
if (get().online && get().dirty) get().flush().catch(() => {
|
|
1211
|
-
});
|
|
1212
|
-
options.onRemoteUpdate?.(newData);
|
|
1260
|
+
commitRemote("pull/success");
|
|
1213
1261
|
} catch (err) {
|
|
1214
1262
|
if (classifyError(err) === "network") {
|
|
1215
1263
|
set({ syncing: false, stale: true }, false, "pull/offline");
|
|
@@ -1218,6 +1266,10 @@ function createStarfishStore(options) {
|
|
|
1218
1266
|
set({ syncing: false, error: err instanceof Error ? err.message : String(err) }, false, "pull/error");
|
|
1219
1267
|
}
|
|
1220
1268
|
},
|
|
1269
|
+
mergeResult: async (result) => {
|
|
1270
|
+
await syncManager.ingest(result);
|
|
1271
|
+
commitRemote("merge/success");
|
|
1272
|
+
},
|
|
1221
1273
|
set: (modifier) => {
|
|
1222
1274
|
try {
|
|
1223
1275
|
const next = options.produce ? options.produce(get().data, modifier) : modifier(get().data);
|
|
@@ -1362,10 +1414,7 @@ function useSyncInit(config) {
|
|
|
1362
1414
|
const onDataRef = useRef(config?.onData);
|
|
1363
1415
|
onDataRef.current = config?.onData;
|
|
1364
1416
|
useEffect(() => {
|
|
1365
|
-
if (!config)
|
|
1366
|
-
setStore(null);
|
|
1367
|
-
return;
|
|
1368
|
-
}
|
|
1417
|
+
if (!config) return;
|
|
1369
1418
|
const client = new StarfishClient({
|
|
1370
1419
|
baseUrl: config.serverUrl,
|
|
1371
1420
|
namespace: config.namespace,
|
|
@@ -1374,7 +1423,15 @@ function useSyncInit(config) {
|
|
|
1374
1423
|
cache: config.cache,
|
|
1375
1424
|
cacheMaxAgeMs: config.cacheMaxAgeMs,
|
|
1376
1425
|
cacheFallbackStatuses: config.cacheFallbackStatuses,
|
|
1377
|
-
|
|
1426
|
+
// Auto-merge: when a background revalidation delivers a fresh snapshot,
|
|
1427
|
+
// push it into the store so the UI heals without waiting for the next pull.
|
|
1428
|
+
// newStore is referenced by closure — safe because onRevalidated only fires
|
|
1429
|
+
// asynchronously, well after the store is created below.
|
|
1430
|
+
onRevalidated: (path, result) => {
|
|
1431
|
+
newStore.getState().mergeResult(result).catch(() => {
|
|
1432
|
+
});
|
|
1433
|
+
config.onRevalidated?.(path, result);
|
|
1434
|
+
}
|
|
1378
1435
|
});
|
|
1379
1436
|
const syncManager = new SyncManager({
|
|
1380
1437
|
client,
|
|
@@ -1416,7 +1473,7 @@ function useSyncInit(config) {
|
|
|
1416
1473
|
config?.encryptor,
|
|
1417
1474
|
config?.storeName
|
|
1418
1475
|
]);
|
|
1419
|
-
return store;
|
|
1476
|
+
return config ? store : null;
|
|
1420
1477
|
}
|
|
1421
1478
|
var _syncStoreRegistry = /* @__PURE__ */ new Map();
|
|
1422
1479
|
function acquireSyncStore(config) {
|
|
@@ -1433,7 +1490,14 @@ function acquireSyncStore(config) {
|
|
|
1433
1490
|
cache: config.cache,
|
|
1434
1491
|
cacheMaxAgeMs: config.cacheMaxAgeMs,
|
|
1435
1492
|
cacheFallbackStatuses: config.cacheFallbackStatuses,
|
|
1436
|
-
|
|
1493
|
+
// Auto-merge: push fresh revalidated snapshots into the store.
|
|
1494
|
+
// store is referenced by closure — safe because onRevalidated only fires
|
|
1495
|
+
// asynchronously, well after the store is created below.
|
|
1496
|
+
onRevalidated: (path, result) => {
|
|
1497
|
+
store.getState().mergeResult(result).catch(() => {
|
|
1498
|
+
});
|
|
1499
|
+
config.onRevalidated?.(path, result);
|
|
1500
|
+
}
|
|
1437
1501
|
});
|
|
1438
1502
|
const syncManager = new SyncManager({
|
|
1439
1503
|
client,
|
|
@@ -1475,10 +1539,7 @@ function useSharedSyncStore(config) {
|
|
|
1475
1539
|
const configRef = useRef(config);
|
|
1476
1540
|
configRef.current = config;
|
|
1477
1541
|
useEffect(() => {
|
|
1478
|
-
if (!storeName)
|
|
1479
|
-
setStore(null);
|
|
1480
|
-
return;
|
|
1481
|
-
}
|
|
1542
|
+
if (!storeName) return;
|
|
1482
1543
|
const acquired = acquireSyncStore(configRef.current);
|
|
1483
1544
|
setStore(acquired);
|
|
1484
1545
|
return () => {
|
|
@@ -1486,7 +1547,7 @@ function useSharedSyncStore(config) {
|
|
|
1486
1547
|
setStore(null);
|
|
1487
1548
|
};
|
|
1488
1549
|
}, [storeName]);
|
|
1489
|
-
return store;
|
|
1550
|
+
return storeName ? store : null;
|
|
1490
1551
|
}
|
|
1491
1552
|
function createStarfishLog(options) {
|
|
1492
1553
|
const { cursor } = options;
|