@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/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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abraca/dabra",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
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);
@@ -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);
@@ -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
  }