@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
package/src/FileBlobStore.ts
CHANGED
|
@@ -75,7 +75,7 @@ export class FileBlobStore extends EventEmitter {
|
|
|
75
75
|
private static readonly NOT_FOUND_TTL = 5 * 60 * 1000;
|
|
76
76
|
|
|
77
77
|
/** Prevents concurrent flush runs. */
|
|
78
|
-
private
|
|
78
|
+
private _flushPromise: Promise<void> | null = null;
|
|
79
79
|
|
|
80
80
|
private readonly _onlineHandler: () => void;
|
|
81
81
|
|
|
@@ -272,6 +272,21 @@ export class FileBlobStore extends EventEmitter {
|
|
|
272
272
|
});
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
+
/**
|
|
276
|
+
* Revoke the in-memory object URL without touching the IDB cache.
|
|
277
|
+
* The next call to getBlobUrl() will re-create a fresh URL from IDB.
|
|
278
|
+
* Use this when an <img> @error fires — the blob data is fine, only
|
|
279
|
+
* the object URL reference is stale.
|
|
280
|
+
*/
|
|
281
|
+
invalidateUrl(docId: string, uploadId: string): void {
|
|
282
|
+
const key = this.blobKey(docId, uploadId);
|
|
283
|
+
const url = this.objectUrls.get(key);
|
|
284
|
+
if (url) {
|
|
285
|
+
URL.revokeObjectURL(url);
|
|
286
|
+
this.objectUrls.delete(key);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
275
290
|
/** Revoke the object URL and remove the blob from cache. */
|
|
276
291
|
async evictBlob(docId: string, uploadId: string): Promise<void> {
|
|
277
292
|
const key = this.blobKey(docId, uploadId);
|
|
@@ -344,29 +359,33 @@ export class FileBlobStore extends EventEmitter {
|
|
|
344
359
|
* Entries that fail are marked with status "error" and left in the queue.
|
|
345
360
|
*/
|
|
346
361
|
async flushQueue(): Promise<void> {
|
|
347
|
-
if (this.
|
|
348
|
-
this.
|
|
349
|
-
|
|
362
|
+
if (this._flushPromise || !this.client) return;
|
|
363
|
+
this._flushPromise = this._doFlush();
|
|
350
364
|
try {
|
|
351
|
-
|
|
352
|
-
const pending = all.filter((e) => e.status === "pending");
|
|
353
|
-
|
|
354
|
-
for (const entry of pending) {
|
|
355
|
-
await this._updateQueueEntry(entry.id, { status: "uploading" });
|
|
356
|
-
this.emit("upload:started", { ...entry, status: "uploading" });
|
|
357
|
-
|
|
358
|
-
try {
|
|
359
|
-
await this.client.upload(entry.docId, entry.file, entry.filename);
|
|
360
|
-
await this._updateQueueEntry(entry.id, { status: "done" });
|
|
361
|
-
this.emit("upload:done", { ...entry, status: "done" });
|
|
362
|
-
} catch (err) {
|
|
363
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
364
|
-
await this._updateQueueEntry(entry.id, { status: "error", error: message });
|
|
365
|
-
this.emit("upload:error", { ...entry, status: "error", error: message });
|
|
366
|
-
}
|
|
367
|
-
}
|
|
365
|
+
await this._flushPromise;
|
|
368
366
|
} finally {
|
|
369
|
-
this.
|
|
367
|
+
this._flushPromise = null;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private async _doFlush(): Promise<void> {
|
|
372
|
+
if (!this.client) return;
|
|
373
|
+
const all = await this.getQueue();
|
|
374
|
+
const pending = all.filter((e) => e.status === "pending");
|
|
375
|
+
|
|
376
|
+
for (const entry of pending) {
|
|
377
|
+
await this._updateQueueEntry(entry.id, { status: "uploading" });
|
|
378
|
+
this.emit("upload:started", { ...entry, status: "uploading" });
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
await this.client.upload(entry.docId, entry.file, entry.filename);
|
|
382
|
+
await this._updateQueueEntry(entry.id, { status: "done" });
|
|
383
|
+
this.emit("upload:done", { ...entry, status: "done" });
|
|
384
|
+
} catch (err) {
|
|
385
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
386
|
+
await this._updateQueueEntry(entry.id, { status: "error", error: message });
|
|
387
|
+
this.emit("upload:error", { ...entry, status: "error", error: message });
|
|
388
|
+
}
|
|
370
389
|
}
|
|
371
390
|
}
|
|
372
391
|
|
package/src/OfflineStore.ts
CHANGED
|
@@ -251,6 +251,28 @@ export class OfflineStore {
|
|
|
251
251
|
);
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Clear all stored data (updates, snapshots, state vectors, subdoc queue).
|
|
256
|
+
* The database itself is kept but emptied.
|
|
257
|
+
*/
|
|
258
|
+
async clearAll(): Promise<void> {
|
|
259
|
+
const db = await this.getDb();
|
|
260
|
+
if (!db) return;
|
|
261
|
+
const storeNames = Array.from(db.objectStoreNames);
|
|
262
|
+
if (storeNames.length === 0) return;
|
|
263
|
+
const tx = db.transaction(storeNames, "readwrite");
|
|
264
|
+
await Promise.all(
|
|
265
|
+
storeNames.map(
|
|
266
|
+
(name) =>
|
|
267
|
+
new Promise<void>((resolve, reject) => {
|
|
268
|
+
const req = tx.objectStore(name).clear();
|
|
269
|
+
req.onsuccess = () => resolve();
|
|
270
|
+
req.onerror = () => reject(req.error);
|
|
271
|
+
}),
|
|
272
|
+
),
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
254
276
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
255
277
|
|
|
256
278
|
destroy() {
|
package/src/SearchIndex.ts
CHANGED
|
@@ -200,6 +200,8 @@ export class SearchIndex {
|
|
|
200
200
|
const queryTrigrams = [...extractTrigrams(query)];
|
|
201
201
|
if (queryTrigrams.length === 0) return [];
|
|
202
202
|
|
|
203
|
+
const maxScoreEntries = limit * 10;
|
|
204
|
+
|
|
203
205
|
return new Promise<SearchResult[]>((resolve, reject) => {
|
|
204
206
|
const tx = db.transaction("postings", "readonly");
|
|
205
207
|
const postings = tx.objectStore("postings");
|
|
@@ -212,6 +214,7 @@ export class SearchIndex {
|
|
|
212
214
|
const docIds: string[] = req.result ?? [];
|
|
213
215
|
for (const docId of docIds) {
|
|
214
216
|
scores.set(docId, (scores.get(docId) ?? 0) + 1);
|
|
217
|
+
if (scores.size >= maxScoreEntries) break;
|
|
215
218
|
}
|
|
216
219
|
remaining--;
|
|
217
220
|
if (remaining === 0) {
|
|
@@ -94,9 +94,9 @@ export class DataChannelRouter extends EventEmitter {
|
|
|
94
94
|
* Send data on a named channel, encrypting if E2EE is active.
|
|
95
95
|
* Falls back to plaintext if no encryptor is set or for exempt channels.
|
|
96
96
|
*/
|
|
97
|
-
async send(name: string, data: Uint8Array): Promise<
|
|
97
|
+
async send(name: string, data: Uint8Array): Promise<boolean> {
|
|
98
98
|
const channel = this.channels.get(name);
|
|
99
|
-
if (!channel || channel.readyState !== "open") return;
|
|
99
|
+
if (!channel || channel.readyState !== "open") return false;
|
|
100
100
|
|
|
101
101
|
if (this.encryptor?.isEstablished && !this.plaintextChannels.has(name)) {
|
|
102
102
|
const encrypted = await this.encryptor.encrypt(data);
|
|
@@ -104,6 +104,7 @@ export class DataChannelRouter extends EventEmitter {
|
|
|
104
104
|
} else {
|
|
105
105
|
channel.send(data);
|
|
106
106
|
}
|
|
107
|
+
return true;
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
private registerChannel(channel: RTCDataChannel): void {
|
|
@@ -106,7 +106,11 @@ export class ManualSignaling extends EventEmitter {
|
|
|
106
106
|
|
|
107
107
|
// Add remote ICE candidates.
|
|
108
108
|
for (const c of offerBlob.candidates) {
|
|
109
|
-
|
|
109
|
+
try {
|
|
110
|
+
await this.pc.addIceCandidate(new RTCIceCandidate(JSON.parse(c)));
|
|
111
|
+
} catch {
|
|
112
|
+
// Skip malformed ICE candidate
|
|
113
|
+
}
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
// Create answer.
|
|
@@ -87,9 +87,21 @@ export class SignalingSocket extends EventEmitter {
|
|
|
87
87
|
return this.config.token;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
private _connectPromise: Promise<void> | null = null;
|
|
91
|
+
|
|
90
92
|
async connect(): Promise<void> {
|
|
91
93
|
if (this.isConnected) return;
|
|
94
|
+
if (this._connectPromise) return this._connectPromise;
|
|
95
|
+
|
|
96
|
+
this._connectPromise = this._doConnect();
|
|
97
|
+
try {
|
|
98
|
+
await this._connectPromise;
|
|
99
|
+
} finally {
|
|
100
|
+
this._connectPromise = null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
92
103
|
|
|
104
|
+
private async _doConnect(): Promise<void> {
|
|
93
105
|
if (this.cancelRetry) {
|
|
94
106
|
this.cancelRetry();
|
|
95
107
|
this.cancelRetry = undefined;
|
|
@@ -18,6 +18,7 @@ import { CHANNEL_NAMES, YJS_MSG } from "./types.ts";
|
|
|
18
18
|
* prevent echo loops with the server-based provider.
|
|
19
19
|
*/
|
|
20
20
|
export class YjsDataChannel {
|
|
21
|
+
public isSynced = false;
|
|
21
22
|
private docUpdateHandler: ((update: Uint8Array, origin: any) => void) | null = null;
|
|
22
23
|
private awarenessUpdateHandler: ((changes: { added: number[]; updated: number[]; removed: number[] }, origin: any) => void) | null = null;
|
|
23
24
|
private channelOpenHandler: ((data: { name: string; channel: RTCDataChannel }) => void) | null = null;
|