@abraca/dabra 2.9.0 → 2.10.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
|
@@ -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 ??
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 {
|