@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.
- package/dist/abracadabra-provider.cjs +94 -9
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +94 -9
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +36 -4
- package/package.json +2 -2
- package/src/BackgroundSyncManager.ts +165 -14
- package/src/BackgroundSyncPersistence.ts +35 -2
- package/src/index.ts +4 -1
|
@@ -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 ??
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 {
|