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.
- package/dist/modern/dexie-cloud-addon.js +599 -449
- package/dist/modern/dexie-cloud-addon.js.map +1 -1
- package/dist/modern/dexie-cloud-addon.min.js +1 -1
- package/dist/modern/dexie-cloud-addon.min.js.map +1 -1
- package/dist/modern/middlewares/blobResolveMiddleware.d.ts +5 -4
- package/dist/modern/service-worker.js +405 -255
- package/dist/modern/service-worker.js.map +1 -1
- package/dist/modern/service-worker.min.js +1 -1
- package/dist/modern/service-worker.min.js.map +1 -1
- package/dist/modern/sync/BlobDownloadTracker.d.ts +80 -20
- package/dist/modern/sync/BlobSavingQueue.d.ts +20 -2
- package/dist/modern/sync/eagerBlobDownloader.d.ts +37 -3
- package/dist/modern/types/TXExpandos.d.ts +2 -0
- package/dist/umd/dexie-cloud-addon.js +606 -456
- package/dist/umd/dexie-cloud-addon.js.map +1 -1
- package/dist/umd/dexie-cloud-addon.min.js +1 -1
- package/dist/umd/dexie-cloud-addon.min.js.map +1 -1
- package/dist/umd/service-worker.js +407 -257
- package/dist/umd/service-worker.js.map +1 -1
- package/dist/umd/service-worker.min.js +1 -1
- package/dist/umd/service-worker.min.js.map +1 -1
- package/package.json +1 -1
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* ==========================================================================
|
|
10
10
|
*
|
|
11
|
-
* Version 4.4.
|
|
11
|
+
* Version 4.4.12, Mon May 25 2026
|
|
12
12
|
*
|
|
13
13
|
* https://dexie.org
|
|
14
14
|
*
|
|
@@ -4263,22 +4263,208 @@ function MessagesFromServerConsumer(db) {
|
|
|
4263
4263
|
}
|
|
4264
4264
|
|
|
4265
4265
|
/**
|
|
4266
|
-
*
|
|
4266
|
+
* BlobSavingQueue - Queues resolved blobs for saving back to IndexedDB.
|
|
4267
4267
|
*
|
|
4268
|
-
*
|
|
4269
|
-
*
|
|
4270
|
-
*
|
|
4271
|
-
* the same ref piggyback on the existing promise.
|
|
4268
|
+
* This is an internal collaborator of BlobDownloadTracker and is not
|
|
4269
|
+
* intended to be used directly by middleware or other code. See
|
|
4270
|
+
* BlobDownloadTracker.enqueueSave().
|
|
4272
4271
|
*
|
|
4273
|
-
*
|
|
4272
|
+
* Uses setTimeout(fn, 0) instead of queueMicrotask to completely isolate
|
|
4273
|
+
* from Dexie's Promise.PSD context. This prevents the save operation
|
|
4274
|
+
* from inheriting any ongoing transaction.
|
|
4275
|
+
*
|
|
4276
|
+
* Each blob is saved atomically using downCore transaction with the specific
|
|
4277
|
+
* keyPath to avoid race conditions with other property changes.
|
|
4278
|
+
*/
|
|
4279
|
+
class BlobSavingQueue {
|
|
4280
|
+
constructor(db, onPersisted) {
|
|
4281
|
+
this.queue = [];
|
|
4282
|
+
this.isProcessing = false;
|
|
4283
|
+
this.drainResolvers = [];
|
|
4284
|
+
this.db = db;
|
|
4285
|
+
this.onPersisted = onPersisted;
|
|
4286
|
+
}
|
|
4287
|
+
/**
|
|
4288
|
+
* Queue a resolved blob for saving.
|
|
4289
|
+
* Only the specific blob property will be updated atomically.
|
|
4290
|
+
*/
|
|
4291
|
+
saveBlobs(tableName, primaryKey, resolvedBlobs) {
|
|
4292
|
+
this.queue.push({
|
|
4293
|
+
tableName,
|
|
4294
|
+
primaryKey,
|
|
4295
|
+
resolvedBlobs,
|
|
4296
|
+
});
|
|
4297
|
+
this.startConsumer();
|
|
4298
|
+
}
|
|
4299
|
+
/**
|
|
4300
|
+
* Returns a promise that resolves when the queue is empty AND no item
|
|
4301
|
+
* is currently being processed. Used by callers that need to know when
|
|
4302
|
+
* all previously enqueued saves have been persisted to IndexedDB before
|
|
4303
|
+
* making decisions based on the on-disk state (e.g., the eager blob
|
|
4304
|
+
* downloader looping over `_hasBlobRefs=1` rows in chunks).
|
|
4305
|
+
*
|
|
4306
|
+
* Note: New work enqueued AFTER drain() is called does NOT extend the
|
|
4307
|
+
* wait. Callers that race against concurrent producers should treat the
|
|
4308
|
+
* returned promise as "queue was empty at some point after this call".
|
|
4309
|
+
*/
|
|
4310
|
+
drain() {
|
|
4311
|
+
if (!this.isProcessing && this.queue.length === 0) {
|
|
4312
|
+
return Promise.resolve();
|
|
4313
|
+
}
|
|
4314
|
+
return new Promise((resolve) => {
|
|
4315
|
+
this.drainResolvers.push(resolve);
|
|
4316
|
+
});
|
|
4317
|
+
}
|
|
4318
|
+
/**
|
|
4319
|
+
* Start the consumer if not already processing.
|
|
4320
|
+
* Uses setTimeout(fn, 0) to completely break out of any
|
|
4321
|
+
* Dexie transaction context (Promise.PSD).
|
|
4322
|
+
*/
|
|
4323
|
+
startConsumer() {
|
|
4324
|
+
if (this.isProcessing)
|
|
4325
|
+
return;
|
|
4326
|
+
this.isProcessing = true;
|
|
4327
|
+
// Use setTimeout to completely isolate from Dexie's PSD context
|
|
4328
|
+
// queueMicrotask would risk inheriting the current transaction
|
|
4329
|
+
setTimeout(() => {
|
|
4330
|
+
this.processQueue();
|
|
4331
|
+
}, 0);
|
|
4332
|
+
}
|
|
4333
|
+
/**
|
|
4334
|
+
* Process all queued blobs.
|
|
4335
|
+
* Runs in a completely isolated context (no inherited transaction).
|
|
4336
|
+
* Uses atomic updates to avoid race conditions.
|
|
4337
|
+
*/
|
|
4338
|
+
processQueue() {
|
|
4339
|
+
const item = this.queue.shift();
|
|
4340
|
+
if (!item) {
|
|
4341
|
+
this.isProcessing = false;
|
|
4342
|
+
// Fire any pending drain() waiters. New saveBlobs() calls that
|
|
4343
|
+
// arrive after this point will start a fresh processing cycle
|
|
4344
|
+
// and have their own drain() semantics.
|
|
4345
|
+
const resolvers = this.drainResolvers;
|
|
4346
|
+
if (resolvers.length > 0) {
|
|
4347
|
+
this.drainResolvers = [];
|
|
4348
|
+
for (const resolve of resolvers)
|
|
4349
|
+
resolve();
|
|
4350
|
+
}
|
|
4351
|
+
return;
|
|
4352
|
+
}
|
|
4353
|
+
// Atomic update of just the blob property
|
|
4354
|
+
this.db
|
|
4355
|
+
.transaction('rw', item.tableName, (tx) => {
|
|
4356
|
+
const trans = tx.idbtrans;
|
|
4357
|
+
trans.disableChangeTracking = true; // Don't regard this as a change for sync purposes
|
|
4358
|
+
trans.disableAccessControl = true; // Bypass any access control checks since this is an internal operation
|
|
4359
|
+
trans.disableBlobResolve = true; // Custom flag to skip blob resolve middleware during this transaction
|
|
4360
|
+
const updateSpec = {};
|
|
4361
|
+
for (const blob of item.resolvedBlobs) {
|
|
4362
|
+
updateSpec[blob.keyPath] = blob.data;
|
|
4363
|
+
}
|
|
4364
|
+
tx.table(item.tableName).update(item.primaryKey, (obj) => {
|
|
4365
|
+
// Check that object still has the same unresolved blob refs before applying update (i.e. it hasn't been modified since we read it)
|
|
4366
|
+
for (const blob of item.resolvedBlobs) {
|
|
4367
|
+
// Verify atomicity - none of the blob properties has been modified since we read it. If any of them was modified, skip updating this item to avoid overwriting user changes.
|
|
4368
|
+
const currentValue = Dexie.getByKeyPath(obj, blob.keyPath);
|
|
4369
|
+
if (currentValue === undefined) {
|
|
4370
|
+
// Blob property was removed - skip updating this blob
|
|
4371
|
+
continue;
|
|
4372
|
+
}
|
|
4373
|
+
if (!isBlobRef(currentValue)) {
|
|
4374
|
+
// Blob property was modified to a non-blob-ref value - skip updating this blob
|
|
4375
|
+
continue;
|
|
4376
|
+
}
|
|
4377
|
+
if (currentValue.ref !== blob.ref) {
|
|
4378
|
+
// Blob property was modified - skip updating this blob
|
|
4379
|
+
return; // Stop. Another items has been queued to fully fix the object.
|
|
4380
|
+
}
|
|
4381
|
+
Dexie.setByKeyPath(obj, blob.keyPath, blob.data);
|
|
4382
|
+
}
|
|
4383
|
+
delete obj._hasBlobRefs; // Clear the _hasBlobRefs marker if all refs was resolved.
|
|
4384
|
+
});
|
|
4385
|
+
// Note: we intentionally do NOT clear trans.mutatedParts here.
|
|
4386
|
+
// Letting the normal mutation signal through means the
|
|
4387
|
+
// blobProgress liveQuery (and any user-defined liveQuery that
|
|
4388
|
+
// depends on the resolved fields) wakes up and reflects progress
|
|
4389
|
+
// as blobs land in IndexedDB.
|
|
4390
|
+
})
|
|
4391
|
+
.catch((error) => {
|
|
4392
|
+
console.error(`Error saving resolved blobs on ${item.tableName}:${item.primaryKey}:`, error);
|
|
4393
|
+
})
|
|
4394
|
+
.finally(() => {
|
|
4395
|
+
// At this point, the transaction has completed (either successfully or with error),
|
|
4396
|
+
// and the blobs have been saved (or failed to save).
|
|
4397
|
+
// Notify the owner (BlobDownloadTracker) so it can release the
|
|
4398
|
+
// in-flight download cache entries for these refs. The cache was
|
|
4399
|
+
// kept alive until now to maximize reuse while the blob was still
|
|
4400
|
+
// in-flight (downloading or queued for save).
|
|
4401
|
+
this.onPersisted(item.resolvedBlobs.map((b) => b.ref));
|
|
4402
|
+
// Process next item in the queue
|
|
4403
|
+
return this.processQueue();
|
|
4404
|
+
});
|
|
4405
|
+
}
|
|
4406
|
+
}
|
|
4407
|
+
|
|
4408
|
+
/**
|
|
4409
|
+
* Owns the full lifecycle of downloaded blobs:
|
|
4410
|
+
* 1. Deduplicates concurrent downloads for the same ref.
|
|
4411
|
+
* 2. Bounds the number of concurrent network fetches (MAX_CONCURRENT)
|
|
4412
|
+
* so that ad-hoc reads can't starve the HTTP connection pool. Calls
|
|
4413
|
+
* beyond the cap queue in FIFO order as slots free. The slot is held
|
|
4414
|
+
* only for the duration of the fetch — NOT until persistence — to
|
|
4415
|
+
* avoid deadlocks when a single object contains more blob refs than
|
|
4416
|
+
* MAX_CONCURRENT (a sequential resolver would otherwise hold every
|
|
4417
|
+
* slot itself while waiting for the next).
|
|
4418
|
+
* 3. Keeps the in-flight promise alive after the network fetch completes,
|
|
4419
|
+
* until the blob has been persisted back to IndexedDB. This way,
|
|
4420
|
+
* readers that ask for the same ref while it is queued for saving
|
|
4421
|
+
* can piggyback on the existing promise instead of refetching.
|
|
4422
|
+
* In-flight membership and slot ownership are independent: a piggyback
|
|
4423
|
+
* reader consumes neither a slot nor extra memory beyond the existing
|
|
4424
|
+
* cached Uint8Array.
|
|
4425
|
+
* 4. Persists resolved blobs via an internal BlobSavingQueue, and
|
|
4426
|
+
* releases the in-flight entry when persistence completes.
|
|
4427
|
+
*
|
|
4428
|
+
* Both the blob-resolve middleware and the eager blob downloader use this
|
|
4429
|
+
* tracker. Instantiate once per DexieCloudDB.
|
|
4430
|
+
*/
|
|
4431
|
+
/**
|
|
4432
|
+
* Maximum number of concurrent blob fetches.
|
|
4433
|
+
*
|
|
4434
|
+
* Historically 6 to match the HTTP/1.1 same-origin connection cap that
|
|
4435
|
+
* browsers enforce. With HTTP/2 (the typical transport for Dexie Cloud
|
|
4436
|
+
* today) many streams multiplex over a single TCP connection, so the
|
|
4437
|
+
* old cap is overly conservative. 10 is a modest bump that still keeps
|
|
4438
|
+
* memory pressure (in-flight Uint8Arrays) and server load bounded.
|
|
4439
|
+
* Can be made configurable via DexieCloudOptions if a real need arises.
|
|
4274
4440
|
*/
|
|
4441
|
+
const MAX_CONCURRENT = 10;
|
|
4275
4442
|
class BlobDownloadTracker {
|
|
4276
4443
|
constructor(db) {
|
|
4277
4444
|
this.inFlight = new Map();
|
|
4445
|
+
this.activeFetches = 0;
|
|
4446
|
+
this.waiting = [];
|
|
4278
4447
|
this.db = db;
|
|
4448
|
+
this.savingQueue = new BlobSavingQueue(db, (refs) => {
|
|
4449
|
+
// Called by the queue when a save transaction has completed
|
|
4450
|
+
// (regardless of success). Drop the in-flight cache entries now —
|
|
4451
|
+
// any future reader will go through IndexedDB instead.
|
|
4452
|
+
for (const ref of refs) {
|
|
4453
|
+
this.inFlight.delete(ref);
|
|
4454
|
+
}
|
|
4455
|
+
});
|
|
4279
4456
|
}
|
|
4280
4457
|
/**
|
|
4281
|
-
* Download a blob, deduplicating concurrent requests for the same ref
|
|
4458
|
+
* Download a blob, deduplicating concurrent requests for the same ref
|
|
4459
|
+
* and respecting the global fetch concurrency cap.
|
|
4460
|
+
*
|
|
4461
|
+
* Lifecycle:
|
|
4462
|
+
* - Slot is acquired before the fetch and released as soon as the
|
|
4463
|
+
* fetch settles (success or failure).
|
|
4464
|
+
* - The in-flight entry survives a successful fetch and lives on
|
|
4465
|
+
* until persistence completes (via enqueueSave) or releaseRefs
|
|
4466
|
+
* is called. On fetch failure, the entry is removed immediately
|
|
4467
|
+
* so a future call can retry.
|
|
4282
4468
|
*
|
|
4283
4469
|
* @param blobRef - The BlobRef to download
|
|
4284
4470
|
* @param dbUrl - Base URL for the database (e.g., 'https://mydb.dexie.cloud')
|
|
@@ -4286,45 +4472,103 @@ class BlobDownloadTracker {
|
|
|
4286
4472
|
download(blobRef, dbUrl) {
|
|
4287
4473
|
let promise = this.inFlight.get(blobRef.ref);
|
|
4288
4474
|
if (!promise) {
|
|
4289
|
-
promise =
|
|
4290
|
-
.then((
|
|
4291
|
-
|
|
4292
|
-
//
|
|
4293
|
-
//
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
// When the promise settles (either fulfilled or rejected), remove it from the in-flight map
|
|
4475
|
+
promise = this.acquireSlot()
|
|
4476
|
+
.then(() => this.downloadBlob(blobRef, dbUrl).finally(() => this.releaseSlot()))
|
|
4477
|
+
.catch((err) => {
|
|
4478
|
+
// On error, remove immediately so a future call can retry.
|
|
4479
|
+
// (Slot already released by the .finally above.)
|
|
4480
|
+
this.inFlight.delete(blobRef.ref);
|
|
4481
|
+
throw err;
|
|
4482
|
+
});
|
|
4298
4483
|
this.inFlight.set(blobRef.ref, promise);
|
|
4299
4484
|
}
|
|
4300
4485
|
return promise;
|
|
4301
4486
|
}
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4487
|
+
/**
|
|
4488
|
+
* Queue resolved blobs for persisting back to IndexedDB.
|
|
4489
|
+
* When the save transaction completes, the corresponding in-flight
|
|
4490
|
+
* entries are released.
|
|
4491
|
+
*/
|
|
4492
|
+
enqueueSave(tableName, primaryKey, resolvedBlobs) {
|
|
4493
|
+
this.savingQueue.saveBlobs(tableName, primaryKey, resolvedBlobs);
|
|
4494
|
+
}
|
|
4495
|
+
/**
|
|
4496
|
+
* Wait until all previously enqueued saves have been persisted to
|
|
4497
|
+
* IndexedDB. Used by callers that need to make decisions based on
|
|
4498
|
+
* on-disk state — e.g., the eager downloader looping over rows with
|
|
4499
|
+
* `_hasBlobRefs=1` in chunks, where each iteration must see the
|
|
4500
|
+
* previous chunk's writes before re-querying.
|
|
4501
|
+
*
|
|
4502
|
+
* New saves enqueued AFTER drainPendingSaves() is called do NOT extend
|
|
4503
|
+
* the wait.
|
|
4504
|
+
*/
|
|
4505
|
+
drainPendingSaves() {
|
|
4506
|
+
return this.savingQueue.drain();
|
|
4507
|
+
}
|
|
4508
|
+
/**
|
|
4509
|
+
* Release in-flight entries without going through the internal saving
|
|
4510
|
+
* queue. Used when the caller persists the blobs itself, or when no
|
|
4511
|
+
* primary key was available and the data won't be persisted at all.
|
|
4512
|
+
*/
|
|
4513
|
+
releaseRefs(refs) {
|
|
4514
|
+
for (const ref of refs) {
|
|
4515
|
+
this.inFlight.delete(ref);
|
|
4320
4516
|
}
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4517
|
+
}
|
|
4518
|
+
acquireSlot() {
|
|
4519
|
+
if (this.activeFetches < MAX_CONCURRENT) {
|
|
4520
|
+
this.activeFetches++;
|
|
4521
|
+
return Promise.resolve();
|
|
4324
4522
|
}
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4523
|
+
return new Promise((resolve) => {
|
|
4524
|
+
this.waiting.push(() => {
|
|
4525
|
+
this.activeFetches++;
|
|
4526
|
+
resolve();
|
|
4527
|
+
});
|
|
4528
|
+
});
|
|
4529
|
+
}
|
|
4530
|
+
releaseSlot() {
|
|
4531
|
+
this.activeFetches--;
|
|
4532
|
+
const next = this.waiting.shift();
|
|
4533
|
+
if (next)
|
|
4534
|
+
next();
|
|
4535
|
+
}
|
|
4536
|
+
/**
|
|
4537
|
+
* Download blob data from server via proxy endpoint.
|
|
4538
|
+
* Uses auth header for authentication (same as sync).
|
|
4539
|
+
* When accessToken is null, the request is made without Authorization header —
|
|
4540
|
+
* this allows downloading blobs from public realms (rlm-public) for
|
|
4541
|
+
* unauthenticated users.
|
|
4542
|
+
*
|
|
4543
|
+
* @param blobRef - The BlobRef to download
|
|
4544
|
+
* @param dbUrl - Base URL for the database (e.g., 'https://mydb.dexie.cloud')
|
|
4545
|
+
*/
|
|
4546
|
+
downloadBlob(blobRef, dbUrl) {
|
|
4547
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
4548
|
+
const accessToken = yield loadCachedAccessToken(this.db);
|
|
4549
|
+
const downloadUrl = `${dbUrl}/blob/${blobRef.ref}`;
|
|
4550
|
+
const headers = {};
|
|
4551
|
+
if (accessToken) {
|
|
4552
|
+
// accessToken may be null for anonymous/unauthenticated users.
|
|
4553
|
+
// Public realm blobs (rlm-public) are accessible without auth.
|
|
4554
|
+
// downloadBlob will omit the Authorization header when token is null.
|
|
4555
|
+
headers['Authorization'] = `Bearer ${accessToken}`;
|
|
4556
|
+
}
|
|
4557
|
+
// cache: 'no-store' prevents the browser from storing this response in its
|
|
4558
|
+
// HTTP cache. The server sets a long Expires/Cache-Control header on blob
|
|
4559
|
+
// responses (blobs are immutable and content-addressed), which would
|
|
4560
|
+
// otherwise cause the browser to keep a copy in its disk cache in addition
|
|
4561
|
+
// to the copy we persist to IndexedDB — doubling storage for every blob.
|
|
4562
|
+
// Since we always persist to IndexedDB and subsequent reads go through
|
|
4563
|
+
// IndexedDB (never re-fetch), the browser cache copy is pure overhead.
|
|
4564
|
+
const response = yield fetch(downloadUrl, { headers, cache: 'no-store' });
|
|
4565
|
+
if (!response.ok) {
|
|
4566
|
+
throw new Error(`Failed to download blob ${blobRef.ref}: ${response.status} ${response.statusText}`);
|
|
4567
|
+
}
|
|
4568
|
+
const arrayBuffer = yield response.arrayBuffer();
|
|
4569
|
+
return new Uint8Array(arrayBuffer);
|
|
4570
|
+
});
|
|
4571
|
+
}
|
|
4328
4572
|
}
|
|
4329
4573
|
|
|
4330
4574
|
const wm$2 = new WeakMap();
|
|
@@ -4545,118 +4789,116 @@ function findBlobRefs(obj) {
|
|
|
4545
4789
|
* Downloads unresolved blobs in the background when blobMode='eager'.
|
|
4546
4790
|
* Called after sync completes to prefetch blobs for offline access.
|
|
4547
4791
|
*
|
|
4792
|
+
* Strategy:
|
|
4793
|
+
* 1. Snapshot the primary keys of all rows currently flagged
|
|
4794
|
+
* `_hasBlobRefs=1` for each syncable table.
|
|
4795
|
+
* 2. Walk that key list in chunks via `bulkGet`. Each `bulkGet`
|
|
4796
|
+
* triggers the blob-resolve middleware, which does all the actual
|
|
4797
|
+
* work — downloading blobs (throttled and deduplicated by the
|
|
4798
|
+
* shared BlobDownloadTracker) and enqueueing them for persistence
|
|
4799
|
+
* via the internal save queue.
|
|
4800
|
+
*
|
|
4801
|
+
* This keeps a single, symmetric code path with normal application
|
|
4802
|
+
* reads, which is important when other middlewares are present
|
|
4803
|
+
* (e.g., a hypothetical encryption middleware): writes from the save
|
|
4804
|
+
* queue and reads from this loop both pass through the full middleware
|
|
4805
|
+
* stack, so on-disk representation stays consistent.
|
|
4806
|
+
*
|
|
4807
|
+
* Why a snapshot of primary keys (rather than re-querying the index)?
|
|
4808
|
+
* - Rows that get resolved by parallel application reads simply
|
|
4809
|
+
* disappear from the table contents we're about to re-fetch; the
|
|
4810
|
+
* middleware skips them since `_hasBlobRefs` is already cleared.
|
|
4811
|
+
* - Stuck rows (e.g., blob 404s) are naturally bypassed: we just
|
|
4812
|
+
* advance to the next chunk in the snapshot. No `seenKeys`
|
|
4813
|
+
* bookkeeping required.
|
|
4814
|
+
* - The snapshot is `string[]`-shaped for typical Dexie Cloud rows
|
|
4815
|
+
* (~36 bytes/UUID), so ~28K keys per MB. Acceptable for any
|
|
4816
|
+
* realistic dataset.
|
|
4817
|
+
*
|
|
4548
4818
|
* Progress is tracked automatically via liveQuery in blobProgress.ts —
|
|
4549
4819
|
* no manual progress reporting needed here.
|
|
4820
|
+
*
|
|
4821
|
+
* --- Throughput note ---
|
|
4822
|
+
* The chunk loop is sequential: bulkGet → wait for all downloads to
|
|
4823
|
+
* settle → next bulkGet. The save queue drains in the background and
|
|
4824
|
+
* does not block iteration (saves no longer need to be persisted before
|
|
4825
|
+
* the next iteration, since we don't re-query the index). For typical
|
|
4826
|
+
* blob sizes (10 KB – 10 MB) the network dominates total time. If
|
|
4827
|
+
* real-world profiling later shows the per-chunk fixed cost matters,
|
|
4828
|
+
* the next bulkGet could be kicked off in parallel with the current
|
|
4829
|
+
* one's middleware work — but we keep it simple until measurements
|
|
4830
|
+
* justify otherwise.
|
|
4550
4831
|
*/
|
|
4832
|
+
// One chunk = one full saturation of the tracker's concurrency semaphore.
|
|
4833
|
+
// Larger chunks would only buffer more downloaded Uint8Arrays in memory
|
|
4834
|
+
// while waiting for the save queue to persist them, without any throughput
|
|
4835
|
+
// benefit (the semaphore is the gate, not the bulkGet).
|
|
4836
|
+
const CHUNK_SIZE = MAX_CONCURRENT - 1; // Leave one slot for parallel app reads that might also trigger downloads
|
|
4551
4837
|
/**
|
|
4552
4838
|
* Download all unresolved blobs in the background.
|
|
4553
4839
|
*
|
|
4554
4840
|
* This is called when blobMode='eager' (default) after sync completes.
|
|
4555
|
-
* BlobRef URLs are signed (SAS tokens) so no auth header needed.
|
|
4556
|
-
*
|
|
4557
|
-
* Each blob is saved atomically using Table.update() to avoid race conditions.
|
|
4558
4841
|
*/
|
|
4559
4842
|
function downloadUnresolvedBlobs(db, downloading$, signal) {
|
|
4560
4843
|
return __awaiter(this, void 0, void 0, function* () {
|
|
4561
|
-
var _a;
|
|
4562
4844
|
const debugLog = (msg) => console.debug(`[dexie-cloud] ${msg}`);
|
|
4563
4845
|
debugLog('Eager download: Starting...');
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
let
|
|
4567
|
-
for (const table of syncedTables) {
|
|
4568
|
-
try {
|
|
4569
|
-
const hasIndex = !!table.schema.idxByName['_hasBlobRefs'];
|
|
4570
|
-
if (!hasIndex)
|
|
4571
|
-
continue;
|
|
4572
|
-
const count = yield table.where('_hasBlobRefs').equals(1).count();
|
|
4573
|
-
if (count > 0) {
|
|
4574
|
-
hasWork = true;
|
|
4575
|
-
break;
|
|
4576
|
-
}
|
|
4577
|
-
}
|
|
4578
|
-
catch (_b) {
|
|
4579
|
-
// skip
|
|
4580
|
-
}
|
|
4581
|
-
}
|
|
4582
|
-
if (!hasWork) {
|
|
4583
|
-
debugLog('Eager download: No blobs remaining, exiting');
|
|
4584
|
-
return;
|
|
4585
|
-
}
|
|
4586
|
-
setDownloadingState(downloading$, true);
|
|
4846
|
+
const syncedTables = getSyncableTables(db).filter((t) => t.schema.indexes.some((idx) => idx.name === '_hasBlobRefs'));
|
|
4847
|
+
let started = false;
|
|
4848
|
+
let totalProcessed = 0;
|
|
4587
4849
|
try {
|
|
4588
|
-
debugLog(`Eager download: Found ${syncedTables.length} syncable tables: ${syncedTables.map((t) => t.name).join(', ')}`);
|
|
4589
4850
|
for (const table of syncedTables) {
|
|
4590
4851
|
if (signal === null || signal === void 0 ? void 0 : signal.aborted)
|
|
4591
4852
|
;
|
|
4853
|
+
let keys;
|
|
4592
4854
|
try {
|
|
4593
|
-
|
|
4594
|
-
const hasIndex = table.schema.indexes.some((idx) => idx.name === '_hasBlobRefs');
|
|
4595
|
-
if (!hasIndex)
|
|
4596
|
-
continue;
|
|
4597
|
-
// Query objects with _hasBlobRefs marker
|
|
4598
|
-
const unresolvedObjects = yield table
|
|
4599
|
-
.where('_hasBlobRefs')
|
|
4600
|
-
.equals(1)
|
|
4601
|
-
.toArray();
|
|
4602
|
-
debugLog(`Eager download: Table ${table.name} has ${unresolvedObjects.length} unresolved objects`);
|
|
4603
|
-
const databaseUrl = (_a = db.cloud.options) === null || _a === void 0 ? void 0 : _a.databaseUrl;
|
|
4604
|
-
if (!databaseUrl)
|
|
4605
|
-
throw new Error('Database URL is required to download blobs');
|
|
4606
|
-
// Download up to MAX_CONCURRENT blobs in parallel
|
|
4607
|
-
const MAX_CONCURRENT = 6;
|
|
4608
|
-
const primaryKey = table.schema.primKey;
|
|
4609
|
-
// Filter to actionable objects first
|
|
4610
|
-
const pending = unresolvedObjects.filter((obj) => {
|
|
4611
|
-
if (!hasUnresolvedBlobRefs(obj))
|
|
4612
|
-
return false;
|
|
4613
|
-
const key = primaryKey.keyPath
|
|
4614
|
-
? Dexie.getByKeyPath(obj, primaryKey.keyPath)
|
|
4615
|
-
: undefined;
|
|
4616
|
-
return key !== undefined;
|
|
4617
|
-
});
|
|
4618
|
-
// Process in parallel with concurrency limit
|
|
4619
|
-
let i = 0;
|
|
4620
|
-
const runNext = () => __awaiter(this, void 0, void 0, function* () {
|
|
4621
|
-
while (i < pending.length) {
|
|
4622
|
-
if (signal === null || signal === void 0 ? void 0 : signal.aborted)
|
|
4623
|
-
;
|
|
4624
|
-
const obj = pending[i++];
|
|
4625
|
-
const key = Dexie.getByKeyPath(obj, primaryKey.keyPath);
|
|
4626
|
-
try {
|
|
4627
|
-
// Refresh token per object — cheap (returns cached) but ensures
|
|
4628
|
-
// we pick up renewed tokens during long download sessions.
|
|
4629
|
-
const resolvedBlobs = [];
|
|
4630
|
-
yield resolveAllBlobRefs(obj, databaseUrl, resolvedBlobs, '', new WeakMap(), db.blobDownloadTracker);
|
|
4631
|
-
const updateSpec = {
|
|
4632
|
-
_hasBlobRefs: undefined,
|
|
4633
|
-
};
|
|
4634
|
-
for (const blob of resolvedBlobs) {
|
|
4635
|
-
updateSpec[blob.keyPath] = blob.data;
|
|
4636
|
-
}
|
|
4637
|
-
debugLog(`Eager download: Updating ${table.name}:${key} with ${resolvedBlobs.length} blobs`);
|
|
4638
|
-
yield table.update(key, updateSpec);
|
|
4639
|
-
// liveQuery in blobProgress.ts auto-detects this change
|
|
4640
|
-
}
|
|
4641
|
-
catch (err) {
|
|
4642
|
-
console.error(`Failed to download blobs for ${table.name}:${key}:`, err);
|
|
4643
|
-
}
|
|
4644
|
-
}
|
|
4645
|
-
});
|
|
4646
|
-
// Launch up to MAX_CONCURRENT workers
|
|
4647
|
-
const workers = [];
|
|
4648
|
-
for (let w = 0; w < Math.min(MAX_CONCURRENT, pending.length); w++) {
|
|
4649
|
-
workers.push(runNext());
|
|
4650
|
-
}
|
|
4651
|
-
yield Promise.all(workers);
|
|
4855
|
+
keys = yield table.where('_hasBlobRefs').equals(1).primaryKeys();
|
|
4652
4856
|
}
|
|
4653
4857
|
catch (err) {
|
|
4654
|
-
|
|
4858
|
+
console.error(`Eager download: failed to list unresolved rows for ${table.name}:`, err);
|
|
4859
|
+
continue;
|
|
4860
|
+
}
|
|
4861
|
+
if (keys.length === 0)
|
|
4862
|
+
continue;
|
|
4863
|
+
if (!started) {
|
|
4864
|
+
setDownloadingState(downloading$, true);
|
|
4865
|
+
started = true;
|
|
4866
|
+
}
|
|
4867
|
+
debugLog(`Eager download: ${table.name} has ${keys.length} row(s)`);
|
|
4868
|
+
for (let i = 0; i < keys.length; i += CHUNK_SIZE) {
|
|
4869
|
+
if (signal === null || signal === void 0 ? void 0 : signal.aborted)
|
|
4870
|
+
;
|
|
4871
|
+
const slice = keys.slice(i, i + CHUNK_SIZE);
|
|
4872
|
+
try {
|
|
4873
|
+
// bulkGet triggers the blob-resolve middleware for each row that
|
|
4874
|
+
// still has `_hasBlobRefs=1`. Rows already resolved by parallel
|
|
4875
|
+
// reads come back without the marker and the middleware no-ops.
|
|
4876
|
+
// Rows that have been deleted return `undefined` and are
|
|
4877
|
+
// likewise skipped.
|
|
4878
|
+
yield table.bulkGet(slice);
|
|
4879
|
+
}
|
|
4880
|
+
catch (err) {
|
|
4881
|
+
console.error(`Eager download: ${table.name} chunk failed:`, err);
|
|
4882
|
+
continue;
|
|
4883
|
+
}
|
|
4884
|
+
totalProcessed += slice.length;
|
|
4885
|
+
debugLog(`Eager download: ${table.name} ${Math.min(i + CHUNK_SIZE, keys.length)}/${keys.length}`);
|
|
4655
4886
|
}
|
|
4656
4887
|
}
|
|
4888
|
+
if (started) {
|
|
4889
|
+
// Make sure all middleware-enqueued saves have landed before we flip
|
|
4890
|
+
// `downloading$` to false — otherwise observers might see a "done"
|
|
4891
|
+
// signal while writes are still in flight.
|
|
4892
|
+
yield db.blobDownloadTracker.drainPendingSaves();
|
|
4893
|
+
debugLog(`Eager download: done (${totalProcessed} row(s) processed)`);
|
|
4894
|
+
}
|
|
4895
|
+
else {
|
|
4896
|
+
debugLog('Eager download: No blobs remaining, exiting');
|
|
4897
|
+
}
|
|
4657
4898
|
}
|
|
4658
4899
|
finally {
|
|
4659
|
-
|
|
4900
|
+
if (started)
|
|
4901
|
+
setDownloadingState(downloading$, false);
|
|
4660
4902
|
}
|
|
4661
4903
|
});
|
|
4662
4904
|
}
|
|
@@ -5992,99 +6234,6 @@ function createMutationTrackingMiddleware({ currentUserObservable, db, }) {
|
|
|
5992
6234
|
};
|
|
5993
6235
|
}
|
|
5994
6236
|
|
|
5995
|
-
/**
|
|
5996
|
-
* BlobSavingQueue - Queues resolved blobs for saving back to IndexedDB
|
|
5997
|
-
*
|
|
5998
|
-
* Uses setTimeout(fn, 0) instead of queueMicrotask to completely isolate
|
|
5999
|
-
* from Dexie's Promise.PSD context. This prevents the save operation
|
|
6000
|
-
* from inheriting any ongoing transaction.
|
|
6001
|
-
*
|
|
6002
|
-
* Each blob is saved atomically using downCore transaction with the specific
|
|
6003
|
-
* keyPath to avoid race conditions with other property changes.
|
|
6004
|
-
*/
|
|
6005
|
-
class BlobSavingQueue {
|
|
6006
|
-
constructor(db) {
|
|
6007
|
-
this.queue = [];
|
|
6008
|
-
this.isProcessing = false;
|
|
6009
|
-
this.db = db;
|
|
6010
|
-
}
|
|
6011
|
-
/**
|
|
6012
|
-
* Queue a resolved blob for saving.
|
|
6013
|
-
* Only the specific blob property will be updated atomically.
|
|
6014
|
-
*/
|
|
6015
|
-
saveBlobs(tableName, primaryKey, resolvedBlobs) {
|
|
6016
|
-
this.queue.push({ tableName, primaryKey, resolvedBlobs });
|
|
6017
|
-
this.startConsumer();
|
|
6018
|
-
}
|
|
6019
|
-
/**
|
|
6020
|
-
* Start the consumer if not already processing.
|
|
6021
|
-
* Uses setTimeout(fn, 0) to completely break out of any
|
|
6022
|
-
* Dexie transaction context (Promise.PSD).
|
|
6023
|
-
*/
|
|
6024
|
-
startConsumer() {
|
|
6025
|
-
if (this.isProcessing)
|
|
6026
|
-
return;
|
|
6027
|
-
this.isProcessing = true;
|
|
6028
|
-
// Use setTimeout to completely isolate from Dexie's PSD context
|
|
6029
|
-
// queueMicrotask would risk inheriting the current transaction
|
|
6030
|
-
setTimeout(() => {
|
|
6031
|
-
this.processQueue();
|
|
6032
|
-
}, 0);
|
|
6033
|
-
}
|
|
6034
|
-
/**
|
|
6035
|
-
* Process all queued blobs.
|
|
6036
|
-
* Runs in a completely isolated context (no inherited transaction).
|
|
6037
|
-
* Uses atomic updates to avoid race conditions.
|
|
6038
|
-
*/
|
|
6039
|
-
processQueue() {
|
|
6040
|
-
const item = this.queue.shift();
|
|
6041
|
-
if (!item) {
|
|
6042
|
-
this.isProcessing = false;
|
|
6043
|
-
return;
|
|
6044
|
-
}
|
|
6045
|
-
// Atomic update of just the blob property
|
|
6046
|
-
this.db
|
|
6047
|
-
.transaction('rw', item.tableName, (tx) => {
|
|
6048
|
-
const trans = tx.idbtrans;
|
|
6049
|
-
trans.disableChangeTracking = true; // Don't regard this as a change for sync purposes
|
|
6050
|
-
trans.disableAccessControl = true; // Bypass any access control checks since this is an internal operation
|
|
6051
|
-
trans.disableBlobResolve = true; // Custom flag to skip blob resolve middleware during this transaction
|
|
6052
|
-
const updateSpec = {};
|
|
6053
|
-
for (const blob of item.resolvedBlobs) {
|
|
6054
|
-
updateSpec[blob.keyPath] = blob.data;
|
|
6055
|
-
}
|
|
6056
|
-
tx.table(item.tableName).update(item.primaryKey, (obj) => {
|
|
6057
|
-
// Check that object still has the same unresolved blob refs before applying update (i.e. it hasn't been modified since we read it)
|
|
6058
|
-
for (const blob of item.resolvedBlobs) {
|
|
6059
|
-
// Verify atomicity - none of the blob properties has been modified since we read it. If any of them was modified, skip updating this item to avoid overwriting user changes.
|
|
6060
|
-
const currentValue = Dexie.getByKeyPath(obj, blob.keyPath);
|
|
6061
|
-
if (currentValue === undefined) {
|
|
6062
|
-
// Blob property was removed - skip updating this blob
|
|
6063
|
-
continue;
|
|
6064
|
-
}
|
|
6065
|
-
if (!isBlobRef(currentValue)) {
|
|
6066
|
-
// Blob property was modified to a non-blob-ref value - skip updating this blob
|
|
6067
|
-
continue;
|
|
6068
|
-
}
|
|
6069
|
-
if (currentValue.ref !== blob.ref) {
|
|
6070
|
-
// Blob property was modified - skip updating this blob
|
|
6071
|
-
return; // Stop. Another items has been queued to fully fix the object.
|
|
6072
|
-
}
|
|
6073
|
-
Dexie.setByKeyPath(obj, blob.keyPath, blob.data);
|
|
6074
|
-
}
|
|
6075
|
-
delete obj._hasBlobRefs; // Clear the _hasBlobRefs marker if all refs was resolved.
|
|
6076
|
-
});
|
|
6077
|
-
})
|
|
6078
|
-
.catch((error) => {
|
|
6079
|
-
console.error(`Error saving resolved blobs on ${item.tableName}:${item.primaryKey}:`, error);
|
|
6080
|
-
})
|
|
6081
|
-
.finally(() => {
|
|
6082
|
-
// Process next item in the queue
|
|
6083
|
-
return this.processQueue();
|
|
6084
|
-
});
|
|
6085
|
-
}
|
|
6086
|
-
}
|
|
6087
|
-
|
|
6088
6237
|
/**
|
|
6089
6238
|
* DBCore Middleware for resolving BlobRefs on read
|
|
6090
6239
|
*
|
|
@@ -6095,10 +6244,11 @@ class BlobSavingQueue {
|
|
|
6095
6244
|
* Uses Dexie.waitFor() only for explicit rw transactions to keep them alive.
|
|
6096
6245
|
* For readonly or implicit transactions, resolves directly (no waitFor needed).
|
|
6097
6246
|
*
|
|
6098
|
-
* Resolved blobs are
|
|
6099
|
-
*
|
|
6100
|
-
* Each blob is saved atomically
|
|
6101
|
-
* avoid race conditions with other
|
|
6247
|
+
* Resolved blobs are persisted via db.blobDownloadTracker.enqueueSave(),
|
|
6248
|
+
* which internally uses a queue that runs in a fresh JS task to completely
|
|
6249
|
+
* isolate from Dexie's transaction context. Each blob is saved atomically
|
|
6250
|
+
* using Table.update() with its keyPath to avoid race conditions with other
|
|
6251
|
+
* property changes.
|
|
6102
6252
|
*
|
|
6103
6253
|
* Blob downloads use Authorization header (same as sync) via the server
|
|
6104
6254
|
* proxy endpoint: GET /blob/{ref}
|
|
@@ -6109,8 +6259,6 @@ function createBlobResolveMiddleware(db) {
|
|
|
6109
6259
|
name: 'blobResolve',
|
|
6110
6260
|
level: 2, // Run above cache (0) and other middlewares (1) to resolve BlobRefs from cached data
|
|
6111
6261
|
create(downlevelDatabase) {
|
|
6112
|
-
// Create a single queue instance for this database
|
|
6113
|
-
const blobSavingQueue = new BlobSavingQueue(db);
|
|
6114
6262
|
return Object.assign(Object.assign({}, downlevelDatabase), { table(tableName) {
|
|
6115
6263
|
var _a;
|
|
6116
6264
|
if (!db.cloud) {
|
|
@@ -6131,7 +6279,7 @@ function createBlobResolveMiddleware(db) {
|
|
|
6131
6279
|
}
|
|
6132
6280
|
return downlevelTable.get(req).then((result) => {
|
|
6133
6281
|
if (result && hasUnresolvedBlobRefs(result)) {
|
|
6134
|
-
return resolveAndSave(downlevelTable, req.trans, req.key, result,
|
|
6282
|
+
return resolveAndSave(downlevelTable, req.trans, req.key, result, db);
|
|
6135
6283
|
}
|
|
6136
6284
|
return result;
|
|
6137
6285
|
});
|
|
@@ -6148,7 +6296,7 @@ function createBlobResolveMiddleware(db) {
|
|
|
6148
6296
|
return results;
|
|
6149
6297
|
return Dexie.Promise.all(results.map((result, index) => {
|
|
6150
6298
|
if (result && hasUnresolvedBlobRefs(result)) {
|
|
6151
|
-
return resolveAndSave(downlevelTable, req.trans, req.keys[index], result,
|
|
6299
|
+
return resolveAndSave(downlevelTable, req.trans, req.keys[index], result, db);
|
|
6152
6300
|
}
|
|
6153
6301
|
return result;
|
|
6154
6302
|
}));
|
|
@@ -6168,7 +6316,7 @@ function createBlobResolveMiddleware(db) {
|
|
|
6168
6316
|
return result;
|
|
6169
6317
|
return Dexie.Promise.all(result.result.map((item) => {
|
|
6170
6318
|
if (item && hasUnresolvedBlobRefs(item)) {
|
|
6171
|
-
return resolveAndSave(downlevelTable, req.trans, undefined, item,
|
|
6319
|
+
return resolveAndSave(downlevelTable, req.trans, undefined, item, db);
|
|
6172
6320
|
}
|
|
6173
6321
|
return item;
|
|
6174
6322
|
})).then((resolved) => (Object.assign(Object.assign({}, result), { result: resolved })));
|
|
@@ -6186,7 +6334,7 @@ function createBlobResolveMiddleware(db) {
|
|
|
6186
6334
|
return cursor; // No values requested, so no resolution needed
|
|
6187
6335
|
if (!dbUrl)
|
|
6188
6336
|
return cursor; // No database URL configured, can't resolve blobs
|
|
6189
|
-
return createBlobResolvingCursor(cursor, downlevelTable,
|
|
6337
|
+
return createBlobResolvingCursor(cursor, downlevelTable, db);
|
|
6190
6338
|
});
|
|
6191
6339
|
} });
|
|
6192
6340
|
} });
|
|
@@ -6203,7 +6351,7 @@ function createBlobResolveMiddleware(db) {
|
|
|
6203
6351
|
* Returns the cursor synchronously. Resolution happens in start() before
|
|
6204
6352
|
* each onNext callback, ensuring cursor.value is always available.
|
|
6205
6353
|
*/
|
|
6206
|
-
function createBlobResolvingCursor(cursor, table,
|
|
6354
|
+
function createBlobResolvingCursor(cursor, table, db) {
|
|
6207
6355
|
// Create wrapped cursor using Object.create() - inherits everything.
|
|
6208
6356
|
// Important: .key and .primaryKey must be explicitly overridden with
|
|
6209
6357
|
// closure-based getters to prevent native IDBCursorWithValue getters from
|
|
@@ -6211,11 +6359,15 @@ function createBlobResolvingCursor(cursor, table, blobSavingQueue, db) {
|
|
|
6211
6359
|
// throws "Illegal invocation" in Chrome 146+.
|
|
6212
6360
|
const wrappedCursor = Object.create(cursor, {
|
|
6213
6361
|
key: {
|
|
6214
|
-
get() {
|
|
6362
|
+
get() {
|
|
6363
|
+
return cursor.key;
|
|
6364
|
+
},
|
|
6215
6365
|
configurable: true,
|
|
6216
6366
|
},
|
|
6217
6367
|
primaryKey: {
|
|
6218
|
-
get() {
|
|
6368
|
+
get() {
|
|
6369
|
+
return cursor.primaryKey;
|
|
6370
|
+
},
|
|
6219
6371
|
configurable: true,
|
|
6220
6372
|
},
|
|
6221
6373
|
value: {
|
|
@@ -6233,7 +6385,7 @@ function createBlobResolvingCursor(cursor, table, blobSavingQueue, db) {
|
|
|
6233
6385
|
onNext();
|
|
6234
6386
|
return;
|
|
6235
6387
|
}
|
|
6236
|
-
resolveAndSave(table, cursor.trans, cursor.primaryKey, rawValue,
|
|
6388
|
+
resolveAndSave(table, cursor.trans, cursor.primaryKey, rawValue, db, true).then((resolved) => {
|
|
6237
6389
|
wrappedCursor.value = resolved;
|
|
6238
6390
|
onNext();
|
|
6239
6391
|
}, (err) => {
|
|
@@ -6261,7 +6413,7 @@ function createBlobResolvingCursor(cursor, table, blobSavingQueue, db) {
|
|
|
6261
6413
|
* Returns Dexie.Promise to preserve PSD context.
|
|
6262
6414
|
*/
|
|
6263
6415
|
function resolveAndSave(table, trans, pKey, // optional. If missing, tries to extract from object using primary key path
|
|
6264
|
-
obj,
|
|
6416
|
+
obj, db, isCursorValue = false // Flag to indicate if we're resolving a cursor value (which may not have a primary key)
|
|
6265
6417
|
) {
|
|
6266
6418
|
var _a;
|
|
6267
6419
|
try {
|
|
@@ -6298,21 +6450,19 @@ obj, blobSavingQueue, db, isCursorValue = false // Flag to indicate if we're res
|
|
|
6298
6450
|
? Dexie.getByKeyPath(obj, primaryKey.keyPath)
|
|
6299
6451
|
: undefined;
|
|
6300
6452
|
if (key !== undefined) {
|
|
6301
|
-
//
|
|
6302
|
-
//
|
|
6303
|
-
//
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
});
|
|
6315
|
-
}
|
|
6453
|
+
// Hand off persistence to the tracker. The tracker owns an
|
|
6454
|
+
// internal save-queue that runs in a fresh JS task (setTimeout 0)
|
|
6455
|
+
// — completely outside any PSD context, so opening a Dexie rw
|
|
6456
|
+
// transaction there is always safe regardless of the calling
|
|
6457
|
+
// context. The tracker also keeps the in-flight download cache
|
|
6458
|
+
// alive until the save completes, so concurrent readers piggyback
|
|
6459
|
+
// on the already-downloaded data instead of refetching.
|
|
6460
|
+
db.blobDownloadTracker.enqueueSave(table.name, key, resolvedBlobs);
|
|
6461
|
+
}
|
|
6462
|
+
else if (resolvedBlobs.length > 0) {
|
|
6463
|
+
// No primary key — we can't persist. Release the in-flight cache
|
|
6464
|
+
// entries explicitly so they don't leak.
|
|
6465
|
+
db.blobDownloadTracker.releaseRefs(resolvedBlobs.map((b) => b.ref));
|
|
6316
6466
|
}
|
|
6317
6467
|
return resolved;
|
|
6318
6468
|
})
|
|
@@ -8340,7 +8490,7 @@ function dexieCloud(dexie) {
|
|
|
8340
8490
|
const downloading$ = createDownloadingState();
|
|
8341
8491
|
dexie.cloud = {
|
|
8342
8492
|
// @ts-ignore
|
|
8343
|
-
version: "4.4.
|
|
8493
|
+
version: "4.4.12",
|
|
8344
8494
|
options: Object.assign({}, DEFAULT_OPTIONS),
|
|
8345
8495
|
schema: null,
|
|
8346
8496
|
get currentUserId() {
|
|
@@ -8785,7 +8935,7 @@ function dexieCloud(dexie) {
|
|
|
8785
8935
|
}
|
|
8786
8936
|
}
|
|
8787
8937
|
// @ts-ignore
|
|
8788
|
-
dexieCloud.version = "4.4.
|
|
8938
|
+
dexieCloud.version = "4.4.12";
|
|
8789
8939
|
Dexie.Cloud = dexieCloud;
|
|
8790
8940
|
|
|
8791
8941
|
// In case the SW lives for a while, let it reuse already opened connections:
|