@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
|
@@ -1005,9 +1005,20 @@ var EventEmitter = class {
|
|
|
1005
1005
|
this.callbacks[event].push(fn);
|
|
1006
1006
|
return this;
|
|
1007
1007
|
}
|
|
1008
|
+
once(event, fn) {
|
|
1009
|
+
const wrapper = (...args) => {
|
|
1010
|
+
this.off(event, wrapper);
|
|
1011
|
+
fn.apply(this, args);
|
|
1012
|
+
};
|
|
1013
|
+
return this.on(event, wrapper);
|
|
1014
|
+
}
|
|
1008
1015
|
emit(event, ...args) {
|
|
1009
1016
|
const callbacks = this.callbacks[event];
|
|
1010
|
-
if (callbacks)
|
|
1017
|
+
if (callbacks) for (const callback of callbacks) try {
|
|
1018
|
+
callback.apply(this, args);
|
|
1019
|
+
} catch (err) {
|
|
1020
|
+
console.error(`[EventEmitter] Error in "${event}" listener:`, err);
|
|
1021
|
+
}
|
|
1011
1022
|
return this;
|
|
1012
1023
|
}
|
|
1013
1024
|
off(event, fn) {
|
|
@@ -1847,7 +1858,11 @@ var AbracadabraWS = class extends EventEmitter {
|
|
|
1847
1858
|
this.resolveConnectionAttempt();
|
|
1848
1859
|
this.lastMessageReceived = getUnixTime();
|
|
1849
1860
|
const documentName = new IncomingMessage(event.data).peekVarString();
|
|
1850
|
-
|
|
1861
|
+
try {
|
|
1862
|
+
this.configuration.providerMap.get(documentName)?.onMessage(event);
|
|
1863
|
+
} catch (err) {
|
|
1864
|
+
console.error(`[AbracadabraWS] Provider onMessage error for "${documentName}":`, err);
|
|
1865
|
+
}
|
|
1851
1866
|
}
|
|
1852
1867
|
resolveConnectionAttempt() {
|
|
1853
1868
|
if (this.connectionAttempt) {
|
|
@@ -2274,6 +2289,10 @@ var AwarenessError = class extends Error {
|
|
|
2274
2289
|
}
|
|
2275
2290
|
};
|
|
2276
2291
|
var AbracadabraBaseProvider = class extends EventEmitter {
|
|
2292
|
+
/** Current WebSocket connection status. */
|
|
2293
|
+
get connectionStatus() {
|
|
2294
|
+
return this.configuration.websocketProvider.status;
|
|
2295
|
+
}
|
|
2277
2296
|
constructor(configuration) {
|
|
2278
2297
|
super();
|
|
2279
2298
|
this.configuration = {
|
|
@@ -2707,6 +2726,22 @@ var OfflineStore = class {
|
|
|
2707
2726
|
const tx = db.transaction("meta", "readwrite");
|
|
2708
2727
|
await txPromise$2(tx.objectStore("meta"), tx.objectStore("meta").put(value, `meta:${key}`));
|
|
2709
2728
|
}
|
|
2729
|
+
/**
|
|
2730
|
+
* Clear all stored data (updates, snapshots, state vectors, subdoc queue).
|
|
2731
|
+
* The database itself is kept but emptied.
|
|
2732
|
+
*/
|
|
2733
|
+
async clearAll() {
|
|
2734
|
+
const db = await this.getDb();
|
|
2735
|
+
if (!db) return;
|
|
2736
|
+
const storeNames = Array.from(db.objectStoreNames);
|
|
2737
|
+
if (storeNames.length === 0) return;
|
|
2738
|
+
const tx = db.transaction(storeNames, "readwrite");
|
|
2739
|
+
await Promise.all(storeNames.map((name) => new Promise((resolve, reject) => {
|
|
2740
|
+
const req = tx.objectStore(name).clear();
|
|
2741
|
+
req.onsuccess = () => resolve();
|
|
2742
|
+
req.onerror = () => reject(req.error);
|
|
2743
|
+
})));
|
|
2744
|
+
}
|
|
2710
2745
|
destroy() {
|
|
2711
2746
|
this._destroyed = true;
|
|
2712
2747
|
this.db = null;
|
|
@@ -2792,7 +2827,9 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
2792
2827
|
this.document.on("subdocs", this.boundHandleYSubdocsChange);
|
|
2793
2828
|
this.on("synced", () => this.flushPendingUpdates());
|
|
2794
2829
|
this.restorePermissionSnapshot();
|
|
2795
|
-
this.ready = this._initFromOfflineStore()
|
|
2830
|
+
this.ready = Promise.race([this._initFromOfflineStore(), new Promise((resolve) => setTimeout(resolve, 5e3))]).catch((err) => {
|
|
2831
|
+
this.emit("error", { message: `Offline store init failed: ${err?.message ?? err}` });
|
|
2832
|
+
});
|
|
2796
2833
|
}
|
|
2797
2834
|
/**
|
|
2798
2835
|
* Extract the server hostname from the provider configuration.
|
|
@@ -2910,7 +2947,9 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
2910
2947
|
}
|
|
2911
2948
|
const msg = parsed;
|
|
2912
2949
|
if (msg.type === "auth_challenge" && msg.challenge && msg.expiresAt) {
|
|
2913
|
-
this.handleAuthChallenge(msg.challenge, msg.expiresAt).catch(() =>
|
|
2950
|
+
this.handleAuthChallenge(msg.challenge, msg.expiresAt).catch((err) => {
|
|
2951
|
+
this.emit("authenticationFailed", { reason: `Auth challenge error: ${err?.message ?? err}` });
|
|
2952
|
+
});
|
|
2914
2953
|
return;
|
|
2915
2954
|
}
|
|
2916
2955
|
if (msg.type === "subdoc_registered" && msg.child_id && msg.parent_id) {
|
|
@@ -2952,6 +2991,14 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
2952
2991
|
createdAt: Date.now()
|
|
2953
2992
|
});
|
|
2954
2993
|
}
|
|
2994
|
+
/** Get a loaded child provider by ID, or null if not yet loaded. */
|
|
2995
|
+
getChild(childId) {
|
|
2996
|
+
return this.childProviders.get(childId) ?? null;
|
|
2997
|
+
}
|
|
2998
|
+
/** Check if a child provider is already loaded. */
|
|
2999
|
+
hasChild(childId) {
|
|
3000
|
+
return this.childProviders.has(childId);
|
|
3001
|
+
}
|
|
2955
3002
|
/**
|
|
2956
3003
|
* Create (or return cached) a child AbracadabraProvider for a given
|
|
2957
3004
|
* child document id. Each child opens its own WebSocket connection because
|
|
@@ -3082,6 +3129,17 @@ var AbracadabraClient = class {
|
|
|
3082
3129
|
get isAuthenticated() {
|
|
3083
3130
|
return this._token !== null;
|
|
3084
3131
|
}
|
|
3132
|
+
/** Check if the current JWT token is present and not expired. */
|
|
3133
|
+
isTokenValid() {
|
|
3134
|
+
if (!this._token) return false;
|
|
3135
|
+
try {
|
|
3136
|
+
const [, payload] = this._token.split(".");
|
|
3137
|
+
const { exp } = JSON.parse(atob(payload));
|
|
3138
|
+
return typeof exp === "number" && exp * 1e3 > Date.now();
|
|
3139
|
+
} catch {
|
|
3140
|
+
return false;
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3085
3143
|
/** Derives ws:// or wss:// URL from the http(s) base URL. */
|
|
3086
3144
|
get wsUrl() {
|
|
3087
3145
|
return this.baseUrl.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://") + "/ws";
|
|
@@ -3175,12 +3233,7 @@ var AbracadabraClient = class {
|
|
|
3175
3233
|
}
|
|
3176
3234
|
/** Get the caller's key envelope for a document (for decrypting the DocKey). */
|
|
3177
3235
|
async getMyKeyEnvelope(docId) {
|
|
3178
|
-
|
|
3179
|
-
return await this.request("GET", `/docs/${encodeURIComponent(docId)}/key-envelope`);
|
|
3180
|
-
} catch (e) {
|
|
3181
|
-
if (typeof e === "object" && e !== null && "status" in e && e.status === 404) return null;
|
|
3182
|
-
throw e;
|
|
3183
|
-
}
|
|
3236
|
+
return this.requestOrNull("GET", `/docs/${encodeURIComponent(docId)}/key-envelope`);
|
|
3184
3237
|
}
|
|
3185
3238
|
/** Upload key envelopes for a document (Owner only). */
|
|
3186
3239
|
async uploadKeyEnvelopes(docId, opts) {
|
|
@@ -3188,12 +3241,7 @@ var AbracadabraClient = class {
|
|
|
3188
3241
|
}
|
|
3189
3242
|
/** Get the X25519 public key for a user. */
|
|
3190
3243
|
async getUserX25519Key(userId) {
|
|
3191
|
-
|
|
3192
|
-
return (await this.request("GET", `/users/${encodeURIComponent(userId)}/x25519-key`)).x25519_key;
|
|
3193
|
-
} catch (e) {
|
|
3194
|
-
if (typeof e === "object" && e !== null && "status" in e && e.status === 404) return null;
|
|
3195
|
-
throw e;
|
|
3196
|
-
}
|
|
3244
|
+
return (await this.requestOrNull("GET", `/users/${encodeURIComponent(userId)}/x25519-key`))?.x25519_key ?? null;
|
|
3197
3245
|
}
|
|
3198
3246
|
/** List all non-revoked keys for a user (Owner/Admin or self). */
|
|
3199
3247
|
async listUserKeys(userId) {
|
|
@@ -3271,14 +3319,15 @@ var AbracadabraClient = class {
|
|
|
3271
3319
|
async listEffectivePermissions(docId) {
|
|
3272
3320
|
try {
|
|
3273
3321
|
return await this.request("GET", `/docs/${encodeURIComponent(docId)}/effective-permissions`);
|
|
3274
|
-
} catch {
|
|
3275
|
-
return {
|
|
3322
|
+
} catch (e) {
|
|
3323
|
+
if (typeof e === "object" && e !== null && "status" in e && e.status === 404) return {
|
|
3276
3324
|
permissions: (await this.listPermissions(docId)).map((p) => ({
|
|
3277
3325
|
...p,
|
|
3278
3326
|
source: "direct"
|
|
3279
3327
|
})),
|
|
3280
3328
|
default_role: "viewer"
|
|
3281
3329
|
};
|
|
3330
|
+
throw e;
|
|
3282
3331
|
}
|
|
3283
3332
|
}
|
|
3284
3333
|
/** Grant or change a user's role on a document (requires Owner). */
|
|
@@ -3357,12 +3406,7 @@ var AbracadabraClient = class {
|
|
|
3357
3406
|
}
|
|
3358
3407
|
/** Get the hub space, or null if none is configured. */
|
|
3359
3408
|
async getHubSpace() {
|
|
3360
|
-
|
|
3361
|
-
return await this.request("GET", "/spaces/hub", { auth: false });
|
|
3362
|
-
} catch (e) {
|
|
3363
|
-
if (typeof e === "object" && e !== null && "status" in e && e.status === 404) return null;
|
|
3364
|
-
throw e;
|
|
3365
|
-
}
|
|
3409
|
+
return this.requestOrNull("GET", "/spaces/hub", { auth: false });
|
|
3366
3410
|
}
|
|
3367
3411
|
/** Create a new space (auth required). */
|
|
3368
3412
|
async createSpace(opts) {
|
|
@@ -3405,25 +3449,35 @@ var AbracadabraClient = class {
|
|
|
3405
3449
|
if (auth && this._token) headers["Authorization"] = `Bearer ${this._token}`;
|
|
3406
3450
|
const init = {
|
|
3407
3451
|
method,
|
|
3408
|
-
headers
|
|
3452
|
+
headers,
|
|
3453
|
+
signal: AbortSignal.timeout(3e4)
|
|
3409
3454
|
};
|
|
3410
3455
|
if (opts?.body !== void 0) {
|
|
3411
3456
|
headers["Content-Type"] = "application/json";
|
|
3412
3457
|
init.body = JSON.stringify(opts.body);
|
|
3413
3458
|
}
|
|
3414
3459
|
const res = await this._fetch(`${this.baseUrl}${path}`, init);
|
|
3415
|
-
if (!res.ok) throw await this.toError(res);
|
|
3460
|
+
if (!res.ok) throw await this.toError(res, method, path);
|
|
3416
3461
|
if (res.status === 204) return;
|
|
3417
3462
|
return res.json();
|
|
3418
3463
|
}
|
|
3419
|
-
async
|
|
3464
|
+
async requestOrNull(method, path, opts) {
|
|
3465
|
+
try {
|
|
3466
|
+
return await this.request(method, path, opts);
|
|
3467
|
+
} catch (e) {
|
|
3468
|
+
if (typeof e === "object" && e !== null && "status" in e && e.status === 404) return null;
|
|
3469
|
+
throw e;
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
async toError(res, method, path) {
|
|
3420
3473
|
let message;
|
|
3421
3474
|
try {
|
|
3422
3475
|
message = (await res.json()).error ?? res.statusText;
|
|
3423
3476
|
} catch {
|
|
3424
3477
|
message = res.statusText;
|
|
3425
3478
|
}
|
|
3426
|
-
const
|
|
3479
|
+
const prefix = method && path ? `${method} ${path}: ` : "";
|
|
3480
|
+
const err = /* @__PURE__ */ new Error(`${prefix}${message} (${res.status})`);
|
|
3427
3481
|
err.status = res.status;
|
|
3428
3482
|
return err;
|
|
3429
3483
|
}
|
|
@@ -7442,6 +7496,7 @@ var SearchIndex = class {
|
|
|
7442
7496
|
if (!db) return [];
|
|
7443
7497
|
const queryTrigrams = [...extractTrigrams(query)];
|
|
7444
7498
|
if (queryTrigrams.length === 0) return [];
|
|
7499
|
+
const maxScoreEntries = limit * 10;
|
|
7445
7500
|
return new Promise((resolve, reject) => {
|
|
7446
7501
|
const tx = db.transaction("postings", "readonly");
|
|
7447
7502
|
const postings = tx.objectStore("postings");
|
|
@@ -7451,7 +7506,10 @@ var SearchIndex = class {
|
|
|
7451
7506
|
const req = postings.get(trigram);
|
|
7452
7507
|
req.onsuccess = () => {
|
|
7453
7508
|
const docIds = req.result ?? [];
|
|
7454
|
-
for (const docId of docIds)
|
|
7509
|
+
for (const docId of docIds) {
|
|
7510
|
+
scores.set(docId, (scores.get(docId) ?? 0) + 1);
|
|
7511
|
+
if (scores.size >= maxScoreEntries) break;
|
|
7512
|
+
}
|
|
7455
7513
|
remaining--;
|
|
7456
7514
|
if (remaining === 0) resolve([...scores.entries()].map(([docId, score]) => ({
|
|
7457
7515
|
docId,
|
|
@@ -7509,7 +7567,7 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
|
|
|
7509
7567
|
this.db = null;
|
|
7510
7568
|
this.objectUrls = /* @__PURE__ */ new Map();
|
|
7511
7569
|
this._notFound = /* @__PURE__ */ new Map();
|
|
7512
|
-
this.
|
|
7570
|
+
this._flushPromise = null;
|
|
7513
7571
|
this.origin = serverOrigin;
|
|
7514
7572
|
this.client = client ?? null;
|
|
7515
7573
|
this._onlineHandler = () => {
|
|
@@ -7651,6 +7709,20 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
|
|
|
7651
7709
|
req.onerror = () => reject(req.error);
|
|
7652
7710
|
});
|
|
7653
7711
|
}
|
|
7712
|
+
/**
|
|
7713
|
+
* Revoke the in-memory object URL without touching the IDB cache.
|
|
7714
|
+
* The next call to getBlobUrl() will re-create a fresh URL from IDB.
|
|
7715
|
+
* Use this when an <img> @error fires — the blob data is fine, only
|
|
7716
|
+
* the object URL reference is stale.
|
|
7717
|
+
*/
|
|
7718
|
+
invalidateUrl(docId, uploadId) {
|
|
7719
|
+
const key = this.blobKey(docId, uploadId);
|
|
7720
|
+
const url = this.objectUrls.get(key);
|
|
7721
|
+
if (url) {
|
|
7722
|
+
URL.revokeObjectURL(url);
|
|
7723
|
+
this.objectUrls.delete(key);
|
|
7724
|
+
}
|
|
7725
|
+
}
|
|
7654
7726
|
/** Revoke the object URL and remove the blob from cache. */
|
|
7655
7727
|
async evictBlob(docId, uploadId) {
|
|
7656
7728
|
const key = this.blobKey(docId, uploadId);
|
|
@@ -7703,38 +7775,42 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
|
|
|
7703
7775
|
* Entries that fail are marked with status "error" and left in the queue.
|
|
7704
7776
|
*/
|
|
7705
7777
|
async flushQueue() {
|
|
7706
|
-
if (this.
|
|
7707
|
-
this.
|
|
7778
|
+
if (this._flushPromise || !this.client) return;
|
|
7779
|
+
this._flushPromise = this._doFlush();
|
|
7708
7780
|
try {
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
7781
|
+
await this._flushPromise;
|
|
7782
|
+
} finally {
|
|
7783
|
+
this._flushPromise = null;
|
|
7784
|
+
}
|
|
7785
|
+
}
|
|
7786
|
+
async _doFlush() {
|
|
7787
|
+
if (!this.client) return;
|
|
7788
|
+
const pending = (await this.getQueue()).filter((e) => e.status === "pending");
|
|
7789
|
+
for (const entry of pending) {
|
|
7790
|
+
await this._updateQueueEntry(entry.id, { status: "uploading" });
|
|
7791
|
+
this.emit("upload:started", {
|
|
7792
|
+
...entry,
|
|
7793
|
+
status: "uploading"
|
|
7794
|
+
});
|
|
7795
|
+
try {
|
|
7796
|
+
await this.client.upload(entry.docId, entry.file, entry.filename);
|
|
7797
|
+
await this._updateQueueEntry(entry.id, { status: "done" });
|
|
7798
|
+
this.emit("upload:done", {
|
|
7713
7799
|
...entry,
|
|
7714
|
-
status: "
|
|
7800
|
+
status: "done"
|
|
7801
|
+
});
|
|
7802
|
+
} catch (err) {
|
|
7803
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7804
|
+
await this._updateQueueEntry(entry.id, {
|
|
7805
|
+
status: "error",
|
|
7806
|
+
error: message
|
|
7807
|
+
});
|
|
7808
|
+
this.emit("upload:error", {
|
|
7809
|
+
...entry,
|
|
7810
|
+
status: "error",
|
|
7811
|
+
error: message
|
|
7715
7812
|
});
|
|
7716
|
-
try {
|
|
7717
|
-
await this.client.upload(entry.docId, entry.file, entry.filename);
|
|
7718
|
-
await this._updateQueueEntry(entry.id, { status: "done" });
|
|
7719
|
-
this.emit("upload:done", {
|
|
7720
|
-
...entry,
|
|
7721
|
-
status: "done"
|
|
7722
|
-
});
|
|
7723
|
-
} catch (err) {
|
|
7724
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
7725
|
-
await this._updateQueueEntry(entry.id, {
|
|
7726
|
-
status: "error",
|
|
7727
|
-
error: message
|
|
7728
|
-
});
|
|
7729
|
-
this.emit("upload:error", {
|
|
7730
|
-
...entry,
|
|
7731
|
-
status: "error",
|
|
7732
|
-
error: message
|
|
7733
|
-
});
|
|
7734
|
-
}
|
|
7735
7813
|
}
|
|
7736
|
-
} finally {
|
|
7737
|
-
this._flushing = false;
|
|
7738
7814
|
}
|
|
7739
7815
|
}
|
|
7740
7816
|
async _updateQueueEntry(id, patch) {
|
|
@@ -7782,10 +7858,13 @@ const HKDF_INFO$1 = new TextEncoder().encode("abracadabra-dockey-v1");
|
|
|
7782
7858
|
function fromBase64$1(b64) {
|
|
7783
7859
|
return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
7784
7860
|
}
|
|
7785
|
-
var DocKeyManager = class {
|
|
7861
|
+
var DocKeyManager = class DocKeyManager {
|
|
7786
7862
|
constructor() {
|
|
7787
7863
|
this.cache = /* @__PURE__ */ new Map();
|
|
7788
7864
|
}
|
|
7865
|
+
static {
|
|
7866
|
+
this.CACHE_TTL = 600 * 1e3;
|
|
7867
|
+
}
|
|
7789
7868
|
/** Generate a new random AES-256-GCM document key. */
|
|
7790
7869
|
static async generateDocKey() {
|
|
7791
7870
|
return crypto.subtle.generateKey({
|
|
@@ -7799,7 +7878,7 @@ var DocKeyManager = class {
|
|
|
7799
7878
|
*/
|
|
7800
7879
|
async getDocKey(docId, client, keystore) {
|
|
7801
7880
|
const cached = this.cache.get(docId);
|
|
7802
|
-
if (cached) return cached.key;
|
|
7881
|
+
if (cached && Date.now() - cached.fetchedAt < DocKeyManager.CACHE_TTL) return cached.key;
|
|
7803
7882
|
const envelope = await client.getMyKeyEnvelope(docId);
|
|
7804
7883
|
if (!envelope) return null;
|
|
7805
7884
|
const x25519PrivKey = await keystore.getX25519PrivateKey();
|
|
@@ -7808,7 +7887,8 @@ var DocKeyManager = class {
|
|
|
7808
7887
|
const docKey = await this._unwrapKey(wrapped, x25519PrivKey, docId);
|
|
7809
7888
|
this.cache.set(docId, {
|
|
7810
7889
|
key: docKey,
|
|
7811
|
-
epoch: envelope.key_epoch
|
|
7890
|
+
epoch: envelope.key_epoch,
|
|
7891
|
+
fetchedAt: Date.now()
|
|
7812
7892
|
});
|
|
7813
7893
|
return docKey;
|
|
7814
7894
|
} finally {
|
|
@@ -8304,6 +8384,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
8304
8384
|
super();
|
|
8305
8385
|
this.syncStates = /* @__PURE__ */ new Map();
|
|
8306
8386
|
this._destroyed = false;
|
|
8387
|
+
this._initPromise = null;
|
|
8307
8388
|
this.rootProvider = rootProvider;
|
|
8308
8389
|
this.client = client;
|
|
8309
8390
|
this.fileBlobStore = fileBlobStore ?? null;
|
|
@@ -8319,18 +8400,36 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
8319
8400
|
this.persistence = new BackgroundSyncPersistence(serverOrigin);
|
|
8320
8401
|
this.semaphore = new Semaphore(this.opts.concurrency);
|
|
8321
8402
|
}
|
|
8403
|
+
/**
|
|
8404
|
+
* Load persisted sync states from IndexedDB into the in-memory map.
|
|
8405
|
+
* Called automatically by syncAll() / syncDoc(); safe to call concurrently.
|
|
8406
|
+
*/
|
|
8407
|
+
async init() {
|
|
8408
|
+
if (!this._initPromise) this._initPromise = this._loadPersistedStates();
|
|
8409
|
+
return this._initPromise;
|
|
8410
|
+
}
|
|
8411
|
+
async _loadPersistedStates() {
|
|
8412
|
+
try {
|
|
8413
|
+
const states = await this.persistence.getAllStates();
|
|
8414
|
+
for (const state of states) this.syncStates.set(state.docId, state);
|
|
8415
|
+
} catch {}
|
|
8416
|
+
}
|
|
8322
8417
|
/** Sync all documents in the root tree. */
|
|
8323
8418
|
async syncAll() {
|
|
8324
8419
|
if (this._destroyed) return;
|
|
8420
|
+
await this.init();
|
|
8325
8421
|
const treeMap = this.rootProvider.document.getMap("doc-tree");
|
|
8326
8422
|
const entries = Array.from(treeMap.entries());
|
|
8327
8423
|
if (entries.length === 0) return;
|
|
8424
|
+
const updatedAtMap = /* @__PURE__ */ new Map();
|
|
8425
|
+
for (const [docId, v] of entries) updatedAtMap.set(docId, v?.updatedAt ?? v?.createdAt ?? 0);
|
|
8328
8426
|
this._prefetchCovers(entries).catch(() => null);
|
|
8329
8427
|
const queue = this._buildQueue(entries);
|
|
8330
|
-
await Promise.all(queue.map((docId) => this._syncWithSemaphore(docId)));
|
|
8428
|
+
await Promise.all(queue.map((docId) => this._syncWithSemaphore(docId, updatedAtMap.get(docId) ?? 0)));
|
|
8331
8429
|
}
|
|
8332
8430
|
/** Sync a single document by ID. */
|
|
8333
8431
|
async syncDoc(docId) {
|
|
8432
|
+
await this.init();
|
|
8334
8433
|
const state = await this._doSyncDoc(docId);
|
|
8335
8434
|
this.syncStates.set(docId, state);
|
|
8336
8435
|
await this.persistence.setState(state).catch(() => null);
|
|
@@ -8355,6 +8454,32 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
8355
8454
|
}, intervalMs);
|
|
8356
8455
|
return () => clearInterval(handle);
|
|
8357
8456
|
}
|
|
8457
|
+
/**
|
|
8458
|
+
* Clear all offline document data and sync state.
|
|
8459
|
+
* Opens each document's OfflineStore and clears its contents, then
|
|
8460
|
+
* resets the background sync persistence. After calling this, all
|
|
8461
|
+
* documents will need to be re-synced.
|
|
8462
|
+
*/
|
|
8463
|
+
async clearAllSyncedData() {
|
|
8464
|
+
const docIds = new Set(this.syncStates.keys());
|
|
8465
|
+
const treeMap = this.rootProvider.document.getMap("doc-tree");
|
|
8466
|
+
for (const docId of treeMap.keys()) docIds.add(docId);
|
|
8467
|
+
let serverOrigin;
|
|
8468
|
+
try {
|
|
8469
|
+
serverOrigin = new URL(this.client.baseUrl ?? "").hostname;
|
|
8470
|
+
} catch {}
|
|
8471
|
+
const clearPromises = Array.from(docIds).map(async (docId) => {
|
|
8472
|
+
try {
|
|
8473
|
+
const store = new OfflineStore(docId, serverOrigin);
|
|
8474
|
+
await store.clearAll();
|
|
8475
|
+
store.destroy();
|
|
8476
|
+
} catch {}
|
|
8477
|
+
});
|
|
8478
|
+
await Promise.all(clearPromises);
|
|
8479
|
+
for (const docId of docIds) await this.persistence.deleteState(docId).catch(() => null);
|
|
8480
|
+
this.syncStates.clear();
|
|
8481
|
+
this._initPromise = null;
|
|
8482
|
+
}
|
|
8358
8483
|
destroy() {
|
|
8359
8484
|
this._destroyed = true;
|
|
8360
8485
|
this.removeAllListeners();
|
|
@@ -8381,8 +8506,16 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
8381
8506
|
items.sort((a, b) => b.priority - a.priority);
|
|
8382
8507
|
return items.map((i) => i.docId);
|
|
8383
8508
|
}
|
|
8384
|
-
async _syncWithSemaphore(docId) {
|
|
8509
|
+
async _syncWithSemaphore(docId, updatedAt) {
|
|
8385
8510
|
if (this._destroyed) return;
|
|
8511
|
+
const existing = this.syncStates.get(docId);
|
|
8512
|
+
if (existing && existing.status === "synced" && existing.lastSynced !== null && existing.lastSynced >= updatedAt) {
|
|
8513
|
+
this.emit("stateChanged", {
|
|
8514
|
+
docId,
|
|
8515
|
+
state: existing
|
|
8516
|
+
});
|
|
8517
|
+
return;
|
|
8518
|
+
}
|
|
8386
8519
|
await this.semaphore.acquire();
|
|
8387
8520
|
try {
|
|
8388
8521
|
const state = await this._doSyncDoc(docId);
|
|
@@ -8408,8 +8541,8 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
8408
8541
|
docId,
|
|
8409
8542
|
state: syncing
|
|
8410
8543
|
});
|
|
8544
|
+
let isE2E = false;
|
|
8411
8545
|
try {
|
|
8412
|
-
let isE2E = false;
|
|
8413
8546
|
try {
|
|
8414
8547
|
isE2E = (await this.client.getDocEncryption(docId)).mode === "e2e";
|
|
8415
8548
|
} catch {}
|
|
@@ -8422,7 +8555,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
8422
8555
|
status: "error",
|
|
8423
8556
|
lastSynced: this.syncStates.get(docId)?.lastSynced ?? null,
|
|
8424
8557
|
error,
|
|
8425
|
-
isE2E
|
|
8558
|
+
isE2E
|
|
8426
8559
|
};
|
|
8427
8560
|
}
|
|
8428
8561
|
}
|
|
@@ -8555,6 +8688,7 @@ var SignalingSocket = class extends EventEmitter {
|
|
|
8555
8688
|
this.connectionAttempt = null;
|
|
8556
8689
|
this.localPeerId = null;
|
|
8557
8690
|
this.isConnected = false;
|
|
8691
|
+
this._connectPromise = null;
|
|
8558
8692
|
this.config = {
|
|
8559
8693
|
url: configuration.url,
|
|
8560
8694
|
token: configuration.token,
|
|
@@ -8574,6 +8708,15 @@ var SignalingSocket = class extends EventEmitter {
|
|
|
8574
8708
|
}
|
|
8575
8709
|
async connect() {
|
|
8576
8710
|
if (this.isConnected) return;
|
|
8711
|
+
if (this._connectPromise) return this._connectPromise;
|
|
8712
|
+
this._connectPromise = this._doConnect();
|
|
8713
|
+
try {
|
|
8714
|
+
await this._connectPromise;
|
|
8715
|
+
} finally {
|
|
8716
|
+
this._connectPromise = null;
|
|
8717
|
+
}
|
|
8718
|
+
}
|
|
8719
|
+
async _doConnect() {
|
|
8577
8720
|
if (this.cancelRetry) {
|
|
8578
8721
|
this.cancelRetry();
|
|
8579
8722
|
this.cancelRetry = void 0;
|
|
@@ -8887,11 +9030,12 @@ var DataChannelRouter = class extends EventEmitter {
|
|
|
8887
9030
|
*/
|
|
8888
9031
|
async send(name, data) {
|
|
8889
9032
|
const channel = this.channels.get(name);
|
|
8890
|
-
if (!channel || channel.readyState !== "open") return;
|
|
9033
|
+
if (!channel || channel.readyState !== "open") return false;
|
|
8891
9034
|
if (this.encryptor?.isEstablished && !this.plaintextChannels.has(name)) {
|
|
8892
9035
|
const encrypted = await this.encryptor.encrypt(data);
|
|
8893
9036
|
channel.send(encrypted);
|
|
8894
9037
|
} else channel.send(data);
|
|
9038
|
+
return true;
|
|
8895
9039
|
}
|
|
8896
9040
|
registerChannel(channel) {
|
|
8897
9041
|
channel.binaryType = "arraybuffer";
|
|
@@ -9059,6 +9203,7 @@ var YjsDataChannel = class {
|
|
|
9059
9203
|
this.document = document;
|
|
9060
9204
|
this.awareness = awareness;
|
|
9061
9205
|
this.router = router;
|
|
9206
|
+
this.isSynced = false;
|
|
9062
9207
|
this.docUpdateHandler = null;
|
|
9063
9208
|
this.awarenessUpdateHandler = null;
|
|
9064
9209
|
this.channelOpenHandler = null;
|
|
@@ -9295,6 +9440,10 @@ var FileTransferChannel = class extends EventEmitter {
|
|
|
9295
9440
|
try {
|
|
9296
9441
|
meta = JSON.parse(json);
|
|
9297
9442
|
} catch {
|
|
9443
|
+
this.emit("receiveError", {
|
|
9444
|
+
transferId: "unknown",
|
|
9445
|
+
error: "Malformed START message: invalid JSON"
|
|
9446
|
+
});
|
|
9298
9447
|
return;
|
|
9299
9448
|
}
|
|
9300
9449
|
this.receives.set(meta.transferId, {
|
|
@@ -10001,7 +10150,9 @@ var ManualSignaling = class extends EventEmitter {
|
|
|
10001
10150
|
type: "offer",
|
|
10002
10151
|
sdp: offerBlob.sdp
|
|
10003
10152
|
}));
|
|
10004
|
-
for (const c of offerBlob.candidates)
|
|
10153
|
+
for (const c of offerBlob.candidates) try {
|
|
10154
|
+
await this.pc.addIceCandidate(new RTCIceCandidate(JSON.parse(c)));
|
|
10155
|
+
} catch {}
|
|
10005
10156
|
const answer = await this.pc.createAnswer();
|
|
10006
10157
|
await this.pc.setLocalDescription(answer);
|
|
10007
10158
|
await gatheringComplete;
|