@abraca/dabra 2.8.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 +138 -10
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +138 -10
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +48 -4
- package/package.json +2 -2
- package/src/AbracadabraWS.ts +72 -2
- package/src/BackgroundSyncManager.ts +165 -14
- package/src/BackgroundSyncPersistence.ts +35 -2
- package/src/index.ts +4 -1
|
@@ -1203,6 +1203,8 @@ var AbracadabraWS = class extends EventEmitter {
|
|
|
1203
1203
|
this.identifier = 0;
|
|
1204
1204
|
this.intervals = { connectionChecker: null };
|
|
1205
1205
|
this.connectionAttempt = null;
|
|
1206
|
+
this.onlineListener = null;
|
|
1207
|
+
this.offlineListener = null;
|
|
1206
1208
|
this.receivedOnOpenPayload = void 0;
|
|
1207
1209
|
this.closeTries = 0;
|
|
1208
1210
|
this.setConfiguration(configuration);
|
|
@@ -1221,8 +1223,39 @@ var AbracadabraWS = class extends EventEmitter {
|
|
|
1221
1223
|
this.on("close", this.onClose.bind(this));
|
|
1222
1224
|
this.on("message", this.onMessage.bind(this));
|
|
1223
1225
|
this.intervals.connectionChecker = setInterval(this.checkConnection.bind(this), this.configuration.messageReconnectTimeout / 10);
|
|
1226
|
+
if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
|
|
1227
|
+
this.onlineListener = this.handleOnline.bind(this);
|
|
1228
|
+
this.offlineListener = this.handleOffline.bind(this);
|
|
1229
|
+
window.addEventListener("online", this.onlineListener);
|
|
1230
|
+
window.addEventListener("offline", this.offlineListener);
|
|
1231
|
+
}
|
|
1224
1232
|
if (this.shouldConnect) this.connect();
|
|
1225
1233
|
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Whether the device currently believes it has network connectivity.
|
|
1236
|
+
*
|
|
1237
|
+
* Treats "unknown" as online: in Node and other non-browser environments
|
|
1238
|
+
* `navigator` (or `navigator.onLine`) is absent, and we must not gate
|
|
1239
|
+
* reconnection there — only the browser exposes a trustworthy signal.
|
|
1240
|
+
*/
|
|
1241
|
+
get isOnline() {
|
|
1242
|
+
return typeof navigator === "undefined" || navigator.onLine !== false;
|
|
1243
|
+
}
|
|
1244
|
+
handleOnline() {
|
|
1245
|
+
if (this.shouldConnect && this.status !== WebSocketStatus.Connected) this.connect();
|
|
1246
|
+
}
|
|
1247
|
+
handleOffline() {
|
|
1248
|
+
if (this.cancelWebsocketRetry) {
|
|
1249
|
+
this.cancelWebsocketRetry();
|
|
1250
|
+
this.cancelWebsocketRetry = void 0;
|
|
1251
|
+
}
|
|
1252
|
+
try {
|
|
1253
|
+
this.webSocket?.close();
|
|
1254
|
+
this.messageQueue = [];
|
|
1255
|
+
} catch (e) {
|
|
1256
|
+
console.error(e);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1226
1259
|
async onOpen(event) {
|
|
1227
1260
|
this.status = WebSocketStatus.Connected;
|
|
1228
1261
|
this.emit("status", { status: WebSocketStatus.Connected });
|
|
@@ -1252,6 +1285,10 @@ var AbracadabraWS = class extends EventEmitter {
|
|
|
1252
1285
|
}
|
|
1253
1286
|
async connect() {
|
|
1254
1287
|
if (this.status === WebSocketStatus.Connected) return;
|
|
1288
|
+
if (!this.isOnline) {
|
|
1289
|
+
this.shouldConnect = true;
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1255
1292
|
if (this.cancelWebsocketRetry) {
|
|
1256
1293
|
this.cancelWebsocketRetry();
|
|
1257
1294
|
this.cancelWebsocketRetry = void 0;
|
|
@@ -1416,7 +1453,7 @@ var AbracadabraWS = class extends EventEmitter {
|
|
|
1416
1453
|
const isRateLimited = event?.code === 4429 || event === 4429;
|
|
1417
1454
|
this.emit("disconnect", { event });
|
|
1418
1455
|
if (isRateLimited) this.emit("rateLimited");
|
|
1419
|
-
if (!this.cancelWebsocketRetry && this.shouldConnect) {
|
|
1456
|
+
if (!this.cancelWebsocketRetry && this.shouldConnect && this.isOnline) {
|
|
1420
1457
|
const delay = isRateLimited ? 6e4 : this.configuration.delay;
|
|
1421
1458
|
setTimeout(() => {
|
|
1422
1459
|
this.connect();
|
|
@@ -1426,6 +1463,12 @@ var AbracadabraWS = class extends EventEmitter {
|
|
|
1426
1463
|
destroy() {
|
|
1427
1464
|
this.shouldConnect = false;
|
|
1428
1465
|
this.emit("destroy");
|
|
1466
|
+
if (typeof window !== "undefined" && typeof window.removeEventListener === "function") {
|
|
1467
|
+
if (this.onlineListener) window.removeEventListener("online", this.onlineListener);
|
|
1468
|
+
if (this.offlineListener) window.removeEventListener("offline", this.offlineListener);
|
|
1469
|
+
}
|
|
1470
|
+
this.onlineListener = null;
|
|
1471
|
+
this.offlineListener = null;
|
|
1429
1472
|
clearInterval(this.intervals.connectionChecker);
|
|
1430
1473
|
this.stopConnectionAttempt();
|
|
1431
1474
|
this.disconnect();
|
|
@@ -15917,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}$/
|
|
|
15917
15960
|
function isValidDocId(id) {
|
|
15918
15961
|
return UUID_RE.test(id);
|
|
15919
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
|
+
}
|
|
15920
15988
|
var BackgroundSyncManager = class extends EventEmitter {
|
|
15921
15989
|
constructor(rootProvider, client, fileBlobStore, opts) {
|
|
15922
15990
|
super();
|
|
@@ -15924,12 +15992,13 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
15924
15992
|
this._warnedInvalidIds = /* @__PURE__ */ new Set();
|
|
15925
15993
|
this._destroyed = false;
|
|
15926
15994
|
this._initPromise = null;
|
|
15995
|
+
this._stragglerTimer = null;
|
|
15927
15996
|
this.rootProvider = rootProvider;
|
|
15928
15997
|
this.client = client;
|
|
15929
15998
|
this.fileBlobStore = fileBlobStore ?? null;
|
|
15930
15999
|
this.opts = {
|
|
15931
16000
|
concurrency: opts?.concurrency ?? 2,
|
|
15932
|
-
syncTimeout: opts?.syncTimeout ??
|
|
16001
|
+
syncTimeout: opts?.syncTimeout ?? 3e4,
|
|
15933
16002
|
prefetchFiles: opts?.prefetchFiles ?? true,
|
|
15934
16003
|
throttleMs: opts?.throttleMs ?? 200,
|
|
15935
16004
|
maxRetries: opts?.maxRetries ?? 2
|
|
@@ -15998,6 +16067,28 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
15998
16067
|
const retryWorkers = Array.from({ length: this.opts.concurrency }, () => retryNext());
|
|
15999
16068
|
await Promise.all(retryWorkers);
|
|
16000
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);
|
|
16001
16092
|
}
|
|
16002
16093
|
/** Sync a single document by ID. */
|
|
16003
16094
|
async syncDoc(docId) {
|
|
@@ -16054,6 +16145,10 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
16054
16145
|
}
|
|
16055
16146
|
destroy() {
|
|
16056
16147
|
this._destroyed = true;
|
|
16148
|
+
if (this._stragglerTimer) {
|
|
16149
|
+
clearTimeout(this._stragglerTimer);
|
|
16150
|
+
this._stragglerTimer = null;
|
|
16151
|
+
}
|
|
16057
16152
|
this.removeAllListeners();
|
|
16058
16153
|
}
|
|
16059
16154
|
/**
|
|
@@ -16075,18 +16170,20 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
16075
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.`);
|
|
16076
16171
|
}
|
|
16077
16172
|
}
|
|
16078
|
-
const items =
|
|
16173
|
+
const items = [];
|
|
16174
|
+
for (const [docId, v] of filtered) {
|
|
16079
16175
|
const state = this.syncStates.get(docId);
|
|
16176
|
+
if (state?.status === "unavailable") continue;
|
|
16080
16177
|
const updatedAt = v?.updatedAt ?? v?.createdAt ?? 0;
|
|
16081
16178
|
let priority;
|
|
16082
16179
|
if (!state || state.status === "pending") priority = Number.MAX_SAFE_INTEGER - updatedAt;
|
|
16083
|
-
else if (state.status === "error") priority = -1;
|
|
16180
|
+
else if (state.status === "error" || state.status === "retrying") priority = -1;
|
|
16084
16181
|
else priority = updatedAt;
|
|
16085
|
-
|
|
16182
|
+
items.push({
|
|
16086
16183
|
docId,
|
|
16087
16184
|
priority
|
|
16088
|
-
};
|
|
16089
|
-
}
|
|
16185
|
+
});
|
|
16186
|
+
}
|
|
16090
16187
|
items.sort((a, b) => b.priority - a.priority);
|
|
16091
16188
|
return items.map((i) => i.docId);
|
|
16092
16189
|
}
|
|
@@ -16094,6 +16191,13 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
16094
16191
|
async _syncWithSemaphore(docId, updatedAt) {
|
|
16095
16192
|
if (this._destroyed) return true;
|
|
16096
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
|
+
}
|
|
16097
16201
|
if (existing && existing.status === "synced" && existing.lastSynced !== null && existing.lastSynced + 200 >= updatedAt) {
|
|
16098
16202
|
this.emit("stateChanged", {
|
|
16099
16203
|
docId,
|
|
@@ -16110,12 +16214,20 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
16110
16214
|
docId,
|
|
16111
16215
|
state
|
|
16112
16216
|
});
|
|
16113
|
-
return state.status !== "error";
|
|
16217
|
+
return state.status !== "error" && state.status !== "retrying";
|
|
16114
16218
|
} finally {
|
|
16115
16219
|
this.semaphore.release();
|
|
16116
16220
|
}
|
|
16117
16221
|
}
|
|
16118
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
|
+
};
|
|
16119
16231
|
const syncing = {
|
|
16120
16232
|
docId,
|
|
16121
16233
|
status: "syncing",
|
|
@@ -16136,11 +16248,23 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
16136
16248
|
else return await this._syncNonE2EDoc(docId);
|
|
16137
16249
|
} catch (err) {
|
|
16138
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;
|
|
16139
16262
|
return {
|
|
16140
16263
|
docId,
|
|
16141
|
-
status: "error",
|
|
16142
|
-
lastSynced
|
|
16264
|
+
status: consecutiveFailures >= FAILURE_THRESHOLD ? "error" : "retrying",
|
|
16265
|
+
lastSynced,
|
|
16143
16266
|
error,
|
|
16267
|
+
consecutiveFailures,
|
|
16144
16268
|
isE2E
|
|
16145
16269
|
};
|
|
16146
16270
|
}
|
|
@@ -16153,6 +16277,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
16153
16277
|
docId,
|
|
16154
16278
|
status: "synced",
|
|
16155
16279
|
lastSynced: Date.now(),
|
|
16280
|
+
consecutiveFailures: 0,
|
|
16156
16281
|
isE2E: false
|
|
16157
16282
|
};
|
|
16158
16283
|
}
|
|
@@ -16176,6 +16301,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
16176
16301
|
docId,
|
|
16177
16302
|
status: "synced",
|
|
16178
16303
|
lastSynced: Math.max(Date.now(), treeUpdatedAt),
|
|
16304
|
+
consecutiveFailures: 0,
|
|
16179
16305
|
isE2E: false
|
|
16180
16306
|
};
|
|
16181
16307
|
} finally {
|
|
@@ -16190,6 +16316,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
16190
16316
|
docId,
|
|
16191
16317
|
status: "skipped",
|
|
16192
16318
|
lastSynced: null,
|
|
16319
|
+
consecutiveFailures: 0,
|
|
16193
16320
|
isE2E: true
|
|
16194
16321
|
};
|
|
16195
16322
|
const childDoc = new Y.Doc({ guid: docId });
|
|
@@ -16219,6 +16346,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
16219
16346
|
docId,
|
|
16220
16347
|
status: "synced",
|
|
16221
16348
|
lastSynced: Math.max(Date.now(), treeUpdatedAt),
|
|
16349
|
+
consecutiveFailures: 0,
|
|
16222
16350
|
isE2E: true
|
|
16223
16351
|
};
|
|
16224
16352
|
} finally {
|