dexie-cloud-addon 4.4.11 → 4.4.12

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.
@@ -1,36 +1,96 @@
1
1
  import type { DexieCloudDB } from '../db/DexieCloudDB';
2
- import { BlobRef } from './blobResolve';
2
+ import { BlobRef, ResolvedBlob } from './blobResolve';
3
3
  /**
4
- * Deduplicates in-flight blob downloads.
4
+ * Owns the full lifecycle of downloaded blobs:
5
+ * 1. Deduplicates concurrent downloads for the same ref.
6
+ * 2. Bounds the number of concurrent network fetches (MAX_CONCURRENT)
7
+ * so that ad-hoc reads can't starve the HTTP connection pool. Calls
8
+ * beyond the cap queue in FIFO order as slots free. The slot is held
9
+ * only for the duration of the fetch — NOT until persistence — to
10
+ * avoid deadlocks when a single object contains more blob refs than
11
+ * MAX_CONCURRENT (a sequential resolver would otherwise hold every
12
+ * slot itself while waiting for the next).
13
+ * 3. Keeps the in-flight promise alive after the network fetch completes,
14
+ * until the blob has been persisted back to IndexedDB. This way,
15
+ * readers that ask for the same ref while it is queued for saving
16
+ * can piggyback on the existing promise instead of refetching.
17
+ * In-flight membership and slot ownership are independent: a piggyback
18
+ * reader consumes neither a slot nor extra memory beyond the existing
19
+ * cached Uint8Array.
20
+ * 4. Persists resolved blobs via an internal BlobSavingQueue, and
21
+ * releases the in-flight entry when persistence completes.
5
22
  *
6
- * Both the blob-resolve middleware and the eager blob downloader may
7
- * try to fetch the same blob concurrently. This tracker ensures each
8
- * unique blob ref is only downloaded once — subsequent requests for
9
- * the same ref piggyback on the existing promise.
23
+ * Both the blob-resolve middleware and the eager blob downloader use this
24
+ * tracker. Instantiate once per DexieCloudDB.
25
+ */
26
+ /**
27
+ * Maximum number of concurrent blob fetches.
10
28
  *
11
- * Instantiate once per DexieCloudDB.
29
+ * Historically 6 to match the HTTP/1.1 same-origin connection cap that
30
+ * browsers enforce. With HTTP/2 (the typical transport for Dexie Cloud
31
+ * today) many streams multiplex over a single TCP connection, so the
32
+ * old cap is overly conservative. 10 is a modest bump that still keeps
33
+ * memory pressure (in-flight Uint8Arrays) and server load bounded.
34
+ * Can be made configurable via DexieCloudOptions if a real need arises.
12
35
  */
36
+ export declare const MAX_CONCURRENT = 10;
13
37
  export declare class BlobDownloadTracker {
14
38
  private inFlight;
15
39
  private db;
40
+ private savingQueue;
41
+ private activeFetches;
42
+ private waiting;
16
43
  constructor(db: DexieCloudDB);
17
44
  /**
18
- * Download a blob, deduplicating concurrent requests for the same ref.
45
+ * Download a blob, deduplicating concurrent requests for the same ref
46
+ * and respecting the global fetch concurrency cap.
47
+ *
48
+ * Lifecycle:
49
+ * - Slot is acquired before the fetch and released as soon as the
50
+ * fetch settles (success or failure).
51
+ * - The in-flight entry survives a successful fetch and lives on
52
+ * until persistence completes (via enqueueSave) or releaseRefs
53
+ * is called. On fetch failure, the entry is removed immediately
54
+ * so a future call can retry.
19
55
  *
20
56
  * @param blobRef - The BlobRef to download
21
57
  * @param dbUrl - Base URL for the database (e.g., 'https://mydb.dexie.cloud')
22
58
  */
23
59
  download(blobRef: BlobRef, dbUrl: string): Promise<Uint8Array>;
60
+ /**
61
+ * Queue resolved blobs for persisting back to IndexedDB.
62
+ * When the save transaction completes, the corresponding in-flight
63
+ * entries are released.
64
+ */
65
+ enqueueSave(tableName: string, primaryKey: any, resolvedBlobs: ResolvedBlob[]): void;
66
+ /**
67
+ * Wait until all previously enqueued saves have been persisted to
68
+ * IndexedDB. Used by callers that need to make decisions based on
69
+ * on-disk state — e.g., the eager downloader looping over rows with
70
+ * `_hasBlobRefs=1` in chunks, where each iteration must see the
71
+ * previous chunk's writes before re-querying.
72
+ *
73
+ * New saves enqueued AFTER drainPendingSaves() is called do NOT extend
74
+ * the wait.
75
+ */
76
+ drainPendingSaves(): Promise<void>;
77
+ /**
78
+ * Release in-flight entries without going through the internal saving
79
+ * queue. Used when the caller persists the blobs itself, or when no
80
+ * primary key was available and the data won't be persisted at all.
81
+ */
82
+ releaseRefs(refs: string[]): void;
83
+ private acquireSlot;
84
+ private releaseSlot;
85
+ /**
86
+ * Download blob data from server via proxy endpoint.
87
+ * Uses auth header for authentication (same as sync).
88
+ * When accessToken is null, the request is made without Authorization header —
89
+ * this allows downloading blobs from public realms (rlm-public) for
90
+ * unauthenticated users.
91
+ *
92
+ * @param blobRef - The BlobRef to download
93
+ * @param dbUrl - Base URL for the database (e.g., 'https://mydb.dexie.cloud')
94
+ */
95
+ private downloadBlob;
24
96
  }
25
- /**
26
- * Download blob data from server via proxy endpoint.
27
- * Uses auth header for authentication (same as sync).
28
- * When accessToken is null, the request is made without Authorization header —
29
- * this allows downloading blobs from public realms (rlm-public) for
30
- * unauthenticated users.
31
- *
32
- * @param blobRef - The BlobRef to download
33
- * @param dbUrl - Base URL for the database (e.g., 'https://mydb.dexie.cloud')
34
- * @param accessToken - Access token for authentication, or null for anonymous access
35
- */
36
- export declare function downloadBlob(blobRef: BlobRef, dbUrl: string, accessToken: string | null): Promise<Uint8Array>;
@@ -1,5 +1,9 @@
1
1
  /**
2
- * BlobSavingQueue - Queues resolved blobs for saving back to IndexedDB
2
+ * BlobSavingQueue - Queues resolved blobs for saving back to IndexedDB.
3
+ *
4
+ * This is an internal collaborator of BlobDownloadTracker and is not
5
+ * intended to be used directly by middleware or other code. See
6
+ * BlobDownloadTracker.enqueueSave().
3
7
  *
4
8
  * Uses setTimeout(fn, 0) instead of queueMicrotask to completely isolate
5
9
  * from Dexie's Promise.PSD context. This prevents the save operation
@@ -14,12 +18,26 @@ export declare class BlobSavingQueue {
14
18
  private queue;
15
19
  private isProcessing;
16
20
  private db;
17
- constructor(db: DexieCloudDB);
21
+ private onPersisted;
22
+ private drainResolvers;
23
+ constructor(db: DexieCloudDB, onPersisted: (refs: string[]) => void);
18
24
  /**
19
25
  * Queue a resolved blob for saving.
20
26
  * Only the specific blob property will be updated atomically.
21
27
  */
22
28
  saveBlobs(tableName: string, primaryKey: any, resolvedBlobs: ResolvedBlob[]): void;
29
+ /**
30
+ * Returns a promise that resolves when the queue is empty AND no item
31
+ * is currently being processed. Used by callers that need to know when
32
+ * all previously enqueued saves have been persisted to IndexedDB before
33
+ * making decisions based on the on-disk state (e.g., the eager blob
34
+ * downloader looping over `_hasBlobRefs=1` rows in chunks).
35
+ *
36
+ * Note: New work enqueued AFTER drain() is called does NOT extend the
37
+ * wait. Callers that race against concurrent producers should treat the
38
+ * returned promise as "queue was empty at some point after this call".
39
+ */
40
+ drain(): Promise<void>;
23
41
  /**
24
42
  * Start the consumer if not already processing.
25
43
  * Uses setTimeout(fn, 0) to completely break out of any
@@ -4,8 +4,45 @@
4
4
  * Downloads unresolved blobs in the background when blobMode='eager'.
5
5
  * Called after sync completes to prefetch blobs for offline access.
6
6
  *
7
+ * Strategy:
8
+ * 1. Snapshot the primary keys of all rows currently flagged
9
+ * `_hasBlobRefs=1` for each syncable table.
10
+ * 2. Walk that key list in chunks via `bulkGet`. Each `bulkGet`
11
+ * triggers the blob-resolve middleware, which does all the actual
12
+ * work — downloading blobs (throttled and deduplicated by the
13
+ * shared BlobDownloadTracker) and enqueueing them for persistence
14
+ * via the internal save queue.
15
+ *
16
+ * This keeps a single, symmetric code path with normal application
17
+ * reads, which is important when other middlewares are present
18
+ * (e.g., a hypothetical encryption middleware): writes from the save
19
+ * queue and reads from this loop both pass through the full middleware
20
+ * stack, so on-disk representation stays consistent.
21
+ *
22
+ * Why a snapshot of primary keys (rather than re-querying the index)?
23
+ * - Rows that get resolved by parallel application reads simply
24
+ * disappear from the table contents we're about to re-fetch; the
25
+ * middleware skips them since `_hasBlobRefs` is already cleared.
26
+ * - Stuck rows (e.g., blob 404s) are naturally bypassed: we just
27
+ * advance to the next chunk in the snapshot. No `seenKeys`
28
+ * bookkeeping required.
29
+ * - The snapshot is `string[]`-shaped for typical Dexie Cloud rows
30
+ * (~36 bytes/UUID), so ~28K keys per MB. Acceptable for any
31
+ * realistic dataset.
32
+ *
7
33
  * Progress is tracked automatically via liveQuery in blobProgress.ts —
8
34
  * no manual progress reporting needed here.
35
+ *
36
+ * --- Throughput note ---
37
+ * The chunk loop is sequential: bulkGet → wait for all downloads to
38
+ * settle → next bulkGet. The save queue drains in the background and
39
+ * does not block iteration (saves no longer need to be persisted before
40
+ * the next iteration, since we don't re-query the index). For typical
41
+ * blob sizes (10 KB – 10 MB) the network dominates total time. If
42
+ * real-world profiling later shows the per-chunk fixed cost matters,
43
+ * the next bulkGet could be kicked off in parallel with the current
44
+ * one's middleware work — but we keep it simple until measurements
45
+ * justify otherwise.
9
46
  */
10
47
  import { BehaviorSubject } from 'rxjs';
11
48
  import { DexieCloudDB } from '../db/DexieCloudDB';
@@ -13,8 +50,5 @@ import { DexieCloudDB } from '../db/DexieCloudDB';
13
50
  * Download all unresolved blobs in the background.
14
51
  *
15
52
  * This is called when blobMode='eager' (default) after sync completes.
16
- * BlobRef URLs are signed (SAS tokens) so no auth header needed.
17
- *
18
- * Each blob is saved atomically using Table.update() to avoid race conditions.
19
53
  */
20
54
  export declare function downloadUnresolvedBlobs(db: DexieCloudDB, downloading$: BehaviorSubject<boolean>, signal?: AbortSignal): Promise<void>;
@@ -1,3 +1,4 @@
1
+ import { ObservabilitySet } from 'dexie';
1
2
  import { DexieCloudSchema } from 'dexie-cloud-common';
2
3
  import { UserLogin } from '../db/entities/UserLogin';
3
4
  export interface TXExpandos {
@@ -8,5 +9,6 @@ export interface TXExpandos {
8
9
  disableAccessControl?: boolean;
9
10
  disableBlobResolve?: boolean;
10
11
  mutationsAdded?: boolean;
12
+ mutatedParts?: ObservabilitySet;
11
13
  opCount: number;
12
14
  }