@drakkar.software/starfish-client 3.0.0-alpha.35 → 3.0.0-alpha.37

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.
@@ -119,6 +119,8 @@ export declare function deriveSyncStatus(state: StarfishState): SyncStatus;
119
119
  export declare function aggregateSyncStatus(statuses: SyncStatus[]): SyncStatus;
120
120
  /** Use the full Starfish store state and actions. */
121
121
  export declare function useStarfish(store: StoreApi<StarfishStore>): StarfishStore;
122
+ /** Subscribe to a fine-grained slice of Starfish store state. Avoids re-renders on unrelated field changes. */
123
+ export declare function useStarfishState<T>(store: StoreApi<StarfishStore>, selector: (state: StarfishState) => T): T;
122
124
  /** Use only the synced data, with an optional selector for fine-grained subscriptions. */
123
125
  export declare function useStarfishData<T = Record<string, unknown>>(store: StoreApi<StarfishStore>, selector?: (data: Record<string, unknown>) => T): T;
124
126
  /** Use the derived sync status (synced | syncing | pending | error | offline). */
@@ -335,6 +335,12 @@ var StarfishClient = class {
335
335
  cacheFallbackStatuses;
336
336
  onRevalidated;
337
337
  revalidating = /* @__PURE__ */ new Set();
338
+ /**
339
+ * In-memory mirror of the latest document timestamp written to each cache
340
+ * key via {@link writeCache}. Updated synchronously so {@link revalidateLoop}
341
+ * can guard against stale overwrites without an extra async cache read.
342
+ */
343
+ latestCacheTimestamp = /* @__PURE__ */ new Map();
338
344
  /**
339
345
  * Installed client-side plugins. Currently stored as inert data; no
340
346
  * hooks fire yet. Extensions can inspect this list if needed.
@@ -545,6 +551,9 @@ var StarfishClient = class {
545
551
  */
546
552
  writeCache(cacheKey, result) {
547
553
  if (!this.cache) return;
554
+ if (result.timestamp > (this.latestCacheTimestamp.get(cacheKey) ?? -1)) {
555
+ this.latestCacheTimestamp.set(cacheKey, result.timestamp);
556
+ }
548
557
  const snapshot = {
549
558
  data: result.data,
550
559
  hash: result.hash,
@@ -606,8 +615,11 @@ var StarfishClient = class {
606
615
  const res = await this.revalidateFetch(pathAndQuery);
607
616
  if (res.ok) {
608
617
  const result = await res.json();
609
- this.writeCache(cacheKey, result);
610
- this.onRevalidated?.(pathAndQuery, result);
618
+ const latestTs = this.latestCacheTimestamp.get(cacheKey) ?? -1;
619
+ if (result.timestamp >= latestTs) {
620
+ this.writeCache(cacheKey, result);
621
+ this.onRevalidated?.(pathAndQuery, result);
622
+ }
611
623
  return;
612
624
  }
613
625
  if (!fallbackSet?.has(res.status)) {
@@ -1012,20 +1024,32 @@ var SyncManager = class {
1012
1024
  * action to absorb a background revalidation result (delivered via
1013
1025
  * {@link StarfishClientOptions.onRevalidated}) into the store.
1014
1026
  *
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.
1027
+ * Like {@link pull}, `ingest` conflict-merges the snapshot against the
1028
+ * established baseline via `this.onConflict` when a checkpoint exists so a
1029
+ * union-merge store does not lose array items when a revalidation result
1030
+ * (e.g. a stale cache-fallback on 429/5xx) is a shorter snapshot. The first
1031
+ * ingest (no checkpoint yet) takes the snapshot wholesale. Sets
1032
+ * `lastFromCache = false` (a revalidation is a live response) so the binding
1033
+ * can clear its `stale` flag.
1034
+ *
1035
+ * **Staleness guard**: if a `push()` advanced `lastCheckpoint` between the
1036
+ * time the revalidation request was sent and the time it resolves, the
1037
+ * result is from an older document version. Ingesting it would clobber the
1038
+ * user's just-saved edit and reset `lastHash` to a stale server hash
1039
+ * (causing a spurious 409 on the next push). We silently drop the result in
1040
+ * that case — the store's post-push state is already correct.
1019
1041
  */
1020
1042
  async ingest(result) {
1021
1043
  if (this.aborted) return;
1044
+ if (result.timestamp < this.lastCheckpoint) return;
1045
+ let incoming;
1022
1046
  if (this.encryptor) {
1023
- const decrypted = await this.encryptor.decrypt(result.data);
1047
+ incoming = await this.encryptor.decrypt(result.data);
1024
1048
  if (this.aborted) return;
1025
- this.localData = decrypted;
1026
1049
  } else {
1027
- this.localData = result.data;
1050
+ incoming = result.data;
1028
1051
  }
1052
+ this.localData = this.lastCheckpoint > 0 ? this.onConflict(this.localData, incoming) : incoming;
1029
1053
  this.lastHash = result.hash;
1030
1054
  this.lastCheckpoint = result.timestamp;
1031
1055
  this.lastFromCache = false;
@@ -1038,17 +1062,15 @@ var SyncManager = class {
1038
1062
  const result = await this.client.pull(this.pullPath, this.lastCheckpoint);
1039
1063
  if (this.aborted) throw new AbortError();
1040
1064
  this.lastFromCache = pullWasFromCache(result);
1065
+ let incoming;
1041
1066
  if (this.encryptor) {
1042
- const decrypted = await this.encryptor.decrypt(result.data);
1067
+ incoming = await this.encryptor.decrypt(result.data);
1043
1068
  if (this.aborted) throw new AbortError();
1044
- this.localData = decrypted;
1045
- result.data = decrypted;
1046
- } else if (this.lastCheckpoint > 0) {
1047
- this.localData = deepMerge(this.localData, result.data);
1048
- result.data = this.localData;
1049
1069
  } else {
1050
- this.localData = result.data;
1070
+ incoming = result.data;
1051
1071
  }
1072
+ this.localData = this.lastCheckpoint > 0 ? this.onConflict(this.localData, incoming) : incoming;
1073
+ result.data = this.localData;
1052
1074
  this.lastHash = result.hash;
1053
1075
  this.lastCheckpoint = result.timestamp;
1054
1076
  this.logger?.pullSuccess(this.loggerName, Math.round(performance.now() - start));
@@ -1343,6 +1365,9 @@ function aggregateSyncStatus(statuses) {
1343
1365
  function useStarfish(store) {
1344
1366
  return useStore(store);
1345
1367
  }
1368
+ function useStarfishState(store, selector) {
1369
+ return useStore(store, selector);
1370
+ }
1346
1371
  function useStarfishData(store, selector) {
1347
1372
  return useStore(
1348
1373
  store,
@@ -1403,7 +1428,7 @@ function useLastSynced(store) {
1403
1428
  }, [store, computeLabel]);
1404
1429
  useEffect(() => {
1405
1430
  const timer = setInterval(() => {
1406
- setLabel(computeLabel());
1431
+ if (!document.hidden) setLabel(computeLabel());
1407
1432
  }, 5e3);
1408
1433
  return () => clearInterval(timer);
1409
1434
  }, [computeLabel]);
@@ -1648,6 +1673,7 @@ export {
1648
1673
  useStarfishData,
1649
1674
  useStarfishLog,
1650
1675
  useStarfishLogItems,
1676
+ useStarfishState,
1651
1677
  useSyncInit,
1652
1678
  useSyncStatus
1653
1679
  };