@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
|
@@ -1233,6 +1233,8 @@ var AbracadabraWS = class extends EventEmitter {
|
|
|
1233
1233
|
this.identifier = 0;
|
|
1234
1234
|
this.intervals = { connectionChecker: null };
|
|
1235
1235
|
this.connectionAttempt = null;
|
|
1236
|
+
this.onlineListener = null;
|
|
1237
|
+
this.offlineListener = null;
|
|
1236
1238
|
this.receivedOnOpenPayload = void 0;
|
|
1237
1239
|
this.closeTries = 0;
|
|
1238
1240
|
this.setConfiguration(configuration);
|
|
@@ -1251,8 +1253,39 @@ var AbracadabraWS = class extends EventEmitter {
|
|
|
1251
1253
|
this.on("close", this.onClose.bind(this));
|
|
1252
1254
|
this.on("message", this.onMessage.bind(this));
|
|
1253
1255
|
this.intervals.connectionChecker = setInterval(this.checkConnection.bind(this), this.configuration.messageReconnectTimeout / 10);
|
|
1256
|
+
if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
|
|
1257
|
+
this.onlineListener = this.handleOnline.bind(this);
|
|
1258
|
+
this.offlineListener = this.handleOffline.bind(this);
|
|
1259
|
+
window.addEventListener("online", this.onlineListener);
|
|
1260
|
+
window.addEventListener("offline", this.offlineListener);
|
|
1261
|
+
}
|
|
1254
1262
|
if (this.shouldConnect) this.connect();
|
|
1255
1263
|
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Whether the device currently believes it has network connectivity.
|
|
1266
|
+
*
|
|
1267
|
+
* Treats "unknown" as online: in Node and other non-browser environments
|
|
1268
|
+
* `navigator` (or `navigator.onLine`) is absent, and we must not gate
|
|
1269
|
+
* reconnection there — only the browser exposes a trustworthy signal.
|
|
1270
|
+
*/
|
|
1271
|
+
get isOnline() {
|
|
1272
|
+
return typeof navigator === "undefined" || navigator.onLine !== false;
|
|
1273
|
+
}
|
|
1274
|
+
handleOnline() {
|
|
1275
|
+
if (this.shouldConnect && this.status !== WebSocketStatus.Connected) this.connect();
|
|
1276
|
+
}
|
|
1277
|
+
handleOffline() {
|
|
1278
|
+
if (this.cancelWebsocketRetry) {
|
|
1279
|
+
this.cancelWebsocketRetry();
|
|
1280
|
+
this.cancelWebsocketRetry = void 0;
|
|
1281
|
+
}
|
|
1282
|
+
try {
|
|
1283
|
+
this.webSocket?.close();
|
|
1284
|
+
this.messageQueue = [];
|
|
1285
|
+
} catch (e) {
|
|
1286
|
+
console.error(e);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1256
1289
|
async onOpen(event) {
|
|
1257
1290
|
this.status = WebSocketStatus.Connected;
|
|
1258
1291
|
this.emit("status", { status: WebSocketStatus.Connected });
|
|
@@ -1282,6 +1315,10 @@ var AbracadabraWS = class extends EventEmitter {
|
|
|
1282
1315
|
}
|
|
1283
1316
|
async connect() {
|
|
1284
1317
|
if (this.status === WebSocketStatus.Connected) return;
|
|
1318
|
+
if (!this.isOnline) {
|
|
1319
|
+
this.shouldConnect = true;
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1285
1322
|
if (this.cancelWebsocketRetry) {
|
|
1286
1323
|
this.cancelWebsocketRetry();
|
|
1287
1324
|
this.cancelWebsocketRetry = void 0;
|
|
@@ -1446,7 +1483,7 @@ var AbracadabraWS = class extends EventEmitter {
|
|
|
1446
1483
|
const isRateLimited = event?.code === 4429 || event === 4429;
|
|
1447
1484
|
this.emit("disconnect", { event });
|
|
1448
1485
|
if (isRateLimited) this.emit("rateLimited");
|
|
1449
|
-
if (!this.cancelWebsocketRetry && this.shouldConnect) {
|
|
1486
|
+
if (!this.cancelWebsocketRetry && this.shouldConnect && this.isOnline) {
|
|
1450
1487
|
const delay = isRateLimited ? 6e4 : this.configuration.delay;
|
|
1451
1488
|
setTimeout(() => {
|
|
1452
1489
|
this.connect();
|
|
@@ -1456,6 +1493,12 @@ var AbracadabraWS = class extends EventEmitter {
|
|
|
1456
1493
|
destroy() {
|
|
1457
1494
|
this.shouldConnect = false;
|
|
1458
1495
|
this.emit("destroy");
|
|
1496
|
+
if (typeof window !== "undefined" && typeof window.removeEventListener === "function") {
|
|
1497
|
+
if (this.onlineListener) window.removeEventListener("online", this.onlineListener);
|
|
1498
|
+
if (this.offlineListener) window.removeEventListener("offline", this.offlineListener);
|
|
1499
|
+
}
|
|
1500
|
+
this.onlineListener = null;
|
|
1501
|
+
this.offlineListener = null;
|
|
1459
1502
|
clearInterval(this.intervals.connectionChecker);
|
|
1460
1503
|
this.stopConnectionAttempt();
|
|
1461
1504
|
this.disconnect();
|
|
@@ -15970,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}$/
|
|
|
15970
16013
|
function isValidDocId(id) {
|
|
15971
16014
|
return UUID_RE.test(id);
|
|
15972
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
|
+
}
|
|
15973
16041
|
var BackgroundSyncManager = class extends EventEmitter {
|
|
15974
16042
|
constructor(rootProvider, client, fileBlobStore, opts) {
|
|
15975
16043
|
super();
|
|
@@ -15977,12 +16045,13 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
15977
16045
|
this._warnedInvalidIds = /* @__PURE__ */ new Set();
|
|
15978
16046
|
this._destroyed = false;
|
|
15979
16047
|
this._initPromise = null;
|
|
16048
|
+
this._stragglerTimer = null;
|
|
15980
16049
|
this.rootProvider = rootProvider;
|
|
15981
16050
|
this.client = client;
|
|
15982
16051
|
this.fileBlobStore = fileBlobStore ?? null;
|
|
15983
16052
|
this.opts = {
|
|
15984
16053
|
concurrency: opts?.concurrency ?? 2,
|
|
15985
|
-
syncTimeout: opts?.syncTimeout ??
|
|
16054
|
+
syncTimeout: opts?.syncTimeout ?? 3e4,
|
|
15986
16055
|
prefetchFiles: opts?.prefetchFiles ?? true,
|
|
15987
16056
|
throttleMs: opts?.throttleMs ?? 200,
|
|
15988
16057
|
maxRetries: opts?.maxRetries ?? 2
|
|
@@ -16051,6 +16120,28 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
16051
16120
|
const retryWorkers = Array.from({ length: this.opts.concurrency }, () => retryNext());
|
|
16052
16121
|
await Promise.all(retryWorkers);
|
|
16053
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);
|
|
16054
16145
|
}
|
|
16055
16146
|
/** Sync a single document by ID. */
|
|
16056
16147
|
async syncDoc(docId) {
|
|
@@ -16107,6 +16198,10 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
16107
16198
|
}
|
|
16108
16199
|
destroy() {
|
|
16109
16200
|
this._destroyed = true;
|
|
16201
|
+
if (this._stragglerTimer) {
|
|
16202
|
+
clearTimeout(this._stragglerTimer);
|
|
16203
|
+
this._stragglerTimer = null;
|
|
16204
|
+
}
|
|
16110
16205
|
this.removeAllListeners();
|
|
16111
16206
|
}
|
|
16112
16207
|
/**
|
|
@@ -16128,18 +16223,20 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
16128
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.`);
|
|
16129
16224
|
}
|
|
16130
16225
|
}
|
|
16131
|
-
const items =
|
|
16226
|
+
const items = [];
|
|
16227
|
+
for (const [docId, v] of filtered) {
|
|
16132
16228
|
const state = this.syncStates.get(docId);
|
|
16229
|
+
if (state?.status === "unavailable") continue;
|
|
16133
16230
|
const updatedAt = v?.updatedAt ?? v?.createdAt ?? 0;
|
|
16134
16231
|
let priority;
|
|
16135
16232
|
if (!state || state.status === "pending") priority = Number.MAX_SAFE_INTEGER - updatedAt;
|
|
16136
|
-
else if (state.status === "error") priority = -1;
|
|
16233
|
+
else if (state.status === "error" || state.status === "retrying") priority = -1;
|
|
16137
16234
|
else priority = updatedAt;
|
|
16138
|
-
|
|
16235
|
+
items.push({
|
|
16139
16236
|
docId,
|
|
16140
16237
|
priority
|
|
16141
|
-
};
|
|
16142
|
-
}
|
|
16238
|
+
});
|
|
16239
|
+
}
|
|
16143
16240
|
items.sort((a, b) => b.priority - a.priority);
|
|
16144
16241
|
return items.map((i) => i.docId);
|
|
16145
16242
|
}
|
|
@@ -16147,6 +16244,13 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
16147
16244
|
async _syncWithSemaphore(docId, updatedAt) {
|
|
16148
16245
|
if (this._destroyed) return true;
|
|
16149
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
|
+
}
|
|
16150
16254
|
if (existing && existing.status === "synced" && existing.lastSynced !== null && existing.lastSynced + 200 >= updatedAt) {
|
|
16151
16255
|
this.emit("stateChanged", {
|
|
16152
16256
|
docId,
|
|
@@ -16163,12 +16267,20 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
16163
16267
|
docId,
|
|
16164
16268
|
state
|
|
16165
16269
|
});
|
|
16166
|
-
return state.status !== "error";
|
|
16270
|
+
return state.status !== "error" && state.status !== "retrying";
|
|
16167
16271
|
} finally {
|
|
16168
16272
|
this.semaphore.release();
|
|
16169
16273
|
}
|
|
16170
16274
|
}
|
|
16171
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
|
+
};
|
|
16172
16284
|
const syncing = {
|
|
16173
16285
|
docId,
|
|
16174
16286
|
status: "syncing",
|
|
@@ -16189,11 +16301,23 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
16189
16301
|
else return await this._syncNonE2EDoc(docId);
|
|
16190
16302
|
} catch (err) {
|
|
16191
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;
|
|
16192
16315
|
return {
|
|
16193
16316
|
docId,
|
|
16194
|
-
status: "error",
|
|
16195
|
-
lastSynced
|
|
16317
|
+
status: consecutiveFailures >= FAILURE_THRESHOLD ? "error" : "retrying",
|
|
16318
|
+
lastSynced,
|
|
16196
16319
|
error,
|
|
16320
|
+
consecutiveFailures,
|
|
16197
16321
|
isE2E
|
|
16198
16322
|
};
|
|
16199
16323
|
}
|
|
@@ -16206,6 +16330,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
16206
16330
|
docId,
|
|
16207
16331
|
status: "synced",
|
|
16208
16332
|
lastSynced: Date.now(),
|
|
16333
|
+
consecutiveFailures: 0,
|
|
16209
16334
|
isE2E: false
|
|
16210
16335
|
};
|
|
16211
16336
|
}
|
|
@@ -16229,6 +16354,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
16229
16354
|
docId,
|
|
16230
16355
|
status: "synced",
|
|
16231
16356
|
lastSynced: Math.max(Date.now(), treeUpdatedAt),
|
|
16357
|
+
consecutiveFailures: 0,
|
|
16232
16358
|
isE2E: false
|
|
16233
16359
|
};
|
|
16234
16360
|
} finally {
|
|
@@ -16243,6 +16369,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
16243
16369
|
docId,
|
|
16244
16370
|
status: "skipped",
|
|
16245
16371
|
lastSynced: null,
|
|
16372
|
+
consecutiveFailures: 0,
|
|
16246
16373
|
isE2E: true
|
|
16247
16374
|
};
|
|
16248
16375
|
const childDoc = new yjs.Doc({ guid: docId });
|
|
@@ -16272,6 +16399,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
16272
16399
|
docId,
|
|
16273
16400
|
status: "synced",
|
|
16274
16401
|
lastSynced: Math.max(Date.now(), treeUpdatedAt),
|
|
16402
|
+
consecutiveFailures: 0,
|
|
16275
16403
|
isE2E: true
|
|
16276
16404
|
};
|
|
16277
16405
|
} finally {
|