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

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.
@@ -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));