@abraca/dabra 1.0.12 → 1.0.14

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/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[]>;
@@ -1390,6 +1393,23 @@ declare class FileBlobStore extends EventEmitter {
1390
1393
  * that haven't been uploaded to the server yet (e.g. offline upload queue).
1391
1394
  */
1392
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>;
1402
+ /** Return metadata for all cached blobs (for storage stats). */
1403
+ getAllCachedEntries(): Promise<Array<{
1404
+ docId: string;
1405
+ uploadId: string;
1406
+ filename: string;
1407
+ mimeType: string;
1408
+ size: number;
1409
+ cachedAt: number;
1410
+ }>>;
1411
+ /** Revoke all object URLs and clear the entire blob cache from IDB. */
1412
+ clearAllBlobs(): Promise<void>;
1393
1413
  /** Revoke the object URL and remove the blob from cache. */
1394
1414
  evictBlob(docId: string, uploadId: string): Promise<void>;
1395
1415
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abraca/dabra",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "description": "abracadabra provider",
5
5
  "keywords": [
6
6
  "abracadabra",
@@ -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`,
@@ -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
- // Check if the WebSocket is still in CONNECTING state (0)
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);
@@ -213,6 +213,65 @@ export class FileBlobStore extends EventEmitter {
213
213
  return url;
214
214
  }
215
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
+
234
+ /** Return metadata for all cached blobs (for storage stats). */
235
+ async getAllCachedEntries(): Promise<Array<{
236
+ docId: string; uploadId: string; filename: string;
237
+ mimeType: string; size: number; cachedAt: number;
238
+ }>> {
239
+ const db = await this.getDb();
240
+ if (!db) return [];
241
+ return new Promise((resolve, reject) => {
242
+ const tx = db.transaction("blobs", "readonly");
243
+ const store = tx.objectStore("blobs");
244
+ const keysReq = store.getAllKeys();
245
+ const valuesReq = store.getAll();
246
+ tx.oncomplete = () => {
247
+ const keys = keysReq.result as string[];
248
+ const values = valuesReq.result as BlobCacheEntry[];
249
+ resolve(keys.map((key, i) => {
250
+ const slashIdx = key.indexOf("/");
251
+ const docId = key.slice(0, slashIdx);
252
+ const uploadId = key.slice(slashIdx + 1);
253
+ const e = values[i]!;
254
+ return { docId, uploadId, filename: e.filename, mimeType: e.mime_type, size: e.blob.size, cachedAt: e.cachedAt };
255
+ }));
256
+ };
257
+ tx.onerror = () => reject(tx.error);
258
+ });
259
+ }
260
+
261
+ /** Revoke all object URLs and clear the entire blob cache from IDB. */
262
+ async clearAllBlobs(): Promise<void> {
263
+ for (const url of this.objectUrls.values()) URL.revokeObjectURL(url);
264
+ this.objectUrls.clear();
265
+ const db = await this.getDb();
266
+ if (!db) return;
267
+ return new Promise((resolve, reject) => {
268
+ const tx = db.transaction("blobs", "readwrite");
269
+ const req = tx.objectStore("blobs").clear();
270
+ req.onsuccess = () => resolve();
271
+ req.onerror = () => reject(req.error);
272
+ });
273
+ }
274
+
216
275
  /** Revoke the object URL and remove the blob from cache. */
217
276
  async evictBlob(docId: string, uploadId: string): Promise<void> {
218
277
  const key = this.blobKey(docId, uploadId);
@@ -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
- this.db?.close();
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
  }