@abraca/dabra 1.0.14 → 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 +176 -67
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +176 -67
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +34 -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 +41 -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 = () => {
|
|
@@ -7651,6 +7693,20 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
|
|
|
7651
7693
|
req.onerror = () => reject(req.error);
|
|
7652
7694
|
});
|
|
7653
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
|
+
}
|
|
7654
7710
|
/** Revoke the object URL and remove the blob from cache. */
|
|
7655
7711
|
async evictBlob(docId, uploadId) {
|
|
7656
7712
|
const key = this.blobKey(docId, uploadId);
|
|
@@ -7703,38 +7759,42 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
|
|
|
7703
7759
|
* Entries that fail are marked with status "error" and left in the queue.
|
|
7704
7760
|
*/
|
|
7705
7761
|
async flushQueue() {
|
|
7706
|
-
if (this.
|
|
7707
|
-
this.
|
|
7762
|
+
if (this._flushPromise || !this.client) return;
|
|
7763
|
+
this._flushPromise = this._doFlush();
|
|
7708
7764
|
try {
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
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", {
|
|
7713
7783
|
...entry,
|
|
7714
|
-
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
|
|
7715
7796
|
});
|
|
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
7797
|
}
|
|
7736
|
-
} finally {
|
|
7737
|
-
this._flushing = false;
|
|
7738
7798
|
}
|
|
7739
7799
|
}
|
|
7740
7800
|
async _updateQueueEntry(id, patch) {
|
|
@@ -7782,10 +7842,13 @@ const HKDF_INFO$1 = new TextEncoder().encode("abracadabra-dockey-v1");
|
|
|
7782
7842
|
function fromBase64$1(b64) {
|
|
7783
7843
|
return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
7784
7844
|
}
|
|
7785
|
-
var DocKeyManager = class {
|
|
7845
|
+
var DocKeyManager = class DocKeyManager {
|
|
7786
7846
|
constructor() {
|
|
7787
7847
|
this.cache = /* @__PURE__ */ new Map();
|
|
7788
7848
|
}
|
|
7849
|
+
static {
|
|
7850
|
+
this.CACHE_TTL = 600 * 1e3;
|
|
7851
|
+
}
|
|
7789
7852
|
/** Generate a new random AES-256-GCM document key. */
|
|
7790
7853
|
static async generateDocKey() {
|
|
7791
7854
|
return crypto.subtle.generateKey({
|
|
@@ -7799,7 +7862,7 @@ var DocKeyManager = class {
|
|
|
7799
7862
|
*/
|
|
7800
7863
|
async getDocKey(docId, client, keystore) {
|
|
7801
7864
|
const cached = this.cache.get(docId);
|
|
7802
|
-
if (cached) return cached.key;
|
|
7865
|
+
if (cached && Date.now() - cached.fetchedAt < DocKeyManager.CACHE_TTL) return cached.key;
|
|
7803
7866
|
const envelope = await client.getMyKeyEnvelope(docId);
|
|
7804
7867
|
if (!envelope) return null;
|
|
7805
7868
|
const x25519PrivKey = await keystore.getX25519PrivateKey();
|
|
@@ -7808,7 +7871,8 @@ var DocKeyManager = class {
|
|
|
7808
7871
|
const docKey = await this._unwrapKey(wrapped, x25519PrivKey, docId);
|
|
7809
7872
|
this.cache.set(docId, {
|
|
7810
7873
|
key: docKey,
|
|
7811
|
-
epoch: envelope.key_epoch
|
|
7874
|
+
epoch: envelope.key_epoch,
|
|
7875
|
+
fetchedAt: Date.now()
|
|
7812
7876
|
});
|
|
7813
7877
|
return docKey;
|
|
7814
7878
|
} finally {
|
|
@@ -8304,6 +8368,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
8304
8368
|
super();
|
|
8305
8369
|
this.syncStates = /* @__PURE__ */ new Map();
|
|
8306
8370
|
this._destroyed = false;
|
|
8371
|
+
this._initPromise = null;
|
|
8307
8372
|
this.rootProvider = rootProvider;
|
|
8308
8373
|
this.client = client;
|
|
8309
8374
|
this.fileBlobStore = fileBlobStore ?? null;
|
|
@@ -8319,18 +8384,36 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
8319
8384
|
this.persistence = new BackgroundSyncPersistence(serverOrigin);
|
|
8320
8385
|
this.semaphore = new Semaphore(this.opts.concurrency);
|
|
8321
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
|
+
}
|
|
8322
8401
|
/** Sync all documents in the root tree. */
|
|
8323
8402
|
async syncAll() {
|
|
8324
8403
|
if (this._destroyed) return;
|
|
8404
|
+
await this.init();
|
|
8325
8405
|
const treeMap = this.rootProvider.document.getMap("doc-tree");
|
|
8326
8406
|
const entries = Array.from(treeMap.entries());
|
|
8327
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);
|
|
8328
8410
|
this._prefetchCovers(entries).catch(() => null);
|
|
8329
8411
|
const queue = this._buildQueue(entries);
|
|
8330
|
-
await Promise.all(queue.map((docId) => this._syncWithSemaphore(docId)));
|
|
8412
|
+
await Promise.all(queue.map((docId) => this._syncWithSemaphore(docId, updatedAtMap.get(docId) ?? 0)));
|
|
8331
8413
|
}
|
|
8332
8414
|
/** Sync a single document by ID. */
|
|
8333
8415
|
async syncDoc(docId) {
|
|
8416
|
+
await this.init();
|
|
8334
8417
|
const state = await this._doSyncDoc(docId);
|
|
8335
8418
|
this.syncStates.set(docId, state);
|
|
8336
8419
|
await this.persistence.setState(state).catch(() => null);
|
|
@@ -8381,8 +8464,16 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
8381
8464
|
items.sort((a, b) => b.priority - a.priority);
|
|
8382
8465
|
return items.map((i) => i.docId);
|
|
8383
8466
|
}
|
|
8384
|
-
async _syncWithSemaphore(docId) {
|
|
8467
|
+
async _syncWithSemaphore(docId, updatedAt) {
|
|
8385
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
|
+
}
|
|
8386
8477
|
await this.semaphore.acquire();
|
|
8387
8478
|
try {
|
|
8388
8479
|
const state = await this._doSyncDoc(docId);
|
|
@@ -8408,8 +8499,8 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
8408
8499
|
docId,
|
|
8409
8500
|
state: syncing
|
|
8410
8501
|
});
|
|
8502
|
+
let isE2E = false;
|
|
8411
8503
|
try {
|
|
8412
|
-
let isE2E = false;
|
|
8413
8504
|
try {
|
|
8414
8505
|
isE2E = (await this.client.getDocEncryption(docId)).mode === "e2e";
|
|
8415
8506
|
} catch {}
|
|
@@ -8422,7 +8513,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
8422
8513
|
status: "error",
|
|
8423
8514
|
lastSynced: this.syncStates.get(docId)?.lastSynced ?? null,
|
|
8424
8515
|
error,
|
|
8425
|
-
isE2E
|
|
8516
|
+
isE2E
|
|
8426
8517
|
};
|
|
8427
8518
|
}
|
|
8428
8519
|
}
|
|
@@ -8555,6 +8646,7 @@ var SignalingSocket = class extends EventEmitter {
|
|
|
8555
8646
|
this.connectionAttempt = null;
|
|
8556
8647
|
this.localPeerId = null;
|
|
8557
8648
|
this.isConnected = false;
|
|
8649
|
+
this._connectPromise = null;
|
|
8558
8650
|
this.config = {
|
|
8559
8651
|
url: configuration.url,
|
|
8560
8652
|
token: configuration.token,
|
|
@@ -8574,6 +8666,15 @@ var SignalingSocket = class extends EventEmitter {
|
|
|
8574
8666
|
}
|
|
8575
8667
|
async connect() {
|
|
8576
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() {
|
|
8577
8678
|
if (this.cancelRetry) {
|
|
8578
8679
|
this.cancelRetry();
|
|
8579
8680
|
this.cancelRetry = void 0;
|
|
@@ -8887,11 +8988,12 @@ var DataChannelRouter = class extends EventEmitter {
|
|
|
8887
8988
|
*/
|
|
8888
8989
|
async send(name, data) {
|
|
8889
8990
|
const channel = this.channels.get(name);
|
|
8890
|
-
if (!channel || channel.readyState !== "open") return;
|
|
8991
|
+
if (!channel || channel.readyState !== "open") return false;
|
|
8891
8992
|
if (this.encryptor?.isEstablished && !this.plaintextChannels.has(name)) {
|
|
8892
8993
|
const encrypted = await this.encryptor.encrypt(data);
|
|
8893
8994
|
channel.send(encrypted);
|
|
8894
8995
|
} else channel.send(data);
|
|
8996
|
+
return true;
|
|
8895
8997
|
}
|
|
8896
8998
|
registerChannel(channel) {
|
|
8897
8999
|
channel.binaryType = "arraybuffer";
|
|
@@ -9059,6 +9161,7 @@ var YjsDataChannel = class {
|
|
|
9059
9161
|
this.document = document;
|
|
9060
9162
|
this.awareness = awareness;
|
|
9061
9163
|
this.router = router;
|
|
9164
|
+
this.isSynced = false;
|
|
9062
9165
|
this.docUpdateHandler = null;
|
|
9063
9166
|
this.awarenessUpdateHandler = null;
|
|
9064
9167
|
this.channelOpenHandler = null;
|
|
@@ -9295,6 +9398,10 @@ var FileTransferChannel = class extends EventEmitter {
|
|
|
9295
9398
|
try {
|
|
9296
9399
|
meta = JSON.parse(json);
|
|
9297
9400
|
} catch {
|
|
9401
|
+
this.emit("receiveError", {
|
|
9402
|
+
transferId: "unknown",
|
|
9403
|
+
error: "Malformed START message: invalid JSON"
|
|
9404
|
+
});
|
|
9298
9405
|
return;
|
|
9299
9406
|
}
|
|
9300
9407
|
this.receives.set(meta.transferId, {
|
|
@@ -10001,7 +10108,9 @@ var ManualSignaling = class extends EventEmitter {
|
|
|
10001
10108
|
type: "offer",
|
|
10002
10109
|
sdp: offerBlob.sdp
|
|
10003
10110
|
}));
|
|
10004
|
-
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 {}
|
|
10005
10114
|
const answer = await this.pc.createAnswer();
|
|
10006
10115
|
await this.pc.setLocalDescription(answer);
|
|
10007
10116
|
await gatheringComplete;
|