@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.
@@ -16013,6 +16013,31 @@ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
16013
16013
  function isValidDocId(id) {
16014
16014
  return UUID_RE.test(id);
16015
16015
  }
16016
+ /**
16017
+ * Consecutive transient failures before a doc is promoted from the calm
16018
+ * `retrying` status to a genuine `error`. Keeps a single timeout/network blip
16019
+ * from flashing a red error badge — the doc only "counts" as broken once it
16020
+ * has failed this many passes in a row.
16021
+ */
16022
+ const FAILURE_THRESHOLD = 3;
16023
+ /** Delay before a one-shot follow-up retry of stragglers after `syncAll()`. */
16024
+ const STRAGGLER_RETRY_DELAY_MS = 6e4;
16025
+ /**
16026
+ * Decide whether a sync failure is permanent (the server will never accept
16027
+ * this doc — deleted, forbidden, or an unparseable id) or merely transient
16028
+ * (timeout, dropped socket, momentary 5xx). Permanent failures are parked as
16029
+ * `unavailable` and never retried; transient ones go through `retrying`.
16030
+ *
16031
+ * Best-effort: inspects a numeric `status`/`code` if the thrown value carries
16032
+ * one, otherwise falls back to message keywords. Defaults to transient so we
16033
+ * never give up on a doc we're unsure about.
16034
+ */
16035
+ function isPermanentSyncError(err) {
16036
+ const status = typeof err === "object" && err !== null ? Number(err.status ?? err.statusCode ?? err.code) : NaN;
16037
+ if (status === 403 || status === 404 || status === 410 || status === 422) return true;
16038
+ const msg = (err instanceof Error ? err.message : String(err)).toLowerCase();
16039
+ return /\b(403|404|410|422)\b/.test(msg) || msg.includes("forbidden") || msg.includes("not found") || msg.includes("does not exist") || msg.includes("unprocessable");
16040
+ }
16016
16041
  var BackgroundSyncManager = class extends EventEmitter {
16017
16042
  constructor(rootProvider, client, fileBlobStore, opts) {
16018
16043
  super();
@@ -16020,12 +16045,13 @@ var BackgroundSyncManager = class extends EventEmitter {
16020
16045
  this._warnedInvalidIds = /* @__PURE__ */ new Set();
16021
16046
  this._destroyed = false;
16022
16047
  this._initPromise = null;
16048
+ this._stragglerTimer = null;
16023
16049
  this.rootProvider = rootProvider;
16024
16050
  this.client = client;
16025
16051
  this.fileBlobStore = fileBlobStore ?? null;
16026
16052
  this.opts = {
16027
16053
  concurrency: opts?.concurrency ?? 2,
16028
- syncTimeout: opts?.syncTimeout ?? 15e3,
16054
+ syncTimeout: opts?.syncTimeout ?? 3e4,
16029
16055
  prefetchFiles: opts?.prefetchFiles ?? true,
16030
16056
  throttleMs: opts?.throttleMs ?? 200,
16031
16057
  maxRetries: opts?.maxRetries ?? 2
@@ -16094,6 +16120,28 @@ var BackgroundSyncManager = class extends EventEmitter {
16094
16120
  const retryWorkers = Array.from({ length: this.opts.concurrency }, () => retryNext());
16095
16121
  await Promise.all(retryWorkers);
16096
16122
  }
16123
+ if (failed.length > 0) this._scheduleStragglerRetry(failed.slice());
16124
+ }
16125
+ /**
16126
+ * Schedule a single delayed retry of the given docs. Replaces any pending
16127
+ * straggler timer so repeated `syncAll()` calls don't stack timers.
16128
+ */
16129
+ _scheduleStragglerRetry(docIds) {
16130
+ if (this._destroyed || docIds.length === 0) return;
16131
+ if (this._stragglerTimer) clearTimeout(this._stragglerTimer);
16132
+ this._stragglerTimer = setTimeout(() => {
16133
+ this._stragglerTimer = null;
16134
+ if (this._destroyed) return;
16135
+ (async () => {
16136
+ const treeMap = this.rootProvider.document.getMap("doc-tree");
16137
+ for (const docId of docIds) {
16138
+ if (this._destroyed) return;
16139
+ const entry = treeMap.get(docId);
16140
+ const updatedAt = entry?.updatedAt ?? entry?.createdAt ?? 0;
16141
+ await this._syncWithSemaphore(docId, updatedAt);
16142
+ }
16143
+ })();
16144
+ }, STRAGGLER_RETRY_DELAY_MS);
16097
16145
  }
16098
16146
  /** Sync a single document by ID. */
16099
16147
  async syncDoc(docId) {
@@ -16150,6 +16198,10 @@ var BackgroundSyncManager = class extends EventEmitter {
16150
16198
  }
16151
16199
  destroy() {
16152
16200
  this._destroyed = true;
16201
+ if (this._stragglerTimer) {
16202
+ clearTimeout(this._stragglerTimer);
16203
+ this._stragglerTimer = null;
16204
+ }
16153
16205
  this.removeAllListeners();
16154
16206
  }
16155
16207
  /**
@@ -16171,18 +16223,20 @@ var BackgroundSyncManager = class extends EventEmitter {
16171
16223
  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.`);
16172
16224
  }
16173
16225
  }
16174
- const items = filtered.map(([docId, v]) => {
16226
+ const items = [];
16227
+ for (const [docId, v] of filtered) {
16175
16228
  const state = this.syncStates.get(docId);
16229
+ if (state?.status === "unavailable") continue;
16176
16230
  const updatedAt = v?.updatedAt ?? v?.createdAt ?? 0;
16177
16231
  let priority;
16178
16232
  if (!state || state.status === "pending") priority = Number.MAX_SAFE_INTEGER - updatedAt;
16179
- else if (state.status === "error") priority = -1;
16233
+ else if (state.status === "error" || state.status === "retrying") priority = -1;
16180
16234
  else priority = updatedAt;
16181
- return {
16235
+ items.push({
16182
16236
  docId,
16183
16237
  priority
16184
- };
16185
- });
16238
+ });
16239
+ }
16186
16240
  items.sort((a, b) => b.priority - a.priority);
16187
16241
  return items.map((i) => i.docId);
16188
16242
  }
@@ -16190,6 +16244,13 @@ var BackgroundSyncManager = class extends EventEmitter {
16190
16244
  async _syncWithSemaphore(docId, updatedAt) {
16191
16245
  if (this._destroyed) return true;
16192
16246
  const existing = this.syncStates.get(docId);
16247
+ if (existing && existing.status === "unavailable") {
16248
+ this.emit("stateChanged", {
16249
+ docId,
16250
+ state: existing
16251
+ });
16252
+ return true;
16253
+ }
16193
16254
  if (existing && existing.status === "synced" && existing.lastSynced !== null && existing.lastSynced + 200 >= updatedAt) {
16194
16255
  this.emit("stateChanged", {
16195
16256
  docId,
@@ -16206,12 +16267,20 @@ var BackgroundSyncManager = class extends EventEmitter {
16206
16267
  docId,
16207
16268
  state
16208
16269
  });
16209
- return state.status !== "error";
16270
+ return state.status !== "error" && state.status !== "retrying";
16210
16271
  } finally {
16211
16272
  this.semaphore.release();
16212
16273
  }
16213
16274
  }
16214
16275
  async _doSyncDoc(docId) {
16276
+ if (!isValidDocId(docId)) return {
16277
+ docId,
16278
+ status: "unavailable",
16279
+ lastSynced: this.syncStates.get(docId)?.lastSynced ?? null,
16280
+ error: "Invalid document id (not a UUID)",
16281
+ consecutiveFailures: 0,
16282
+ isE2E: false
16283
+ };
16215
16284
  const syncing = {
16216
16285
  docId,
16217
16286
  status: "syncing",
@@ -16232,11 +16301,23 @@ var BackgroundSyncManager = class extends EventEmitter {
16232
16301
  else return await this._syncNonE2EDoc(docId);
16233
16302
  } catch (err) {
16234
16303
  const error = err instanceof Error ? err.message : String(err);
16304
+ const prev = this.syncStates.get(docId);
16305
+ const lastSynced = prev?.lastSynced ?? null;
16306
+ if (isPermanentSyncError(err)) return {
16307
+ docId,
16308
+ status: "unavailable",
16309
+ lastSynced,
16310
+ error,
16311
+ consecutiveFailures: 0,
16312
+ isE2E
16313
+ };
16314
+ const consecutiveFailures = (prev?.consecutiveFailures ?? 0) + 1;
16235
16315
  return {
16236
16316
  docId,
16237
- status: "error",
16238
- lastSynced: this.syncStates.get(docId)?.lastSynced ?? null,
16317
+ status: consecutiveFailures >= FAILURE_THRESHOLD ? "error" : "retrying",
16318
+ lastSynced,
16239
16319
  error,
16320
+ consecutiveFailures,
16240
16321
  isE2E
16241
16322
  };
16242
16323
  }
@@ -16249,6 +16330,7 @@ var BackgroundSyncManager = class extends EventEmitter {
16249
16330
  docId,
16250
16331
  status: "synced",
16251
16332
  lastSynced: Date.now(),
16333
+ consecutiveFailures: 0,
16252
16334
  isE2E: false
16253
16335
  };
16254
16336
  }
@@ -16272,6 +16354,7 @@ var BackgroundSyncManager = class extends EventEmitter {
16272
16354
  docId,
16273
16355
  status: "synced",
16274
16356
  lastSynced: Math.max(Date.now(), treeUpdatedAt),
16357
+ consecutiveFailures: 0,
16275
16358
  isE2E: false
16276
16359
  };
16277
16360
  } finally {
@@ -16286,6 +16369,7 @@ var BackgroundSyncManager = class extends EventEmitter {
16286
16369
  docId,
16287
16370
  status: "skipped",
16288
16371
  lastSynced: null,
16372
+ consecutiveFailures: 0,
16289
16373
  isE2E: true
16290
16374
  };
16291
16375
  const childDoc = new yjs.Doc({ guid: docId });
@@ -16315,6 +16399,7 @@ var BackgroundSyncManager = class extends EventEmitter {
16315
16399
  docId,
16316
16400
  status: "synced",
16317
16401
  lastSynced: Math.max(Date.now(), treeUpdatedAt),
16402
+ consecutiveFailures: 0,
16318
16403
  isE2E: true
16319
16404
  };
16320
16405
  } finally {