@abraca/dabra 1.0.14 → 1.0.16
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 +218 -67
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +218 -67
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +46 -3
- package/package.json +1 -1
- package/src/AbracadabraBaseProvider.ts +7 -1
- package/src/AbracadabraClient.ts +52 -42
- package/src/AbracadabraProvider.ts +19 -2
- package/src/AbracadabraWS.ts +5 -1
- package/src/BackgroundSyncManager.ts +99 -4
- package/src/DocKeyManager.ts +6 -3
- package/src/EventEmitter.ts +16 -1
- package/src/FileBlobStore.ts +41 -22
- package/src/OfflineStore.ts +22 -0
- package/src/SearchIndex.ts +3 -0
- package/src/webrtc/DataChannelRouter.ts +3 -2
- package/src/webrtc/FileTransferChannel.ts +1 -0
- package/src/webrtc/ManualSignaling.ts +5 -1
- package/src/webrtc/SignalingSocket.ts +12 -0
- package/src/webrtc/YjsDataChannel.ts +1 -0
|
@@ -975,9 +975,20 @@ var EventEmitter = class {
|
|
|
975
975
|
this.callbacks[event].push(fn);
|
|
976
976
|
return this;
|
|
977
977
|
}
|
|
978
|
+
once(event, fn) {
|
|
979
|
+
const wrapper = (...args) => {
|
|
980
|
+
this.off(event, wrapper);
|
|
981
|
+
fn.apply(this, args);
|
|
982
|
+
};
|
|
983
|
+
return this.on(event, wrapper);
|
|
984
|
+
}
|
|
978
985
|
emit(event, ...args) {
|
|
979
986
|
const callbacks = this.callbacks[event];
|
|
980
|
-
if (callbacks)
|
|
987
|
+
if (callbacks) for (const callback of callbacks) try {
|
|
988
|
+
callback.apply(this, args);
|
|
989
|
+
} catch (err) {
|
|
990
|
+
console.error(`[EventEmitter] Error in "${event}" listener:`, err);
|
|
991
|
+
}
|
|
981
992
|
return this;
|
|
982
993
|
}
|
|
983
994
|
off(event, fn) {
|
|
@@ -1817,7 +1828,11 @@ var AbracadabraWS = class extends EventEmitter {
|
|
|
1817
1828
|
this.resolveConnectionAttempt();
|
|
1818
1829
|
this.lastMessageReceived = getUnixTime();
|
|
1819
1830
|
const documentName = new IncomingMessage(event.data).peekVarString();
|
|
1820
|
-
|
|
1831
|
+
try {
|
|
1832
|
+
this.configuration.providerMap.get(documentName)?.onMessage(event);
|
|
1833
|
+
} catch (err) {
|
|
1834
|
+
console.error(`[AbracadabraWS] Provider onMessage error for "${documentName}":`, err);
|
|
1835
|
+
}
|
|
1821
1836
|
}
|
|
1822
1837
|
resolveConnectionAttempt() {
|
|
1823
1838
|
if (this.connectionAttempt) {
|
|
@@ -2244,6 +2259,10 @@ var AwarenessError = class extends Error {
|
|
|
2244
2259
|
}
|
|
2245
2260
|
};
|
|
2246
2261
|
var AbracadabraBaseProvider = class extends EventEmitter {
|
|
2262
|
+
/** Current WebSocket connection status. */
|
|
2263
|
+
get connectionStatus() {
|
|
2264
|
+
return this.configuration.websocketProvider.status;
|
|
2265
|
+
}
|
|
2247
2266
|
constructor(configuration) {
|
|
2248
2267
|
super();
|
|
2249
2268
|
this.configuration = {
|
|
@@ -2677,6 +2696,22 @@ var OfflineStore = class {
|
|
|
2677
2696
|
const tx = db.transaction("meta", "readwrite");
|
|
2678
2697
|
await txPromise$2(tx.objectStore("meta"), tx.objectStore("meta").put(value, `meta:${key}`));
|
|
2679
2698
|
}
|
|
2699
|
+
/**
|
|
2700
|
+
* Clear all stored data (updates, snapshots, state vectors, subdoc queue).
|
|
2701
|
+
* The database itself is kept but emptied.
|
|
2702
|
+
*/
|
|
2703
|
+
async clearAll() {
|
|
2704
|
+
const db = await this.getDb();
|
|
2705
|
+
if (!db) return;
|
|
2706
|
+
const storeNames = Array.from(db.objectStoreNames);
|
|
2707
|
+
if (storeNames.length === 0) return;
|
|
2708
|
+
const tx = db.transaction(storeNames, "readwrite");
|
|
2709
|
+
await Promise.all(storeNames.map((name) => new Promise((resolve, reject) => {
|
|
2710
|
+
const req = tx.objectStore(name).clear();
|
|
2711
|
+
req.onsuccess = () => resolve();
|
|
2712
|
+
req.onerror = () => reject(req.error);
|
|
2713
|
+
})));
|
|
2714
|
+
}
|
|
2680
2715
|
destroy() {
|
|
2681
2716
|
this._destroyed = true;
|
|
2682
2717
|
this.db = null;
|
|
@@ -2762,7 +2797,9 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
2762
2797
|
this.document.on("subdocs", this.boundHandleYSubdocsChange);
|
|
2763
2798
|
this.on("synced", () => this.flushPendingUpdates());
|
|
2764
2799
|
this.restorePermissionSnapshot();
|
|
2765
|
-
this.ready = this._initFromOfflineStore()
|
|
2800
|
+
this.ready = Promise.race([this._initFromOfflineStore(), new Promise((resolve) => setTimeout(resolve, 5e3))]).catch((err) => {
|
|
2801
|
+
this.emit("error", { message: `Offline store init failed: ${err?.message ?? err}` });
|
|
2802
|
+
});
|
|
2766
2803
|
}
|
|
2767
2804
|
/**
|
|
2768
2805
|
* Extract the server hostname from the provider configuration.
|
|
@@ -2880,7 +2917,9 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
2880
2917
|
}
|
|
2881
2918
|
const msg = parsed;
|
|
2882
2919
|
if (msg.type === "auth_challenge" && msg.challenge && msg.expiresAt) {
|
|
2883
|
-
this.handleAuthChallenge(msg.challenge, msg.expiresAt).catch(() =>
|
|
2920
|
+
this.handleAuthChallenge(msg.challenge, msg.expiresAt).catch((err) => {
|
|
2921
|
+
this.emit("authenticationFailed", { reason: `Auth challenge error: ${err?.message ?? err}` });
|
|
2922
|
+
});
|
|
2884
2923
|
return;
|
|
2885
2924
|
}
|
|
2886
2925
|
if (msg.type === "subdoc_registered" && msg.child_id && msg.parent_id) {
|
|
@@ -2922,6 +2961,14 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
2922
2961
|
createdAt: Date.now()
|
|
2923
2962
|
});
|
|
2924
2963
|
}
|
|
2964
|
+
/** Get a loaded child provider by ID, or null if not yet loaded. */
|
|
2965
|
+
getChild(childId) {
|
|
2966
|
+
return this.childProviders.get(childId) ?? null;
|
|
2967
|
+
}
|
|
2968
|
+
/** Check if a child provider is already loaded. */
|
|
2969
|
+
hasChild(childId) {
|
|
2970
|
+
return this.childProviders.has(childId);
|
|
2971
|
+
}
|
|
2925
2972
|
/**
|
|
2926
2973
|
* Create (or return cached) a child AbracadabraProvider for a given
|
|
2927
2974
|
* child document id. Each child opens its own WebSocket connection because
|
|
@@ -3052,6 +3099,17 @@ var AbracadabraClient = class {
|
|
|
3052
3099
|
get isAuthenticated() {
|
|
3053
3100
|
return this._token !== null;
|
|
3054
3101
|
}
|
|
3102
|
+
/** Check if the current JWT token is present and not expired. */
|
|
3103
|
+
isTokenValid() {
|
|
3104
|
+
if (!this._token) return false;
|
|
3105
|
+
try {
|
|
3106
|
+
const [, payload] = this._token.split(".");
|
|
3107
|
+
const { exp } = JSON.parse(atob(payload));
|
|
3108
|
+
return typeof exp === "number" && exp * 1e3 > Date.now();
|
|
3109
|
+
} catch {
|
|
3110
|
+
return false;
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3055
3113
|
/** Derives ws:// or wss:// URL from the http(s) base URL. */
|
|
3056
3114
|
get wsUrl() {
|
|
3057
3115
|
return this.baseUrl.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://") + "/ws";
|
|
@@ -3145,12 +3203,7 @@ var AbracadabraClient = class {
|
|
|
3145
3203
|
}
|
|
3146
3204
|
/** Get the caller's key envelope for a document (for decrypting the DocKey). */
|
|
3147
3205
|
async getMyKeyEnvelope(docId) {
|
|
3148
|
-
|
|
3149
|
-
return await this.request("GET", `/docs/${encodeURIComponent(docId)}/key-envelope`);
|
|
3150
|
-
} catch (e) {
|
|
3151
|
-
if (typeof e === "object" && e !== null && "status" in e && e.status === 404) return null;
|
|
3152
|
-
throw e;
|
|
3153
|
-
}
|
|
3206
|
+
return this.requestOrNull("GET", `/docs/${encodeURIComponent(docId)}/key-envelope`);
|
|
3154
3207
|
}
|
|
3155
3208
|
/** Upload key envelopes for a document (Owner only). */
|
|
3156
3209
|
async uploadKeyEnvelopes(docId, opts) {
|
|
@@ -3158,12 +3211,7 @@ var AbracadabraClient = class {
|
|
|
3158
3211
|
}
|
|
3159
3212
|
/** Get the X25519 public key for a user. */
|
|
3160
3213
|
async getUserX25519Key(userId) {
|
|
3161
|
-
|
|
3162
|
-
return (await this.request("GET", `/users/${encodeURIComponent(userId)}/x25519-key`)).x25519_key;
|
|
3163
|
-
} catch (e) {
|
|
3164
|
-
if (typeof e === "object" && e !== null && "status" in e && e.status === 404) return null;
|
|
3165
|
-
throw e;
|
|
3166
|
-
}
|
|
3214
|
+
return (await this.requestOrNull("GET", `/users/${encodeURIComponent(userId)}/x25519-key`))?.x25519_key ?? null;
|
|
3167
3215
|
}
|
|
3168
3216
|
/** List all non-revoked keys for a user (Owner/Admin or self). */
|
|
3169
3217
|
async listUserKeys(userId) {
|
|
@@ -3241,14 +3289,15 @@ var AbracadabraClient = class {
|
|
|
3241
3289
|
async listEffectivePermissions(docId) {
|
|
3242
3290
|
try {
|
|
3243
3291
|
return await this.request("GET", `/docs/${encodeURIComponent(docId)}/effective-permissions`);
|
|
3244
|
-
} catch {
|
|
3245
|
-
return {
|
|
3292
|
+
} catch (e) {
|
|
3293
|
+
if (typeof e === "object" && e !== null && "status" in e && e.status === 404) return {
|
|
3246
3294
|
permissions: (await this.listPermissions(docId)).map((p) => ({
|
|
3247
3295
|
...p,
|
|
3248
3296
|
source: "direct"
|
|
3249
3297
|
})),
|
|
3250
3298
|
default_role: "viewer"
|
|
3251
3299
|
};
|
|
3300
|
+
throw e;
|
|
3252
3301
|
}
|
|
3253
3302
|
}
|
|
3254
3303
|
/** Grant or change a user's role on a document (requires Owner). */
|
|
@@ -3327,12 +3376,7 @@ var AbracadabraClient = class {
|
|
|
3327
3376
|
}
|
|
3328
3377
|
/** Get the hub space, or null if none is configured. */
|
|
3329
3378
|
async getHubSpace() {
|
|
3330
|
-
|
|
3331
|
-
return await this.request("GET", "/spaces/hub", { auth: false });
|
|
3332
|
-
} catch (e) {
|
|
3333
|
-
if (typeof e === "object" && e !== null && "status" in e && e.status === 404) return null;
|
|
3334
|
-
throw e;
|
|
3335
|
-
}
|
|
3379
|
+
return this.requestOrNull("GET", "/spaces/hub", { auth: false });
|
|
3336
3380
|
}
|
|
3337
3381
|
/** Create a new space (auth required). */
|
|
3338
3382
|
async createSpace(opts) {
|
|
@@ -3375,25 +3419,35 @@ var AbracadabraClient = class {
|
|
|
3375
3419
|
if (auth && this._token) headers["Authorization"] = `Bearer ${this._token}`;
|
|
3376
3420
|
const init = {
|
|
3377
3421
|
method,
|
|
3378
|
-
headers
|
|
3422
|
+
headers,
|
|
3423
|
+
signal: AbortSignal.timeout(3e4)
|
|
3379
3424
|
};
|
|
3380
3425
|
if (opts?.body !== void 0) {
|
|
3381
3426
|
headers["Content-Type"] = "application/json";
|
|
3382
3427
|
init.body = JSON.stringify(opts.body);
|
|
3383
3428
|
}
|
|
3384
3429
|
const res = await this._fetch(`${this.baseUrl}${path}`, init);
|
|
3385
|
-
if (!res.ok) throw await this.toError(res);
|
|
3430
|
+
if (!res.ok) throw await this.toError(res, method, path);
|
|
3386
3431
|
if (res.status === 204) return;
|
|
3387
3432
|
return res.json();
|
|
3388
3433
|
}
|
|
3389
|
-
async
|
|
3434
|
+
async requestOrNull(method, path, opts) {
|
|
3435
|
+
try {
|
|
3436
|
+
return await this.request(method, path, opts);
|
|
3437
|
+
} catch (e) {
|
|
3438
|
+
if (typeof e === "object" && e !== null && "status" in e && e.status === 404) return null;
|
|
3439
|
+
throw e;
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
async toError(res, method, path) {
|
|
3390
3443
|
let message;
|
|
3391
3444
|
try {
|
|
3392
3445
|
message = (await res.json()).error ?? res.statusText;
|
|
3393
3446
|
} catch {
|
|
3394
3447
|
message = res.statusText;
|
|
3395
3448
|
}
|
|
3396
|
-
const
|
|
3449
|
+
const prefix = method && path ? `${method} ${path}: ` : "";
|
|
3450
|
+
const err = /* @__PURE__ */ new Error(`${prefix}${message} (${res.status})`);
|
|
3397
3451
|
err.status = res.status;
|
|
3398
3452
|
return err;
|
|
3399
3453
|
}
|
|
@@ -7412,6 +7466,7 @@ var SearchIndex = class {
|
|
|
7412
7466
|
if (!db) return [];
|
|
7413
7467
|
const queryTrigrams = [...extractTrigrams(query)];
|
|
7414
7468
|
if (queryTrigrams.length === 0) return [];
|
|
7469
|
+
const maxScoreEntries = limit * 10;
|
|
7415
7470
|
return new Promise((resolve, reject) => {
|
|
7416
7471
|
const tx = db.transaction("postings", "readonly");
|
|
7417
7472
|
const postings = tx.objectStore("postings");
|
|
@@ -7421,7 +7476,10 @@ var SearchIndex = class {
|
|
|
7421
7476
|
const req = postings.get(trigram);
|
|
7422
7477
|
req.onsuccess = () => {
|
|
7423
7478
|
const docIds = req.result ?? [];
|
|
7424
|
-
for (const docId of docIds)
|
|
7479
|
+
for (const docId of docIds) {
|
|
7480
|
+
scores.set(docId, (scores.get(docId) ?? 0) + 1);
|
|
7481
|
+
if (scores.size >= maxScoreEntries) break;
|
|
7482
|
+
}
|
|
7425
7483
|
remaining--;
|
|
7426
7484
|
if (remaining === 0) resolve([...scores.entries()].map(([docId, score]) => ({
|
|
7427
7485
|
docId,
|
|
@@ -7479,7 +7537,7 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
|
|
|
7479
7537
|
this.db = null;
|
|
7480
7538
|
this.objectUrls = /* @__PURE__ */ new Map();
|
|
7481
7539
|
this._notFound = /* @__PURE__ */ new Map();
|
|
7482
|
-
this.
|
|
7540
|
+
this._flushPromise = null;
|
|
7483
7541
|
this.origin = serverOrigin;
|
|
7484
7542
|
this.client = client ?? null;
|
|
7485
7543
|
this._onlineHandler = () => {
|
|
@@ -7621,6 +7679,20 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
|
|
|
7621
7679
|
req.onerror = () => reject(req.error);
|
|
7622
7680
|
});
|
|
7623
7681
|
}
|
|
7682
|
+
/**
|
|
7683
|
+
* Revoke the in-memory object URL without touching the IDB cache.
|
|
7684
|
+
* The next call to getBlobUrl() will re-create a fresh URL from IDB.
|
|
7685
|
+
* Use this when an <img> @error fires — the blob data is fine, only
|
|
7686
|
+
* the object URL reference is stale.
|
|
7687
|
+
*/
|
|
7688
|
+
invalidateUrl(docId, uploadId) {
|
|
7689
|
+
const key = this.blobKey(docId, uploadId);
|
|
7690
|
+
const url = this.objectUrls.get(key);
|
|
7691
|
+
if (url) {
|
|
7692
|
+
URL.revokeObjectURL(url);
|
|
7693
|
+
this.objectUrls.delete(key);
|
|
7694
|
+
}
|
|
7695
|
+
}
|
|
7624
7696
|
/** Revoke the object URL and remove the blob from cache. */
|
|
7625
7697
|
async evictBlob(docId, uploadId) {
|
|
7626
7698
|
const key = this.blobKey(docId, uploadId);
|
|
@@ -7673,38 +7745,42 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
|
|
|
7673
7745
|
* Entries that fail are marked with status "error" and left in the queue.
|
|
7674
7746
|
*/
|
|
7675
7747
|
async flushQueue() {
|
|
7676
|
-
if (this.
|
|
7677
|
-
this.
|
|
7748
|
+
if (this._flushPromise || !this.client) return;
|
|
7749
|
+
this._flushPromise = this._doFlush();
|
|
7678
7750
|
try {
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
|
|
7751
|
+
await this._flushPromise;
|
|
7752
|
+
} finally {
|
|
7753
|
+
this._flushPromise = null;
|
|
7754
|
+
}
|
|
7755
|
+
}
|
|
7756
|
+
async _doFlush() {
|
|
7757
|
+
if (!this.client) return;
|
|
7758
|
+
const pending = (await this.getQueue()).filter((e) => e.status === "pending");
|
|
7759
|
+
for (const entry of pending) {
|
|
7760
|
+
await this._updateQueueEntry(entry.id, { status: "uploading" });
|
|
7761
|
+
this.emit("upload:started", {
|
|
7762
|
+
...entry,
|
|
7763
|
+
status: "uploading"
|
|
7764
|
+
});
|
|
7765
|
+
try {
|
|
7766
|
+
await this.client.upload(entry.docId, entry.file, entry.filename);
|
|
7767
|
+
await this._updateQueueEntry(entry.id, { status: "done" });
|
|
7768
|
+
this.emit("upload:done", {
|
|
7683
7769
|
...entry,
|
|
7684
|
-
status: "
|
|
7770
|
+
status: "done"
|
|
7771
|
+
});
|
|
7772
|
+
} catch (err) {
|
|
7773
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7774
|
+
await this._updateQueueEntry(entry.id, {
|
|
7775
|
+
status: "error",
|
|
7776
|
+
error: message
|
|
7777
|
+
});
|
|
7778
|
+
this.emit("upload:error", {
|
|
7779
|
+
...entry,
|
|
7780
|
+
status: "error",
|
|
7781
|
+
error: message
|
|
7685
7782
|
});
|
|
7686
|
-
try {
|
|
7687
|
-
await this.client.upload(entry.docId, entry.file, entry.filename);
|
|
7688
|
-
await this._updateQueueEntry(entry.id, { status: "done" });
|
|
7689
|
-
this.emit("upload:done", {
|
|
7690
|
-
...entry,
|
|
7691
|
-
status: "done"
|
|
7692
|
-
});
|
|
7693
|
-
} catch (err) {
|
|
7694
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
7695
|
-
await this._updateQueueEntry(entry.id, {
|
|
7696
|
-
status: "error",
|
|
7697
|
-
error: message
|
|
7698
|
-
});
|
|
7699
|
-
this.emit("upload:error", {
|
|
7700
|
-
...entry,
|
|
7701
|
-
status: "error",
|
|
7702
|
-
error: message
|
|
7703
|
-
});
|
|
7704
|
-
}
|
|
7705
7783
|
}
|
|
7706
|
-
} finally {
|
|
7707
|
-
this._flushing = false;
|
|
7708
7784
|
}
|
|
7709
7785
|
}
|
|
7710
7786
|
async _updateQueueEntry(id, patch) {
|
|
@@ -7752,10 +7828,13 @@ const HKDF_INFO$1 = new TextEncoder().encode("abracadabra-dockey-v1");
|
|
|
7752
7828
|
function fromBase64$1(b64) {
|
|
7753
7829
|
return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
7754
7830
|
}
|
|
7755
|
-
var DocKeyManager = class {
|
|
7831
|
+
var DocKeyManager = class DocKeyManager {
|
|
7756
7832
|
constructor() {
|
|
7757
7833
|
this.cache = /* @__PURE__ */ new Map();
|
|
7758
7834
|
}
|
|
7835
|
+
static {
|
|
7836
|
+
this.CACHE_TTL = 600 * 1e3;
|
|
7837
|
+
}
|
|
7759
7838
|
/** Generate a new random AES-256-GCM document key. */
|
|
7760
7839
|
static async generateDocKey() {
|
|
7761
7840
|
return crypto.subtle.generateKey({
|
|
@@ -7769,7 +7848,7 @@ var DocKeyManager = class {
|
|
|
7769
7848
|
*/
|
|
7770
7849
|
async getDocKey(docId, client, keystore) {
|
|
7771
7850
|
const cached = this.cache.get(docId);
|
|
7772
|
-
if (cached) return cached.key;
|
|
7851
|
+
if (cached && Date.now() - cached.fetchedAt < DocKeyManager.CACHE_TTL) return cached.key;
|
|
7773
7852
|
const envelope = await client.getMyKeyEnvelope(docId);
|
|
7774
7853
|
if (!envelope) return null;
|
|
7775
7854
|
const x25519PrivKey = await keystore.getX25519PrivateKey();
|
|
@@ -7778,7 +7857,8 @@ var DocKeyManager = class {
|
|
|
7778
7857
|
const docKey = await this._unwrapKey(wrapped, x25519PrivKey, docId);
|
|
7779
7858
|
this.cache.set(docId, {
|
|
7780
7859
|
key: docKey,
|
|
7781
|
-
epoch: envelope.key_epoch
|
|
7860
|
+
epoch: envelope.key_epoch,
|
|
7861
|
+
fetchedAt: Date.now()
|
|
7782
7862
|
});
|
|
7783
7863
|
return docKey;
|
|
7784
7864
|
} finally {
|
|
@@ -8252,6 +8332,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
8252
8332
|
super();
|
|
8253
8333
|
this.syncStates = /* @__PURE__ */ new Map();
|
|
8254
8334
|
this._destroyed = false;
|
|
8335
|
+
this._initPromise = null;
|
|
8255
8336
|
this.rootProvider = rootProvider;
|
|
8256
8337
|
this.client = client;
|
|
8257
8338
|
this.fileBlobStore = fileBlobStore ?? null;
|
|
@@ -8267,18 +8348,36 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
8267
8348
|
this.persistence = new BackgroundSyncPersistence(serverOrigin);
|
|
8268
8349
|
this.semaphore = new Semaphore(this.opts.concurrency);
|
|
8269
8350
|
}
|
|
8351
|
+
/**
|
|
8352
|
+
* Load persisted sync states from IndexedDB into the in-memory map.
|
|
8353
|
+
* Called automatically by syncAll() / syncDoc(); safe to call concurrently.
|
|
8354
|
+
*/
|
|
8355
|
+
async init() {
|
|
8356
|
+
if (!this._initPromise) this._initPromise = this._loadPersistedStates();
|
|
8357
|
+
return this._initPromise;
|
|
8358
|
+
}
|
|
8359
|
+
async _loadPersistedStates() {
|
|
8360
|
+
try {
|
|
8361
|
+
const states = await this.persistence.getAllStates();
|
|
8362
|
+
for (const state of states) this.syncStates.set(state.docId, state);
|
|
8363
|
+
} catch {}
|
|
8364
|
+
}
|
|
8270
8365
|
/** Sync all documents in the root tree. */
|
|
8271
8366
|
async syncAll() {
|
|
8272
8367
|
if (this._destroyed) return;
|
|
8368
|
+
await this.init();
|
|
8273
8369
|
const treeMap = this.rootProvider.document.getMap("doc-tree");
|
|
8274
8370
|
const entries = Array.from(treeMap.entries());
|
|
8275
8371
|
if (entries.length === 0) return;
|
|
8372
|
+
const updatedAtMap = /* @__PURE__ */ new Map();
|
|
8373
|
+
for (const [docId, v] of entries) updatedAtMap.set(docId, v?.updatedAt ?? v?.createdAt ?? 0);
|
|
8276
8374
|
this._prefetchCovers(entries).catch(() => null);
|
|
8277
8375
|
const queue = this._buildQueue(entries);
|
|
8278
|
-
await Promise.all(queue.map((docId) => this._syncWithSemaphore(docId)));
|
|
8376
|
+
await Promise.all(queue.map((docId) => this._syncWithSemaphore(docId, updatedAtMap.get(docId) ?? 0)));
|
|
8279
8377
|
}
|
|
8280
8378
|
/** Sync a single document by ID. */
|
|
8281
8379
|
async syncDoc(docId) {
|
|
8380
|
+
await this.init();
|
|
8282
8381
|
const state = await this._doSyncDoc(docId);
|
|
8283
8382
|
this.syncStates.set(docId, state);
|
|
8284
8383
|
await this.persistence.setState(state).catch(() => null);
|
|
@@ -8303,6 +8402,32 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
8303
8402
|
}, intervalMs);
|
|
8304
8403
|
return () => clearInterval(handle);
|
|
8305
8404
|
}
|
|
8405
|
+
/**
|
|
8406
|
+
* Clear all offline document data and sync state.
|
|
8407
|
+
* Opens each document's OfflineStore and clears its contents, then
|
|
8408
|
+
* resets the background sync persistence. After calling this, all
|
|
8409
|
+
* documents will need to be re-synced.
|
|
8410
|
+
*/
|
|
8411
|
+
async clearAllSyncedData() {
|
|
8412
|
+
const docIds = new Set(this.syncStates.keys());
|
|
8413
|
+
const treeMap = this.rootProvider.document.getMap("doc-tree");
|
|
8414
|
+
for (const docId of treeMap.keys()) docIds.add(docId);
|
|
8415
|
+
let serverOrigin;
|
|
8416
|
+
try {
|
|
8417
|
+
serverOrigin = new URL(this.client.baseUrl ?? "").hostname;
|
|
8418
|
+
} catch {}
|
|
8419
|
+
const clearPromises = Array.from(docIds).map(async (docId) => {
|
|
8420
|
+
try {
|
|
8421
|
+
const store = new OfflineStore(docId, serverOrigin);
|
|
8422
|
+
await store.clearAll();
|
|
8423
|
+
store.destroy();
|
|
8424
|
+
} catch {}
|
|
8425
|
+
});
|
|
8426
|
+
await Promise.all(clearPromises);
|
|
8427
|
+
for (const docId of docIds) await this.persistence.deleteState(docId).catch(() => null);
|
|
8428
|
+
this.syncStates.clear();
|
|
8429
|
+
this._initPromise = null;
|
|
8430
|
+
}
|
|
8306
8431
|
destroy() {
|
|
8307
8432
|
this._destroyed = true;
|
|
8308
8433
|
this.removeAllListeners();
|
|
@@ -8329,8 +8454,16 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
8329
8454
|
items.sort((a, b) => b.priority - a.priority);
|
|
8330
8455
|
return items.map((i) => i.docId);
|
|
8331
8456
|
}
|
|
8332
|
-
async _syncWithSemaphore(docId) {
|
|
8457
|
+
async _syncWithSemaphore(docId, updatedAt) {
|
|
8333
8458
|
if (this._destroyed) return;
|
|
8459
|
+
const existing = this.syncStates.get(docId);
|
|
8460
|
+
if (existing && existing.status === "synced" && existing.lastSynced !== null && existing.lastSynced >= updatedAt) {
|
|
8461
|
+
this.emit("stateChanged", {
|
|
8462
|
+
docId,
|
|
8463
|
+
state: existing
|
|
8464
|
+
});
|
|
8465
|
+
return;
|
|
8466
|
+
}
|
|
8334
8467
|
await this.semaphore.acquire();
|
|
8335
8468
|
try {
|
|
8336
8469
|
const state = await this._doSyncDoc(docId);
|
|
@@ -8356,8 +8489,8 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
8356
8489
|
docId,
|
|
8357
8490
|
state: syncing
|
|
8358
8491
|
});
|
|
8492
|
+
let isE2E = false;
|
|
8359
8493
|
try {
|
|
8360
|
-
let isE2E = false;
|
|
8361
8494
|
try {
|
|
8362
8495
|
isE2E = (await this.client.getDocEncryption(docId)).mode === "e2e";
|
|
8363
8496
|
} catch {}
|
|
@@ -8370,7 +8503,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
8370
8503
|
status: "error",
|
|
8371
8504
|
lastSynced: this.syncStates.get(docId)?.lastSynced ?? null,
|
|
8372
8505
|
error,
|
|
8373
|
-
isE2E
|
|
8506
|
+
isE2E
|
|
8374
8507
|
};
|
|
8375
8508
|
}
|
|
8376
8509
|
}
|
|
@@ -8503,6 +8636,7 @@ var SignalingSocket = class extends EventEmitter {
|
|
|
8503
8636
|
this.connectionAttempt = null;
|
|
8504
8637
|
this.localPeerId = null;
|
|
8505
8638
|
this.isConnected = false;
|
|
8639
|
+
this._connectPromise = null;
|
|
8506
8640
|
this.config = {
|
|
8507
8641
|
url: configuration.url,
|
|
8508
8642
|
token: configuration.token,
|
|
@@ -8522,6 +8656,15 @@ var SignalingSocket = class extends EventEmitter {
|
|
|
8522
8656
|
}
|
|
8523
8657
|
async connect() {
|
|
8524
8658
|
if (this.isConnected) return;
|
|
8659
|
+
if (this._connectPromise) return this._connectPromise;
|
|
8660
|
+
this._connectPromise = this._doConnect();
|
|
8661
|
+
try {
|
|
8662
|
+
await this._connectPromise;
|
|
8663
|
+
} finally {
|
|
8664
|
+
this._connectPromise = null;
|
|
8665
|
+
}
|
|
8666
|
+
}
|
|
8667
|
+
async _doConnect() {
|
|
8525
8668
|
if (this.cancelRetry) {
|
|
8526
8669
|
this.cancelRetry();
|
|
8527
8670
|
this.cancelRetry = void 0;
|
|
@@ -8835,11 +8978,12 @@ var DataChannelRouter = class extends EventEmitter {
|
|
|
8835
8978
|
*/
|
|
8836
8979
|
async send(name, data) {
|
|
8837
8980
|
const channel = this.channels.get(name);
|
|
8838
|
-
if (!channel || channel.readyState !== "open") return;
|
|
8981
|
+
if (!channel || channel.readyState !== "open") return false;
|
|
8839
8982
|
if (this.encryptor?.isEstablished && !this.plaintextChannels.has(name)) {
|
|
8840
8983
|
const encrypted = await this.encryptor.encrypt(data);
|
|
8841
8984
|
channel.send(encrypted);
|
|
8842
8985
|
} else channel.send(data);
|
|
8986
|
+
return true;
|
|
8843
8987
|
}
|
|
8844
8988
|
registerChannel(channel) {
|
|
8845
8989
|
channel.binaryType = "arraybuffer";
|
|
@@ -9007,6 +9151,7 @@ var YjsDataChannel = class {
|
|
|
9007
9151
|
this.document = document;
|
|
9008
9152
|
this.awareness = awareness;
|
|
9009
9153
|
this.router = router;
|
|
9154
|
+
this.isSynced = false;
|
|
9010
9155
|
this.docUpdateHandler = null;
|
|
9011
9156
|
this.awarenessUpdateHandler = null;
|
|
9012
9157
|
this.channelOpenHandler = null;
|
|
@@ -9243,6 +9388,10 @@ var FileTransferChannel = class extends EventEmitter {
|
|
|
9243
9388
|
try {
|
|
9244
9389
|
meta = JSON.parse(json);
|
|
9245
9390
|
} catch {
|
|
9391
|
+
this.emit("receiveError", {
|
|
9392
|
+
transferId: "unknown",
|
|
9393
|
+
error: "Malformed START message: invalid JSON"
|
|
9394
|
+
});
|
|
9246
9395
|
return;
|
|
9247
9396
|
}
|
|
9248
9397
|
this.receives.set(meta.transferId, {
|
|
@@ -9949,7 +10098,9 @@ var ManualSignaling = class extends EventEmitter {
|
|
|
9949
10098
|
type: "offer",
|
|
9950
10099
|
sdp: offerBlob.sdp
|
|
9951
10100
|
}));
|
|
9952
|
-
for (const c of offerBlob.candidates)
|
|
10101
|
+
for (const c of offerBlob.candidates) try {
|
|
10102
|
+
await this.pc.addIceCandidate(new RTCIceCandidate(JSON.parse(c)));
|
|
10103
|
+
} catch {}
|
|
9953
10104
|
const answer = await this.pc.createAnswer();
|
|
9954
10105
|
await this.pc.setLocalDescription(answer);
|
|
9955
10106
|
await gatheringComplete;
|