@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
|
@@ -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 = {
|
|
@@ -2762,7 +2781,9 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
2762
2781
|
this.document.on("subdocs", this.boundHandleYSubdocsChange);
|
|
2763
2782
|
this.on("synced", () => this.flushPendingUpdates());
|
|
2764
2783
|
this.restorePermissionSnapshot();
|
|
2765
|
-
this.ready = this._initFromOfflineStore()
|
|
2784
|
+
this.ready = Promise.race([this._initFromOfflineStore(), new Promise((resolve) => setTimeout(resolve, 5e3))]).catch((err) => {
|
|
2785
|
+
this.emit("error", { message: `Offline store init failed: ${err?.message ?? err}` });
|
|
2786
|
+
});
|
|
2766
2787
|
}
|
|
2767
2788
|
/**
|
|
2768
2789
|
* Extract the server hostname from the provider configuration.
|
|
@@ -2880,7 +2901,9 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
2880
2901
|
}
|
|
2881
2902
|
const msg = parsed;
|
|
2882
2903
|
if (msg.type === "auth_challenge" && msg.challenge && msg.expiresAt) {
|
|
2883
|
-
this.handleAuthChallenge(msg.challenge, msg.expiresAt).catch(() =>
|
|
2904
|
+
this.handleAuthChallenge(msg.challenge, msg.expiresAt).catch((err) => {
|
|
2905
|
+
this.emit("authenticationFailed", { reason: `Auth challenge error: ${err?.message ?? err}` });
|
|
2906
|
+
});
|
|
2884
2907
|
return;
|
|
2885
2908
|
}
|
|
2886
2909
|
if (msg.type === "subdoc_registered" && msg.child_id && msg.parent_id) {
|
|
@@ -2922,6 +2945,14 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
2922
2945
|
createdAt: Date.now()
|
|
2923
2946
|
});
|
|
2924
2947
|
}
|
|
2948
|
+
/** Get a loaded child provider by ID, or null if not yet loaded. */
|
|
2949
|
+
getChild(childId) {
|
|
2950
|
+
return this.childProviders.get(childId) ?? null;
|
|
2951
|
+
}
|
|
2952
|
+
/** Check if a child provider is already loaded. */
|
|
2953
|
+
hasChild(childId) {
|
|
2954
|
+
return this.childProviders.has(childId);
|
|
2955
|
+
}
|
|
2925
2956
|
/**
|
|
2926
2957
|
* Create (or return cached) a child AbracadabraProvider for a given
|
|
2927
2958
|
* child document id. Each child opens its own WebSocket connection because
|
|
@@ -3052,6 +3083,17 @@ var AbracadabraClient = class {
|
|
|
3052
3083
|
get isAuthenticated() {
|
|
3053
3084
|
return this._token !== null;
|
|
3054
3085
|
}
|
|
3086
|
+
/** Check if the current JWT token is present and not expired. */
|
|
3087
|
+
isTokenValid() {
|
|
3088
|
+
if (!this._token) return false;
|
|
3089
|
+
try {
|
|
3090
|
+
const [, payload] = this._token.split(".");
|
|
3091
|
+
const { exp } = JSON.parse(atob(payload));
|
|
3092
|
+
return typeof exp === "number" && exp * 1e3 > Date.now();
|
|
3093
|
+
} catch {
|
|
3094
|
+
return false;
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3055
3097
|
/** Derives ws:// or wss:// URL from the http(s) base URL. */
|
|
3056
3098
|
get wsUrl() {
|
|
3057
3099
|
return this.baseUrl.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://") + "/ws";
|
|
@@ -3145,12 +3187,7 @@ var AbracadabraClient = class {
|
|
|
3145
3187
|
}
|
|
3146
3188
|
/** Get the caller's key envelope for a document (for decrypting the DocKey). */
|
|
3147
3189
|
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
|
-
}
|
|
3190
|
+
return this.requestOrNull("GET", `/docs/${encodeURIComponent(docId)}/key-envelope`);
|
|
3154
3191
|
}
|
|
3155
3192
|
/** Upload key envelopes for a document (Owner only). */
|
|
3156
3193
|
async uploadKeyEnvelopes(docId, opts) {
|
|
@@ -3158,12 +3195,7 @@ var AbracadabraClient = class {
|
|
|
3158
3195
|
}
|
|
3159
3196
|
/** Get the X25519 public key for a user. */
|
|
3160
3197
|
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
|
-
}
|
|
3198
|
+
return (await this.requestOrNull("GET", `/users/${encodeURIComponent(userId)}/x25519-key`))?.x25519_key ?? null;
|
|
3167
3199
|
}
|
|
3168
3200
|
/** List all non-revoked keys for a user (Owner/Admin or self). */
|
|
3169
3201
|
async listUserKeys(userId) {
|
|
@@ -3241,14 +3273,15 @@ var AbracadabraClient = class {
|
|
|
3241
3273
|
async listEffectivePermissions(docId) {
|
|
3242
3274
|
try {
|
|
3243
3275
|
return await this.request("GET", `/docs/${encodeURIComponent(docId)}/effective-permissions`);
|
|
3244
|
-
} catch {
|
|
3245
|
-
return {
|
|
3276
|
+
} catch (e) {
|
|
3277
|
+
if (typeof e === "object" && e !== null && "status" in e && e.status === 404) return {
|
|
3246
3278
|
permissions: (await this.listPermissions(docId)).map((p) => ({
|
|
3247
3279
|
...p,
|
|
3248
3280
|
source: "direct"
|
|
3249
3281
|
})),
|
|
3250
3282
|
default_role: "viewer"
|
|
3251
3283
|
};
|
|
3284
|
+
throw e;
|
|
3252
3285
|
}
|
|
3253
3286
|
}
|
|
3254
3287
|
/** Grant or change a user's role on a document (requires Owner). */
|
|
@@ -3327,12 +3360,7 @@ var AbracadabraClient = class {
|
|
|
3327
3360
|
}
|
|
3328
3361
|
/** Get the hub space, or null if none is configured. */
|
|
3329
3362
|
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
|
-
}
|
|
3363
|
+
return this.requestOrNull("GET", "/spaces/hub", { auth: false });
|
|
3336
3364
|
}
|
|
3337
3365
|
/** Create a new space (auth required). */
|
|
3338
3366
|
async createSpace(opts) {
|
|
@@ -3375,25 +3403,35 @@ var AbracadabraClient = class {
|
|
|
3375
3403
|
if (auth && this._token) headers["Authorization"] = `Bearer ${this._token}`;
|
|
3376
3404
|
const init = {
|
|
3377
3405
|
method,
|
|
3378
|
-
headers
|
|
3406
|
+
headers,
|
|
3407
|
+
signal: AbortSignal.timeout(3e4)
|
|
3379
3408
|
};
|
|
3380
3409
|
if (opts?.body !== void 0) {
|
|
3381
3410
|
headers["Content-Type"] = "application/json";
|
|
3382
3411
|
init.body = JSON.stringify(opts.body);
|
|
3383
3412
|
}
|
|
3384
3413
|
const res = await this._fetch(`${this.baseUrl}${path}`, init);
|
|
3385
|
-
if (!res.ok) throw await this.toError(res);
|
|
3414
|
+
if (!res.ok) throw await this.toError(res, method, path);
|
|
3386
3415
|
if (res.status === 204) return;
|
|
3387
3416
|
return res.json();
|
|
3388
3417
|
}
|
|
3389
|
-
async
|
|
3418
|
+
async requestOrNull(method, path, opts) {
|
|
3419
|
+
try {
|
|
3420
|
+
return await this.request(method, path, opts);
|
|
3421
|
+
} catch (e) {
|
|
3422
|
+
if (typeof e === "object" && e !== null && "status" in e && e.status === 404) return null;
|
|
3423
|
+
throw e;
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
async toError(res, method, path) {
|
|
3390
3427
|
let message;
|
|
3391
3428
|
try {
|
|
3392
3429
|
message = (await res.json()).error ?? res.statusText;
|
|
3393
3430
|
} catch {
|
|
3394
3431
|
message = res.statusText;
|
|
3395
3432
|
}
|
|
3396
|
-
const
|
|
3433
|
+
const prefix = method && path ? `${method} ${path}: ` : "";
|
|
3434
|
+
const err = /* @__PURE__ */ new Error(`${prefix}${message} (${res.status})`);
|
|
3397
3435
|
err.status = res.status;
|
|
3398
3436
|
return err;
|
|
3399
3437
|
}
|
|
@@ -7412,6 +7450,7 @@ var SearchIndex = class {
|
|
|
7412
7450
|
if (!db) return [];
|
|
7413
7451
|
const queryTrigrams = [...extractTrigrams(query)];
|
|
7414
7452
|
if (queryTrigrams.length === 0) return [];
|
|
7453
|
+
const maxScoreEntries = limit * 10;
|
|
7415
7454
|
return new Promise((resolve, reject) => {
|
|
7416
7455
|
const tx = db.transaction("postings", "readonly");
|
|
7417
7456
|
const postings = tx.objectStore("postings");
|
|
@@ -7421,7 +7460,10 @@ var SearchIndex = class {
|
|
|
7421
7460
|
const req = postings.get(trigram);
|
|
7422
7461
|
req.onsuccess = () => {
|
|
7423
7462
|
const docIds = req.result ?? [];
|
|
7424
|
-
for (const docId of docIds)
|
|
7463
|
+
for (const docId of docIds) {
|
|
7464
|
+
scores.set(docId, (scores.get(docId) ?? 0) + 1);
|
|
7465
|
+
if (scores.size >= maxScoreEntries) break;
|
|
7466
|
+
}
|
|
7425
7467
|
remaining--;
|
|
7426
7468
|
if (remaining === 0) resolve([...scores.entries()].map(([docId, score]) => ({
|
|
7427
7469
|
docId,
|
|
@@ -7479,7 +7521,7 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
|
|
|
7479
7521
|
this.db = null;
|
|
7480
7522
|
this.objectUrls = /* @__PURE__ */ new Map();
|
|
7481
7523
|
this._notFound = /* @__PURE__ */ new Map();
|
|
7482
|
-
this.
|
|
7524
|
+
this._flushPromise = null;
|
|
7483
7525
|
this.origin = serverOrigin;
|
|
7484
7526
|
this.client = client ?? null;
|
|
7485
7527
|
this._onlineHandler = () => {
|
|
@@ -7579,6 +7621,62 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
|
|
|
7579
7621
|
const tx = db.transaction("blobs", "readonly");
|
|
7580
7622
|
return (await txPromise(tx.objectStore("blobs"), tx.objectStore("blobs").get(key)))?.blob ?? null;
|
|
7581
7623
|
}
|
|
7624
|
+
/** Return metadata for all cached blobs (for storage stats). */
|
|
7625
|
+
async getAllCachedEntries() {
|
|
7626
|
+
const db = await this.getDb();
|
|
7627
|
+
if (!db) return [];
|
|
7628
|
+
return new Promise((resolve, reject) => {
|
|
7629
|
+
const tx = db.transaction("blobs", "readonly");
|
|
7630
|
+
const store = tx.objectStore("blobs");
|
|
7631
|
+
const keysReq = store.getAllKeys();
|
|
7632
|
+
const valuesReq = store.getAll();
|
|
7633
|
+
tx.oncomplete = () => {
|
|
7634
|
+
const keys = keysReq.result;
|
|
7635
|
+
const values = valuesReq.result;
|
|
7636
|
+
resolve(keys.map((key, i) => {
|
|
7637
|
+
const slashIdx = key.indexOf("/");
|
|
7638
|
+
const docId = key.slice(0, slashIdx);
|
|
7639
|
+
const uploadId = key.slice(slashIdx + 1);
|
|
7640
|
+
const e = values[i];
|
|
7641
|
+
return {
|
|
7642
|
+
docId,
|
|
7643
|
+
uploadId,
|
|
7644
|
+
filename: e.filename,
|
|
7645
|
+
mimeType: e.mime_type,
|
|
7646
|
+
size: e.blob.size,
|
|
7647
|
+
cachedAt: e.cachedAt
|
|
7648
|
+
};
|
|
7649
|
+
}));
|
|
7650
|
+
};
|
|
7651
|
+
tx.onerror = () => reject(tx.error);
|
|
7652
|
+
});
|
|
7653
|
+
}
|
|
7654
|
+
/** Revoke all object URLs and clear the entire blob cache from IDB. */
|
|
7655
|
+
async clearAllBlobs() {
|
|
7656
|
+
for (const url of this.objectUrls.values()) URL.revokeObjectURL(url);
|
|
7657
|
+
this.objectUrls.clear();
|
|
7658
|
+
const db = await this.getDb();
|
|
7659
|
+
if (!db) return;
|
|
7660
|
+
return new Promise((resolve, reject) => {
|
|
7661
|
+
const req = db.transaction("blobs", "readwrite").objectStore("blobs").clear();
|
|
7662
|
+
req.onsuccess = () => resolve();
|
|
7663
|
+
req.onerror = () => reject(req.error);
|
|
7664
|
+
});
|
|
7665
|
+
}
|
|
7666
|
+
/**
|
|
7667
|
+
* Revoke the in-memory object URL without touching the IDB cache.
|
|
7668
|
+
* The next call to getBlobUrl() will re-create a fresh URL from IDB.
|
|
7669
|
+
* Use this when an <img> @error fires — the blob data is fine, only
|
|
7670
|
+
* the object URL reference is stale.
|
|
7671
|
+
*/
|
|
7672
|
+
invalidateUrl(docId, uploadId) {
|
|
7673
|
+
const key = this.blobKey(docId, uploadId);
|
|
7674
|
+
const url = this.objectUrls.get(key);
|
|
7675
|
+
if (url) {
|
|
7676
|
+
URL.revokeObjectURL(url);
|
|
7677
|
+
this.objectUrls.delete(key);
|
|
7678
|
+
}
|
|
7679
|
+
}
|
|
7582
7680
|
/** Revoke the object URL and remove the blob from cache. */
|
|
7583
7681
|
async evictBlob(docId, uploadId) {
|
|
7584
7682
|
const key = this.blobKey(docId, uploadId);
|
|
@@ -7631,38 +7729,42 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
|
|
|
7631
7729
|
* Entries that fail are marked with status "error" and left in the queue.
|
|
7632
7730
|
*/
|
|
7633
7731
|
async flushQueue() {
|
|
7634
|
-
if (this.
|
|
7635
|
-
this.
|
|
7732
|
+
if (this._flushPromise || !this.client) return;
|
|
7733
|
+
this._flushPromise = this._doFlush();
|
|
7636
7734
|
try {
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
7735
|
+
await this._flushPromise;
|
|
7736
|
+
} finally {
|
|
7737
|
+
this._flushPromise = null;
|
|
7738
|
+
}
|
|
7739
|
+
}
|
|
7740
|
+
async _doFlush() {
|
|
7741
|
+
if (!this.client) return;
|
|
7742
|
+
const pending = (await this.getQueue()).filter((e) => e.status === "pending");
|
|
7743
|
+
for (const entry of pending) {
|
|
7744
|
+
await this._updateQueueEntry(entry.id, { status: "uploading" });
|
|
7745
|
+
this.emit("upload:started", {
|
|
7746
|
+
...entry,
|
|
7747
|
+
status: "uploading"
|
|
7748
|
+
});
|
|
7749
|
+
try {
|
|
7750
|
+
await this.client.upload(entry.docId, entry.file, entry.filename);
|
|
7751
|
+
await this._updateQueueEntry(entry.id, { status: "done" });
|
|
7752
|
+
this.emit("upload:done", {
|
|
7641
7753
|
...entry,
|
|
7642
|
-
status: "
|
|
7754
|
+
status: "done"
|
|
7755
|
+
});
|
|
7756
|
+
} catch (err) {
|
|
7757
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7758
|
+
await this._updateQueueEntry(entry.id, {
|
|
7759
|
+
status: "error",
|
|
7760
|
+
error: message
|
|
7761
|
+
});
|
|
7762
|
+
this.emit("upload:error", {
|
|
7763
|
+
...entry,
|
|
7764
|
+
status: "error",
|
|
7765
|
+
error: message
|
|
7643
7766
|
});
|
|
7644
|
-
try {
|
|
7645
|
-
await this.client.upload(entry.docId, entry.file, entry.filename);
|
|
7646
|
-
await this._updateQueueEntry(entry.id, { status: "done" });
|
|
7647
|
-
this.emit("upload:done", {
|
|
7648
|
-
...entry,
|
|
7649
|
-
status: "done"
|
|
7650
|
-
});
|
|
7651
|
-
} catch (err) {
|
|
7652
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
7653
|
-
await this._updateQueueEntry(entry.id, {
|
|
7654
|
-
status: "error",
|
|
7655
|
-
error: message
|
|
7656
|
-
});
|
|
7657
|
-
this.emit("upload:error", {
|
|
7658
|
-
...entry,
|
|
7659
|
-
status: "error",
|
|
7660
|
-
error: message
|
|
7661
|
-
});
|
|
7662
|
-
}
|
|
7663
7767
|
}
|
|
7664
|
-
} finally {
|
|
7665
|
-
this._flushing = false;
|
|
7666
7768
|
}
|
|
7667
7769
|
}
|
|
7668
7770
|
async _updateQueueEntry(id, patch) {
|
|
@@ -7710,10 +7812,13 @@ const HKDF_INFO$1 = new TextEncoder().encode("abracadabra-dockey-v1");
|
|
|
7710
7812
|
function fromBase64$1(b64) {
|
|
7711
7813
|
return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
7712
7814
|
}
|
|
7713
|
-
var DocKeyManager = class {
|
|
7815
|
+
var DocKeyManager = class DocKeyManager {
|
|
7714
7816
|
constructor() {
|
|
7715
7817
|
this.cache = /* @__PURE__ */ new Map();
|
|
7716
7818
|
}
|
|
7819
|
+
static {
|
|
7820
|
+
this.CACHE_TTL = 600 * 1e3;
|
|
7821
|
+
}
|
|
7717
7822
|
/** Generate a new random AES-256-GCM document key. */
|
|
7718
7823
|
static async generateDocKey() {
|
|
7719
7824
|
return crypto.subtle.generateKey({
|
|
@@ -7727,7 +7832,7 @@ var DocKeyManager = class {
|
|
|
7727
7832
|
*/
|
|
7728
7833
|
async getDocKey(docId, client, keystore) {
|
|
7729
7834
|
const cached = this.cache.get(docId);
|
|
7730
|
-
if (cached) return cached.key;
|
|
7835
|
+
if (cached && Date.now() - cached.fetchedAt < DocKeyManager.CACHE_TTL) return cached.key;
|
|
7731
7836
|
const envelope = await client.getMyKeyEnvelope(docId);
|
|
7732
7837
|
if (!envelope) return null;
|
|
7733
7838
|
const x25519PrivKey = await keystore.getX25519PrivateKey();
|
|
@@ -7736,7 +7841,8 @@ var DocKeyManager = class {
|
|
|
7736
7841
|
const docKey = await this._unwrapKey(wrapped, x25519PrivKey, docId);
|
|
7737
7842
|
this.cache.set(docId, {
|
|
7738
7843
|
key: docKey,
|
|
7739
|
-
epoch: envelope.key_epoch
|
|
7844
|
+
epoch: envelope.key_epoch,
|
|
7845
|
+
fetchedAt: Date.now()
|
|
7740
7846
|
});
|
|
7741
7847
|
return docKey;
|
|
7742
7848
|
} finally {
|
|
@@ -8210,6 +8316,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
8210
8316
|
super();
|
|
8211
8317
|
this.syncStates = /* @__PURE__ */ new Map();
|
|
8212
8318
|
this._destroyed = false;
|
|
8319
|
+
this._initPromise = null;
|
|
8213
8320
|
this.rootProvider = rootProvider;
|
|
8214
8321
|
this.client = client;
|
|
8215
8322
|
this.fileBlobStore = fileBlobStore ?? null;
|
|
@@ -8225,18 +8332,36 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
8225
8332
|
this.persistence = new BackgroundSyncPersistence(serverOrigin);
|
|
8226
8333
|
this.semaphore = new Semaphore(this.opts.concurrency);
|
|
8227
8334
|
}
|
|
8335
|
+
/**
|
|
8336
|
+
* Load persisted sync states from IndexedDB into the in-memory map.
|
|
8337
|
+
* Called automatically by syncAll() / syncDoc(); safe to call concurrently.
|
|
8338
|
+
*/
|
|
8339
|
+
async init() {
|
|
8340
|
+
if (!this._initPromise) this._initPromise = this._loadPersistedStates();
|
|
8341
|
+
return this._initPromise;
|
|
8342
|
+
}
|
|
8343
|
+
async _loadPersistedStates() {
|
|
8344
|
+
try {
|
|
8345
|
+
const states = await this.persistence.getAllStates();
|
|
8346
|
+
for (const state of states) this.syncStates.set(state.docId, state);
|
|
8347
|
+
} catch {}
|
|
8348
|
+
}
|
|
8228
8349
|
/** Sync all documents in the root tree. */
|
|
8229
8350
|
async syncAll() {
|
|
8230
8351
|
if (this._destroyed) return;
|
|
8352
|
+
await this.init();
|
|
8231
8353
|
const treeMap = this.rootProvider.document.getMap("doc-tree");
|
|
8232
8354
|
const entries = Array.from(treeMap.entries());
|
|
8233
8355
|
if (entries.length === 0) return;
|
|
8356
|
+
const updatedAtMap = /* @__PURE__ */ new Map();
|
|
8357
|
+
for (const [docId, v] of entries) updatedAtMap.set(docId, v?.updatedAt ?? v?.createdAt ?? 0);
|
|
8234
8358
|
this._prefetchCovers(entries).catch(() => null);
|
|
8235
8359
|
const queue = this._buildQueue(entries);
|
|
8236
|
-
await Promise.all(queue.map((docId) => this._syncWithSemaphore(docId)));
|
|
8360
|
+
await Promise.all(queue.map((docId) => this._syncWithSemaphore(docId, updatedAtMap.get(docId) ?? 0)));
|
|
8237
8361
|
}
|
|
8238
8362
|
/** Sync a single document by ID. */
|
|
8239
8363
|
async syncDoc(docId) {
|
|
8364
|
+
await this.init();
|
|
8240
8365
|
const state = await this._doSyncDoc(docId);
|
|
8241
8366
|
this.syncStates.set(docId, state);
|
|
8242
8367
|
await this.persistence.setState(state).catch(() => null);
|
|
@@ -8287,8 +8412,16 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
8287
8412
|
items.sort((a, b) => b.priority - a.priority);
|
|
8288
8413
|
return items.map((i) => i.docId);
|
|
8289
8414
|
}
|
|
8290
|
-
async _syncWithSemaphore(docId) {
|
|
8415
|
+
async _syncWithSemaphore(docId, updatedAt) {
|
|
8291
8416
|
if (this._destroyed) return;
|
|
8417
|
+
const existing = this.syncStates.get(docId);
|
|
8418
|
+
if (existing && existing.status === "synced" && existing.lastSynced !== null && existing.lastSynced >= updatedAt) {
|
|
8419
|
+
this.emit("stateChanged", {
|
|
8420
|
+
docId,
|
|
8421
|
+
state: existing
|
|
8422
|
+
});
|
|
8423
|
+
return;
|
|
8424
|
+
}
|
|
8292
8425
|
await this.semaphore.acquire();
|
|
8293
8426
|
try {
|
|
8294
8427
|
const state = await this._doSyncDoc(docId);
|
|
@@ -8314,8 +8447,8 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
8314
8447
|
docId,
|
|
8315
8448
|
state: syncing
|
|
8316
8449
|
});
|
|
8450
|
+
let isE2E = false;
|
|
8317
8451
|
try {
|
|
8318
|
-
let isE2E = false;
|
|
8319
8452
|
try {
|
|
8320
8453
|
isE2E = (await this.client.getDocEncryption(docId)).mode === "e2e";
|
|
8321
8454
|
} catch {}
|
|
@@ -8328,7 +8461,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
8328
8461
|
status: "error",
|
|
8329
8462
|
lastSynced: this.syncStates.get(docId)?.lastSynced ?? null,
|
|
8330
8463
|
error,
|
|
8331
|
-
isE2E
|
|
8464
|
+
isE2E
|
|
8332
8465
|
};
|
|
8333
8466
|
}
|
|
8334
8467
|
}
|
|
@@ -8461,6 +8594,7 @@ var SignalingSocket = class extends EventEmitter {
|
|
|
8461
8594
|
this.connectionAttempt = null;
|
|
8462
8595
|
this.localPeerId = null;
|
|
8463
8596
|
this.isConnected = false;
|
|
8597
|
+
this._connectPromise = null;
|
|
8464
8598
|
this.config = {
|
|
8465
8599
|
url: configuration.url,
|
|
8466
8600
|
token: configuration.token,
|
|
@@ -8480,6 +8614,15 @@ var SignalingSocket = class extends EventEmitter {
|
|
|
8480
8614
|
}
|
|
8481
8615
|
async connect() {
|
|
8482
8616
|
if (this.isConnected) return;
|
|
8617
|
+
if (this._connectPromise) return this._connectPromise;
|
|
8618
|
+
this._connectPromise = this._doConnect();
|
|
8619
|
+
try {
|
|
8620
|
+
await this._connectPromise;
|
|
8621
|
+
} finally {
|
|
8622
|
+
this._connectPromise = null;
|
|
8623
|
+
}
|
|
8624
|
+
}
|
|
8625
|
+
async _doConnect() {
|
|
8483
8626
|
if (this.cancelRetry) {
|
|
8484
8627
|
this.cancelRetry();
|
|
8485
8628
|
this.cancelRetry = void 0;
|
|
@@ -8793,11 +8936,12 @@ var DataChannelRouter = class extends EventEmitter {
|
|
|
8793
8936
|
*/
|
|
8794
8937
|
async send(name, data) {
|
|
8795
8938
|
const channel = this.channels.get(name);
|
|
8796
|
-
if (!channel || channel.readyState !== "open") return;
|
|
8939
|
+
if (!channel || channel.readyState !== "open") return false;
|
|
8797
8940
|
if (this.encryptor?.isEstablished && !this.plaintextChannels.has(name)) {
|
|
8798
8941
|
const encrypted = await this.encryptor.encrypt(data);
|
|
8799
8942
|
channel.send(encrypted);
|
|
8800
8943
|
} else channel.send(data);
|
|
8944
|
+
return true;
|
|
8801
8945
|
}
|
|
8802
8946
|
registerChannel(channel) {
|
|
8803
8947
|
channel.binaryType = "arraybuffer";
|
|
@@ -8965,6 +9109,7 @@ var YjsDataChannel = class {
|
|
|
8965
9109
|
this.document = document;
|
|
8966
9110
|
this.awareness = awareness;
|
|
8967
9111
|
this.router = router;
|
|
9112
|
+
this.isSynced = false;
|
|
8968
9113
|
this.docUpdateHandler = null;
|
|
8969
9114
|
this.awarenessUpdateHandler = null;
|
|
8970
9115
|
this.channelOpenHandler = null;
|
|
@@ -9201,6 +9346,10 @@ var FileTransferChannel = class extends EventEmitter {
|
|
|
9201
9346
|
try {
|
|
9202
9347
|
meta = JSON.parse(json);
|
|
9203
9348
|
} catch {
|
|
9349
|
+
this.emit("receiveError", {
|
|
9350
|
+
transferId: "unknown",
|
|
9351
|
+
error: "Malformed START message: invalid JSON"
|
|
9352
|
+
});
|
|
9204
9353
|
return;
|
|
9205
9354
|
}
|
|
9206
9355
|
this.receives.set(meta.transferId, {
|
|
@@ -9907,7 +10056,9 @@ var ManualSignaling = class extends EventEmitter {
|
|
|
9907
10056
|
type: "offer",
|
|
9908
10057
|
sdp: offerBlob.sdp
|
|
9909
10058
|
}));
|
|
9910
|
-
for (const c of offerBlob.candidates)
|
|
10059
|
+
for (const c of offerBlob.candidates) try {
|
|
10060
|
+
await this.pc.addIceCandidate(new RTCIceCandidate(JSON.parse(c)));
|
|
10061
|
+
} catch {}
|
|
9911
10062
|
const answer = await this.pc.createAnswer();
|
|
9912
10063
|
await this.pc.setLocalDescription(answer);
|
|
9913
10064
|
await gatheringComplete;
|