@abraca/dabra 1.0.11 → 1.0.13
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 +30 -6
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +30 -6
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +12 -0
- package/package.json +1 -1
- package/src/AbracadabraClient.ts +1 -1
- package/src/AbracadabraWS.ts +16 -3
- package/src/FileBlobStore.ts +35 -1
- package/src/OfflineStore.ts +11 -4
package/dist/index.d.ts
CHANGED
|
@@ -163,6 +163,7 @@ declare class OfflineStore {
|
|
|
163
163
|
*/
|
|
164
164
|
constructor(docId: string, serverOrigin?: string);
|
|
165
165
|
private dbPromise;
|
|
166
|
+
private _destroyed;
|
|
166
167
|
private getDb;
|
|
167
168
|
persistUpdate(update: Uint8Array): Promise<void>;
|
|
168
169
|
getPendingUpdates(): Promise<Uint8Array[]>;
|
|
@@ -359,6 +360,8 @@ declare class AbracadabraClient {
|
|
|
359
360
|
/** Create a child document under a parent (requires write permission). */
|
|
360
361
|
createChild(docId: string, opts?: {
|
|
361
362
|
child_id?: string;
|
|
363
|
+
doc_type?: string;
|
|
364
|
+
label?: string;
|
|
362
365
|
}): Promise<DocumentMeta>;
|
|
363
366
|
/** List all permissions for a document (requires read access). */
|
|
364
367
|
listPermissions(docId: string): Promise<PermissionEntry[]>;
|
|
@@ -1368,6 +1371,9 @@ declare class FileBlobStore extends EventEmitter {
|
|
|
1368
1371
|
private db;
|
|
1369
1372
|
/** Tracks active object URLs so we can revoke them on destroy. */
|
|
1370
1373
|
private readonly objectUrls;
|
|
1374
|
+
/** Keys that returned 404 from the server — avoids repeated requests. TTL: 5 min. */
|
|
1375
|
+
private readonly _notFound;
|
|
1376
|
+
private static readonly NOT_FOUND_TTL;
|
|
1371
1377
|
/** Prevents concurrent flush runs. */
|
|
1372
1378
|
private _flushing;
|
|
1373
1379
|
private readonly _onlineHandler;
|
|
@@ -1387,6 +1393,12 @@ declare class FileBlobStore extends EventEmitter {
|
|
|
1387
1393
|
* that haven't been uploaded to the server yet (e.g. offline upload queue).
|
|
1388
1394
|
*/
|
|
1389
1395
|
putBlob(docId: string, uploadId: string, blob: Blob, filename: string): Promise<string>;
|
|
1396
|
+
/**
|
|
1397
|
+
* Retrieve the raw Blob from IDB for a previously cached upload.
|
|
1398
|
+
* Returns null if the blob has been evicted or was never stored.
|
|
1399
|
+
* Use this to re-upload a file after a page reload.
|
|
1400
|
+
*/
|
|
1401
|
+
getBlob(docId: string, uploadId: string): Promise<Blob | null>;
|
|
1390
1402
|
/** Revoke the object URL and remove the blob from cache. */
|
|
1391
1403
|
evictBlob(docId: string, uploadId: string): Promise<void>;
|
|
1392
1404
|
/**
|
package/package.json
CHANGED
package/src/AbracadabraClient.ts
CHANGED
|
@@ -330,7 +330,7 @@ export class AbracadabraClient {
|
|
|
330
330
|
}
|
|
331
331
|
|
|
332
332
|
/** Create a child document under a parent (requires write permission). */
|
|
333
|
-
async createChild(docId: string, opts?: { child_id?: string }): Promise<DocumentMeta> {
|
|
333
|
+
async createChild(docId: string, opts?: { child_id?: string; doc_type?: string; label?: string }): Promise<DocumentMeta> {
|
|
334
334
|
return this.request<DocumentMeta>(
|
|
335
335
|
"POST",
|
|
336
336
|
`/docs/${encodeURIComponent(docId)}/children`,
|
package/src/AbracadabraWS.ts
CHANGED
|
@@ -349,12 +349,21 @@ export class AbracadabraWS extends EventEmitter {
|
|
|
349
349
|
delete this.webSocketHandlers[identifier];
|
|
350
350
|
});
|
|
351
351
|
|
|
352
|
+
// After removing our handlers, ensure there is always at least one error
|
|
353
|
+
// listener on the socket. Without one, any asynchronously-fired error
|
|
354
|
+
// (e.g. "WebSocket was closed before the connection was established" when
|
|
355
|
+
// closing a CONNECTING socket) will propagate as an uncaught exception in
|
|
356
|
+
// Node.js. The no-op listener absorbs these post-cleanup errors safely.
|
|
357
|
+
if (this.webSocket.readyState !== 3 /* CLOSED */) {
|
|
358
|
+
this.webSocket.addEventListener("error", () => {});
|
|
359
|
+
}
|
|
360
|
+
|
|
352
361
|
try {
|
|
353
|
-
|
|
354
|
-
// If so, calling close() might throw in some environments or be race-prone
|
|
355
|
-
if (this.webSocket.readyState !== 0 && this.webSocket.readyState !== 3) {
|
|
362
|
+
if (this.webSocket.readyState !== 0 /* CONNECTING */ && this.webSocket.readyState !== 3 /* CLOSED */) {
|
|
356
363
|
this.webSocket.close();
|
|
357
364
|
}
|
|
365
|
+
// CONNECTING (0): already being closed by disconnect(), or will be closed imminently.
|
|
366
|
+
// CLOSED (3): nothing to do.
|
|
358
367
|
} catch (e) {
|
|
359
368
|
// Ignore errors during close
|
|
360
369
|
}
|
|
@@ -539,6 +548,10 @@ export class AbracadabraWS extends EventEmitter {
|
|
|
539
548
|
}
|
|
540
549
|
|
|
541
550
|
destroy() {
|
|
551
|
+
// Mark as not wanting a connection FIRST so that onClose (fired by disconnect()
|
|
552
|
+
// below) does not schedule a new reconnect setTimeout after we've torn down.
|
|
553
|
+
this.shouldConnect = false;
|
|
554
|
+
|
|
542
555
|
this.emit("destroy");
|
|
543
556
|
|
|
544
557
|
clearInterval(this.intervals.connectionChecker);
|
package/src/FileBlobStore.ts
CHANGED
|
@@ -70,6 +70,10 @@ export class FileBlobStore extends EventEmitter {
|
|
|
70
70
|
/** Tracks active object URLs so we can revoke them on destroy. */
|
|
71
71
|
private readonly objectUrls = new Map<string, string>();
|
|
72
72
|
|
|
73
|
+
/** Keys that returned 404 from the server — avoids repeated requests. TTL: 5 min. */
|
|
74
|
+
private readonly _notFound = new Map<string, number>();
|
|
75
|
+
private static readonly NOT_FOUND_TTL = 5 * 60 * 1000;
|
|
76
|
+
|
|
73
77
|
/** Prevents concurrent flush runs. */
|
|
74
78
|
private _flushing = false;
|
|
75
79
|
|
|
@@ -137,10 +141,21 @@ export class FileBlobStore extends EventEmitter {
|
|
|
137
141
|
|
|
138
142
|
// Not cached — try downloading from server (requires a client)
|
|
139
143
|
if (!this.client) return null;
|
|
144
|
+
|
|
145
|
+
// Skip if we recently got a 404 for this key
|
|
146
|
+
const nfTime = this._notFound.get(key);
|
|
147
|
+
if (nfTime && Date.now() - nfTime < FileBlobStore.NOT_FOUND_TTL) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
140
151
|
let blob: Blob;
|
|
141
152
|
try {
|
|
142
153
|
blob = await this.client.getUpload(docId, uploadId);
|
|
143
|
-
} catch {
|
|
154
|
+
} catch (err: unknown) {
|
|
155
|
+
const status = (err as any)?.status;
|
|
156
|
+
if (status === 404) {
|
|
157
|
+
this._notFound.set(key, Date.now());
|
|
158
|
+
}
|
|
144
159
|
return null;
|
|
145
160
|
}
|
|
146
161
|
|
|
@@ -175,6 +190,7 @@ export class FileBlobStore extends EventEmitter {
|
|
|
175
190
|
if (typeof window === "undefined") return URL.createObjectURL(blob);
|
|
176
191
|
|
|
177
192
|
const key = this.blobKey(docId, uploadId);
|
|
193
|
+
this._notFound.delete(key);
|
|
178
194
|
|
|
179
195
|
// Return existing URL if already cached in-memory
|
|
180
196
|
const existing = this.objectUrls.get(key);
|
|
@@ -197,6 +213,24 @@ export class FileBlobStore extends EventEmitter {
|
|
|
197
213
|
return url;
|
|
198
214
|
}
|
|
199
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Retrieve the raw Blob from IDB for a previously cached upload.
|
|
218
|
+
* Returns null if the blob has been evicted or was never stored.
|
|
219
|
+
* Use this to re-upload a file after a page reload.
|
|
220
|
+
*/
|
|
221
|
+
async getBlob(docId: string, uploadId: string): Promise<Blob | null> {
|
|
222
|
+
const db = await this.getDb();
|
|
223
|
+
if (!db) return null;
|
|
224
|
+
|
|
225
|
+
const key = this.blobKey(docId, uploadId);
|
|
226
|
+
const tx = db.transaction("blobs", "readonly");
|
|
227
|
+
const entry = await txPromise<BlobCacheEntry | undefined>(
|
|
228
|
+
tx.objectStore("blobs"),
|
|
229
|
+
tx.objectStore("blobs").get(key),
|
|
230
|
+
);
|
|
231
|
+
return entry?.blob ?? null;
|
|
232
|
+
}
|
|
233
|
+
|
|
200
234
|
/** Revoke the object URL and remove the blob from cache. */
|
|
201
235
|
async evictBlob(docId: string, uploadId: string): Promise<void> {
|
|
202
236
|
const key = this.blobKey(docId, uploadId);
|
package/src/OfflineStore.ts
CHANGED
|
@@ -79,8 +79,10 @@ export class OfflineStore {
|
|
|
79
79
|
|
|
80
80
|
private dbPromise: Promise<IDBDatabase | null> | null = null;
|
|
81
81
|
|
|
82
|
+
private _destroyed = false;
|
|
83
|
+
|
|
82
84
|
private getDb(): Promise<IDBDatabase | null> {
|
|
83
|
-
if (!idbAvailable()) return Promise.resolve(null);
|
|
85
|
+
if (this._destroyed || !idbAvailable()) return Promise.resolve(null);
|
|
84
86
|
if (!this.dbPromise) {
|
|
85
87
|
// Cache the promise so concurrent callers share a single open operation.
|
|
86
88
|
this.dbPromise = openDb(this.storeKey).catch(() => null).then(db => {
|
|
@@ -234,7 +236,7 @@ export class OfflineStore {
|
|
|
234
236
|
const tx = db.transaction("meta", "readonly");
|
|
235
237
|
const result = await txPromise<string | undefined>(
|
|
236
238
|
tx.objectStore("meta"),
|
|
237
|
-
tx.objectStore("meta").get(key),
|
|
239
|
+
tx.objectStore("meta").get(`meta:${key}`),
|
|
238
240
|
);
|
|
239
241
|
return result ?? null;
|
|
240
242
|
}
|
|
@@ -245,14 +247,19 @@ export class OfflineStore {
|
|
|
245
247
|
const tx = db.transaction("meta", "readwrite");
|
|
246
248
|
await txPromise(
|
|
247
249
|
tx.objectStore("meta"),
|
|
248
|
-
tx.objectStore("meta").put(value, key),
|
|
250
|
+
tx.objectStore("meta").put(value, `meta:${key}`),
|
|
249
251
|
);
|
|
250
252
|
}
|
|
251
253
|
|
|
252
254
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
253
255
|
|
|
254
256
|
destroy() {
|
|
255
|
-
|
|
257
|
+
// Set the destroyed flag first so getDb() returns null for any new operations.
|
|
258
|
+
// Do NOT call db.close() here — in-flight IDB transactions hold their own
|
|
259
|
+
// reference to the database and closing it prematurely causes InvalidStateError.
|
|
260
|
+
// The DB will be closed and garbage-collected once all transactions complete.
|
|
261
|
+
this._destroyed = true;
|
|
256
262
|
this.db = null;
|
|
263
|
+
this.dbPromise = null;
|
|
257
264
|
}
|
|
258
265
|
}
|