@abraca/dabra 1.0.13 → 1.0.15
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 +45 -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 +56 -4
- package/src/DocKeyManager.ts +6 -3
- package/src/EventEmitter.ts +16 -1
- package/src/FileBlobStore.ts +82 -22
- 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 = {
|
|
@@ -2792,7 +2811,9 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
2792
2811
|
this.document.on("subdocs", this.boundHandleYSubdocsChange);
|
|
2793
2812
|
this.on("synced", () => this.flushPendingUpdates());
|
|
2794
2813
|
this.restorePermissionSnapshot();
|
|
2795
|
-
this.ready = this._initFromOfflineStore()
|
|
2814
|
+
this.ready = Promise.race([this._initFromOfflineStore(), new Promise((resolve) => setTimeout(resolve, 5e3))]).catch((err) => {
|
|
2815
|
+
this.emit("error", { message: `Offline store init failed: ${err?.message ?? err}` });
|
|
2816
|
+
});
|
|
2796
2817
|
}
|
|
2797
2818
|
/**
|
|
2798
2819
|
* Extract the server hostname from the provider configuration.
|
|
@@ -2910,7 +2931,9 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
2910
2931
|
}
|
|
2911
2932
|
const msg = parsed;
|
|
2912
2933
|
if (msg.type === "auth_challenge" && msg.challenge && msg.expiresAt) {
|
|
2913
|
-
this.handleAuthChallenge(msg.challenge, msg.expiresAt).catch(() =>
|
|
2934
|
+
this.handleAuthChallenge(msg.challenge, msg.expiresAt).catch((err) => {
|
|
2935
|
+
this.emit("authenticationFailed", { reason: `Auth challenge error: ${err?.message ?? err}` });
|
|
2936
|
+
});
|
|
2914
2937
|
return;
|
|
2915
2938
|
}
|
|
2916
2939
|
if (msg.type === "subdoc_registered" && msg.child_id && msg.parent_id) {
|
|
@@ -2952,6 +2975,14 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
2952
2975
|
createdAt: Date.now()
|
|
2953
2976
|
});
|
|
2954
2977
|
}
|
|
2978
|
+
/** Get a loaded child provider by ID, or null if not yet loaded. */
|
|
2979
|
+
getChild(childId) {
|
|
2980
|
+
return this.childProviders.get(childId) ?? null;
|
|
2981
|
+
}
|
|
2982
|
+
/** Check if a child provider is already loaded. */
|
|
2983
|
+
hasChild(childId) {
|
|
2984
|
+
return this.childProviders.has(childId);
|
|
2985
|
+
}
|
|
2955
2986
|
/**
|
|
2956
2987
|
* Create (or return cached) a child AbracadabraProvider for a given
|
|
2957
2988
|
* child document id. Each child opens its own WebSocket connection because
|
|
@@ -3082,6 +3113,17 @@ var AbracadabraClient = class {
|
|
|
3082
3113
|
get isAuthenticated() {
|
|
3083
3114
|
return this._token !== null;
|
|
3084
3115
|
}
|
|
3116
|
+
/** Check if the current JWT token is present and not expired. */
|
|
3117
|
+
isTokenValid() {
|
|
3118
|
+
if (!this._token) return false;
|
|
3119
|
+
try {
|
|
3120
|
+
const [, payload] = this._token.split(".");
|
|
3121
|
+
const { exp } = JSON.parse(atob(payload));
|
|
3122
|
+
return typeof exp === "number" && exp * 1e3 > Date.now();
|
|
3123
|
+
} catch {
|
|
3124
|
+
return false;
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3085
3127
|
/** Derives ws:// or wss:// URL from the http(s) base URL. */
|
|
3086
3128
|
get wsUrl() {
|
|
3087
3129
|
return this.baseUrl.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://") + "/ws";
|
|
@@ -3175,12 +3217,7 @@ var AbracadabraClient = class {
|
|
|
3175
3217
|
}
|
|
3176
3218
|
/** Get the caller's key envelope for a document (for decrypting the DocKey). */
|
|
3177
3219
|
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
|
-
}
|
|
3220
|
+
return this.requestOrNull("GET", `/docs/${encodeURIComponent(docId)}/key-envelope`);
|
|
3184
3221
|
}
|
|
3185
3222
|
/** Upload key envelopes for a document (Owner only). */
|
|
3186
3223
|
async uploadKeyEnvelopes(docId, opts) {
|
|
@@ -3188,12 +3225,7 @@ var AbracadabraClient = class {
|
|
|
3188
3225
|
}
|
|
3189
3226
|
/** Get the X25519 public key for a user. */
|
|
3190
3227
|
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
|
-
}
|
|
3228
|
+
return (await this.requestOrNull("GET", `/users/${encodeURIComponent(userId)}/x25519-key`))?.x25519_key ?? null;
|
|
3197
3229
|
}
|
|
3198
3230
|
/** List all non-revoked keys for a user (Owner/Admin or self). */
|
|
3199
3231
|
async listUserKeys(userId) {
|
|
@@ -3271,14 +3303,15 @@ var AbracadabraClient = class {
|
|
|
3271
3303
|
async listEffectivePermissions(docId) {
|
|
3272
3304
|
try {
|
|
3273
3305
|
return await this.request("GET", `/docs/${encodeURIComponent(docId)}/effective-permissions`);
|
|
3274
|
-
} catch {
|
|
3275
|
-
return {
|
|
3306
|
+
} catch (e) {
|
|
3307
|
+
if (typeof e === "object" && e !== null && "status" in e && e.status === 404) return {
|
|
3276
3308
|
permissions: (await this.listPermissions(docId)).map((p) => ({
|
|
3277
3309
|
...p,
|
|
3278
3310
|
source: "direct"
|
|
3279
3311
|
})),
|
|
3280
3312
|
default_role: "viewer"
|
|
3281
3313
|
};
|
|
3314
|
+
throw e;
|
|
3282
3315
|
}
|
|
3283
3316
|
}
|
|
3284
3317
|
/** Grant or change a user's role on a document (requires Owner). */
|
|
@@ -3357,12 +3390,7 @@ var AbracadabraClient = class {
|
|
|
3357
3390
|
}
|
|
3358
3391
|
/** Get the hub space, or null if none is configured. */
|
|
3359
3392
|
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
|
-
}
|
|
3393
|
+
return this.requestOrNull("GET", "/spaces/hub", { auth: false });
|
|
3366
3394
|
}
|
|
3367
3395
|
/** Create a new space (auth required). */
|
|
3368
3396
|
async createSpace(opts) {
|
|
@@ -3405,25 +3433,35 @@ var AbracadabraClient = class {
|
|
|
3405
3433
|
if (auth && this._token) headers["Authorization"] = `Bearer ${this._token}`;
|
|
3406
3434
|
const init = {
|
|
3407
3435
|
method,
|
|
3408
|
-
headers
|
|
3436
|
+
headers,
|
|
3437
|
+
signal: AbortSignal.timeout(3e4)
|
|
3409
3438
|
};
|
|
3410
3439
|
if (opts?.body !== void 0) {
|
|
3411
3440
|
headers["Content-Type"] = "application/json";
|
|
3412
3441
|
init.body = JSON.stringify(opts.body);
|
|
3413
3442
|
}
|
|
3414
3443
|
const res = await this._fetch(`${this.baseUrl}${path}`, init);
|
|
3415
|
-
if (!res.ok) throw await this.toError(res);
|
|
3444
|
+
if (!res.ok) throw await this.toError(res, method, path);
|
|
3416
3445
|
if (res.status === 204) return;
|
|
3417
3446
|
return res.json();
|
|
3418
3447
|
}
|
|
3419
|
-
async
|
|
3448
|
+
async requestOrNull(method, path, opts) {
|
|
3449
|
+
try {
|
|
3450
|
+
return await this.request(method, path, opts);
|
|
3451
|
+
} catch (e) {
|
|
3452
|
+
if (typeof e === "object" && e !== null && "status" in e && e.status === 404) return null;
|
|
3453
|
+
throw e;
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
async toError(res, method, path) {
|
|
3420
3457
|
let message;
|
|
3421
3458
|
try {
|
|
3422
3459
|
message = (await res.json()).error ?? res.statusText;
|
|
3423
3460
|
} catch {
|
|
3424
3461
|
message = res.statusText;
|
|
3425
3462
|
}
|
|
3426
|
-
const
|
|
3463
|
+
const prefix = method && path ? `${method} ${path}: ` : "";
|
|
3464
|
+
const err = /* @__PURE__ */ new Error(`${prefix}${message} (${res.status})`);
|
|
3427
3465
|
err.status = res.status;
|
|
3428
3466
|
return err;
|
|
3429
3467
|
}
|
|
@@ -7442,6 +7480,7 @@ var SearchIndex = class {
|
|
|
7442
7480
|
if (!db) return [];
|
|
7443
7481
|
const queryTrigrams = [...extractTrigrams(query)];
|
|
7444
7482
|
if (queryTrigrams.length === 0) return [];
|
|
7483
|
+
const maxScoreEntries = limit * 10;
|
|
7445
7484
|
return new Promise((resolve, reject) => {
|
|
7446
7485
|
const tx = db.transaction("postings", "readonly");
|
|
7447
7486
|
const postings = tx.objectStore("postings");
|
|
@@ -7451,7 +7490,10 @@ var SearchIndex = class {
|
|
|
7451
7490
|
const req = postings.get(trigram);
|
|
7452
7491
|
req.onsuccess = () => {
|
|
7453
7492
|
const docIds = req.result ?? [];
|
|
7454
|
-
for (const docId of docIds)
|
|
7493
|
+
for (const docId of docIds) {
|
|
7494
|
+
scores.set(docId, (scores.get(docId) ?? 0) + 1);
|
|
7495
|
+
if (scores.size >= maxScoreEntries) break;
|
|
7496
|
+
}
|
|
7455
7497
|
remaining--;
|
|
7456
7498
|
if (remaining === 0) resolve([...scores.entries()].map(([docId, score]) => ({
|
|
7457
7499
|
docId,
|
|
@@ -7509,7 +7551,7 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
|
|
|
7509
7551
|
this.db = null;
|
|
7510
7552
|
this.objectUrls = /* @__PURE__ */ new Map();
|
|
7511
7553
|
this._notFound = /* @__PURE__ */ new Map();
|
|
7512
|
-
this.
|
|
7554
|
+
this._flushPromise = null;
|
|
7513
7555
|
this.origin = serverOrigin;
|
|
7514
7556
|
this.client = client ?? null;
|
|
7515
7557
|
this._onlineHandler = () => {
|
|
@@ -7609,6 +7651,62 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
|
|
|
7609
7651
|
const tx = db.transaction("blobs", "readonly");
|
|
7610
7652
|
return (await txPromise(tx.objectStore("blobs"), tx.objectStore("blobs").get(key)))?.blob ?? null;
|
|
7611
7653
|
}
|
|
7654
|
+
/** Return metadata for all cached blobs (for storage stats). */
|
|
7655
|
+
async getAllCachedEntries() {
|
|
7656
|
+
const db = await this.getDb();
|
|
7657
|
+
if (!db) return [];
|
|
7658
|
+
return new Promise((resolve, reject) => {
|
|
7659
|
+
const tx = db.transaction("blobs", "readonly");
|
|
7660
|
+
const store = tx.objectStore("blobs");
|
|
7661
|
+
const keysReq = store.getAllKeys();
|
|
7662
|
+
const valuesReq = store.getAll();
|
|
7663
|
+
tx.oncomplete = () => {
|
|
7664
|
+
const keys = keysReq.result;
|
|
7665
|
+
const values = valuesReq.result;
|
|
7666
|
+
resolve(keys.map((key, i) => {
|
|
7667
|
+
const slashIdx = key.indexOf("/");
|
|
7668
|
+
const docId = key.slice(0, slashIdx);
|
|
7669
|
+
const uploadId = key.slice(slashIdx + 1);
|
|
7670
|
+
const e = values[i];
|
|
7671
|
+
return {
|
|
7672
|
+
docId,
|
|
7673
|
+
uploadId,
|
|
7674
|
+
filename: e.filename,
|
|
7675
|
+
mimeType: e.mime_type,
|
|
7676
|
+
size: e.blob.size,
|
|
7677
|
+
cachedAt: e.cachedAt
|
|
7678
|
+
};
|
|
7679
|
+
}));
|
|
7680
|
+
};
|
|
7681
|
+
tx.onerror = () => reject(tx.error);
|
|
7682
|
+
});
|
|
7683
|
+
}
|
|
7684
|
+
/** Revoke all object URLs and clear the entire blob cache from IDB. */
|
|
7685
|
+
async clearAllBlobs() {
|
|
7686
|
+
for (const url of this.objectUrls.values()) URL.revokeObjectURL(url);
|
|
7687
|
+
this.objectUrls.clear();
|
|
7688
|
+
const db = await this.getDb();
|
|
7689
|
+
if (!db) return;
|
|
7690
|
+
return new Promise((resolve, reject) => {
|
|
7691
|
+
const req = db.transaction("blobs", "readwrite").objectStore("blobs").clear();
|
|
7692
|
+
req.onsuccess = () => resolve();
|
|
7693
|
+
req.onerror = () => reject(req.error);
|
|
7694
|
+
});
|
|
7695
|
+
}
|
|
7696
|
+
/**
|
|
7697
|
+
* Revoke the in-memory object URL without touching the IDB cache.
|
|
7698
|
+
* The next call to getBlobUrl() will re-create a fresh URL from IDB.
|
|
7699
|
+
* Use this when an <img> @error fires — the blob data is fine, only
|
|
7700
|
+
* the object URL reference is stale.
|
|
7701
|
+
*/
|
|
7702
|
+
invalidateUrl(docId, uploadId) {
|
|
7703
|
+
const key = this.blobKey(docId, uploadId);
|
|
7704
|
+
const url = this.objectUrls.get(key);
|
|
7705
|
+
if (url) {
|
|
7706
|
+
URL.revokeObjectURL(url);
|
|
7707
|
+
this.objectUrls.delete(key);
|
|
7708
|
+
}
|
|
7709
|
+
}
|
|
7612
7710
|
/** Revoke the object URL and remove the blob from cache. */
|
|
7613
7711
|
async evictBlob(docId, uploadId) {
|
|
7614
7712
|
const key = this.blobKey(docId, uploadId);
|
|
@@ -7661,38 +7759,42 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
|
|
|
7661
7759
|
* Entries that fail are marked with status "error" and left in the queue.
|
|
7662
7760
|
*/
|
|
7663
7761
|
async flushQueue() {
|
|
7664
|
-
if (this.
|
|
7665
|
-
this.
|
|
7762
|
+
if (this._flushPromise || !this.client) return;
|
|
7763
|
+
this._flushPromise = this._doFlush();
|
|
7666
7764
|
try {
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
|
|
7765
|
+
await this._flushPromise;
|
|
7766
|
+
} finally {
|
|
7767
|
+
this._flushPromise = null;
|
|
7768
|
+
}
|
|
7769
|
+
}
|
|
7770
|
+
async _doFlush() {
|
|
7771
|
+
if (!this.client) return;
|
|
7772
|
+
const pending = (await this.getQueue()).filter((e) => e.status === "pending");
|
|
7773
|
+
for (const entry of pending) {
|
|
7774
|
+
await this._updateQueueEntry(entry.id, { status: "uploading" });
|
|
7775
|
+
this.emit("upload:started", {
|
|
7776
|
+
...entry,
|
|
7777
|
+
status: "uploading"
|
|
7778
|
+
});
|
|
7779
|
+
try {
|
|
7780
|
+
await this.client.upload(entry.docId, entry.file, entry.filename);
|
|
7781
|
+
await this._updateQueueEntry(entry.id, { status: "done" });
|
|
7782
|
+
this.emit("upload:done", {
|
|
7671
7783
|
...entry,
|
|
7672
|
-
status: "
|
|
7784
|
+
status: "done"
|
|
7785
|
+
});
|
|
7786
|
+
} catch (err) {
|
|
7787
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7788
|
+
await this._updateQueueEntry(entry.id, {
|
|
7789
|
+
status: "error",
|
|
7790
|
+
error: message
|
|
7791
|
+
});
|
|
7792
|
+
this.emit("upload:error", {
|
|
7793
|
+
...entry,
|
|
7794
|
+
status: "error",
|
|
7795
|
+
error: message
|
|
7673
7796
|
});
|
|
7674
|
-
try {
|
|
7675
|
-
await this.client.upload(entry.docId, entry.file, entry.filename);
|
|
7676
|
-
await this._updateQueueEntry(entry.id, { status: "done" });
|
|
7677
|
-
this.emit("upload:done", {
|
|
7678
|
-
...entry,
|
|
7679
|
-
status: "done"
|
|
7680
|
-
});
|
|
7681
|
-
} catch (err) {
|
|
7682
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
7683
|
-
await this._updateQueueEntry(entry.id, {
|
|
7684
|
-
status: "error",
|
|
7685
|
-
error: message
|
|
7686
|
-
});
|
|
7687
|
-
this.emit("upload:error", {
|
|
7688
|
-
...entry,
|
|
7689
|
-
status: "error",
|
|
7690
|
-
error: message
|
|
7691
|
-
});
|
|
7692
|
-
}
|
|
7693
7797
|
}
|
|
7694
|
-
} finally {
|
|
7695
|
-
this._flushing = false;
|
|
7696
7798
|
}
|
|
7697
7799
|
}
|
|
7698
7800
|
async _updateQueueEntry(id, patch) {
|
|
@@ -7740,10 +7842,13 @@ const HKDF_INFO$1 = new TextEncoder().encode("abracadabra-dockey-v1");
|
|
|
7740
7842
|
function fromBase64$1(b64) {
|
|
7741
7843
|
return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
7742
7844
|
}
|
|
7743
|
-
var DocKeyManager = class {
|
|
7845
|
+
var DocKeyManager = class DocKeyManager {
|
|
7744
7846
|
constructor() {
|
|
7745
7847
|
this.cache = /* @__PURE__ */ new Map();
|
|
7746
7848
|
}
|
|
7849
|
+
static {
|
|
7850
|
+
this.CACHE_TTL = 600 * 1e3;
|
|
7851
|
+
}
|
|
7747
7852
|
/** Generate a new random AES-256-GCM document key. */
|
|
7748
7853
|
static async generateDocKey() {
|
|
7749
7854
|
return crypto.subtle.generateKey({
|
|
@@ -7757,7 +7862,7 @@ var DocKeyManager = class {
|
|
|
7757
7862
|
*/
|
|
7758
7863
|
async getDocKey(docId, client, keystore) {
|
|
7759
7864
|
const cached = this.cache.get(docId);
|
|
7760
|
-
if (cached) return cached.key;
|
|
7865
|
+
if (cached && Date.now() - cached.fetchedAt < DocKeyManager.CACHE_TTL) return cached.key;
|
|
7761
7866
|
const envelope = await client.getMyKeyEnvelope(docId);
|
|
7762
7867
|
if (!envelope) return null;
|
|
7763
7868
|
const x25519PrivKey = await keystore.getX25519PrivateKey();
|
|
@@ -7766,7 +7871,8 @@ var DocKeyManager = class {
|
|
|
7766
7871
|
const docKey = await this._unwrapKey(wrapped, x25519PrivKey, docId);
|
|
7767
7872
|
this.cache.set(docId, {
|
|
7768
7873
|
key: docKey,
|
|
7769
|
-
epoch: envelope.key_epoch
|
|
7874
|
+
epoch: envelope.key_epoch,
|
|
7875
|
+
fetchedAt: Date.now()
|
|
7770
7876
|
});
|
|
7771
7877
|
return docKey;
|
|
7772
7878
|
} finally {
|
|
@@ -8262,6 +8368,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
8262
8368
|
super();
|
|
8263
8369
|
this.syncStates = /* @__PURE__ */ new Map();
|
|
8264
8370
|
this._destroyed = false;
|
|
8371
|
+
this._initPromise = null;
|
|
8265
8372
|
this.rootProvider = rootProvider;
|
|
8266
8373
|
this.client = client;
|
|
8267
8374
|
this.fileBlobStore = fileBlobStore ?? null;
|
|
@@ -8277,18 +8384,36 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
8277
8384
|
this.persistence = new BackgroundSyncPersistence(serverOrigin);
|
|
8278
8385
|
this.semaphore = new Semaphore(this.opts.concurrency);
|
|
8279
8386
|
}
|
|
8387
|
+
/**
|
|
8388
|
+
* Load persisted sync states from IndexedDB into the in-memory map.
|
|
8389
|
+
* Called automatically by syncAll() / syncDoc(); safe to call concurrently.
|
|
8390
|
+
*/
|
|
8391
|
+
async init() {
|
|
8392
|
+
if (!this._initPromise) this._initPromise = this._loadPersistedStates();
|
|
8393
|
+
return this._initPromise;
|
|
8394
|
+
}
|
|
8395
|
+
async _loadPersistedStates() {
|
|
8396
|
+
try {
|
|
8397
|
+
const states = await this.persistence.getAllStates();
|
|
8398
|
+
for (const state of states) this.syncStates.set(state.docId, state);
|
|
8399
|
+
} catch {}
|
|
8400
|
+
}
|
|
8280
8401
|
/** Sync all documents in the root tree. */
|
|
8281
8402
|
async syncAll() {
|
|
8282
8403
|
if (this._destroyed) return;
|
|
8404
|
+
await this.init();
|
|
8283
8405
|
const treeMap = this.rootProvider.document.getMap("doc-tree");
|
|
8284
8406
|
const entries = Array.from(treeMap.entries());
|
|
8285
8407
|
if (entries.length === 0) return;
|
|
8408
|
+
const updatedAtMap = /* @__PURE__ */ new Map();
|
|
8409
|
+
for (const [docId, v] of entries) updatedAtMap.set(docId, v?.updatedAt ?? v?.createdAt ?? 0);
|
|
8286
8410
|
this._prefetchCovers(entries).catch(() => null);
|
|
8287
8411
|
const queue = this._buildQueue(entries);
|
|
8288
|
-
await Promise.all(queue.map((docId) => this._syncWithSemaphore(docId)));
|
|
8412
|
+
await Promise.all(queue.map((docId) => this._syncWithSemaphore(docId, updatedAtMap.get(docId) ?? 0)));
|
|
8289
8413
|
}
|
|
8290
8414
|
/** Sync a single document by ID. */
|
|
8291
8415
|
async syncDoc(docId) {
|
|
8416
|
+
await this.init();
|
|
8292
8417
|
const state = await this._doSyncDoc(docId);
|
|
8293
8418
|
this.syncStates.set(docId, state);
|
|
8294
8419
|
await this.persistence.setState(state).catch(() => null);
|
|
@@ -8339,8 +8464,16 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
8339
8464
|
items.sort((a, b) => b.priority - a.priority);
|
|
8340
8465
|
return items.map((i) => i.docId);
|
|
8341
8466
|
}
|
|
8342
|
-
async _syncWithSemaphore(docId) {
|
|
8467
|
+
async _syncWithSemaphore(docId, updatedAt) {
|
|
8343
8468
|
if (this._destroyed) return;
|
|
8469
|
+
const existing = this.syncStates.get(docId);
|
|
8470
|
+
if (existing && existing.status === "synced" && existing.lastSynced !== null && existing.lastSynced >= updatedAt) {
|
|
8471
|
+
this.emit("stateChanged", {
|
|
8472
|
+
docId,
|
|
8473
|
+
state: existing
|
|
8474
|
+
});
|
|
8475
|
+
return;
|
|
8476
|
+
}
|
|
8344
8477
|
await this.semaphore.acquire();
|
|
8345
8478
|
try {
|
|
8346
8479
|
const state = await this._doSyncDoc(docId);
|
|
@@ -8366,8 +8499,8 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
8366
8499
|
docId,
|
|
8367
8500
|
state: syncing
|
|
8368
8501
|
});
|
|
8502
|
+
let isE2E = false;
|
|
8369
8503
|
try {
|
|
8370
|
-
let isE2E = false;
|
|
8371
8504
|
try {
|
|
8372
8505
|
isE2E = (await this.client.getDocEncryption(docId)).mode === "e2e";
|
|
8373
8506
|
} catch {}
|
|
@@ -8380,7 +8513,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
8380
8513
|
status: "error",
|
|
8381
8514
|
lastSynced: this.syncStates.get(docId)?.lastSynced ?? null,
|
|
8382
8515
|
error,
|
|
8383
|
-
isE2E
|
|
8516
|
+
isE2E
|
|
8384
8517
|
};
|
|
8385
8518
|
}
|
|
8386
8519
|
}
|
|
@@ -8513,6 +8646,7 @@ var SignalingSocket = class extends EventEmitter {
|
|
|
8513
8646
|
this.connectionAttempt = null;
|
|
8514
8647
|
this.localPeerId = null;
|
|
8515
8648
|
this.isConnected = false;
|
|
8649
|
+
this._connectPromise = null;
|
|
8516
8650
|
this.config = {
|
|
8517
8651
|
url: configuration.url,
|
|
8518
8652
|
token: configuration.token,
|
|
@@ -8532,6 +8666,15 @@ var SignalingSocket = class extends EventEmitter {
|
|
|
8532
8666
|
}
|
|
8533
8667
|
async connect() {
|
|
8534
8668
|
if (this.isConnected) return;
|
|
8669
|
+
if (this._connectPromise) return this._connectPromise;
|
|
8670
|
+
this._connectPromise = this._doConnect();
|
|
8671
|
+
try {
|
|
8672
|
+
await this._connectPromise;
|
|
8673
|
+
} finally {
|
|
8674
|
+
this._connectPromise = null;
|
|
8675
|
+
}
|
|
8676
|
+
}
|
|
8677
|
+
async _doConnect() {
|
|
8535
8678
|
if (this.cancelRetry) {
|
|
8536
8679
|
this.cancelRetry();
|
|
8537
8680
|
this.cancelRetry = void 0;
|
|
@@ -8845,11 +8988,12 @@ var DataChannelRouter = class extends EventEmitter {
|
|
|
8845
8988
|
*/
|
|
8846
8989
|
async send(name, data) {
|
|
8847
8990
|
const channel = this.channels.get(name);
|
|
8848
|
-
if (!channel || channel.readyState !== "open") return;
|
|
8991
|
+
if (!channel || channel.readyState !== "open") return false;
|
|
8849
8992
|
if (this.encryptor?.isEstablished && !this.plaintextChannels.has(name)) {
|
|
8850
8993
|
const encrypted = await this.encryptor.encrypt(data);
|
|
8851
8994
|
channel.send(encrypted);
|
|
8852
8995
|
} else channel.send(data);
|
|
8996
|
+
return true;
|
|
8853
8997
|
}
|
|
8854
8998
|
registerChannel(channel) {
|
|
8855
8999
|
channel.binaryType = "arraybuffer";
|
|
@@ -9017,6 +9161,7 @@ var YjsDataChannel = class {
|
|
|
9017
9161
|
this.document = document;
|
|
9018
9162
|
this.awareness = awareness;
|
|
9019
9163
|
this.router = router;
|
|
9164
|
+
this.isSynced = false;
|
|
9020
9165
|
this.docUpdateHandler = null;
|
|
9021
9166
|
this.awarenessUpdateHandler = null;
|
|
9022
9167
|
this.channelOpenHandler = null;
|
|
@@ -9253,6 +9398,10 @@ var FileTransferChannel = class extends EventEmitter {
|
|
|
9253
9398
|
try {
|
|
9254
9399
|
meta = JSON.parse(json);
|
|
9255
9400
|
} catch {
|
|
9401
|
+
this.emit("receiveError", {
|
|
9402
|
+
transferId: "unknown",
|
|
9403
|
+
error: "Malformed START message: invalid JSON"
|
|
9404
|
+
});
|
|
9256
9405
|
return;
|
|
9257
9406
|
}
|
|
9258
9407
|
this.receives.set(meta.transferId, {
|
|
@@ -9959,7 +10108,9 @@ var ManualSignaling = class extends EventEmitter {
|
|
|
9959
10108
|
type: "offer",
|
|
9960
10109
|
sdp: offerBlob.sdp
|
|
9961
10110
|
}));
|
|
9962
|
-
for (const c of offerBlob.candidates)
|
|
10111
|
+
for (const c of offerBlob.candidates) try {
|
|
10112
|
+
await this.pc.addIceCandidate(new RTCIceCandidate(JSON.parse(c)));
|
|
10113
|
+
} catch {}
|
|
9963
10114
|
const answer = await this.pc.createAnswer();
|
|
9964
10115
|
await this.pc.setLocalDescription(answer);
|
|
9965
10116
|
await gatheringComplete;
|