@abraca/dabra 2.9.0 → 2.11.0

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.
@@ -15960,6 +15960,31 @@ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
15960
15960
  function isValidDocId(id) {
15961
15961
  return UUID_RE.test(id);
15962
15962
  }
15963
+ /**
15964
+ * Consecutive transient failures before a doc is promoted from the calm
15965
+ * `retrying` status to a genuine `error`. Keeps a single timeout/network blip
15966
+ * from flashing a red error badge — the doc only "counts" as broken once it
15967
+ * has failed this many passes in a row.
15968
+ */
15969
+ const FAILURE_THRESHOLD = 3;
15970
+ /** Delay before a one-shot follow-up retry of stragglers after `syncAll()`. */
15971
+ const STRAGGLER_RETRY_DELAY_MS = 6e4;
15972
+ /**
15973
+ * Decide whether a sync failure is permanent (the server will never accept
15974
+ * this doc — deleted, forbidden, or an unparseable id) or merely transient
15975
+ * (timeout, dropped socket, momentary 5xx). Permanent failures are parked as
15976
+ * `unavailable` and never retried; transient ones go through `retrying`.
15977
+ *
15978
+ * Best-effort: inspects a numeric `status`/`code` if the thrown value carries
15979
+ * one, otherwise falls back to message keywords. Defaults to transient so we
15980
+ * never give up on a doc we're unsure about.
15981
+ */
15982
+ function isPermanentSyncError(err) {
15983
+ const status = typeof err === "object" && err !== null ? Number(err.status ?? err.statusCode ?? err.code) : NaN;
15984
+ if (status === 403 || status === 404 || status === 410 || status === 422) return true;
15985
+ const msg = (err instanceof Error ? err.message : String(err)).toLowerCase();
15986
+ return /\b(403|404|410|422)\b/.test(msg) || msg.includes("forbidden") || msg.includes("not found") || msg.includes("does not exist") || msg.includes("unprocessable");
15987
+ }
15963
15988
  var BackgroundSyncManager = class extends EventEmitter {
15964
15989
  constructor(rootProvider, client, fileBlobStore, opts) {
15965
15990
  super();
@@ -15967,12 +15992,13 @@ var BackgroundSyncManager = class extends EventEmitter {
15967
15992
  this._warnedInvalidIds = /* @__PURE__ */ new Set();
15968
15993
  this._destroyed = false;
15969
15994
  this._initPromise = null;
15995
+ this._stragglerTimer = null;
15970
15996
  this.rootProvider = rootProvider;
15971
15997
  this.client = client;
15972
15998
  this.fileBlobStore = fileBlobStore ?? null;
15973
15999
  this.opts = {
15974
16000
  concurrency: opts?.concurrency ?? 2,
15975
- syncTimeout: opts?.syncTimeout ?? 15e3,
16001
+ syncTimeout: opts?.syncTimeout ?? 3e4,
15976
16002
  prefetchFiles: opts?.prefetchFiles ?? true,
15977
16003
  throttleMs: opts?.throttleMs ?? 200,
15978
16004
  maxRetries: opts?.maxRetries ?? 2
@@ -16041,6 +16067,28 @@ var BackgroundSyncManager = class extends EventEmitter {
16041
16067
  const retryWorkers = Array.from({ length: this.opts.concurrency }, () => retryNext());
16042
16068
  await Promise.all(retryWorkers);
16043
16069
  }
16070
+ if (failed.length > 0) this._scheduleStragglerRetry(failed.slice());
16071
+ }
16072
+ /**
16073
+ * Schedule a single delayed retry of the given docs. Replaces any pending
16074
+ * straggler timer so repeated `syncAll()` calls don't stack timers.
16075
+ */
16076
+ _scheduleStragglerRetry(docIds) {
16077
+ if (this._destroyed || docIds.length === 0) return;
16078
+ if (this._stragglerTimer) clearTimeout(this._stragglerTimer);
16079
+ this._stragglerTimer = setTimeout(() => {
16080
+ this._stragglerTimer = null;
16081
+ if (this._destroyed) return;
16082
+ (async () => {
16083
+ const treeMap = this.rootProvider.document.getMap("doc-tree");
16084
+ for (const docId of docIds) {
16085
+ if (this._destroyed) return;
16086
+ const entry = treeMap.get(docId);
16087
+ const updatedAt = entry?.updatedAt ?? entry?.createdAt ?? 0;
16088
+ await this._syncWithSemaphore(docId, updatedAt);
16089
+ }
16090
+ })();
16091
+ }, STRAGGLER_RETRY_DELAY_MS);
16044
16092
  }
16045
16093
  /** Sync a single document by ID. */
16046
16094
  async syncDoc(docId) {
@@ -16097,6 +16145,10 @@ var BackgroundSyncManager = class extends EventEmitter {
16097
16145
  }
16098
16146
  destroy() {
16099
16147
  this._destroyed = true;
16148
+ if (this._stragglerTimer) {
16149
+ clearTimeout(this._stragglerTimer);
16150
+ this._stragglerTimer = null;
16151
+ }
16100
16152
  this.removeAllListeners();
16101
16153
  }
16102
16154
  /**
@@ -16118,18 +16170,20 @@ var BackgroundSyncManager = class extends EventEmitter {
16118
16170
  console.warn(`[BackgroundSyncManager] skipping non-UUID doc id "${docId}" in doc-tree — server would reject it (422). Likely a stale entry from an earlier seeder; remove it from the tree to silence this.`);
16119
16171
  }
16120
16172
  }
16121
- const items = filtered.map(([docId, v]) => {
16173
+ const items = [];
16174
+ for (const [docId, v] of filtered) {
16122
16175
  const state = this.syncStates.get(docId);
16176
+ if (state?.status === "unavailable") continue;
16123
16177
  const updatedAt = v?.updatedAt ?? v?.createdAt ?? 0;
16124
16178
  let priority;
16125
16179
  if (!state || state.status === "pending") priority = Number.MAX_SAFE_INTEGER - updatedAt;
16126
- else if (state.status === "error") priority = -1;
16180
+ else if (state.status === "error" || state.status === "retrying") priority = -1;
16127
16181
  else priority = updatedAt;
16128
- return {
16182
+ items.push({
16129
16183
  docId,
16130
16184
  priority
16131
- };
16132
- });
16185
+ });
16186
+ }
16133
16187
  items.sort((a, b) => b.priority - a.priority);
16134
16188
  return items.map((i) => i.docId);
16135
16189
  }
@@ -16137,6 +16191,13 @@ var BackgroundSyncManager = class extends EventEmitter {
16137
16191
  async _syncWithSemaphore(docId, updatedAt) {
16138
16192
  if (this._destroyed) return true;
16139
16193
  const existing = this.syncStates.get(docId);
16194
+ if (existing && existing.status === "unavailable") {
16195
+ this.emit("stateChanged", {
16196
+ docId,
16197
+ state: existing
16198
+ });
16199
+ return true;
16200
+ }
16140
16201
  if (existing && existing.status === "synced" && existing.lastSynced !== null && existing.lastSynced + 200 >= updatedAt) {
16141
16202
  this.emit("stateChanged", {
16142
16203
  docId,
@@ -16153,12 +16214,20 @@ var BackgroundSyncManager = class extends EventEmitter {
16153
16214
  docId,
16154
16215
  state
16155
16216
  });
16156
- return state.status !== "error";
16217
+ return state.status !== "error" && state.status !== "retrying";
16157
16218
  } finally {
16158
16219
  this.semaphore.release();
16159
16220
  }
16160
16221
  }
16161
16222
  async _doSyncDoc(docId) {
16223
+ if (!isValidDocId(docId)) return {
16224
+ docId,
16225
+ status: "unavailable",
16226
+ lastSynced: this.syncStates.get(docId)?.lastSynced ?? null,
16227
+ error: "Invalid document id (not a UUID)",
16228
+ consecutiveFailures: 0,
16229
+ isE2E: false
16230
+ };
16162
16231
  const syncing = {
16163
16232
  docId,
16164
16233
  status: "syncing",
@@ -16179,11 +16248,23 @@ var BackgroundSyncManager = class extends EventEmitter {
16179
16248
  else return await this._syncNonE2EDoc(docId);
16180
16249
  } catch (err) {
16181
16250
  const error = err instanceof Error ? err.message : String(err);
16251
+ const prev = this.syncStates.get(docId);
16252
+ const lastSynced = prev?.lastSynced ?? null;
16253
+ if (isPermanentSyncError(err)) return {
16254
+ docId,
16255
+ status: "unavailable",
16256
+ lastSynced,
16257
+ error,
16258
+ consecutiveFailures: 0,
16259
+ isE2E
16260
+ };
16261
+ const consecutiveFailures = (prev?.consecutiveFailures ?? 0) + 1;
16182
16262
  return {
16183
16263
  docId,
16184
- status: "error",
16185
- lastSynced: this.syncStates.get(docId)?.lastSynced ?? null,
16264
+ status: consecutiveFailures >= FAILURE_THRESHOLD ? "error" : "retrying",
16265
+ lastSynced,
16186
16266
  error,
16267
+ consecutiveFailures,
16187
16268
  isE2E
16188
16269
  };
16189
16270
  }
@@ -16196,6 +16277,7 @@ var BackgroundSyncManager = class extends EventEmitter {
16196
16277
  docId,
16197
16278
  status: "synced",
16198
16279
  lastSynced: Date.now(),
16280
+ consecutiveFailures: 0,
16199
16281
  isE2E: false
16200
16282
  };
16201
16283
  }
@@ -16219,6 +16301,7 @@ var BackgroundSyncManager = class extends EventEmitter {
16219
16301
  docId,
16220
16302
  status: "synced",
16221
16303
  lastSynced: Math.max(Date.now(), treeUpdatedAt),
16304
+ consecutiveFailures: 0,
16222
16305
  isE2E: false
16223
16306
  };
16224
16307
  } finally {
@@ -16233,6 +16316,7 @@ var BackgroundSyncManager = class extends EventEmitter {
16233
16316
  docId,
16234
16317
  status: "skipped",
16235
16318
  lastSynced: null,
16319
+ consecutiveFailures: 0,
16236
16320
  isE2E: true
16237
16321
  };
16238
16322
  const childDoc = new Y.Doc({ guid: docId });
@@ -16262,6 +16346,7 @@ var BackgroundSyncManager = class extends EventEmitter {
16262
16346
  docId,
16263
16347
  status: "synced",
16264
16348
  lastSynced: Math.max(Date.now(), treeUpdatedAt),
16349
+ consecutiveFailures: 0,
16265
16350
  isE2E: true
16266
16351
  };
16267
16352
  } finally {