@drakkar.software/starfish-client 3.0.0-alpha.36 → 3.0.0-alpha.38

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). */
@@ -941,6 +941,8 @@ var SyncManager = class {
941
941
  localData = {};
942
942
  aborted = false;
943
943
  lastFromCache = false;
944
+ /** True once {@link seedFromCache} has successfully seeded localData from the cache. */
945
+ seeded = false;
944
946
  constructor(options) {
945
947
  this.client = options.client;
946
948
  this.pullPath = options.pullPath;
@@ -962,6 +964,24 @@ var SyncManager = class {
962
964
  getData() {
963
965
  return { ...this.localData };
964
966
  }
967
+ /**
968
+ * Returns true when `pull()` / `ingest()` should merge against the current
969
+ * `localData` rather than replace it wholesale.
970
+ *
971
+ * Two situations establish a merge baseline:
972
+ * - A successful prior pull/ingest advanced `lastCheckpoint` beyond 0 (the
973
+ * normal steady-state case, unchanged since alpha.36).
974
+ * - A cache seed painted `localData` via {@link seedFromCache} AND the store
975
+ * uses a custom conflict resolver (i.e. NOT the default `deepMerge`). For a
976
+ * union/custom resolver the seeded snapshot is a real baseline that must not
977
+ * be clobbered by a short first live response (a cache-fallback on 429/5xx
978
+ * or a momentarily-short concurrent server snapshot). For the default
979
+ * `deepMerge` resolver we keep the pre-fix wholesale-replace behaviour so
980
+ * non-union stores are byte-identical to alpha.36.
981
+ */
982
+ hasMergeBaseline() {
983
+ return this.lastCheckpoint > 0 || this.seeded && this.onConflict !== deepMerge;
984
+ }
965
985
  /**
966
986
  * Merge a remote snapshot with local (optimistic) data using this manager's
967
987
  * conflict resolver — the same resolver the push-conflict path uses. A plain
@@ -996,7 +1016,19 @@ var SyncManager = class {
996
1016
  * WITHOUT touching the network, decrypting in memory for E2E collections.
997
1017
  * Returns whether anything was seeded (false on a miss, an expired entry, or
998
1018
  * a decrypt failure — e.g. keyring skew). Call once on store creation before
999
- * the initial live {@link pull}, which then supersedes the seeded snapshot.
1019
+ * the initial live {@link pull}.
1020
+ *
1021
+ * `lastCheckpoint` is intentionally left at 0 so the first live pull sends a
1022
+ * full (re)sync request to the server, not a delta. However, for stores with
1023
+ * a custom conflict resolver (e.g. `createUnionMerge`) the seeded snapshot is
1024
+ * treated as a merge baseline: {@link hasMergeBaseline} returns true, so the
1025
+ * first pull/ingest merges against the seed rather than replacing it wholesale.
1026
+ * This closes the bootstrap window where a short first-pull response (a cache-
1027
+ * fallback on 429/5xx or a momentarily-short concurrent snapshot) would
1028
+ * silently drop items the resolver was configured to preserve. For the default
1029
+ * `deepMerge` resolver the first pull still takes the snapshot wholesale —
1030
+ * behaviour is byte-identical to alpha.36.
1031
+ *
1000
1032
  * Requires the client to have been built with a `cache`.
1001
1033
  */
1002
1034
  async seedFromCache() {
@@ -1012,6 +1044,7 @@ var SyncManager = class {
1012
1044
  if (this.aborted) return false;
1013
1045
  this.localData = data;
1014
1046
  this.lastHash = cached.hash;
1047
+ this.seeded = true;
1015
1048
  this.lastFromCache = true;
1016
1049
  return true;
1017
1050
  }
@@ -1025,12 +1058,15 @@ var SyncManager = class {
1025
1058
  * {@link StarfishClientOptions.onRevalidated}) into the store.
1026
1059
  *
1027
1060
  * 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.
1061
+ * established baseline via `this.onConflict` when a merge baseline exists
1062
+ * ({@link hasMergeBaseline}) — so a union-merge store does not lose array
1063
+ * items when a revalidation result (e.g. a stale cache-fallback on 429/5xx)
1064
+ * is a shorter snapshot. The baseline is established by either a prior
1065
+ * pull/ingest that advanced `lastCheckpoint`, or by a successful
1066
+ * {@link seedFromCache} for a store with a custom resolver. The first ingest
1067
+ * without such a baseline takes the snapshot wholesale (default `deepMerge`
1068
+ * stores are byte-identical to alpha.36). Sets `lastFromCache = false` (a
1069
+ * revalidation is a live response) so the binding can clear its `stale` flag.
1034
1070
  *
1035
1071
  * **Staleness guard**: if a `push()` advanced `lastCheckpoint` between the
1036
1072
  * time the revalidation request was sent and the time it resolves, the
@@ -1049,7 +1085,7 @@ var SyncManager = class {
1049
1085
  } else {
1050
1086
  incoming = result.data;
1051
1087
  }
1052
- this.localData = this.lastCheckpoint > 0 ? this.onConflict(this.localData, incoming) : incoming;
1088
+ this.localData = this.hasMergeBaseline() ? this.onConflict(this.localData, incoming) : incoming;
1053
1089
  this.lastHash = result.hash;
1054
1090
  this.lastCheckpoint = result.timestamp;
1055
1091
  this.lastFromCache = false;
@@ -1069,7 +1105,7 @@ var SyncManager = class {
1069
1105
  } else {
1070
1106
  incoming = result.data;
1071
1107
  }
1072
- this.localData = this.lastCheckpoint > 0 ? this.onConflict(this.localData, incoming) : incoming;
1108
+ this.localData = this.hasMergeBaseline() ? this.onConflict(this.localData, incoming) : incoming;
1073
1109
  result.data = this.localData;
1074
1110
  this.lastHash = result.hash;
1075
1111
  this.lastCheckpoint = result.timestamp;
@@ -1365,6 +1401,9 @@ function aggregateSyncStatus(statuses) {
1365
1401
  function useStarfish(store) {
1366
1402
  return useStore(store);
1367
1403
  }
1404
+ function useStarfishState(store, selector) {
1405
+ return useStore(store, selector);
1406
+ }
1368
1407
  function useStarfishData(store, selector) {
1369
1408
  return useStore(
1370
1409
  store,
@@ -1425,7 +1464,7 @@ function useLastSynced(store) {
1425
1464
  }, [store, computeLabel]);
1426
1465
  useEffect(() => {
1427
1466
  const timer = setInterval(() => {
1428
- setLabel(computeLabel());
1467
+ if (!document.hidden) setLabel(computeLabel());
1429
1468
  }, 5e3);
1430
1469
  return () => clearInterval(timer);
1431
1470
  }, [computeLabel]);
@@ -1670,6 +1709,7 @@ export {
1670
1709
  useStarfishData,
1671
1710
  useStarfishLog,
1672
1711
  useStarfishLogItems,
1712
+ useStarfishState,
1673
1713
  useSyncInit,
1674
1714
  useSyncStatus
1675
1715
  };