dexie-cloud-addon 4.3.9 → 4.4.1

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.
Files changed (172) hide show
  1. package/dist/modern/DexieCloudAPI.d.ts +17 -0
  2. package/dist/modern/DexieCloudOptions.d.ts +19 -0
  3. package/dist/modern/TSON.d.ts +0 -6
  4. package/dist/modern/db/DexieCloudDB.d.ts +2 -0
  5. package/dist/modern/db/entities/EntityCommon.d.ts +1 -0
  6. package/dist/modern/dexie-cloud-addon.js +3776 -2354
  7. package/dist/modern/dexie-cloud-addon.js.map +1 -1
  8. package/dist/modern/dexie-cloud-addon.min.js +1 -1
  9. package/dist/modern/dexie-cloud-addon.min.js.map +1 -1
  10. package/dist/modern/middlewares/blobResolveMiddleware.d.ts +21 -0
  11. package/dist/modern/service-worker.js +2195 -773
  12. package/dist/modern/service-worker.js.map +1 -1
  13. package/dist/modern/service-worker.min.js +1 -1
  14. package/dist/modern/service-worker.min.js.map +1 -1
  15. package/dist/modern/sync/BlobDownloadTracker.d.ts +33 -0
  16. package/dist/modern/sync/BlobSavingQueue.d.ts +35 -0
  17. package/dist/modern/sync/blobOffloading.d.ts +38 -0
  18. package/dist/modern/sync/blobProgress.d.ts +25 -0
  19. package/dist/modern/sync/blobResolve.d.ts +85 -0
  20. package/dist/modern/sync/eagerBlobDownloader.d.ts +20 -0
  21. package/dist/modern/sync/loadCachedAccessToken.d.ts +2 -0
  22. package/dist/modern/types/DXCAlert.d.ts +6 -0
  23. package/dist/modern/types/TXExpandos.d.ts +1 -0
  24. package/dist/umd/dexie-cloud-addon.js +3867 -2445
  25. package/dist/umd/dexie-cloud-addon.js.map +1 -1
  26. package/dist/umd/dexie-cloud-addon.min.js +1 -1
  27. package/dist/umd/dexie-cloud-addon.min.js.map +1 -1
  28. package/dist/umd/service-worker.js +2330 -908
  29. package/dist/umd/service-worker.js.map +1 -1
  30. package/dist/umd/service-worker.min.js +1 -1
  31. package/dist/umd/service-worker.min.js.map +1 -1
  32. package/package.json +5 -6
  33. package/dist/modern/default-ui/AuthProviderButton.d.ts +0 -21
  34. package/dist/modern/default-ui/ProviderSelectionDialog.d.ts +0 -7
  35. package/dist/modern/default-ui/SelectDialog.d.ts +0 -10
  36. package/dist/modern/dexie-cloud-addon.min.js.gz +0 -0
  37. package/dist/umd/DISABLE_SERVICEWORKER_STRATEGY.d.ts +0 -1
  38. package/dist/umd/DXCWebSocketStatus.d.ts +0 -1
  39. package/dist/umd/DexieCloudAPI.d.ts +0 -75
  40. package/dist/umd/DexieCloudOptions.d.ts +0 -27
  41. package/dist/umd/DexieCloudSyncOptions.d.ts +0 -4
  42. package/dist/umd/DexieCloudTable.d.ts +0 -18
  43. package/dist/umd/InvalidLicenseError.d.ts +0 -5
  44. package/dist/umd/Invite.d.ts +0 -8
  45. package/dist/umd/PermissionChecker.d.ts +0 -15
  46. package/dist/umd/TSON.d.ts +0 -17
  47. package/dist/umd/WSObservable.d.ts +0 -72
  48. package/dist/umd/associate.d.ts +0 -1
  49. package/dist/umd/authentication/AuthPersistedContext.d.ts +0 -9
  50. package/dist/umd/authentication/TokenErrorResponseError.d.ts +0 -10
  51. package/dist/umd/authentication/TokenExpiredError.d.ts +0 -3
  52. package/dist/umd/authentication/UNAUTHORIZED_USER.d.ts +0 -2
  53. package/dist/umd/authentication/authenticate.d.ts +0 -13
  54. package/dist/umd/authentication/interactWithUser.d.ts +0 -21
  55. package/dist/umd/authentication/login.d.ts +0 -3
  56. package/dist/umd/authentication/logout.d.ts +0 -5
  57. package/dist/umd/authentication/otpFetchTokenCallback.d.ts +0 -3
  58. package/dist/umd/authentication/setCurrentUser.d.ts +0 -14
  59. package/dist/umd/authentication/waitUntil.d.ts +0 -3
  60. package/dist/umd/computeSyncState.d.ts +0 -4
  61. package/dist/umd/createSharedValueObservable.d.ts +0 -3
  62. package/dist/umd/currentUserEmitter.d.ts +0 -3
  63. package/dist/umd/db/DexieCloudDB.d.ts +0 -61
  64. package/dist/umd/db/entities/BaseRevisionMapEntry.d.ts +0 -5
  65. package/dist/umd/db/entities/EntityCommon.d.ts +0 -5
  66. package/dist/umd/db/entities/GuardedJob.d.ts +0 -5
  67. package/dist/umd/db/entities/Member.d.ts +0 -19
  68. package/dist/umd/db/entities/PersistedSyncState.d.ts +0 -22
  69. package/dist/umd/db/entities/Realm.d.ts +0 -14
  70. package/dist/umd/db/entities/Role.d.ts +0 -11
  71. package/dist/umd/db/entities/UserLogin.d.ts +0 -23
  72. package/dist/umd/default-ui/Dialog.d.ts +0 -5
  73. package/dist/umd/default-ui/LoginDialog.d.ts +0 -3
  74. package/dist/umd/default-ui/Styles.d.ts +0 -3
  75. package/dist/umd/default-ui/index.d.ts +0 -24
  76. package/dist/umd/define-ydoc-trigger.d.ts +0 -3
  77. package/dist/umd/dexie-cloud-addon.d.ts +0 -3
  78. package/dist/umd/dexie-cloud-addon.js.gz +0 -0
  79. package/dist/umd/dexie-cloud-addon.min.js.gz +0 -0
  80. package/dist/umd/dexie-cloud-client.d.ts +0 -23
  81. package/dist/umd/errors/HttpError.d.ts +0 -5
  82. package/dist/umd/extend-dexie-interface.d.ts +0 -23
  83. package/dist/umd/getGlobalRolesObservable.d.ts +0 -5
  84. package/dist/umd/getInternalAccessControlObservable.d.ts +0 -12
  85. package/dist/umd/getInvitesObservable.d.ts +0 -23
  86. package/dist/umd/getPermissionsLookupObservable.d.ts +0 -16
  87. package/dist/umd/getTiedRealmId.d.ts +0 -2
  88. package/dist/umd/helpers/BroadcastedAndLocalEvent.d.ts +0 -8
  89. package/dist/umd/helpers/CancelToken.d.ts +0 -4
  90. package/dist/umd/helpers/IS_SERVICE_WORKER.d.ts +0 -1
  91. package/dist/umd/helpers/SWBroadcastChannel.d.ts +0 -12
  92. package/dist/umd/helpers/allSettled.d.ts +0 -1
  93. package/dist/umd/helpers/bulkUpdate.d.ts +0 -4
  94. package/dist/umd/helpers/computeRealmSetHash.d.ts +0 -2
  95. package/dist/umd/helpers/date-constants.d.ts +0 -5
  96. package/dist/umd/helpers/flatten.d.ts +0 -1
  97. package/dist/umd/helpers/getMutationTable.d.ts +0 -1
  98. package/dist/umd/helpers/getSyncableTables.d.ts +0 -4
  99. package/dist/umd/helpers/getTableFromMutationTable.d.ts +0 -1
  100. package/dist/umd/helpers/makeArray.d.ts +0 -1
  101. package/dist/umd/helpers/randomString.d.ts +0 -1
  102. package/dist/umd/helpers/resolveText.d.ts +0 -16
  103. package/dist/umd/helpers/throwVersionIncrementNeeded.d.ts +0 -1
  104. package/dist/umd/helpers/visibilityState.d.ts +0 -1
  105. package/dist/umd/isEagerSyncDisabled.d.ts +0 -2
  106. package/dist/umd/isFirefox.d.ts +0 -1
  107. package/dist/umd/isSafari.d.ts +0 -2
  108. package/dist/umd/mapValueObservable.d.ts +0 -5
  109. package/dist/umd/mergePermissions.d.ts +0 -2
  110. package/dist/umd/middleware-helpers/guardedTable.d.ts +0 -11
  111. package/dist/umd/middleware-helpers/idGenerationHelpers.d.ts +0 -18
  112. package/dist/umd/middlewares/createIdGenerationMiddleware.d.ts +0 -3
  113. package/dist/umd/middlewares/createImplicitPropSetterMiddleware.d.ts +0 -3
  114. package/dist/umd/middlewares/createMutationTrackingMiddleware.d.ts +0 -17
  115. package/dist/umd/middlewares/outstandingTransaction.d.ts +0 -4
  116. package/dist/umd/overrideParseStoresSpec.d.ts +0 -4
  117. package/dist/umd/performInitialSync.d.ts +0 -4
  118. package/dist/umd/permissions.d.ts +0 -9
  119. package/dist/umd/prodLog.d.ts +0 -9
  120. package/dist/umd/service-worker.d.ts +0 -1
  121. package/dist/umd/sync/DEXIE_CLOUD_SYNCER_ID.d.ts +0 -1
  122. package/dist/umd/sync/LocalSyncWorker.d.ts +0 -7
  123. package/dist/umd/sync/SyncRequiredError.d.ts +0 -3
  124. package/dist/umd/sync/applyServerChanges.d.ts +0 -3
  125. package/dist/umd/sync/connectWebSocket.d.ts +0 -2
  126. package/dist/umd/sync/encodeIdsForServer.d.ts +0 -4
  127. package/dist/umd/sync/extractRealm.d.ts +0 -2
  128. package/dist/umd/sync/getLatestRevisionsPerTable.d.ts +0 -6
  129. package/dist/umd/sync/getTablesToSyncify.d.ts +0 -3
  130. package/dist/umd/sync/isOnline.d.ts +0 -1
  131. package/dist/umd/sync/isSyncNeeded.d.ts +0 -2
  132. package/dist/umd/sync/listClientChanges.d.ts +0 -9
  133. package/dist/umd/sync/listSyncifiedChanges.d.ts +0 -5
  134. package/dist/umd/sync/messageConsumerIsReady.d.ts +0 -2
  135. package/dist/umd/sync/messagesFromServerQueue.d.ts +0 -8
  136. package/dist/umd/sync/modifyLocalObjectsWithNewUserId.d.ts +0 -4
  137. package/dist/umd/sync/myId.d.ts +0 -1
  138. package/dist/umd/sync/numUnsyncedMutations.d.ts +0 -2
  139. package/dist/umd/sync/old_startSyncingClientChanges.d.ts +0 -39
  140. package/dist/umd/sync/performGuardedJob.d.ts +0 -2
  141. package/dist/umd/sync/ratelimit.d.ts +0 -3
  142. package/dist/umd/sync/registerSyncEvent.d.ts +0 -3
  143. package/dist/umd/sync/sync.d.ts +0 -15
  144. package/dist/umd/sync/syncIfPossible.d.ts +0 -5
  145. package/dist/umd/sync/syncWithServer.d.ts +0 -6
  146. package/dist/umd/sync/triggerSync.d.ts +0 -2
  147. package/dist/umd/sync/updateBaseRevs.d.ts +0 -5
  148. package/dist/umd/types/DXCAlert.d.ts +0 -25
  149. package/dist/umd/types/DXCInputField.d.ts +0 -11
  150. package/dist/umd/types/DXCUserInteraction.d.ts +0 -93
  151. package/dist/umd/types/NewIdOptions.d.ts +0 -3
  152. package/dist/umd/types/SWMessageEvent.d.ts +0 -3
  153. package/dist/umd/types/SWSyncEvent.d.ts +0 -4
  154. package/dist/umd/types/SyncState.d.ts +0 -9
  155. package/dist/umd/types/TXExpandos.d.ts +0 -11
  156. package/dist/umd/updateSchemaFromOptions.d.ts +0 -3
  157. package/dist/umd/userIsActive.d.ts +0 -7
  158. package/dist/umd/verifyConfig.d.ts +0 -2
  159. package/dist/umd/verifySchema.d.ts +0 -2
  160. package/dist/umd/yjs/YDexieCloudSyncState.d.ts +0 -3
  161. package/dist/umd/yjs/YTable.d.ts +0 -3
  162. package/dist/umd/yjs/applyYMessages.d.ts +0 -9
  163. package/dist/umd/yjs/awareness.d.ts +0 -3
  164. package/dist/umd/yjs/createYClientUpdateObservable.d.ts +0 -4
  165. package/dist/umd/yjs/createYHandler.d.ts +0 -2
  166. package/dist/umd/yjs/downloadYDocsFromServer.d.ts +0 -3
  167. package/dist/umd/yjs/getUpdatesTable.d.ts +0 -3
  168. package/dist/umd/yjs/listUpdatesSince.d.ts +0 -3
  169. package/dist/umd/yjs/listYClientMessagesAndStateVector.d.ts +0 -26
  170. package/dist/umd/yjs/reopenDocSignal.d.ts +0 -10
  171. package/dist/umd/yjs/updateYSyncStates.d.ts +0 -6
  172. /package/dist/{umd/authentication/currentUserObservable.d.ts → modern/sync/blobOffloading.test.d.ts} +0 -0
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * ==========================================================================
10
10
  *
11
- * Version 4.3.9, Thu Jan 29 2026
11
+ * Version 4.4.1, Thu Mar 19 2026
12
12
  *
13
13
  * https://dexie.org
14
14
  *
@@ -322,238 +322,719 @@
322
322
  }
323
323
  }
324
324
 
325
- const hasArrayBufferFromBase64 = "fromBase64" in Uint8Array; // https://github.com/tc39/proposal-arraybuffer-base64;
326
- const hasArrayBufferToBase64 = "toBase64" in Uint8Array.prototype; // https://github.com/tc39/proposal-arraybuffer-base64;
327
- const b64decode = typeof Buffer !== "undefined"
328
- ? (base64) => Buffer.from(base64, "base64") // Node
329
- : hasArrayBufferFromBase64
330
- ? // @ts-ignore: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/fromBase64
331
- (base64) => Uint8Array.fromBase64(base64) // Modern javascript standard
332
- : (base64) => {
333
- // Legacy DOM workaround
334
- const binary_string = atob(base64);
335
- const len = binary_string.length;
336
- const bytes = new Uint8Array(len);
337
- for (var i = 0; i < len; i++) {
338
- bytes[i] = binary_string.charCodeAt(i);
339
- }
340
- return bytes;
341
- };
342
- const b64encode = typeof Buffer !== "undefined"
343
- ? (b) => {
344
- // Node
345
- if (ArrayBuffer.isView(b)) {
346
- return Buffer.from(b.buffer, b.byteOffset, b.byteLength).toString("base64");
347
- }
348
- else {
349
- return Buffer.from(b).toString("base64");
325
+ const { toString: toStr } = {};
326
+ function getToStringTag(val) {
327
+ return toStr.call(val).slice(8, -1);
328
+ }
329
+ function escapeDollarProps(value) {
330
+ const keys = Object.keys(value);
331
+ let dollarKeys = null;
332
+ for (let i = 0, l = keys.length; i < l; ++i) {
333
+ if (keys[i][0] === "$") {
334
+ dollarKeys = dollarKeys || [];
335
+ dollarKeys.push(keys[i]);
350
336
  }
351
337
  }
352
- : hasArrayBufferToBase64
353
- ? (b) => {
354
- // Uint8Array.prototype.toBase64 is available in modern browsers
355
- const u8a = ArrayBuffer.isView(b) ? b : new Uint8Array(b);
356
- // @ts-ignore: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/toBase64
357
- return u8a.toBase64();
358
- }
359
- : (b) => {
360
- // Legacy DOM workaround
361
- const u8a = ArrayBuffer.isView(b) ? b : new Uint8Array(b);
362
- const CHUNK_SIZE = 0x1000;
363
- const strs = [];
364
- for (let i = 0, l = u8a.length; i < l; i += CHUNK_SIZE) {
365
- const chunk = u8a.subarray(i, i + CHUNK_SIZE);
366
- strs.push(String.fromCharCode.apply(null, chunk));
338
+ if (!dollarKeys)
339
+ return value;
340
+ const clone = { ...value };
341
+ for (const k of dollarKeys) {
342
+ delete clone[k];
343
+ }
344
+ for (const k of dollarKeys) {
345
+ clone["$" + k] = value[k];
346
+ }
347
+ return clone;
348
+ }
349
+ const ObjectDef = {
350
+ replace: escapeDollarProps,
351
+ };
352
+ function TypesonSimplified(...typeDefsInputs) {
353
+ const typeDefs = typeDefsInputs.reduce((p, c) => ({ ...p, ...c }), typeDefsInputs.reduce((p, c) => ({ ...c, ...p }), {}));
354
+ const protoMap = new WeakMap();
355
+ return {
356
+ stringify(value, alternateChannel, space) {
357
+ const json = JSON.stringify(value, function (key) {
358
+ const realVal = this[key];
359
+ const typeDef = getTypeDef(realVal);
360
+ return typeDef
361
+ ? typeDef.replace(realVal, alternateChannel, typeDefs)
362
+ : realVal;
363
+ }, space);
364
+ return json;
365
+ },
366
+ parse(tson, alternateChannel) {
367
+ const stack = [];
368
+ return JSON.parse(tson, function (key, value) {
369
+ //
370
+ // Parent Part
371
+ //
372
+ const type = value?.$t;
373
+ if (type) {
374
+ const typeDef = typeDefs[type];
375
+ value = typeDef
376
+ ? typeDef.revive(value, alternateChannel, typeDefs)
377
+ : value;
378
+ }
379
+ let top = stack[stack.length - 1];
380
+ if (top && top[0] === value) {
381
+ // Do what the kid told us to
382
+ // Unescape dollar props
383
+ value = { ...value };
384
+ // Delete keys that children wanted us to delete
385
+ for (const k of top[1])
386
+ delete value[k];
387
+ // Set keys that children wanted us to set
388
+ for (const [k, v] of Object.entries(top[2])) {
389
+ value[k] = v;
390
+ }
391
+ stack.pop();
392
+ }
393
+ //
394
+ // Child part
395
+ //
396
+ if (value === undefined || (key[0] === "$" && key !== "$t")) {
397
+ top = stack[stack.length - 1];
398
+ let deletes;
399
+ let mods;
400
+ if (top && top[0] === this) {
401
+ deletes = top[1];
402
+ mods = top[2];
403
+ }
404
+ else {
405
+ stack.push([this, (deletes = []), (mods = {})]);
406
+ }
407
+ if (key[0] === "$" && key !== "$t") {
408
+ // Unescape props (also preserves undefined if this is a combo)
409
+ deletes.push(key);
410
+ mods[key.substr(1)] = value;
411
+ }
412
+ else {
413
+ // Preserve undefined
414
+ mods[key] = undefined;
415
+ }
416
+ }
417
+ return value;
418
+ });
419
+ },
420
+ };
421
+ function getTypeDef(realVal) {
422
+ const type = typeof realVal;
423
+ switch (typeof realVal) {
424
+ case "object":
425
+ case "function": {
426
+ // "object", "function", null
427
+ if (realVal === null)
428
+ return null;
429
+ const proto = Object.getPrototypeOf(realVal);
430
+ if (!proto)
431
+ return ObjectDef;
432
+ let typeDef = protoMap.get(proto);
433
+ if (typeDef !== undefined)
434
+ return typeDef; // Null counts to! So the caching of Array.prototype also counts.
435
+ const toStringTag = getToStringTag(realVal);
436
+ const entry = Object.entries(typeDefs).find(([typeName, typeDef]) => typeDef?.test?.(realVal, toStringTag) ?? typeName === toStringTag);
437
+ typeDef = entry?.[1];
438
+ if (!typeDef) {
439
+ typeDef = Array.isArray(realVal)
440
+ ? null
441
+ : typeof realVal === "function"
442
+ ? typeDefs.function || null
443
+ : ObjectDef;
444
+ }
445
+ protoMap.set(proto, typeDef);
446
+ return typeDef;
367
447
  }
368
- return btoa(strs.join(""));
369
- };
370
-
371
- function computeRealmSetHash(_a) {
372
- return __awaiter(this, arguments, void 0, function* ({ realms, inviteRealms, }) {
373
- const data = JSON.stringify([
374
- ...realms.map((realmId) => ({ realmId, accepted: true })),
375
- ...inviteRealms.map((realmId) => ({ realmId, accepted: false })),
376
- ].sort((a, b) => a.realmId < b.realmId ? -1 : a.realmId > b.realmId ? 1 : 0));
377
- const byteArray = new TextEncoder().encode(data);
378
- const digestBytes = yield crypto.subtle.digest('SHA-1', byteArray);
379
- const base64 = b64encode(digestBytes);
380
- return base64;
381
- });
448
+ default:
449
+ return typeDefs[type];
450
+ }
451
+ }
382
452
  }
383
453
 
384
- function getSyncableTables(db) {
385
- return Object.entries(db.cloud.schema || {})
386
- .filter(([, { markedForSync }]) => markedForSync)
387
- .map(([tbl]) => db.tables.filter(({ name }) => name === tbl)[0])
388
- .filter(cloudTableSchema => cloudTableSchema);
454
+ class FakeBlob {
455
+ constructor(buf, type) {
456
+ this.buf = buf;
457
+ this.type = type;
458
+ }
389
459
  }
390
460
 
391
- function getMutationTable(tableName) {
392
- return `$${tableName}_mutations`;
461
+ /**
462
+ * TSONRef - Reference to a blob stored separately from the main data.
463
+ *
464
+ * When TSON parses data containing blob references, it creates TSONRef
465
+ * instances instead of the actual binary data. The client can then
466
+ * resolve these refs asynchronously.
467
+ *
468
+ * @example
469
+ * ```typescript
470
+ * // Configure resolver
471
+ * TSONRef.resolver = async (ref) => {
472
+ * const response = await fetch(`/blob/${ref.ref}`);
473
+ * return response.arrayBuffer();
474
+ * };
475
+ *
476
+ * // After parsing, resolve all refs in an object
477
+ * await resolveAllRefs(data);
478
+ * ```
479
+ */
480
+ var _a;
481
+ /** Symbol for type checking TSONRef instances */
482
+ const TSON_REF_SYMBOL = Symbol.for('TSONRef');
483
+ /**
484
+ * TSONRef represents a reference to binary data stored as a blob.
485
+ */
486
+ class TSONRef {
487
+ constructor(
488
+ /** Original TSON type: 'ArrayBuffer', 'Blob', 'Uint8Array', etc */
489
+ type,
490
+ /** Blob reference ID (UUID) */
491
+ ref,
492
+ /** Size in bytes */
493
+ size,
494
+ /** Content-Type (for Blob type) */
495
+ contentType) {
496
+ this.type = type;
497
+ this.ref = ref;
498
+ this.size = size;
499
+ this.contentType = contentType;
500
+ /** Type brand for runtime identification */
501
+ this[_a] = true;
502
+ Object.freeze(this);
503
+ }
504
+ /**
505
+ * Resolve this reference to actual data.
506
+ * Requires TSONRef.resolver to be configured.
507
+ */
508
+ async resolve() {
509
+ if (!TSONRef.resolver) {
510
+ throw new Error('TSONRef.resolver not configured. ' +
511
+ 'Set TSONRef.resolver to a function that fetches blobs.');
512
+ }
513
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
514
+ const data = await TSONRef.resolver(this);
515
+ return this.reconstruct(data);
516
+ }
517
+ /**
518
+ * Reconstruct the original type from ArrayBuffer.
519
+ * Validates byte alignment for TypedArrays that require it.
520
+ */
521
+ reconstruct(data) {
522
+ // Helper to validate alignment for multi-byte TypedArrays
523
+ const validateAlignment = (bytesPerElement, typeName) => {
524
+ if (data.byteLength % bytesPerElement !== 0) {
525
+ throw new RangeError(`Buffer length ${data.byteLength} is not aligned to ${bytesPerElement} bytes for ${typeName}`);
526
+ }
527
+ };
528
+ switch (this.type) {
529
+ case 'ArrayBuffer':
530
+ return data;
531
+ case 'Uint8Array':
532
+ return new Uint8Array(data);
533
+ case 'Blob':
534
+ return new Blob([data], { type: this.contentType });
535
+ // Handle other TypedArrays with alignment validation
536
+ case 'Int8Array':
537
+ return new Int8Array(data);
538
+ case 'Uint8ClampedArray':
539
+ return new Uint8ClampedArray(data);
540
+ case 'Int16Array':
541
+ validateAlignment(2, 'Int16Array');
542
+ return new Int16Array(data);
543
+ case 'Uint16Array':
544
+ validateAlignment(2, 'Uint16Array');
545
+ return new Uint16Array(data);
546
+ case 'Int32Array':
547
+ validateAlignment(4, 'Int32Array');
548
+ return new Int32Array(data);
549
+ case 'Uint32Array':
550
+ validateAlignment(4, 'Uint32Array');
551
+ return new Uint32Array(data);
552
+ case 'Float32Array':
553
+ validateAlignment(4, 'Float32Array');
554
+ return new Float32Array(data);
555
+ case 'Float64Array':
556
+ validateAlignment(8, 'Float64Array');
557
+ return new Float64Array(data);
558
+ case 'BigInt64Array':
559
+ validateAlignment(8, 'BigInt64Array');
560
+ return new BigInt64Array(data);
561
+ case 'BigUint64Array':
562
+ validateAlignment(8, 'BigUint64Array');
563
+ return new BigUint64Array(data);
564
+ default:
565
+ console.warn(`Unknown TSONRef type: ${this.type}, returning ArrayBuffer`);
566
+ return data;
567
+ }
568
+ }
569
+ /**
570
+ * Check if a value is a TSONRef instance.
571
+ */
572
+ static isTSONRef(value) {
573
+ return (value !== null &&
574
+ typeof value === 'object' &&
575
+ TSON_REF_SYMBOL in value &&
576
+ value[TSON_REF_SYMBOL] === true);
577
+ }
578
+ /**
579
+ * Check if a value is TSONRef serialized data (has $ref).
580
+ */
581
+ static isTSONRefData(value) {
582
+ return (value !== null &&
583
+ typeof value === 'object' &&
584
+ '$ref' in value &&
585
+ '$t' in value &&
586
+ '$size' in value);
587
+ }
588
+ /**
589
+ * Create TSONRef from serialized data.
590
+ */
591
+ static fromData(data) {
592
+ return new TSONRef(data.$t, data.$ref, data.$size, data.$ct);
593
+ }
594
+ /**
595
+ * Serialize to JSON-compatible format.
596
+ */
597
+ toJSON() {
598
+ const result = {
599
+ $t: this.type,
600
+ $ref: this.ref,
601
+ $size: this.size,
602
+ };
603
+ if (this.contentType) {
604
+ result.$ct = this.contentType;
605
+ }
606
+ return result;
607
+ }
393
608
  }
609
+ _a = TSON_REF_SYMBOL;
610
+ /** Symbol for type checking */
611
+ TSONRef.TYPE_SYMBOL = TSON_REF_SYMBOL;
612
+ /** Global resolver function - must be configured before resolving */
613
+ TSONRef.resolver = null;
394
614
 
395
- function getTableFromMutationTable(mutationTable) {
396
- var _a;
397
- const tableName = (_a = /^\$(.*)_mutations$/.exec(mutationTable)) === null || _a === void 0 ? void 0 : _a[1];
398
- if (!tableName)
399
- throw new Error(`Given mutationTable ${mutationTable} is not correct`);
400
- return tableName;
615
+ function readBlobSync(b) {
616
+ const req = new XMLHttpRequest();
617
+ req.overrideMimeType("text/plain; charset=x-user-defined");
618
+ const url = URL.createObjectURL(b);
619
+ try {
620
+ req.open("GET", url, false); // Sync
621
+ req.send();
622
+ if (req.status !== 200 && req.status !== 0) {
623
+ throw new Error("Bad Blob access: " + req.status);
624
+ }
625
+ return req.responseText;
626
+ }
627
+ finally {
628
+ URL.revokeObjectURL(url);
629
+ }
401
630
  }
402
631
 
403
- const concat = [].concat;
404
- function flatten(a) {
405
- return concat.apply([], a);
406
- }
632
+ const numberTypeDef = {
633
+ number: {
634
+ replace: (num) => {
635
+ switch (true) {
636
+ case isNaN(num):
637
+ return { $t: "number", v: "NaN" };
638
+ case num === Infinity:
639
+ return { $t: "number", v: "Infinity" };
640
+ case num === -Infinity:
641
+ return { $t: "number", v: "-Infinity" };
642
+ default:
643
+ return num;
644
+ }
645
+ },
646
+ revive: ({ v }) => Number(v),
647
+ },
648
+ };
407
649
 
408
- function listClientChanges(mutationTables_1, db_1) {
409
- return __awaiter(this, arguments, void 0, function* (mutationTables, db, { since = {}, limit = Infinity } = {}) {
410
- const allMutsOnTables = yield Promise.all(mutationTables.map((mutationTable) => __awaiter(this, void 0, void 0, function* () {
411
- const tableName = getTableFromMutationTable(mutationTable.name);
412
- const lastRevision = since[tableName];
413
- let query = lastRevision
414
- ? mutationTable.where('rev').above(lastRevision)
415
- : mutationTable;
416
- if (limit < Infinity)
417
- query = query.limit(limit);
418
- let muts = yield query.toArray();
419
- muts = canonicalizeToUpdateOps(muts);
420
- muts = removeRedundantUpdateOps(muts);
421
- const rv = muts.map((mut) => ({
422
- table: tableName,
423
- mut,
424
- }));
425
- return rv;
426
- })));
427
- // Sort by time to get a true order of the operations (between tables)
428
- const sorted = flatten(allMutsOnTables).sort((a, b) => a.mut.txid === b.mut.txid
429
- ? a.mut.opNo - b.mut.opNo // Within same transaction, sort by opNo
430
- : a.mut.ts - b.mut.ts // Different transactions - sort by timestamp when mutation resolved
431
- );
432
- const result = [];
433
- let currentEntry = null;
434
- let currentTxid = null;
435
- for (const { table, mut } of sorted) {
436
- if (currentEntry &&
437
- currentEntry.table === table &&
438
- currentTxid === mut.txid) {
439
- currentEntry.muts.push(mut);
440
- }
441
- else {
442
- currentEntry = {
443
- table,
444
- muts: [mut],
445
- };
446
- currentTxid = mut.txid;
447
- result.push(currentEntry);
650
+ const dateTypeDef = {
651
+ Date: {
652
+ replace: (date) => ({
653
+ $t: "Date",
654
+ v: isNaN(date.getTime()) ? "NaN" : date.toISOString(),
655
+ }),
656
+ revive: ({ v }) => new Date(v === "NaN" ? NaN : Date.parse(v)),
657
+ },
658
+ };
659
+
660
+ const setTypeDef = {
661
+ Set: {
662
+ replace: (set) => ({
663
+ $t: "Set",
664
+ v: Array.from(set),
665
+ }),
666
+ revive: ({ v }) => new Set(v),
667
+ },
668
+ };
669
+
670
+ const mapTypeDef = {
671
+ Map: {
672
+ replace: (map) => ({
673
+ $t: "Map",
674
+ v: Array.from(map.entries()),
675
+ }),
676
+ revive: ({ v }) => new Map(v),
677
+ },
678
+ };
679
+
680
+ const _global = typeof globalThis !== "undefined" // All modern environments (node, bun, deno, browser, workers, webview etc)
681
+ ? globalThis
682
+ : typeof self !== "undefined" // Older browsers, workers, webview, window etc
683
+ ? self
684
+ : typeof global !== "undefined" // Older versions of node
685
+ ? global
686
+ : undefined; // Unsupported environment. No idea to return 'this' since we are in a module or a function scope anyway.
687
+
688
+ const typedArrayTypeDefs = [
689
+ "Int8Array",
690
+ "Uint8Array",
691
+ "Uint8ClampedArray",
692
+ "Int16Array",
693
+ "Uint16Array",
694
+ "Int32Array",
695
+ "Uint32Array",
696
+ "Float32Array",
697
+ "Float64Array",
698
+ "DataView",
699
+ "BigInt64Array",
700
+ "BigUint64Array",
701
+ ].reduce((specs, typeName) => ({
702
+ ...specs,
703
+ [typeName]: {
704
+ // Replace passes the typed array into $t, buffer so that
705
+ // the ArrayBuffer typedef takes care of further handling of the buffer:
706
+ // {$t:"Uint8Array",buffer:{$t:"ArrayBuffer",idx:0}}
707
+ // CHANGED ABOVE! Now shortcutting that for more sparse format of the typed arrays
708
+ // to contain the b64 property directly.
709
+ replace: (a, _, typeDefs) => {
710
+ const buffer = a.buffer;
711
+ const slicedBuffer = a.byteOffset === 0 && a.byteLength === buffer.byteLength
712
+ ? buffer
713
+ : buffer.slice(a.byteOffset, a.byteOffset + a.byteLength);
714
+ const result = {
715
+ $t: typeName,
716
+ v: typeDefs.ArrayBuffer.replace(slicedBuffer, _, typeDefs).v,
717
+ };
718
+ return result;
719
+ },
720
+ revive: ({ v }, _, typeDefs) => {
721
+ const TypedArray = _global[typeName];
722
+ return (TypedArray &&
723
+ new TypedArray(typeDefs.ArrayBuffer.revive({ v }, _, typeDefs)));
724
+ },
725
+ },
726
+ }), {});
727
+
728
+ const hasArrayBufferFromBase64 = "fromBase64" in Uint8Array; // https://github.com/tc39/proposal-arraybuffer-base64;
729
+ const hasArrayBufferToBase64 = "toBase64" in Uint8Array.prototype; // https://github.com/tc39/proposal-arraybuffer-base64;
730
+ const b64decode = typeof Buffer !== "undefined"
731
+ ? (base64) => Buffer.from(base64, "base64") // Node
732
+ : hasArrayBufferFromBase64
733
+ ? // @ts-ignore: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/fromBase64
734
+ (base64) => Uint8Array.fromBase64(base64) // Modern javascript standard
735
+ : (base64) => {
736
+ // Legacy DOM workaround
737
+ const binary_string = atob(base64);
738
+ const len = binary_string.length;
739
+ const bytes = new Uint8Array(len);
740
+ for (var i = 0; i < len; i++) {
741
+ bytes[i] = binary_string.charCodeAt(i);
448
742
  }
743
+ return bytes;
744
+ };
745
+ const b64encode = typeof Buffer !== "undefined"
746
+ ? (b) => {
747
+ // Node
748
+ if (ArrayBuffer.isView(b)) {
749
+ return Buffer.from(b.buffer, b.byteOffset, b.byteLength).toString("base64");
449
750
  }
450
- // Filter out those tables that doesn't have any mutations:
451
- return result;
452
- });
453
- }
454
- function removeRedundantUpdateOps(muts) {
455
- const updateCoverage = new Map();
456
- for (const mut of muts) {
457
- if (mut.type === 'update') {
458
- if (mut.keys.length !== 1 || mut.changeSpecs.length !== 1) {
459
- continue; // Don't optimize multi-key updates
460
- }
461
- const strKey = '' + mut.keys[0];
462
- const changeSpecs = mut.changeSpecs[0];
463
- if (Object.values(changeSpecs).some(v => typeof v === "object" && v && "@@propmod" in v)) {
464
- continue; // Cannot optimize if any PropModification is present
465
- }
466
- let keyCoverage = updateCoverage.get(strKey);
467
- if (keyCoverage) {
468
- keyCoverage.push({ txid: mut.txid, updateSpec: changeSpecs });
469
- }
470
- else {
471
- updateCoverage.set(strKey, [{ txid: mut.txid, updateSpec: changeSpecs }]);
472
- }
751
+ else {
752
+ return Buffer.from(b).toString("base64");
473
753
  }
474
754
  }
475
- muts = muts.filter(mut => {
476
- // Only apply optimization to update mutations that are single-key
477
- if (mut.type !== 'update')
478
- return true;
479
- if (mut.keys.length !== 1 || mut.changeSpecs.length !== 1)
480
- return true;
481
- // Check if this has PropModifications - if so, skip optimization
482
- const changeSpecs = mut.changeSpecs[0];
483
- if (Object.values(changeSpecs).some(v => typeof v === "object" && v && "@@propmod" in v)) {
484
- return true; // Cannot optimize if any PropModification is present
755
+ : hasArrayBufferToBase64
756
+ ? (b) => {
757
+ // Uint8Array.prototype.toBase64 is available in modern browsers
758
+ const u8a = ArrayBuffer.isView(b) ? b : new Uint8Array(b);
759
+ // @ts-ignore: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/toBase64
760
+ return u8a.toBase64();
485
761
  }
486
- // Keep track of properties that aren't overlapped by later transactions
487
- const unoverlappedProps = new Set(Object.keys(mut.changeSpecs[0]));
488
- const strKey = '' + mut.keys[0];
489
- const keyCoverage = updateCoverage.get(strKey);
490
- if (!keyCoverage)
491
- return true; // No coverage info - cannot optimize
492
- for (let i = keyCoverage.length - 1; i >= 0; --i) {
493
- const { txid, updateSpec } = keyCoverage[i];
494
- if (txid === mut.txid)
495
- break; // Stop when reaching own txid
496
- // If all changes in updateSpec are covered by all props on all mut.changeSpecs then
497
- // txid is redundant and can be removed.
498
- for (const keyPath of Object.keys(updateSpec)) {
499
- unoverlappedProps.delete(keyPath);
762
+ : (b) => {
763
+ // Legacy DOM workaround
764
+ const u8a = ArrayBuffer.isView(b) ? b : new Uint8Array(b);
765
+ const CHUNK_SIZE = 0x1000;
766
+ const strs = [];
767
+ for (let i = 0, l = u8a.length; i < l; i += CHUNK_SIZE) {
768
+ const chunk = u8a.subarray(i, i + CHUNK_SIZE);
769
+ strs.push(String.fromCharCode.apply(null, Array.from(chunk)));
500
770
  }
501
- }
502
- if (unoverlappedProps.size === 0) {
503
- // This operation is completely overlapped by later operations. It can be removed.
504
- return false;
505
- }
506
- return true;
507
- });
508
- return muts;
771
+ return btoa(strs.join(""));
772
+ };
773
+
774
+ function b64LexEncode(b) {
775
+ return b64ToLex(b64encode(b));
509
776
  }
510
- function canonicalizeToUpdateOps(muts) {
511
- muts = muts.map(mut => {
512
- if (mut.type === 'modify' && mut.criteria.index === null) {
513
- // The criteria is on primary key. Convert to an update operation instead.
514
- // It is simpler for the server to handle and also more efficient.
515
- const updateMut = Object.assign(Object.assign({}, mut), { criteria: undefined, changeSpec: undefined, type: 'update', keys: mut.keys, changeSpecs: [mut.changeSpec] });
516
- delete updateMut.criteria;
517
- delete updateMut.changeSpec;
518
- return updateMut;
519
- }
520
- return mut;
521
- });
522
- return muts;
777
+ function b64LexDecode(b64Lex) {
778
+ return b64decode(lexToB64(b64Lex));
523
779
  }
524
-
525
- function randomString$1(bytes) {
526
- const buf = new Uint8Array(bytes);
527
- if (typeof crypto !== 'undefined') {
528
- crypto.getRandomValues(buf);
529
- }
530
- else {
531
- for (let i = 0; i < bytes; i++)
532
- buf[i] = Math.floor(Math.random() * 256);
533
- }
534
- if (typeof Buffer !== 'undefined' && Buffer.from) {
535
- return Buffer.from(buf).toString('base64');
780
+ function b64ToLex(base64) {
781
+ var encoded = "";
782
+ for (var i = 0, length = base64.length; i < length; i++) {
783
+ encoded += ENCODE_TABLE[base64[i]];
536
784
  }
537
- else if (typeof btoa !== 'undefined') {
538
- return btoa(String.fromCharCode.apply(null, buf));
785
+ return encoded;
786
+ }
787
+ function lexToB64(base64lex) {
788
+ // only accept string input
789
+ if (typeof base64lex !== "string") {
790
+ throw new Error("invalid decoder input: " + base64lex);
539
791
  }
540
- else {
541
- throw new Error('No btoa or Buffer available');
792
+ var base64 = "";
793
+ for (var i = 0, length = base64lex.length; i < length; i++) {
794
+ base64 += DECODE_TABLE[base64lex[i]];
542
795
  }
796
+ return base64;
543
797
  }
544
-
545
- function assert(b) {
546
- if (!b)
547
- throw new Error('Assertion Failed');
548
- }
549
- const _hasOwn = {}.hasOwnProperty;
550
- function hasOwn(obj, prop) {
551
- return _hasOwn.call(obj, prop);
552
- }
553
- function setByKeyPath(obj, keyPath, value) {
554
- if (!obj || keyPath === undefined)
555
- return;
556
- if ('isFrozen' in Object && Object.isFrozen(obj))
798
+ const DECODE_TABLE = {
799
+ "-": "=",
800
+ "0": "A",
801
+ "1": "B",
802
+ "2": "C",
803
+ "3": "D",
804
+ "4": "E",
805
+ "5": "F",
806
+ "6": "G",
807
+ "7": "H",
808
+ "8": "I",
809
+ "9": "J",
810
+ A: "K",
811
+ B: "L",
812
+ C: "M",
813
+ D: "N",
814
+ E: "O",
815
+ F: "P",
816
+ G: "Q",
817
+ H: "R",
818
+ I: "S",
819
+ J: "T",
820
+ K: "U",
821
+ L: "V",
822
+ M: "W",
823
+ N: "X",
824
+ O: "Y",
825
+ P: "Z",
826
+ Q: "a",
827
+ R: "b",
828
+ S: "c",
829
+ T: "d",
830
+ U: "e",
831
+ V: "f",
832
+ W: "g",
833
+ X: "h",
834
+ Y: "i",
835
+ Z: "j",
836
+ _: "k",
837
+ a: "l",
838
+ b: "m",
839
+ c: "n",
840
+ d: "o",
841
+ e: "p",
842
+ f: "q",
843
+ g: "r",
844
+ h: "s",
845
+ i: "t",
846
+ j: "u",
847
+ k: "v",
848
+ l: "w",
849
+ m: "x",
850
+ n: "y",
851
+ o: "z",
852
+ p: "0",
853
+ q: "1",
854
+ r: "2",
855
+ s: "3",
856
+ t: "4",
857
+ u: "5",
858
+ v: "6",
859
+ w: "7",
860
+ x: "8",
861
+ y: "9",
862
+ z: "+",
863
+ "|": "/",
864
+ };
865
+ const ENCODE_TABLE = {};
866
+ for (const c of Object.keys(DECODE_TABLE)) {
867
+ ENCODE_TABLE[DECODE_TABLE[c]] = c;
868
+ }
869
+
870
+ const arrayBufferTypeDef = {
871
+ ArrayBuffer: {
872
+ replace: (ab) => ({
873
+ $t: "ArrayBuffer",
874
+ v: b64LexEncode(ab),
875
+ }),
876
+ revive: ({ v }) => {
877
+ const ba = b64LexDecode(v);
878
+ const buf = ba.buffer.byteLength === ba.byteLength
879
+ ? ba.buffer
880
+ : ba.buffer.slice(ba.byteOffset, ba.byteOffset + ba.byteLength);
881
+ return buf;
882
+ },
883
+ },
884
+ };
885
+
886
+ function string2ArrayBuffer(str) {
887
+ const array = new Uint8Array(str.length);
888
+ for (let i = 0; i < str.length; ++i) {
889
+ array[i] = str.charCodeAt(i); // & 0xff;
890
+ }
891
+ return array.buffer;
892
+ }
893
+
894
+ const blobTypeDef = {
895
+ Blob: {
896
+ test: (blob, toStringTag) => toStringTag === "Blob" || blob instanceof FakeBlob,
897
+ replace: (blob) => ({
898
+ $t: "Blob",
899
+ v: blob instanceof FakeBlob
900
+ ? b64encode(blob.buf)
901
+ : b64encode(string2ArrayBuffer(readBlobSync(blob))),
902
+ type: blob.type,
903
+ }),
904
+ revive: ({ type, v }) => {
905
+ const ab = b64decode(v);
906
+ const buf = ab.buffer.byteLength === ab.byteLength
907
+ ? ab.buffer
908
+ : ab.buffer.slice(ab.byteOffset, ab.byteOffset + ab.byteLength);
909
+ return typeof Blob !== "undefined"
910
+ ? new Blob([new Uint8Array(buf)], { type })
911
+ : new FakeBlob(buf, type);
912
+ },
913
+ },
914
+ };
915
+
916
+ ({
917
+ ...numberTypeDef,
918
+ ...dateTypeDef,
919
+ ...setTypeDef,
920
+ ...mapTypeDef,
921
+ ...typedArrayTypeDefs,
922
+ ...arrayBufferTypeDef,
923
+ ...blobTypeDef, // Should be moved to another preset for DOM types (or universal? since it supports node as well with FakeBlob)
924
+ });
925
+
926
+ const fileTypeDef = {
927
+ File: {
928
+ test: (file, toStringTag) => toStringTag === "File",
929
+ replace: (file) => ({
930
+ $t: "File",
931
+ v: b64encode(string2ArrayBuffer(readBlobSync(file))),
932
+ type: file.type,
933
+ name: file.name,
934
+ lastModified: new Date(file.lastModified).toISOString(),
935
+ }),
936
+ revive: ({ type, v, name, lastModified }) => {
937
+ const ab = b64decode(v);
938
+ const buf = ab.buffer.byteLength === ab.byteLength
939
+ ? ab.buffer
940
+ : ab.buffer.slice(ab.byteOffset, ab.byteOffset + ab.byteLength);
941
+ return new File([new Uint8Array(buf)], name, {
942
+ type,
943
+ lastModified: new Date(lastModified).getTime(),
944
+ });
945
+ },
946
+ },
947
+ };
948
+
949
+ /** The undefined type is not part of builtin but can be manually added.
950
+ * The reason for supporting undefined is if the following object should be revived correctly:
951
+ *
952
+ * {foo: undefined}
953
+ *
954
+ * Without including this typedef, the revived object would just be {}.
955
+ * If including this typedef, the revived object would be {foo: undefined}.
956
+ */
957
+ const undefinedTypeDef = {
958
+ undefined: {
959
+ replace: () => ({
960
+ $t: "undefined",
961
+ }),
962
+ revive: () => undefined,
963
+ },
964
+ };
965
+
966
+ const getRandomValues$1 = typeof crypto !== "undefined"
967
+ ? crypto.getRandomValues.bind(crypto)
968
+ : (buf) => {
969
+ for (let i = 0; i < buf.length; ++i) {
970
+ buf[i] = Math.floor(Math.random() * 256);
971
+ }
972
+ };
973
+ let time$1 = 0;
974
+ /**
975
+ * Generates unique ID where bytes 0-6 represents a timestampish value
976
+ * instead of random, similary to UUID version 1 but with random istead of MAC address.
977
+ *
978
+ * With "timestampish" we mean milliseconds from 1970 approximately, as in bulk-creation
979
+ * scenarios, milliseconds in future will be used (while creating more than 1 id per
980
+ * millisecond)
981
+ *
982
+ * This is similary UUID version 1 but with random instead of Mac, and with
983
+ * support for generating unique IDs the same millisecond.
984
+ *
985
+ * It's even more similar to the "version 6" proposal at
986
+ * https://bradleypeabody.github.io/uuidv6/.
987
+ *
988
+ * Difference from "version 6" proposal is that we keep the clock-sequence within
989
+ * the timestamp part to allow 9 more bits for randomness. This is at the cost of
990
+ * knwoing how exact the time-stamp is. But since we anyway don't expect a perfect
991
+ * time stamps as many clients may have wrong time settings, what we want is just
992
+ * a sorted ID, still universially unique.
993
+ *
994
+ * Random part is totally 73 bits entropy, which basically means that a collisions would
995
+ * be likely if 9 444 732 965 739 290 427 392 devices was generating ids during the exact same
996
+ * millisecond.
997
+ *
998
+ */
999
+ function newId() {
1000
+ const a = new Uint8Array(18);
1001
+ const timePart = new Uint8Array(a.buffer, 0, 6);
1002
+ const now = Date.now(); // Will fit into 6 bytes until year 10 895.
1003
+ if (time$1 >= now) {
1004
+ // User is bulk-creating objects the same millisecond.
1005
+ // Increment the time part by one millisecond for each item.
1006
+ // If bulk-creating 1,000,000 rows client-side in 0 seconds,
1007
+ // the last time-stamp will be 1,000 seconds in future, which is no biggie at all.
1008
+ // The point is to create a nice order of the generated IDs instead of
1009
+ // using random ids.
1010
+ ++time$1;
1011
+ }
1012
+ else {
1013
+ time$1 = now;
1014
+ }
1015
+ timePart[0] = time$1 / 0x10000000000;
1016
+ timePart[1] = time$1 / 0x100000000;
1017
+ timePart[2] = time$1 / 0x1000000;
1018
+ timePart[3] = time$1 / 0x10000;
1019
+ timePart[4] = time$1 / 0x100;
1020
+ timePart[5] = time$1;
1021
+ const randomPart = new Uint8Array(a.buffer, 6);
1022
+ getRandomValues$1(randomPart);
1023
+ return b64LexEncode(a);
1024
+ }
1025
+
1026
+ function assert(b) {
1027
+ if (!b)
1028
+ throw new Error('Assertion Failed');
1029
+ }
1030
+ const _hasOwn = {}.hasOwnProperty;
1031
+ function hasOwn(obj, prop) {
1032
+ return _hasOwn.call(obj, prop);
1033
+ }
1034
+ function setByKeyPath(obj, keyPath, value) {
1035
+ if (!obj || keyPath === undefined)
1036
+ return;
1037
+ if ('isFrozen' in Object && Object.isFrozen(obj))
557
1038
  return;
558
1039
  if (typeof keyPath !== 'string' && 'length' in keyPath) {
559
1040
  assert(typeof value !== 'string' && 'length' in value);
@@ -602,7 +1083,7 @@
602
1083
  }
603
1084
  }
604
1085
  }
605
- const randomString = typeof self !== 'undefined' && typeof crypto !== 'undefined' ? (bytes, randomFill = crypto.getRandomValues.bind(crypto)) => {
1086
+ const randomString$1 = typeof self !== 'undefined' && typeof crypto !== 'undefined' ? (bytes, randomFill = crypto.getRandomValues.bind(crypto)) => {
606
1087
  // Web
607
1088
  const buf = new Uint8Array(bytes);
608
1089
  randomFill(buf);
@@ -2376,77 +2857,251 @@
2376
2857
  };
2377
2858
  }
2378
2859
 
2379
- function listSyncifiedChanges(tablesToSyncify, currentUser, schema, alreadySyncedRealms) {
2380
- return __awaiter(this, void 0, void 0, function* () {
2381
- const txid = `upload-${randomString$1(8)}`;
2382
- if (currentUser.isLoggedIn) {
2383
- if (tablesToSyncify.length > 0) {
2384
- const ignoredRealms = new Set(alreadySyncedRealms || []);
2385
- const upserts = yield Promise.all(tablesToSyncify.map((table) => __awaiter(this, void 0, void 0, function* () {
2386
- const { extractKey } = table.core.schema.primaryKey;
2387
- if (!extractKey)
2388
- return { table: table.name, muts: [] }; // Outbound tables are not synced.
2389
- const dexieCloudTableSchema = schema[table.name];
2390
- const query = (dexieCloudTableSchema === null || dexieCloudTableSchema === void 0 ? void 0 : dexieCloudTableSchema.generatedGlobalId)
2391
- ? table.filter((item) => {
2392
- extractKey(item);
2393
- return (!ignoredRealms.has(item.realmId || '') &&
2394
- //(id[0] !== '#' || !!item.$ts) && // Private obj need no sync if not changed
2395
- isValidAtID(extractKey(item), dexieCloudTableSchema === null || dexieCloudTableSchema === void 0 ? void 0 : dexieCloudTableSchema.idPrefix));
2396
- })
2397
- : table.filter((item) => {
2398
- const id = extractKey(item);
2399
- return (!ignoredRealms.has(item.realmId || '') &&
2400
- //(id[0] !== '#' || !!item.$ts) && // Private obj need no sync if not changed
2401
- isValidSyncableID(id));
2402
- });
2403
- const unsyncedObjects = yield query.toArray();
2404
- if (unsyncedObjects.length > 0) {
2405
- const mut = {
2406
- type: 'upsert',
2407
- values: unsyncedObjects,
2408
- keys: unsyncedObjects.map(extractKey),
2409
- userId: currentUser.userId,
2410
- txid,
2411
- };
2412
- return {
2413
- table: table.name,
2414
- muts: [mut],
2415
- };
2416
- }
2417
- else {
2418
- return {
2419
- table: table.name,
2420
- muts: [],
2421
- };
2422
- }
2423
- })));
2424
- return upserts.filter((op) => op.muts.length > 0);
2425
- }
2426
- }
2427
- return [];
2860
+ function computeRealmSetHash(_a) {
2861
+ return __awaiter(this, arguments, void 0, function* ({ realms, inviteRealms, }) {
2862
+ const data = JSON.stringify([
2863
+ ...realms.map((realmId) => ({ realmId, accepted: true })),
2864
+ ...inviteRealms.map((realmId) => ({ realmId, accepted: false })),
2865
+ ].sort((a, b) => a.realmId < b.realmId ? -1 : a.realmId > b.realmId ? 1 : 0));
2866
+ const byteArray = new TextEncoder().encode(data);
2867
+ const digestBytes = yield crypto.subtle.digest('SHA-1', byteArray);
2868
+ const base64 = b64encode(digestBytes);
2869
+ return base64;
2428
2870
  });
2429
2871
  }
2430
2872
 
2431
- function getTablesToSyncify(db, syncState) {
2432
- const syncedTables = (syncState === null || syncState === void 0 ? void 0 : syncState.syncedTables) || [];
2433
- const syncableTables = getSyncableTables(db);
2434
- const tablesToSyncify = syncableTables.filter((tbl) => !syncedTables.includes(tbl.name));
2435
- return tablesToSyncify;
2873
+ function getSyncableTables(db) {
2874
+ return Object.entries(db.cloud.schema || {})
2875
+ .filter(([, { markedForSync }]) => markedForSync)
2876
+ .map(([tbl]) => db.tables.find(({ name }) => name === tbl))
2877
+ .filter((syncableTable) => !!syncableTable);
2436
2878
  }
2437
2879
 
2438
- class TokenErrorResponseError extends Error {
2439
- constructor({ title, message, messageCode, messageParams, }) {
2440
- super(message);
2441
- this.name = 'TokenErrorResponseError';
2442
- this.title = title;
2443
- this.messageCode = messageCode;
2444
- this.messageParams = messageParams;
2445
- }
2880
+ function getMutationTable(tableName) {
2881
+ return `$${tableName}_mutations`;
2446
2882
  }
2447
2883
 
2448
- /** Email/envelope icon data URL for OTP option */
2449
- const EmailIcon = `data:image/svg+xml;base64,${btoa('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="#666666" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M22 6L12 13 2 6"/></svg>')}`;
2884
+ function getTableFromMutationTable(mutationTable) {
2885
+ var _a;
2886
+ const tableName = (_a = /^\$(.*)_mutations$/.exec(mutationTable)) === null || _a === void 0 ? void 0 : _a[1];
2887
+ if (!tableName)
2888
+ throw new Error(`Given mutationTable ${mutationTable} is not correct`);
2889
+ return tableName;
2890
+ }
2891
+
2892
+ const concat = [].concat;
2893
+ function flatten(a) {
2894
+ return concat.apply([], a);
2895
+ }
2896
+
2897
+ function listClientChanges(mutationTables_1, db_1) {
2898
+ return __awaiter(this, arguments, void 0, function* (mutationTables, db, { since = {}, limit = Infinity } = {}) {
2899
+ const allMutsOnTables = yield Promise.all(mutationTables.map((mutationTable) => __awaiter(this, void 0, void 0, function* () {
2900
+ const tableName = getTableFromMutationTable(mutationTable.name);
2901
+ const lastRevision = since[tableName];
2902
+ let query = lastRevision
2903
+ ? mutationTable.where('rev').above(lastRevision)
2904
+ : mutationTable;
2905
+ if (limit < Infinity)
2906
+ query = query.limit(limit);
2907
+ let muts = yield query.toArray();
2908
+ muts = canonicalizeToUpdateOps(muts);
2909
+ muts = removeRedundantUpdateOps(muts);
2910
+ const rv = muts.map((mut) => ({
2911
+ table: tableName,
2912
+ mut,
2913
+ }));
2914
+ return rv;
2915
+ })));
2916
+ // Sort by time to get a true order of the operations (between tables)
2917
+ const sorted = flatten(allMutsOnTables).sort((a, b) => a.mut.txid === b.mut.txid
2918
+ ? a.mut.opNo - b.mut.opNo // Within same transaction, sort by opNo
2919
+ : a.mut.ts - b.mut.ts // Different transactions - sort by timestamp when mutation resolved
2920
+ );
2921
+ const result = [];
2922
+ let currentEntry = null;
2923
+ let currentTxid = null;
2924
+ for (const { table, mut } of sorted) {
2925
+ if (currentEntry &&
2926
+ currentEntry.table === table &&
2927
+ currentTxid === mut.txid) {
2928
+ currentEntry.muts.push(mut);
2929
+ }
2930
+ else {
2931
+ currentEntry = {
2932
+ table,
2933
+ muts: [mut],
2934
+ };
2935
+ currentTxid = mut.txid;
2936
+ result.push(currentEntry);
2937
+ }
2938
+ }
2939
+ // Filter out those tables that doesn't have any mutations:
2940
+ return result;
2941
+ });
2942
+ }
2943
+ function removeRedundantUpdateOps(muts) {
2944
+ const updateCoverage = new Map();
2945
+ for (const mut of muts) {
2946
+ if (mut.type === 'update') {
2947
+ if (mut.keys.length !== 1 || mut.changeSpecs.length !== 1) {
2948
+ continue; // Don't optimize multi-key updates
2949
+ }
2950
+ const strKey = '' + mut.keys[0];
2951
+ const changeSpecs = mut.changeSpecs[0];
2952
+ if (Object.values(changeSpecs).some(v => typeof v === "object" && v && "@@propmod" in v)) {
2953
+ continue; // Cannot optimize if any PropModification is present
2954
+ }
2955
+ let keyCoverage = updateCoverage.get(strKey);
2956
+ if (keyCoverage) {
2957
+ keyCoverage.push({ txid: mut.txid, updateSpec: changeSpecs });
2958
+ }
2959
+ else {
2960
+ updateCoverage.set(strKey, [{ txid: mut.txid, updateSpec: changeSpecs }]);
2961
+ }
2962
+ }
2963
+ }
2964
+ muts = muts.filter(mut => {
2965
+ // Only apply optimization to update mutations that are single-key
2966
+ if (mut.type !== 'update')
2967
+ return true;
2968
+ if (mut.keys.length !== 1 || mut.changeSpecs.length !== 1)
2969
+ return true;
2970
+ // Check if this has PropModifications - if so, skip optimization
2971
+ const changeSpecs = mut.changeSpecs[0];
2972
+ if (Object.values(changeSpecs).some(v => typeof v === "object" && v && "@@propmod" in v)) {
2973
+ return true; // Cannot optimize if any PropModification is present
2974
+ }
2975
+ // Keep track of properties that aren't overlapped by later transactions
2976
+ const unoverlappedProps = new Set(Object.keys(mut.changeSpecs[0]));
2977
+ const strKey = '' + mut.keys[0];
2978
+ const keyCoverage = updateCoverage.get(strKey);
2979
+ if (!keyCoverage)
2980
+ return true; // No coverage info - cannot optimize
2981
+ for (let i = keyCoverage.length - 1; i >= 0; --i) {
2982
+ const { txid, updateSpec } = keyCoverage[i];
2983
+ if (txid === mut.txid)
2984
+ break; // Stop when reaching own txid
2985
+ // If all changes in updateSpec are covered by all props on all mut.changeSpecs then
2986
+ // txid is redundant and can be removed.
2987
+ for (const keyPath of Object.keys(updateSpec)) {
2988
+ unoverlappedProps.delete(keyPath);
2989
+ }
2990
+ }
2991
+ if (unoverlappedProps.size === 0) {
2992
+ // This operation is completely overlapped by later operations. It can be removed.
2993
+ return false;
2994
+ }
2995
+ return true;
2996
+ });
2997
+ return muts;
2998
+ }
2999
+ function canonicalizeToUpdateOps(muts) {
3000
+ muts = muts.map(mut => {
3001
+ if (mut.type === 'modify' && mut.criteria.index === null) {
3002
+ // The criteria is on primary key. Convert to an update operation instead.
3003
+ // It is simpler for the server to handle and also more efficient.
3004
+ const updateMut = Object.assign(Object.assign({}, mut), { criteria: undefined, changeSpec: undefined, type: 'update', keys: mut.keys, changeSpecs: [mut.changeSpec] });
3005
+ delete updateMut.criteria;
3006
+ delete updateMut.changeSpec;
3007
+ return updateMut;
3008
+ }
3009
+ return mut;
3010
+ });
3011
+ return muts;
3012
+ }
3013
+
3014
+ function randomString(bytes) {
3015
+ const buf = new Uint8Array(bytes);
3016
+ if (typeof crypto !== 'undefined') {
3017
+ crypto.getRandomValues(buf);
3018
+ }
3019
+ else {
3020
+ for (let i = 0; i < bytes; i++)
3021
+ buf[i] = Math.floor(Math.random() * 256);
3022
+ }
3023
+ if (typeof Buffer !== 'undefined' && Buffer.from) {
3024
+ return Buffer.from(buf).toString('base64');
3025
+ }
3026
+ else if (typeof btoa !== 'undefined') {
3027
+ return btoa(String.fromCharCode.apply(null, buf));
3028
+ }
3029
+ else {
3030
+ throw new Error('No btoa or Buffer available');
3031
+ }
3032
+ }
3033
+
3034
+ function listSyncifiedChanges(tablesToSyncify, currentUser, schema, alreadySyncedRealms) {
3035
+ return __awaiter(this, void 0, void 0, function* () {
3036
+ const txid = `upload-${randomString(8)}`;
3037
+ if (currentUser.isLoggedIn) {
3038
+ if (tablesToSyncify.length > 0) {
3039
+ const ignoredRealms = new Set(alreadySyncedRealms || []);
3040
+ const upserts = yield Promise.all(tablesToSyncify.map((table) => __awaiter(this, void 0, void 0, function* () {
3041
+ const { extractKey } = table.core.schema.primaryKey;
3042
+ if (!extractKey)
3043
+ return { table: table.name, muts: [] }; // Outbound tables are not synced.
3044
+ const dexieCloudTableSchema = schema[table.name];
3045
+ const query = (dexieCloudTableSchema === null || dexieCloudTableSchema === void 0 ? void 0 : dexieCloudTableSchema.generatedGlobalId)
3046
+ ? table.filter((item) => {
3047
+ extractKey(item);
3048
+ return (!ignoredRealms.has(item.realmId || '') &&
3049
+ //(id[0] !== '#' || !!item.$ts) && // Private obj need no sync if not changed
3050
+ isValidAtID(extractKey(item), dexieCloudTableSchema === null || dexieCloudTableSchema === void 0 ? void 0 : dexieCloudTableSchema.idPrefix));
3051
+ })
3052
+ : table.filter((item) => {
3053
+ const id = extractKey(item);
3054
+ return (!ignoredRealms.has(item.realmId || '') &&
3055
+ //(id[0] !== '#' || !!item.$ts) && // Private obj need no sync if not changed
3056
+ isValidSyncableID(id));
3057
+ });
3058
+ const unsyncedObjects = yield query.toArray();
3059
+ if (unsyncedObjects.length > 0) {
3060
+ const mut = {
3061
+ type: 'upsert',
3062
+ values: unsyncedObjects,
3063
+ keys: unsyncedObjects.map(extractKey),
3064
+ userId: currentUser.userId,
3065
+ txid,
3066
+ };
3067
+ return {
3068
+ table: table.name,
3069
+ muts: [mut],
3070
+ };
3071
+ }
3072
+ else {
3073
+ return {
3074
+ table: table.name,
3075
+ muts: [],
3076
+ };
3077
+ }
3078
+ })));
3079
+ return upserts.filter((op) => op.muts.length > 0);
3080
+ }
3081
+ }
3082
+ return [];
3083
+ });
3084
+ }
3085
+
3086
+ function getTablesToSyncify(db, syncState) {
3087
+ const syncedTables = (syncState === null || syncState === void 0 ? void 0 : syncState.syncedTables) || [];
3088
+ const syncableTables = getSyncableTables(db);
3089
+ const tablesToSyncify = syncableTables.filter((tbl) => !syncedTables.includes(tbl.name));
3090
+ return tablesToSyncify;
3091
+ }
3092
+
3093
+ class TokenErrorResponseError extends Error {
3094
+ constructor({ title, message, messageCode, messageParams, }) {
3095
+ super(message);
3096
+ this.name = 'TokenErrorResponseError';
3097
+ this.title = title;
3098
+ this.messageCode = messageCode;
3099
+ this.messageParams = messageParams;
3100
+ }
3101
+ }
3102
+
3103
+ /** Email/envelope icon data URL for OTP option */
3104
+ const EmailIcon = `data:image/svg+xml;base64,${btoa('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="#666666" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M22 6L12 13 2 6"/></svg>')}`;
2450
3105
  /**
2451
3106
  * Converts an OAuthProviderInfo to a generic DXCOption.
2452
3107
  */
@@ -2670,6 +3325,9 @@
2670
3325
  }
2671
3326
  }
2672
3327
 
3328
+ const SECONDS = 1000;
3329
+ const MINUTES = 60 * SECONDS;
3330
+
2673
3331
  function loadAccessToken(db) {
2674
3332
  return __awaiter(this, void 0, void 0, function* () {
2675
3333
  var _a, _b, _c;
@@ -2678,7 +3336,7 @@
2678
3336
  if (!accessToken)
2679
3337
  return null;
2680
3338
  const expTime = (_a = accessTokenExpiration === null || accessTokenExpiration === void 0 ? void 0 : accessTokenExpiration.getTime()) !== null && _a !== void 0 ? _a : Infinity;
2681
- if (expTime > Date.now() && (((_b = currentUser.license) === null || _b === void 0 ? void 0 : _b.status) || 'ok') === 'ok') {
3339
+ if (expTime > (Date.now() + 5 * MINUTES) && (((_b = currentUser.license) === null || _b === void 0 ? void 0 : _b.status) || 'ok') === 'ok') {
2682
3340
  return currentUser;
2683
3341
  }
2684
3342
  if (!refreshToken) {
@@ -2799,608 +3457,104 @@
2799
3457
  if (response2.type === 'error') {
2800
3458
  throw new TokenErrorResponseError(response2);
2801
3459
  }
2802
- if (response2.type !== 'tokens')
2803
- throw new Error(`Unexpected response type from token endpoint: ${response2.type}`);
2804
- /*const licenseStatus = response2.claims.license || 'ok';
2805
- if (licenseStatus !== 'ok') {
2806
- throw new InvalidLicenseError(licenseStatus);
2807
- }*/
2808
- context.accessToken = response2.accessToken;
2809
- context.accessTokenExpiration = new Date(response2.accessTokenExpiration);
2810
- context.refreshToken = response2.refreshToken;
2811
- if (response2.refreshTokenExpiration) {
2812
- context.refreshTokenExpiration = new Date(response2.refreshTokenExpiration);
2813
- }
2814
- context.userId = response2.claims.sub;
2815
- context.email = response2.claims.email;
2816
- context.name = response2.claims.name;
2817
- context.claims = response2.claims;
2818
- context.license = {
2819
- type: response2.userType,
2820
- status: response2.claims.license || 'ok',
2821
- };
2822
- context.data = response2.data;
2823
- if (response2.evalDaysLeft != null) {
2824
- context.license.evalDaysLeft = response2.evalDaysLeft;
2825
- }
2826
- if (response2.userValidUntil != null) {
2827
- context.license.validUntil = new Date(response2.userValidUntil);
2828
- }
2829
- if (response2.alerts && response2.alerts.length > 0) {
2830
- yield interactWithUser(userInteraction, {
2831
- type: 'message-alert',
2832
- title: 'Authentication Alert',
2833
- fields: {},
2834
- alerts: response2.alerts,
2835
- });
2836
- }
2837
- return context;
2838
- }
2839
- catch (error) {
2840
- // OAuth redirect is not an error - page is navigating away
2841
- if (error instanceof OAuthRedirectError || (error === null || error === void 0 ? void 0 : error.name) === 'OAuthRedirectError') {
2842
- throw error; // Re-throw without logging
2843
- }
2844
- if (error instanceof TokenErrorResponseError) {
2845
- yield alertUser(userInteraction, error.title, {
2846
- type: 'error',
2847
- messageCode: error.messageCode,
2848
- message: error.message,
2849
- messageParams: {},
2850
- });
2851
- throw error;
2852
- }
2853
- let message = `We're having a problem authenticating right now.`;
2854
- console.error(`Error authenticating`, error);
2855
- if (error instanceof TypeError) {
2856
- const isOffline = typeof navigator !== undefined && !navigator.onLine;
2857
- if (isOffline) {
2858
- message = `You seem to be offline. Please connect to the internet and try again.`;
2859
- }
2860
- else if (Dexie.debug || (typeof location !== 'undefined' && (location.hostname === 'localhost' || location.hostname === '127.0.0.1'))) {
2861
- // The audience is most likely the developer. Suggest to whitelist the localhost origin:
2862
- message = `Could not connect to server. Please verify that your origin '${location.origin}' is whitelisted using \`npx dexie-cloud whitelist\``;
2863
- }
2864
- else {
2865
- message = `Could not connect to server. Please verify the connection.`;
2866
- }
2867
- yield alertUser(userInteraction, 'Authentication Failed', {
2868
- type: 'error',
2869
- messageCode: 'GENERIC_ERROR',
2870
- message,
2871
- messageParams: {},
2872
- }).catch(() => { });
2873
- }
2874
- throw error;
2875
- }
2876
- });
2877
- }
2878
- function spkiToPEM(keydata) {
2879
- const keydataB64 = b64encode(keydata);
2880
- const keydataB64Pem = formatAsPem(keydataB64);
2881
- return keydataB64Pem;
2882
- }
2883
- function formatAsPem(str) {
2884
- let finalString = '-----BEGIN PUBLIC KEY-----\n';
2885
- while (str.length > 0) {
2886
- finalString += str.substring(0, 64) + '\n';
2887
- str = str.substring(64);
2888
- }
2889
- finalString = finalString + '-----END PUBLIC KEY-----';
2890
- return finalString;
2891
- }
2892
-
2893
- const { toString: toStr } = {};
2894
- function getToStringTag(val) {
2895
- return toStr.call(val).slice(8, -1);
2896
- }
2897
- function escapeDollarProps(value) {
2898
- const keys = Object.keys(value);
2899
- let dollarKeys = null;
2900
- for (let i = 0, l = keys.length; i < l; ++i) {
2901
- if (keys[i][0] === "$") {
2902
- dollarKeys = dollarKeys || [];
2903
- dollarKeys.push(keys[i]);
2904
- }
2905
- }
2906
- if (!dollarKeys)
2907
- return value;
2908
- const clone = { ...value };
2909
- for (const k of dollarKeys) {
2910
- delete clone[k];
2911
- }
2912
- for (const k of dollarKeys) {
2913
- clone["$" + k] = value[k];
2914
- }
2915
- return clone;
2916
- }
2917
- const ObjectDef = {
2918
- replace: escapeDollarProps,
2919
- };
2920
- function TypesonSimplified(...typeDefsInputs) {
2921
- const typeDefs = typeDefsInputs.reduce((p, c) => ({ ...p, ...c }), typeDefsInputs.reduce((p, c) => ({ ...c, ...p }), {}));
2922
- const protoMap = new WeakMap();
2923
- return {
2924
- stringify(value, alternateChannel, space) {
2925
- const json = JSON.stringify(value, function (key) {
2926
- const realVal = this[key];
2927
- const typeDef = getTypeDef(realVal);
2928
- return typeDef
2929
- ? typeDef.replace(realVal, alternateChannel, typeDefs)
2930
- : realVal;
2931
- }, space);
2932
- return json;
2933
- },
2934
- parse(tson, alternateChannel) {
2935
- const stack = [];
2936
- return JSON.parse(tson, function (key, value) {
2937
- //
2938
- // Parent Part
2939
- //
2940
- const type = value === null || value === void 0 ? void 0 : value.$t;
2941
- if (type) {
2942
- const typeDef = typeDefs[type];
2943
- value = typeDef
2944
- ? typeDef.revive(value, alternateChannel, typeDefs)
2945
- : value;
2946
- }
2947
- let top = stack[stack.length - 1];
2948
- if (top && top[0] === value) {
2949
- // Do what the kid told us to
2950
- // Unescape dollar props
2951
- value = { ...value };
2952
- // Delete keys that children wanted us to delete
2953
- for (const k of top[1])
2954
- delete value[k];
2955
- // Set keys that children wanted us to set
2956
- for (const [k, v] of Object.entries(top[2])) {
2957
- value[k] = v;
2958
- }
2959
- stack.pop();
2960
- }
2961
- //
2962
- // Child part
2963
- //
2964
- if (value === undefined || (key[0] === "$" && key !== "$t")) {
2965
- top = stack[stack.length - 1];
2966
- let deletes;
2967
- let mods;
2968
- if (top && top[0] === this) {
2969
- deletes = top[1];
2970
- mods = top[2];
2971
- }
2972
- else {
2973
- stack.push([this, (deletes = []), (mods = {})]);
2974
- }
2975
- if (key[0] === "$" && key !== "$t") {
2976
- // Unescape props (also preserves undefined if this is a combo)
2977
- deletes.push(key);
2978
- mods[key.substr(1)] = value;
2979
- }
2980
- else {
2981
- // Preserve undefined
2982
- mods[key] = undefined;
2983
- }
2984
- }
2985
- return value;
2986
- });
2987
- },
2988
- };
2989
- function getTypeDef(realVal) {
2990
- const type = typeof realVal;
2991
- switch (typeof realVal) {
2992
- case "object":
2993
- case "function": {
2994
- // "object", "function", null
2995
- if (realVal === null)
2996
- return null;
2997
- const proto = Object.getPrototypeOf(realVal);
2998
- if (!proto)
2999
- return ObjectDef;
3000
- let typeDef = protoMap.get(proto);
3001
- if (typeDef !== undefined)
3002
- return typeDef; // Null counts to! So the caching of Array.prototype also counts.
3003
- const toStringTag = getToStringTag(realVal);
3004
- const entry = Object.entries(typeDefs).find(([typeName, typeDef]) => { var _a, _b; return (_b = (_a = typeDef === null || typeDef === void 0 ? void 0 : typeDef.test) === null || _a === void 0 ? void 0 : _a.call(typeDef, realVal, toStringTag)) !== null && _b !== void 0 ? _b : typeName === toStringTag; });
3005
- typeDef = entry === null || entry === void 0 ? void 0 : entry[1];
3006
- if (!typeDef) {
3007
- typeDef = Array.isArray(realVal)
3008
- ? null
3009
- : typeof realVal === "function"
3010
- ? typeDefs.function || null
3011
- : ObjectDef;
3012
- }
3013
- protoMap.set(proto, typeDef);
3014
- return typeDef;
3015
- }
3016
- default:
3017
- return typeDefs[type];
3018
- }
3019
- }
3020
- }
3021
-
3022
- const BisonBinaryTypes = {
3023
- Blob: {
3024
- test: (blob, toStringTag) => toStringTag === "Blob",
3025
- replace: (blob, altChannel) => {
3026
- const i = altChannel.length;
3027
- altChannel.push(blob);
3028
- return {
3029
- $t: "Blob",
3030
- mimeType: blob.type,
3031
- i,
3032
- };
3033
- },
3034
- revive: ({ i, mimeType }, altChannel) => new Blob([altChannel[i]], { type: mimeType }),
3035
- },
3036
- };
3037
-
3038
- var numberDef = {
3039
- number: {
3040
- replace: (num) => {
3041
- switch (true) {
3042
- case isNaN(num):
3043
- return { $t: "number", v: "NaN" };
3044
- case num === Infinity:
3045
- return { $t: "number", v: "Infinity" };
3046
- case num === -Infinity:
3047
- return { $t: "number", v: "-Infinity" };
3048
- default:
3049
- return num;
3050
- }
3051
- },
3052
- revive: ({ v }) => Number(v),
3053
- },
3054
- };
3055
-
3056
- const bigIntDef$1 = {
3057
- bigint: {
3058
- replace: (realVal) => {
3059
- return { $t: "bigint", v: "" + realVal };
3060
- },
3061
- revive: (obj) => BigInt(obj.v),
3062
- },
3063
- };
3064
-
3065
- var DateDef = {
3066
- Date: {
3067
- replace: (date) => ({
3068
- $t: "Date",
3069
- v: isNaN(date.getTime()) ? "NaN" : date.toISOString(),
3070
- }),
3071
- revive: ({ v }) => new Date(v === "NaN" ? NaN : Date.parse(v)),
3072
- },
3073
- };
3074
-
3075
- var SetDef = {
3076
- Set: {
3077
- replace: (set) => ({
3078
- $t: "Set",
3079
- v: Array.from(set.entries()),
3080
- }),
3081
- revive: ({ v }) => new Set(v),
3082
- },
3083
- };
3084
-
3085
- var MapDef = {
3086
- Map: {
3087
- replace: (map) => ({
3088
- $t: "Map",
3089
- v: Array.from(map.entries()),
3090
- }),
3091
- revive: ({ v }) => new Map(v),
3092
- },
3093
- };
3094
-
3095
- const _global = typeof globalThis !== "undefined" // All modern environments (node, bun, deno, browser, workers, webview etc)
3096
- ? globalThis
3097
- : typeof self !== "undefined" // Older browsers, workers, webview, window etc
3098
- ? self
3099
- : typeof global !== "undefined" // Older versions of node
3100
- ? global
3101
- : undefined; // Unsupported environment. No idea to return 'this' since we are in a module or a function scope anyway.
3102
-
3103
- var TypedArraysDefs = [
3104
- "Int8Array",
3105
- "Uint8Array",
3106
- "Uint8ClampedArray",
3107
- "Int16Array",
3108
- "Uint16Array",
3109
- "Int32Array",
3110
- "Uint32Array",
3111
- "Float32Array",
3112
- "Float64Array",
3113
- "DataView",
3114
- "BigInt64Array",
3115
- "BigUint64Array",
3116
- ].reduce((specs, typeName) => ({
3117
- ...specs,
3118
- [typeName]: {
3119
- // Replace passes the the typed array into $t, buffer so that
3120
- // the ArrayBuffer typedef takes care of further handling of the buffer:
3121
- // {$t:"Uint8Array",buffer:{$t:"ArrayBuffer",idx:0}}
3122
- // CHANGED ABOVE! Now shortcutting that for more sparse format of the typed arrays
3123
- // to contain the b64 property directly.
3124
- replace: (a, _, typeDefs) => {
3125
- const result = {
3126
- $t: typeName,
3127
- v: typeDefs.ArrayBuffer.replace(a.byteOffset === 0 && a.byteLength === a.buffer.byteLength
3128
- ? a.buffer
3129
- : a.buffer.slice(a.byteOffset, a.byteOffset + a.byteLength), _, typeDefs).v,
3130
- };
3131
- return result;
3132
- },
3133
- revive: ({ v }, _, typeDefs) => {
3134
- const TypedArray = _global[typeName];
3135
- return (TypedArray &&
3136
- new TypedArray(typeDefs.ArrayBuffer.revive({ v }, _, typeDefs)));
3137
- },
3138
- },
3139
- }), {});
3140
-
3141
- function b64LexEncode(b) {
3142
- return b64ToLex(b64encode(b));
3143
- }
3144
- function b64LexDecode(b64Lex) {
3145
- return b64decode(lexToB64(b64Lex));
3146
- }
3147
- function b64ToLex(base64) {
3148
- var encoded = "";
3149
- for (var i = 0, length = base64.length; i < length; i++) {
3150
- encoded += ENCODE_TABLE[base64[i]];
3151
- }
3152
- return encoded;
3153
- }
3154
- function lexToB64(base64lex) {
3155
- // only accept string input
3156
- if (typeof base64lex !== "string") {
3157
- throw new Error("invalid decoder input: " + base64lex);
3158
- }
3159
- var base64 = "";
3160
- for (var i = 0, length = base64lex.length; i < length; i++) {
3161
- base64 += DECODE_TABLE[base64lex[i]];
3162
- }
3163
- return base64;
3164
- }
3165
- const DECODE_TABLE = {
3166
- "-": "=",
3167
- "0": "A",
3168
- "1": "B",
3169
- "2": "C",
3170
- "3": "D",
3171
- "4": "E",
3172
- "5": "F",
3173
- "6": "G",
3174
- "7": "H",
3175
- "8": "I",
3176
- "9": "J",
3177
- A: "K",
3178
- B: "L",
3179
- C: "M",
3180
- D: "N",
3181
- E: "O",
3182
- F: "P",
3183
- G: "Q",
3184
- H: "R",
3185
- I: "S",
3186
- J: "T",
3187
- K: "U",
3188
- L: "V",
3189
- M: "W",
3190
- N: "X",
3191
- O: "Y",
3192
- P: "Z",
3193
- Q: "a",
3194
- R: "b",
3195
- S: "c",
3196
- T: "d",
3197
- U: "e",
3198
- V: "f",
3199
- W: "g",
3200
- X: "h",
3201
- Y: "i",
3202
- Z: "j",
3203
- _: "k",
3204
- a: "l",
3205
- b: "m",
3206
- c: "n",
3207
- d: "o",
3208
- e: "p",
3209
- f: "q",
3210
- g: "r",
3211
- h: "s",
3212
- i: "t",
3213
- j: "u",
3214
- k: "v",
3215
- l: "w",
3216
- m: "x",
3217
- n: "y",
3218
- o: "z",
3219
- p: "0",
3220
- q: "1",
3221
- r: "2",
3222
- s: "3",
3223
- t: "4",
3224
- u: "5",
3225
- v: "6",
3226
- w: "7",
3227
- x: "8",
3228
- y: "9",
3229
- z: "+",
3230
- "|": "/",
3231
- };
3232
- const ENCODE_TABLE = {};
3233
- for (const c of Object.keys(DECODE_TABLE)) {
3234
- ENCODE_TABLE[DECODE_TABLE[c]] = c;
3235
- }
3236
-
3237
- var ArrayBufferDef = {
3238
- ArrayBuffer: {
3239
- replace: (ab) => ({
3240
- $t: "ArrayBuffer",
3241
- v: b64LexEncode(ab),
3242
- }),
3243
- revive: ({ v }) => {
3244
- const ba = b64LexDecode(v);
3245
- return ba.buffer.byteLength === ba.byteLength
3246
- ? ba.buffer
3247
- : ba.buffer.slice(ba.byteOffset, ba.byteOffset + ba.byteLength);
3248
- },
3249
- },
3250
- };
3251
-
3252
- class FakeBlob {
3253
- constructor(buf, type) {
3254
- this.buf = buf;
3255
- this.type = type;
3256
- }
3257
- }
3258
-
3259
- function readBlobSync(b) {
3260
- const req = new XMLHttpRequest();
3261
- req.overrideMimeType("text/plain; charset=x-user-defined");
3262
- req.open("GET", URL.createObjectURL(b), false); // Sync
3263
- req.send();
3264
- if (req.status !== 200 && req.status !== 0) {
3265
- throw new Error("Bad Blob access: " + req.status);
3266
- }
3267
- return req.responseText;
3268
- }
3269
-
3270
- function string2ArrayBuffer(str) {
3271
- const array = new Uint8Array(str.length);
3272
- for (let i = 0; i < str.length; ++i) {
3273
- array[i] = str.charCodeAt(i); // & 0xff;
3274
- }
3275
- return array.buffer;
3276
- }
3277
-
3278
- var BlobDef = {
3279
- Blob: {
3280
- test: (blob, toStringTag) => toStringTag === "Blob" || blob instanceof FakeBlob,
3281
- replace: (blob) => ({
3282
- $t: "Blob",
3283
- v: blob instanceof FakeBlob
3284
- ? b64encode(blob.buf)
3285
- : b64encode(string2ArrayBuffer(readBlobSync(blob))),
3286
- type: blob.type,
3287
- }),
3288
- revive: ({ type, v }) => {
3289
- const ab = b64decode(v);
3290
- return typeof Blob !== undefined
3291
- ? new Blob([ab])
3292
- : new FakeBlob(ab.buffer, type);
3293
- },
3294
- },
3295
- };
3296
-
3297
- const builtin = {
3298
- ...numberDef,
3299
- ...bigIntDef$1,
3300
- ...DateDef,
3301
- ...SetDef,
3302
- ...MapDef,
3303
- ...TypedArraysDefs,
3304
- ...ArrayBufferDef,
3305
- ...BlobDef, // Should be moved to another preset for DOM types (or universal? since it supports node as well with FakeBlob)
3306
- };
3307
-
3308
- function Bison(...typeDefsInputs) {
3309
- const tson = TypesonSimplified(builtin, BisonBinaryTypes, ...typeDefsInputs);
3310
- return {
3311
- toBinary(value) {
3312
- const [blob, json] = this.stringify(value);
3313
- const lenBuf = new ArrayBuffer(4);
3314
- new DataView(lenBuf).setUint32(0, blob.size);
3315
- return new Blob([lenBuf, blob, json]);
3316
- },
3317
- stringify(value) {
3318
- const binaries = [];
3319
- const json = tson.stringify(value, binaries);
3320
- const blob = new Blob(binaries.map((b) => {
3321
- const lenBuf = new ArrayBuffer(4);
3322
- new DataView(lenBuf).setUint32(0, "byteLength" in b ? b.byteLength : b.size);
3323
- return new Blob([lenBuf, b]);
3324
- }));
3325
- return [blob, json];
3326
- },
3327
- async parse(json, binData) {
3328
- let pos = 0;
3329
- const arrayBuffers = [];
3330
- const buf = await readBlobBinary(binData);
3331
- const view = new DataView(buf);
3332
- while (pos < buf.byteLength) {
3333
- const len = view.getUint32(pos);
3334
- pos += 4;
3335
- const ab = buf.slice(pos, pos + len);
3336
- pos += len;
3337
- arrayBuffers.push(ab);
3338
- }
3339
- return tson.parse(json, arrayBuffers);
3340
- },
3341
- async fromBinary(blob) {
3342
- const len = new DataView(await readBlobBinary(blob.slice(0, 4))).getUint32(0);
3343
- const binData = blob.slice(4, len + 4);
3344
- const json = await readBlob(blob.slice(len + 4));
3345
- return await this.parse(json, binData);
3346
- },
3347
- };
3348
- }
3349
- function readBlob(blob) {
3350
- return new Promise((resolve, reject) => {
3351
- const reader = new FileReader();
3352
- reader.onabort = (ev) => reject(new Error("file read aborted"));
3353
- reader.onerror = (ev) => reject(ev.target.error);
3354
- reader.onload = (ev) => resolve(ev.target.result);
3355
- reader.readAsText(blob);
3460
+ if (response2.type !== 'tokens')
3461
+ throw new Error(`Unexpected response type from token endpoint: ${response2.type}`);
3462
+ /*const licenseStatus = response2.claims.license || 'ok';
3463
+ if (licenseStatus !== 'ok') {
3464
+ throw new InvalidLicenseError(licenseStatus);
3465
+ }*/
3466
+ context.accessToken = response2.accessToken;
3467
+ context.accessTokenExpiration = new Date(response2.accessTokenExpiration);
3468
+ context.refreshToken = response2.refreshToken;
3469
+ if (response2.refreshTokenExpiration) {
3470
+ context.refreshTokenExpiration = new Date(response2.refreshTokenExpiration);
3471
+ }
3472
+ context.userId = response2.claims.sub;
3473
+ context.email = response2.claims.email;
3474
+ context.name = response2.claims.name;
3475
+ context.claims = response2.claims;
3476
+ context.license = {
3477
+ type: response2.userType,
3478
+ status: response2.claims.license || 'ok',
3479
+ };
3480
+ context.data = response2.data;
3481
+ if (response2.evalDaysLeft != null) {
3482
+ context.license.evalDaysLeft = response2.evalDaysLeft;
3483
+ }
3484
+ if (response2.userValidUntil != null) {
3485
+ context.license.validUntil = new Date(response2.userValidUntil);
3486
+ }
3487
+ if (response2.alerts && response2.alerts.length > 0) {
3488
+ yield interactWithUser(userInteraction, {
3489
+ type: 'message-alert',
3490
+ title: 'Authentication Alert',
3491
+ fields: {},
3492
+ alerts: response2.alerts,
3493
+ });
3494
+ }
3495
+ return context;
3496
+ }
3497
+ catch (error) {
3498
+ // OAuth redirect is not an error - page is navigating away
3499
+ if (error instanceof OAuthRedirectError || (error === null || error === void 0 ? void 0 : error.name) === 'OAuthRedirectError') {
3500
+ throw error; // Re-throw without logging
3501
+ }
3502
+ if (error instanceof TokenErrorResponseError) {
3503
+ yield alertUser(userInteraction, error.title, {
3504
+ type: 'error',
3505
+ messageCode: error.messageCode,
3506
+ message: error.message,
3507
+ messageParams: {},
3508
+ });
3509
+ throw error;
3510
+ }
3511
+ let message = `We're having a problem authenticating right now.`;
3512
+ console.error(`Error authenticating`, error);
3513
+ if (error instanceof TypeError) {
3514
+ const isOffline = typeof navigator !== 'undefined' && !navigator.onLine;
3515
+ if (isOffline) {
3516
+ message = `You seem to be offline. Please connect to the internet and try again.`;
3517
+ }
3518
+ else if (typeof location !== 'undefined' && (Dexie.debug || location.hostname === 'localhost' || location.hostname === '127.0.0.1')) {
3519
+ // The audience is most likely the developer. Suggest to whitelist the localhost origin:
3520
+ const whitelistCommand = `npx dexie-cloud whitelist ${location.origin}`;
3521
+ message = `Could not connect to server. Please verify that your origin '${location.origin}' is whitelisted using \`npx dexie-cloud whitelist\``;
3522
+ yield alertUser(userInteraction, 'Authentication Failed', {
3523
+ type: 'error',
3524
+ messageCode: 'GENERIC_ERROR',
3525
+ message,
3526
+ messageParams: {},
3527
+ copyText: whitelistCommand,
3528
+ }).catch(() => { });
3529
+ }
3530
+ else {
3531
+ message = `Could not connect to server. Please verify the connection.`;
3532
+ yield alertUser(userInteraction, 'Authentication Failed', {
3533
+ type: 'error',
3534
+ messageCode: 'GENERIC_ERROR',
3535
+ message,
3536
+ messageParams: {},
3537
+ }).catch(() => { });
3538
+ }
3539
+ }
3540
+ throw error;
3541
+ }
3356
3542
  });
3357
3543
  }
3358
- function readBlobBinary(blob) {
3359
- return new Promise((resolve, reject) => {
3360
- const reader = new FileReader();
3361
- reader.onabort = (ev) => reject(new Error("file read aborted"));
3362
- reader.onerror = (ev) => reject(ev.target.error);
3363
- reader.onload = (ev) => resolve(ev.target.result);
3364
- reader.readAsArrayBuffer(blob);
3365
- });
3544
+ function spkiToPEM(keydata) {
3545
+ const keydataB64 = b64encode(keydata);
3546
+ const keydataB64Pem = formatAsPem(keydataB64);
3547
+ return keydataB64Pem;
3548
+ }
3549
+ function formatAsPem(str) {
3550
+ let finalString = '-----BEGIN PUBLIC KEY-----\n';
3551
+ while (str.length > 0) {
3552
+ finalString += str.substring(0, 64) + '\n';
3553
+ str = str.substring(64);
3554
+ }
3555
+ finalString = finalString + '-----END PUBLIC KEY-----';
3556
+ return finalString;
3366
3557
  }
3367
-
3368
- /** The undefined type is not part of builtin but can be manually added.
3369
- * The reason for supporting undefined is if the following object should be revived correctly:
3370
- *
3371
- * {foo: undefined}
3372
- *
3373
- * Without including this typedef, the revived object would just be {}.
3374
- * If including this typedef, the revived object would be {foo: undefined}.
3375
- */
3376
- var undefinedDef = {
3377
- undefined: {
3378
- replace: () => ({
3379
- $t: "undefined"
3380
- }),
3381
- revive: () => undefined,
3382
- },
3383
- };
3384
-
3385
- var FileDef = {
3386
- File: {
3387
- test: (file, toStringTag) => toStringTag === "File",
3388
- replace: (file) => ({
3389
- $t: "File",
3390
- v: b64encode(string2ArrayBuffer(readBlobSync(file))),
3391
- type: file.type,
3392
- name: file.name,
3393
- lastModified: new Date(file.lastModified).toISOString(),
3394
- }),
3395
- revive: ({ type, v, name, lastModified }) => {
3396
- const ab = b64decode(v);
3397
- return new File([ab], name, {
3398
- type,
3399
- lastModified: new Date(lastModified).getTime(),
3400
- });
3401
- },
3402
- },
3403
- };
3404
3558
 
3405
3559
  // Since server revisions are stored in bigints, we need to handle clients without
3406
3560
  // bigint support to not fail when serverRevision is passed over to client.
@@ -3437,7 +3591,7 @@
3437
3591
  revive: ({ v }) => new FakeBigInt(v),
3438
3592
  },
3439
3593
  };
3440
- const defs = Object.assign(Object.assign(Object.assign(Object.assign({}, undefinedDef), bigIntDef), FileDef), { PropModification: {
3594
+ const defs = Object.assign(Object.assign(Object.assign(Object.assign({}, undefinedTypeDef), bigIntDef), fileTypeDef), { PropModification: {
3441
3595
  test: (val) => val instanceof Dexie.PropModification,
3442
3596
  replace: (propModification) => {
3443
3597
  return Object.assign({ $t: 'PropModification' }, propModification['@@propmod']);
@@ -3449,8 +3603,14 @@
3449
3603
  return new Dexie.PropModification(propModSpec);
3450
3604
  },
3451
3605
  } });
3452
- const TSON = TypesonSimplified(builtin, defs);
3453
- const BISON = Bison(defs);
3606
+ const TSON = TypesonSimplified(
3607
+ // Standard type definitions - TSON is transparent to BlobRefs
3608
+ // BlobRefs use _bt convention and are handled by blobResolveMiddleware, not TSON
3609
+ typedArrayTypeDefs, arrayBufferTypeDef, blobTypeDef,
3610
+ // Non-binary built-in types
3611
+ numberTypeDef, dateTypeDef, setTypeDef, mapTypeDef,
3612
+ // Custom type definitions
3613
+ defs);
3454
3614
 
3455
3615
  class HttpError extends Error {
3456
3616
  constructor(res, message) {
@@ -3566,7 +3726,7 @@
3566
3726
  // Push changes to server using fetch
3567
3727
  //
3568
3728
  const headers = {
3569
- Accept: 'application/json, application/x-bison, application/x-bison-stream',
3729
+ Accept: 'application/json',
3570
3730
  'Content-Type': 'application/tson',
3571
3731
  };
3572
3732
  const updatedUser = yield loadAccessToken(db);
@@ -3585,7 +3745,7 @@
3585
3745
  headers.Authorization = `Bearer ${accessToken}`;
3586
3746
  }
3587
3747
  const syncRequest = {
3588
- v: 2,
3748
+ v: 3, // v3 = supports BlobRef
3589
3749
  dbID: syncState === null || syncState === void 0 ? void 0 : syncState.remoteDbId,
3590
3750
  clientIdentity,
3591
3751
  schema: schema || {},
@@ -3623,8 +3783,9 @@
3623
3783
  }
3624
3784
  switch (res.headers.get('content-type')) {
3625
3785
  case 'application/x-bison':
3626
- return BISON.fromBinary(yield res.blob());
3627
- case 'application/x-bison-stream': //return BisonWebStreamReader(BISON, res);
3786
+ case 'application/x-bison-stream':
3787
+ // BISON format deprecated - throw error if server sends it
3788
+ throw new Error('BISON format no longer supported. Server should send application/json.');
3628
3789
  default:
3629
3790
  case 'application/json': {
3630
3791
  const text = yield res.text();
@@ -3742,6 +3903,194 @@
3742
3903
  });
3743
3904
  }
3744
3905
 
3906
+ /**
3907
+ * Check if a value is a BlobRef (offloaded binary data)
3908
+ * A BlobRef has _bt (type), ref (blob ID), but no v (inline data)
3909
+ */
3910
+ function isBlobRef(value) {
3911
+ if (typeof value !== 'object' || value === null)
3912
+ return false;
3913
+ const obj = value;
3914
+ return (typeof obj._bt === 'string' &&
3915
+ typeof obj.ref === 'string' &&
3916
+ obj.v === undefined // No inline data = it's a reference
3917
+ );
3918
+ }
3919
+ /**
3920
+ * Check if a value is a serialized TSONRef (after IndexedDB storage)
3921
+ * Has 'type' instead of '$t', and no Symbol marker
3922
+ */
3923
+ function isSerializedTSONRef(value) {
3924
+ if (typeof value !== 'object' || value === null)
3925
+ return false;
3926
+ const obj = value;
3927
+ return (typeof obj.type === 'string' &&
3928
+ typeof obj.ref === 'string' &&
3929
+ typeof obj.size === 'number' &&
3930
+ obj._bt === undefined // Not a raw BlobRef
3931
+ );
3932
+ }
3933
+ /**
3934
+ * Recursively check if an object contains any BlobRefs
3935
+ */
3936
+ function hasBlobRefs(obj, visited = new WeakSet()) {
3937
+ if (obj === null || obj === undefined) {
3938
+ return false;
3939
+ }
3940
+ if (isBlobRef(obj)) {
3941
+ return true;
3942
+ }
3943
+ if (typeof obj !== 'object') {
3944
+ return false;
3945
+ }
3946
+ // Avoid circular references - check BEFORE processing
3947
+ if (visited.has(obj)) {
3948
+ return false;
3949
+ }
3950
+ visited.add(obj);
3951
+ // Skip special objects that can't contain BlobRefs
3952
+ if (obj instanceof Date || obj instanceof RegExp || obj instanceof Blob) {
3953
+ return false;
3954
+ }
3955
+ if (obj instanceof ArrayBuffer || ArrayBuffer.isView(obj)) {
3956
+ return false;
3957
+ }
3958
+ if (Array.isArray(obj)) {
3959
+ return obj.some(item => hasBlobRefs(item, visited));
3960
+ }
3961
+ // Only traverse POJOs
3962
+ if (obj.constructor === Object) {
3963
+ return Object.values(obj).some(value => hasBlobRefs(value, visited));
3964
+ }
3965
+ return false;
3966
+ }
3967
+ /**
3968
+ * Convert downloaded Uint8Array to the original type specified in BlobRef
3969
+ */
3970
+ function convertToOriginalType(data, ref) {
3971
+ // String type: decode UTF-8 back to string
3972
+ if (ref._bt === 'string') {
3973
+ return new TextDecoder().decode(data);
3974
+ }
3975
+ // Get the underlying ArrayBuffer (handle shared buffer case)
3976
+ const buffer = data.buffer.byteLength === data.byteLength
3977
+ ? data.buffer
3978
+ : data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
3979
+ switch (ref._bt) {
3980
+ case 'Blob':
3981
+ return new Blob([new Uint8Array(buffer)], { type: ref.ct || '' });
3982
+ case 'ArrayBuffer':
3983
+ return buffer;
3984
+ case 'Uint8Array':
3985
+ return data;
3986
+ case 'Int8Array':
3987
+ return new Int8Array(buffer);
3988
+ case 'Uint8ClampedArray':
3989
+ return new Uint8ClampedArray(buffer);
3990
+ case 'Int16Array':
3991
+ return new Int16Array(buffer);
3992
+ case 'Uint16Array':
3993
+ return new Uint16Array(buffer);
3994
+ case 'Int32Array':
3995
+ return new Int32Array(buffer);
3996
+ case 'Uint32Array':
3997
+ return new Uint32Array(buffer);
3998
+ case 'Float32Array':
3999
+ return new Float32Array(buffer);
4000
+ case 'Float64Array':
4001
+ return new Float64Array(buffer);
4002
+ case 'BigInt64Array':
4003
+ return new BigInt64Array(buffer);
4004
+ case 'BigUint64Array':
4005
+ return new BigUint64Array(buffer);
4006
+ case 'DataView':
4007
+ return new DataView(buffer);
4008
+ default:
4009
+ // Fallback to Uint8Array for unknown types
4010
+ return data;
4011
+ }
4012
+ }
4013
+ /**
4014
+ * Recursively resolve all BlobRefs in an object and collect them for queueing.
4015
+ * Returns a new object with BlobRefs replaced by their original type data,
4016
+ * and populates the resolvedBlobs array with keyPath info for each blob.
4017
+ *
4018
+ * @param obj - Object to resolve
4019
+ * @param dbUrl - Base URL for the database
4020
+ * @param accessToken - Access token for blob downloads
4021
+ * @param resolvedBlobs - Array to collect resolved blob info
4022
+ * @param currentPath - Current property path (for tracking)
4023
+ * @param visited - WeakMap for circular reference detection
4024
+ */
4025
+ function resolveAllBlobRefs(obj_1, dbUrl_1) {
4026
+ return __awaiter(this, arguments, void 0, function* (obj, dbUrl, resolvedBlobs = [], currentPath = '', visited = new WeakMap(), tracker) {
4027
+ if (obj == null) { // null or undefined
4028
+ return obj;
4029
+ }
4030
+ // Check if this is a BlobRef - resolve it and track it
4031
+ if (isBlobRef(obj)) {
4032
+ const rawData = yield tracker.download(obj, dbUrl);
4033
+ const data = convertToOriginalType(rawData, obj);
4034
+ resolvedBlobs.push({ keyPath: currentPath, data, ref: obj.ref });
4035
+ return data;
4036
+ }
4037
+ // Handle arrays
4038
+ if (Array.isArray(obj)) {
4039
+ // Avoid circular references - check and set BEFORE iterating
4040
+ if (visited.has(obj)) {
4041
+ return visited.get(obj);
4042
+ }
4043
+ const result = [];
4044
+ visited.set(obj, result); // Set before iterating to handle self-references
4045
+ for (let i = 0; i < obj.length; i++) {
4046
+ const itemPath = currentPath ? `${currentPath}.${i}` : `${i}`;
4047
+ result.push(yield resolveAllBlobRefs(obj[i], dbUrl, resolvedBlobs, itemPath, visited, tracker));
4048
+ }
4049
+ return result;
4050
+ }
4051
+ // Handle POJO objects only (not Date, RegExp, Blob, ArrayBuffer, etc.)
4052
+ if (typeof obj === 'object' && obj.constructor === Object) {
4053
+ // Avoid circular references
4054
+ if (visited.has(obj)) {
4055
+ return visited.get(obj);
4056
+ }
4057
+ const result = {};
4058
+ visited.set(obj, result);
4059
+ for (const [propName, value] of Object.entries(obj)) {
4060
+ // Skip the _hasBlobRefs marker itself
4061
+ if (propName === '_hasBlobRefs') {
4062
+ continue;
4063
+ }
4064
+ const propPath = currentPath ? `${currentPath}.${propName}` : propName;
4065
+ result[propName] = yield resolveAllBlobRefs(value, dbUrl, resolvedBlobs, propPath, visited, tracker);
4066
+ }
4067
+ return result;
4068
+ }
4069
+ return obj;
4070
+ });
4071
+ }
4072
+ /**
4073
+ * Check if an object has unresolved BlobRefs
4074
+ */
4075
+ function hasUnresolvedBlobRefs(obj) {
4076
+ return (typeof obj === 'object' &&
4077
+ obj !== null &&
4078
+ obj._hasBlobRefs === 1);
4079
+ }
4080
+
4081
+ /**
4082
+ * If the incoming value contains BlobRefs (e.g. offloaded strings or binaries),
4083
+ * mark it with _hasBlobRefs = 1 so the blobResolveMiddleware will resolve them
4084
+ * on the next read.
4085
+ */
4086
+ function markIfHasBlobRefs(obj) {
4087
+ if (obj !== null &&
4088
+ typeof obj === 'object' &&
4089
+ obj.constructor === Object &&
4090
+ hasBlobRefs(obj)) {
4091
+ obj._hasBlobRefs = 1;
4092
+ }
4093
+ }
3745
4094
  function applyServerChanges(changes, db) {
3746
4095
  return __awaiter(this, void 0, void 0, function* () {
3747
4096
  console.debug('Applying server changes', changes, Dexie.currentTransaction);
@@ -3777,6 +4126,7 @@
3777
4126
  const keys = mut.keys.map(keyDecoder);
3778
4127
  switch (mut.type) {
3779
4128
  case 'insert':
4129
+ mut.values.forEach(markIfHasBlobRefs);
3780
4130
  if (primaryKey.outbound) {
3781
4131
  yield table.bulkAdd(mut.values, keys);
3782
4132
  }
@@ -3789,6 +4139,7 @@
3789
4139
  }
3790
4140
  break;
3791
4141
  case 'upsert':
4142
+ mut.values.forEach(markIfHasBlobRefs);
3792
4143
  if (primaryKey.outbound) {
3793
4144
  yield table.bulkPut(mut.values, keys);
3794
4145
  }
@@ -13207,7 +13558,7 @@
13207
13558
  *
13208
13559
  * ==========================================================================
13209
13560
  *
13210
- * Version 4.2.2, Thu Jan 29 2026
13561
+ * Version 4.4.0, Thu Mar 19 2026
13211
13562
  *
13212
13563
  * https://dexie.org
13213
13564
  *
@@ -13402,7 +13753,7 @@
13402
13753
  };
13403
13754
  }
13404
13755
 
13405
- const wm$3 = new WeakMap();
13756
+ const wm$4 = new WeakMap();
13406
13757
  function createEvents() {
13407
13758
  return Dexie.Dexie.Events(null, 'load', 'sync', 'error');
13408
13759
  }
@@ -13421,7 +13772,7 @@
13421
13772
  }
13422
13773
  static load(doc, options) {
13423
13774
  var _a;
13424
- let p = wm$3.get(doc);
13775
+ let p = wm$4.get(doc);
13425
13776
  if (p) {
13426
13777
  ++p.refCount;
13427
13778
  if ((options === null || options === void 0 ? void 0 : options.gracePeriod) != null &&
@@ -13436,14 +13787,14 @@
13436
13787
  else {
13437
13788
  p = new DexieYProvider(doc);
13438
13789
  p.graceTimeout = (_a = options === null || options === void 0 ? void 0 : options.gracePeriod) !== null && _a !== void 0 ? _a : -1;
13439
- wm$3.set(doc, p);
13790
+ wm$4.set(doc, p);
13440
13791
  }
13441
13792
  return p;
13442
13793
  }
13443
13794
  static release(doc) {
13444
13795
  if (!doc || destroyedDocs.has(doc))
13445
13796
  return; // Document already destroyed.
13446
- const p = wm$3.get(doc);
13797
+ const p = wm$4.get(doc);
13447
13798
  if (p) {
13448
13799
  // There is a provider connected to the doc
13449
13800
  if (--p.refCount <= 0) {
@@ -13488,7 +13839,7 @@
13488
13839
  });
13489
13840
  }
13490
13841
  static for(doc) {
13491
- return wm$3.get(doc);
13842
+ return wm$4.get(doc);
13492
13843
  }
13493
13844
  static get currentUpdateRow() {
13494
13845
  return currentUpdateRow;
@@ -13576,7 +13927,7 @@
13576
13927
  destroy() {
13577
13928
  var _a, _b, _c;
13578
13929
  console.debug(`Y.Doc ${(_b = (_a = this.doc) === null || _a === void 0 ? void 0 : _a.meta) === null || _b === void 0 ? void 0 : _b.parentId} was destroyed`);
13579
- wm$3.delete(this.doc);
13930
+ wm$4.delete(this.doc);
13580
13931
  this.doc = null;
13581
13932
  this.destroyed = true;
13582
13933
  this.refCount = 0;
@@ -13704,6 +14055,340 @@
13704
14055
  });
13705
14056
  }
13706
14057
 
14058
+ /**
14059
+ * Blob Offloading for Dexie Cloud
14060
+ *
14061
+ * Handles uploading large blobs to blob storage before sync,
14062
+ * and resolving BlobRefs when reading from the database.
14063
+ */
14064
+ // Blobs >= 4KB are offloaded to blob storage
14065
+ const BLOB_OFFLOAD_THRESHOLD = 4096;
14066
+ // Default max string length before offloading (32KB characters)
14067
+ const DEFAULT_MAX_STRING_LENGTH = 32768;
14068
+ // Cache: once we know the server doesn't support blob storage, skip future uploads.
14069
+ // Maps databaseUrl → boolean (true = supported, false = not supported).
14070
+ const blobEndpointSupported = new Map();
14071
+ /**
14072
+ * Cross-realm type detection helpers (performance-optimized)
14073
+ *
14074
+ * When code runs in different JavaScript realms (e.g., Service Worker context),
14075
+ * `instanceof` checks can fail because each realm has its own global constructors.
14076
+ * We use Object.prototype.toString which works reliably across realms.
14077
+ *
14078
+ * Performance considerations (this is a hot path - every property is checked):
14079
+ * - Early return for primitives via typeof
14080
+ * - Static Set for O(1) TypedArray tag lookup
14081
+ * - Single typeTag call per check
14082
+ */
14083
+ // TypedArray/DataView tags for size check
14084
+ const ARRAYBUFFER_VIEW_TAGS = new Set([
14085
+ 'Int8Array', 'Uint8Array', 'Uint8ClampedArray',
14086
+ 'Int16Array', 'Uint16Array', 'Int32Array', 'Uint32Array',
14087
+ 'Float32Array', 'Float64Array', 'BigInt64Array', 'BigUint64Array',
14088
+ 'DataView'
14089
+ ]);
14090
+ // Static Set for O(1) lookup of binary type tags
14091
+ const BINARY_TYPE_TAGS = new Set([
14092
+ 'Blob',
14093
+ 'File',
14094
+ 'ArrayBuffer',
14095
+ ...ARRAYBUFFER_VIEW_TAGS,
14096
+ ]);
14097
+ /**
14098
+ * Get the [[Class]] internal property via Object.prototype.toString
14099
+ */
14100
+ function getTypeTag(value) {
14101
+ return Object.prototype.toString.call(value).slice(8, -1);
14102
+ }
14103
+ /**
14104
+ * Get the original type name for a value
14105
+ */
14106
+ function getOrigType(value) {
14107
+ const tag = getTypeTag(value);
14108
+ if (tag === 'Blob' || tag === 'File')
14109
+ return 'Blob';
14110
+ if (tag === 'ArrayBuffer')
14111
+ return 'ArrayBuffer';
14112
+ return tag;
14113
+ }
14114
+ /**
14115
+ * Check if a value should be offloaded to blob storage
14116
+ * Performance-optimized for hot path traversal.
14117
+ */
14118
+ function shouldOffloadBlob(value) {
14119
+ // Fast path: primitives (most common case)
14120
+ // typeof returns: "string", "number", "boolean", "undefined", "symbol", "bigint", "function", "object"
14121
+ const t = typeof value;
14122
+ if (t !== 'object' || value === null)
14123
+ return false;
14124
+ // Get type tag once (cross-realm safe)
14125
+ const tag = getTypeTag(value);
14126
+ // Quick check: is this even a binary type?
14127
+ if (!BINARY_TYPE_TAGS.has(tag))
14128
+ return false;
14129
+ // Blob/File: always offload regardless of size.
14130
+ // This ensures blobs are never stored inline in IndexedDB, which avoids
14131
+ // issues with synchronous blob reading (e.g. in service workers where
14132
+ // XMLHttpRequest is unavailable — see #2182).
14133
+ if (tag === 'Blob' || tag === 'File') {
14134
+ return true;
14135
+ }
14136
+ // ArrayBuffer/TypedArray/DataView: only offload above threshold
14137
+ if (tag === 'ArrayBuffer') {
14138
+ return value.byteLength >= BLOB_OFFLOAD_THRESHOLD;
14139
+ }
14140
+ // TypedArray or DataView
14141
+ return value.byteLength >= BLOB_OFFLOAD_THRESHOLD;
14142
+ }
14143
+ /**
14144
+ * Upload a blob to the blob storage endpoint
14145
+ */
14146
+ function uploadBlob(databaseUrl, getCachedAccessToken, blob) {
14147
+ return __awaiter(this, void 0, void 0, function* () {
14148
+ const accessToken = yield getCachedAccessToken();
14149
+ if (!accessToken) {
14150
+ throw new Error('Failed to load access token for blob upload');
14151
+ }
14152
+ const blobId = newId();
14153
+ // URL format: {databaseUrl}/blob/{blobId}
14154
+ const url = `${databaseUrl}/blob/${blobId}`;
14155
+ let body;
14156
+ let contentType;
14157
+ let size;
14158
+ const origType = getOrigType(blob);
14159
+ // Use type tag for cross-realm compatible checks
14160
+ const tag = getTypeTag(blob);
14161
+ if (tag === 'Blob' || tag === 'File') {
14162
+ body = blob;
14163
+ contentType = blob.type || 'application/octet-stream';
14164
+ size = blob.size;
14165
+ }
14166
+ else if (tag === 'ArrayBuffer') {
14167
+ body = blob;
14168
+ contentType = 'application/octet-stream';
14169
+ size = blob.byteLength;
14170
+ }
14171
+ else if (ARRAYBUFFER_VIEW_TAGS.has(tag)) {
14172
+ // ArrayBufferView (TypedArray or DataView) - create a proper ArrayBuffer copy
14173
+ const view = blob;
14174
+ const arrayBuffer = new ArrayBuffer(view.byteLength);
14175
+ new Uint8Array(arrayBuffer).set(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));
14176
+ body = arrayBuffer;
14177
+ contentType = 'application/octet-stream';
14178
+ size = view.byteLength;
14179
+ }
14180
+ else {
14181
+ throw new Error(`Unsupported blob type: ${tag}`);
14182
+ }
14183
+ // Add content type as query param for the server to store
14184
+ const uploadUrl = `${url}?ct=${encodeURIComponent(contentType)}`;
14185
+ const response = yield fetch(uploadUrl, {
14186
+ method: 'PUT',
14187
+ headers: {
14188
+ 'Authorization': `Bearer ${accessToken}`,
14189
+ 'Content-Type': contentType,
14190
+ },
14191
+ body,
14192
+ });
14193
+ if (!response.ok) {
14194
+ if (response.status === 404 || response.status === 405) {
14195
+ // Server doesn't support blob storage endpoint — fall back to inline storage.
14196
+ // This happens when a new client connects to an older server (pre-3.0).
14197
+ return null;
14198
+ }
14199
+ throw new Error(`Failed to upload blob: ${response.status} ${response.statusText}`);
14200
+ }
14201
+ // The server returns the ref with version prefix (e.g., "1:blobId")
14202
+ const result = yield response.json();
14203
+ // Return BlobRef with server's ref (includes version) and original type preserved in _bt
14204
+ return Object.assign({ _bt: origType, ref: result.ref, size: size }, (origType === 'Blob' ? { ct: contentType } : {}) // Only include content type for Blobs
14205
+ );
14206
+ });
14207
+ }
14208
+ function offloadBlobsAndMarkDirty(obj_1, databaseUrl_1, getCachedAccessToken_1) {
14209
+ return __awaiter(this, arguments, void 0, function* (obj, databaseUrl, getCachedAccessToken, maxStringLength = DEFAULT_MAX_STRING_LENGTH) {
14210
+ const dirtyFlag = { dirty: false };
14211
+ const result = yield offloadBlobs(obj, databaseUrl, getCachedAccessToken, maxStringLength, dirtyFlag);
14212
+ // Mark the object as dirty for sync if any blobs were offloaded
14213
+ if (dirtyFlag.dirty && typeof result === 'object' && result !== null && result.constructor === Object) {
14214
+ result._hasBlobRefs = 1;
14215
+ }
14216
+ return result;
14217
+ });
14218
+ }
14219
+ /**
14220
+ * Recursively scan an object for large blobs and upload them
14221
+ * Returns a new object with blobs replaced by BlobRefs
14222
+ */
14223
+ function offloadBlobs(obj_1, databaseUrl_1, getCachedAccessToken_1) {
14224
+ return __awaiter(this, arguments, void 0, function* (obj, databaseUrl, getCachedAccessToken, maxStringLength = DEFAULT_MAX_STRING_LENGTH, dirtyFlag = { dirty: false }, visited = new WeakSet()) {
14225
+ if (obj === null || obj === undefined) {
14226
+ return obj;
14227
+ }
14228
+ // Check if this is a long string that should be offloaded
14229
+ if (typeof obj === 'string' && obj.length > maxStringLength && maxStringLength !== Infinity) {
14230
+ if (blobEndpointSupported.get(databaseUrl) === false) {
14231
+ return obj;
14232
+ }
14233
+ const blob = new Blob([obj], { type: 'text/plain;charset=utf-8' });
14234
+ const blobRef = yield uploadBlob(databaseUrl, getCachedAccessToken, blob);
14235
+ if (blobRef === null) {
14236
+ blobEndpointSupported.set(databaseUrl, false);
14237
+ return obj;
14238
+ }
14239
+ blobEndpointSupported.set(databaseUrl, true);
14240
+ dirtyFlag.dirty = true;
14241
+ // Mark as string type so it's resolved back to string, not Blob
14242
+ return Object.assign(Object.assign({}, blobRef), { _bt: 'string' });
14243
+ }
14244
+ // Check if this is a blob that should be offloaded
14245
+ if (shouldOffloadBlob(obj)) {
14246
+ if (blobEndpointSupported.get(databaseUrl) === false) {
14247
+ // Server known to not support blob storage — keep inline
14248
+ return obj;
14249
+ }
14250
+ const blobRef = yield uploadBlob(databaseUrl, getCachedAccessToken, obj);
14251
+ if (blobRef === null) {
14252
+ // Server doesn't support blob storage — keep original inline
14253
+ blobEndpointSupported.set(databaseUrl, false);
14254
+ return obj;
14255
+ }
14256
+ blobEndpointSupported.set(databaseUrl, true);
14257
+ dirtyFlag.dirty = true;
14258
+ return blobRef;
14259
+ }
14260
+ if (typeof obj !== 'object') {
14261
+ return obj;
14262
+ }
14263
+ // Avoid circular references - check BEFORE processing
14264
+ if (visited.has(obj)) {
14265
+ return obj;
14266
+ }
14267
+ visited.add(obj);
14268
+ // Handle arrays
14269
+ if (Array.isArray(obj)) {
14270
+ const result = [];
14271
+ for (const item of obj) {
14272
+ result.push(yield offloadBlobs(item, databaseUrl, getCachedAccessToken, maxStringLength, dirtyFlag, visited));
14273
+ }
14274
+ return result;
14275
+ }
14276
+ // Traverse plain objects (POJO-like) - use prototype check since IndexedDB
14277
+ // may return objects where constructor !== Object
14278
+ const proto = Object.getPrototypeOf(obj);
14279
+ if (proto !== Object.prototype && proto !== null) {
14280
+ return obj;
14281
+ }
14282
+ const result = {};
14283
+ for (const [key, value] of Object.entries(obj)) {
14284
+ result[key] = yield offloadBlobs(value, databaseUrl, getCachedAccessToken, maxStringLength, dirtyFlag, visited);
14285
+ }
14286
+ return result;
14287
+ });
14288
+ }
14289
+ /**
14290
+ * Process a DBOperationsSet and offload any large blobs
14291
+ * Returns a new DBOperationsSet with blobs replaced by BlobRefs
14292
+ */
14293
+ function offloadBlobsInOperations(operations_1, databaseUrl_1, getCachedAccessToken_1) {
14294
+ return __awaiter(this, arguments, void 0, function* (operations, databaseUrl, getCachedAccessToken, maxStringLength = DEFAULT_MAX_STRING_LENGTH) {
14295
+ const result = [];
14296
+ for (const tableOps of operations) {
14297
+ const processedMuts = [];
14298
+ for (const mut of tableOps.muts) {
14299
+ const processedMut = yield offloadBlobsInOperation(mut, databaseUrl, getCachedAccessToken, maxStringLength);
14300
+ processedMuts.push(processedMut);
14301
+ }
14302
+ result.push({
14303
+ table: tableOps.table,
14304
+ muts: processedMuts,
14305
+ });
14306
+ }
14307
+ return result;
14308
+ });
14309
+ }
14310
+ function offloadBlobsInOperation(op_1, databaseUrl_1, getCachedAccessToken_1) {
14311
+ return __awaiter(this, arguments, void 0, function* (op, databaseUrl, getCachedAccessToken, maxStringLength = DEFAULT_MAX_STRING_LENGTH) {
14312
+ switch (op.type) {
14313
+ case 'insert':
14314
+ case 'upsert': {
14315
+ const processedValues = yield Promise.all(op.values.map(value => offloadBlobsAndMarkDirty(value, databaseUrl, getCachedAccessToken, maxStringLength)));
14316
+ return Object.assign(Object.assign({}, op), { values: processedValues });
14317
+ }
14318
+ case 'update': {
14319
+ const processedChangeSpecs = yield Promise.all(op.changeSpecs.map(spec => offloadBlobsAndMarkDirty(spec, databaseUrl, getCachedAccessToken, maxStringLength)));
14320
+ return Object.assign(Object.assign({}, op), { changeSpecs: processedChangeSpecs });
14321
+ }
14322
+ case 'modify': {
14323
+ const processedChangeSpec = yield offloadBlobsAndMarkDirty(op.changeSpec, databaseUrl, getCachedAccessToken, maxStringLength);
14324
+ return Object.assign(Object.assign({}, op), { changeSpec: processedChangeSpec });
14325
+ }
14326
+ case 'delete':
14327
+ // No blobs in delete operations
14328
+ return op;
14329
+ default:
14330
+ return op;
14331
+ }
14332
+ });
14333
+ }
14334
+ /**
14335
+ * Check if there are any large blobs in the operations that need offloading
14336
+ * This is a quick check to avoid unnecessary processing
14337
+ */
14338
+ function hasLargeBlobsInOperations(operations, maxStringLength = DEFAULT_MAX_STRING_LENGTH) {
14339
+ for (const tableOps of operations) {
14340
+ for (const mut of tableOps.muts) {
14341
+ if (hasLargeBlobsInOperation(mut, maxStringLength)) {
14342
+ return true;
14343
+ }
14344
+ }
14345
+ }
14346
+ return false;
14347
+ }
14348
+ function hasLargeBlobsInOperation(op, maxStringLength) {
14349
+ switch (op.type) {
14350
+ case 'insert':
14351
+ case 'upsert':
14352
+ return op.values.some(value => hasLargeBlobs(value, maxStringLength));
14353
+ case 'update':
14354
+ return op.changeSpecs.some(spec => hasLargeBlobs(spec, maxStringLength));
14355
+ case 'modify':
14356
+ return hasLargeBlobs(op.changeSpec, maxStringLength);
14357
+ default:
14358
+ return false;
14359
+ }
14360
+ }
14361
+ function hasLargeBlobs(obj, maxStringLength, visited = new WeakSet()) {
14362
+ if (obj === null || obj === undefined) {
14363
+ return false;
14364
+ }
14365
+ // Check long strings
14366
+ if (typeof obj === 'string' && obj.length > maxStringLength && maxStringLength !== Infinity) {
14367
+ return true;
14368
+ }
14369
+ if (shouldOffloadBlob(obj)) {
14370
+ return true;
14371
+ }
14372
+ if (typeof obj !== 'object') {
14373
+ return false;
14374
+ }
14375
+ // Avoid circular references - check BEFORE processing
14376
+ if (visited.has(obj)) {
14377
+ return false;
14378
+ }
14379
+ visited.add(obj);
14380
+ if (Array.isArray(obj)) {
14381
+ return obj.some(item => hasLargeBlobs(item, maxStringLength, visited));
14382
+ }
14383
+ // Traverse plain objects (POJO-like) - use duck typing since IndexedDB
14384
+ // may return objects where constructor !== Object
14385
+ const proto = Object.getPrototypeOf(obj);
14386
+ if (proto === Object.prototype || proto === null) {
14387
+ return Object.values(obj).some(value => hasLargeBlobs(value, maxStringLength, visited));
14388
+ }
14389
+ return false;
14390
+ }
14391
+
13707
14392
  function updateYSyncStates(lastUpdateIdsBeforeSync, receivedUntilsAfterSync, db) {
13708
14393
  return __awaiter(this, void 0, void 0, function* () {
13709
14394
  var _a, _b, _c, _d, _e;
@@ -13885,6 +14570,33 @@
13885
14570
  });
13886
14571
  }
13887
14572
 
14573
+ const wm$3 = new WeakMap();
14574
+ function loadCachedAccessToken(db) {
14575
+ var _a, _b, _c, _d;
14576
+ let cached = wm$3.get(db);
14577
+ if (cached && cached.expiration > Date.now() + 5 * MINUTES) {
14578
+ return Promise.resolve(cached.accessToken);
14579
+ }
14580
+ const currentUser = db.cloud.currentUser.value;
14581
+ if (currentUser && currentUser.accessToken && ((_b = (_a = currentUser.accessTokenExpiration) === null || _a === void 0 ? void 0 : _a.getTime()) !== null && _b !== void 0 ? _b : Infinity) > Date.now() + 5 * MINUTES) {
14582
+ wm$3.set(db, {
14583
+ accessToken: currentUser.accessToken,
14584
+ expiration: (_d = (_c = currentUser.accessTokenExpiration) === null || _c === void 0 ? void 0 : _c.getTime()) !== null && _d !== void 0 ? _d : Infinity
14585
+ });
14586
+ return Promise.resolve(currentUser.accessToken);
14587
+ }
14588
+ return Dexie.ignoreTransaction(() => loadAccessToken(db).then(user => {
14589
+ var _a, _b;
14590
+ if (user === null || user === void 0 ? void 0 : user.accessToken) {
14591
+ wm$3.set(db, {
14592
+ accessToken: user.accessToken,
14593
+ expiration: (_b = (_a = user.accessTokenExpiration) === null || _a === void 0 ? void 0 : _a.getTime()) !== null && _b !== void 0 ? _b : Infinity
14594
+ });
14595
+ }
14596
+ return (user === null || user === void 0 ? void 0 : user.accessToken) || null;
14597
+ }));
14598
+ }
14599
+
13888
14600
  const CURRENT_SYNC_WORKER = 'currentSyncWorker';
13889
14601
  function sync(db, options, schema, syncOptions) {
13890
14602
  return _sync(db, options, schema, syncOptions)
@@ -13933,7 +14645,7 @@
13933
14645
  return __awaiter(this, arguments, void 0, function* (db, options, schema, { isInitialSync, cancelToken, justCheckIfNeeded, purpose } = {
13934
14646
  isInitialSync: false,
13935
14647
  }) {
13936
- var _a;
14648
+ var _a, _b, _c;
13937
14649
  if (!justCheckIfNeeded) {
13938
14650
  console.debug('SYNC STARTED', { isInitialSync, purpose });
13939
14651
  }
@@ -14002,12 +14714,21 @@
14002
14714
  return false;
14003
14715
  }
14004
14716
  const latestRevisions = getLatestRevisionsPerTable(clientChangeSet, syncState === null || syncState === void 0 ? void 0 : syncState.latestRevisions);
14005
- const clientIdentity = (syncState === null || syncState === void 0 ? void 0 : syncState.clientIdentity) || randomString(16);
14717
+ const clientIdentity = (syncState === null || syncState === void 0 ? void 0 : syncState.clientIdentity) || randomString$1(16);
14718
+ //
14719
+ // Offload large blobs to blob storage before sync
14720
+ //
14721
+ let processedChangeSet = clientChangeSet;
14722
+ const maxStringLength = (_c = (_b = db.cloud.options) === null || _b === void 0 ? void 0 : _b.maxStringLength) !== null && _c !== void 0 ? _c : 32768;
14723
+ const hasLargeBlobs = hasLargeBlobsInOperations(clientChangeSet, maxStringLength);
14724
+ if (hasLargeBlobs) {
14725
+ processedChangeSet = yield offloadBlobsInOperations(clientChangeSet, databaseUrl, () => loadCachedAccessToken(db), maxStringLength);
14726
+ }
14006
14727
  //
14007
14728
  // Push changes to server
14008
14729
  //
14009
14730
  throwIfCancelled(cancelToken);
14010
- const res = yield syncWithServer(clientChangeSet, yMessages, syncState, baseRevs, db, databaseUrl, schema, clientIdentity, currentUser);
14731
+ const res = yield syncWithServer(processedChangeSet, yMessages, syncState, baseRevs, db, databaseUrl, schema, clientIdentity, currentUser);
14011
14732
  console.debug('Sync response', res);
14012
14733
  //
14013
14734
  // Apply changes locally and clear old change entries:
@@ -14410,6 +15131,65 @@
14410
15131
  };
14411
15132
  }
14412
15133
 
15134
+ /**
15135
+ * Deduplicates in-flight blob downloads.
15136
+ *
15137
+ * Both the blob-resolve middleware and the eager blob downloader may
15138
+ * try to fetch the same blob concurrently. This tracker ensures each
15139
+ * unique blob ref is only downloaded once — subsequent requests for
15140
+ * the same ref piggyback on the existing promise.
15141
+ *
15142
+ * Instantiate once per DexieCloudDB.
15143
+ */
15144
+ class BlobDownloadTracker {
15145
+ constructor(db) {
15146
+ this.inFlight = new Map();
15147
+ this.db = db;
15148
+ }
15149
+ /**
15150
+ * Download a blob, deduplicating concurrent requests for the same ref.
15151
+ *
15152
+ * @param blobRef - The BlobRef to download
15153
+ * @param dbUrl - Base URL for the database (e.g., 'https://mydb.dexie.cloud')
15154
+ */
15155
+ download(blobRef, dbUrl) {
15156
+ let promise = this.inFlight.get(blobRef.ref);
15157
+ if (!promise) {
15158
+ promise = loadCachedAccessToken(this.db).then(accessToken => {
15159
+ if (!accessToken)
15160
+ throw new Error("No access token available for blob download");
15161
+ return downloadBlob(blobRef, dbUrl, accessToken);
15162
+ }).finally(() => this.inFlight.delete(blobRef.ref));
15163
+ // When the promise settles (either fulfilled or rejected), remove it from the in-flight map
15164
+ this.inFlight.set(blobRef.ref, promise);
15165
+ }
15166
+ return promise;
15167
+ }
15168
+ }
15169
+ /**
15170
+ * Download blob data from server via proxy endpoint.
15171
+ * Uses auth header for authentication (same as sync).
15172
+ *
15173
+ * @param blobRef - The BlobRef to download
15174
+ * @param dbUrl - Base URL for the database (e.g., 'https://mydb.dexie.cloud')
15175
+ * @param accessToken - Access token for authentication
15176
+ */
15177
+ function downloadBlob(blobRef, dbUrl, accessToken) {
15178
+ return __awaiter(this, void 0, void 0, function* () {
15179
+ const downloadUrl = `${dbUrl}/blob/${blobRef.ref}`;
15180
+ const response = yield fetch(downloadUrl, {
15181
+ headers: {
15182
+ 'Authorization': `Bearer ${accessToken}`
15183
+ }
15184
+ });
15185
+ if (!response.ok) {
15186
+ throw new Error(`Failed to download blob ${blobRef.ref}: ${response.status} ${response.statusText}`);
15187
+ }
15188
+ const arrayBuffer = yield response.arrayBuffer();
15189
+ return new Uint8Array(arrayBuffer);
15190
+ });
15191
+ }
15192
+
14413
15193
  const wm$2 = new WeakMap();
14414
15194
  const DEXIE_CLOUD_SCHEMA = {
14415
15195
  members: '@id, [userId+realmId], [email+realmId], realmId',
@@ -14424,7 +15204,7 @@
14424
15204
  function DexieCloudDB(dx) {
14425
15205
  if ('vip' in dx)
14426
15206
  dx = dx['vip']; // Avoid race condition. Always map to a vipped dexie that don't block during db.on.ready().
14427
- let db = wm$2.get(dx.cloud);
15207
+ let db = wm$2.get(dx);
14428
15208
  if (!db) {
14429
15209
  const localSyncEvent = new rxjs.Subject();
14430
15210
  let syncStateChangedEvent = new BroadcastedAndLocalEvent(`syncstatechanged-${dx.name}`);
@@ -14443,7 +15223,9 @@
14443
15223
  get tables() {
14444
15224
  return dx.tables;
14445
15225
  },
14446
- cloud: dx.cloud,
15226
+ get cloud() {
15227
+ return dx.cloud;
15228
+ },
14447
15229
  get $jobs() {
14448
15230
  return dx.table('$jobs');
14449
15231
  },
@@ -14512,7 +15294,8 @@
14512
15294
  Object.assign(db, helperMethods);
14513
15295
  db.messageConsumer = MessagesFromServerConsumer(db);
14514
15296
  db.messageProducer = new rxjs.Subject();
14515
- wm$2.set(dx.cloud, db);
15297
+ db.blobDownloadTracker = new BlobDownloadTracker(db);
15298
+ wm$2.set(dx, db);
14516
15299
  }
14517
15300
  return db;
14518
15301
  }
@@ -14522,6 +15305,221 @@
14522
15305
  keyPath ? ('[' + [].join.call(keyPath, '+') + ']') : "";
14523
15306
  }
14524
15307
 
15308
+ /**
15309
+ * Blob Progress Tracking
15310
+ *
15311
+ * Uses liveQuery to reactively track unresolved blob refs.
15312
+ * Any change to _hasBlobRefs in any syncable table automatically
15313
+ * triggers a re-scan — no manual updateBlobProgress() needed.
15314
+ */
15315
+ /**
15316
+ * BehaviorSubject for the isDownloading flag, controlled by eagerBlobDownloader.
15317
+ */
15318
+ function createDownloadingState() {
15319
+ return new rxjs.BehaviorSubject(false);
15320
+ }
15321
+ /**
15322
+ * Set downloading state.
15323
+ */
15324
+ function setDownloadingState(downloading$, isDownloading) {
15325
+ if (downloading$.value !== isDownloading) {
15326
+ downloading$.next(isDownloading);
15327
+ }
15328
+ }
15329
+ /**
15330
+ * Create a liveQuery-based Observable<BlobProgress>.
15331
+ *
15332
+ * Combines a liveQuery (blobsRemaining, bytesRemaining) with an external
15333
+ * isDownloading flag controlled by the eager downloader.
15334
+ */
15335
+ function observeBlobProgress(db, downloading$) {
15336
+ const blobStats$ = rxjs.from(Dexie.liveQuery(() => __awaiter(this, void 0, void 0, function* () {
15337
+ let blobsRemaining = 0;
15338
+ let bytesRemaining = 0;
15339
+ const syncedTables = getSyncableTables(db);
15340
+ yield db.dx.transaction('r', syncedTables, (tx) => __awaiter(this, void 0, void 0, function* () {
15341
+ tx.idbtrans.disableBlobResolve = true;
15342
+ for (const table of syncedTables) {
15343
+ const hasIndex = !!table.schema.idxByName['_hasBlobRefs'];
15344
+ if (!hasIndex)
15345
+ continue;
15346
+ const unresolvedObjects = yield table
15347
+ .where('_hasBlobRefs')
15348
+ .equals(1)
15349
+ .toArray();
15350
+ for (const obj of unresolvedObjects) {
15351
+ const blobs = findBlobRefs(obj);
15352
+ blobsRemaining += blobs.length;
15353
+ bytesRemaining += blobs.reduce((sum, blob) => sum + (blob.size || 0), 0);
15354
+ }
15355
+ }
15356
+ }));
15357
+ return { blobsRemaining, bytesRemaining };
15358
+ })));
15359
+ return rxjs.combineLatest([blobStats$, downloading$]).pipe(operators.map(([stats, isDownloading]) => ({
15360
+ isDownloading: isDownloading && stats.blobsRemaining > 0,
15361
+ blobsRemaining: stats.blobsRemaining,
15362
+ bytesRemaining: stats.bytesRemaining,
15363
+ })), operators.share({ resetOnRefCountZero: () => rxjs.timer(2000) }) // Keep alive for 2s after last unsubscription to avoid rapid re-subscriptions during UI updates
15364
+ );
15365
+ }
15366
+ /**
15367
+ * Find all unresolved refs (BlobRef or TSONRef) in an object (recursive).
15368
+ * Handles both live TSONRef instances and serialized TSONRefs (after IndexedDB).
15369
+ */
15370
+ function findBlobRefs(obj) {
15371
+ const refs = [];
15372
+ function scan(value) {
15373
+ if (value === null || value === undefined)
15374
+ return;
15375
+ if (typeof value !== 'object')
15376
+ return;
15377
+ if (TSONRef.isTSONRef(value)) {
15378
+ refs.push({ ref: value.ref, size: value.size });
15379
+ return;
15380
+ }
15381
+ if (isSerializedTSONRef(value)) {
15382
+ const obj = value;
15383
+ refs.push({ ref: obj.ref, size: obj.size });
15384
+ return;
15385
+ }
15386
+ if (isBlobRef(value)) {
15387
+ refs.push({ ref: value.ref, size: value.size || 0 });
15388
+ return;
15389
+ }
15390
+ if (Array.isArray(value)) {
15391
+ value.forEach(scan);
15392
+ }
15393
+ else if (value.constructor === Object) {
15394
+ Object.values(value).forEach(scan);
15395
+ }
15396
+ }
15397
+ scan(obj);
15398
+ return refs;
15399
+ }
15400
+
15401
+ /**
15402
+ * Eager Blob Downloader
15403
+ *
15404
+ * Downloads unresolved blobs in the background when blobMode='eager'.
15405
+ * Called after sync completes to prefetch blobs for offline access.
15406
+ *
15407
+ * Progress is tracked automatically via liveQuery in blobProgress.ts —
15408
+ * no manual progress reporting needed here.
15409
+ */
15410
+ /**
15411
+ * Download all unresolved blobs in the background.
15412
+ *
15413
+ * This is called when blobMode='eager' (default) after sync completes.
15414
+ * BlobRef URLs are signed (SAS tokens) so no auth header needed.
15415
+ *
15416
+ * Each blob is saved atomically using Table.update() to avoid race conditions.
15417
+ */
15418
+ function downloadUnresolvedBlobs(db, downloading$, signal) {
15419
+ return __awaiter(this, void 0, void 0, function* () {
15420
+ var _a;
15421
+ const debugLog = (msg) => console.debug(`[dexie-cloud] ${msg}`);
15422
+ debugLog('Eager download: Starting...');
15423
+ // Scan for unresolved blobs
15424
+ const syncedTables = getSyncableTables(db);
15425
+ let hasWork = false;
15426
+ for (const table of syncedTables) {
15427
+ try {
15428
+ const hasIndex = !!table.schema.idxByName['_hasBlobRefs'];
15429
+ if (!hasIndex)
15430
+ continue;
15431
+ const count = yield table.where('_hasBlobRefs').equals(1).count();
15432
+ if (count > 0) {
15433
+ hasWork = true;
15434
+ break;
15435
+ }
15436
+ }
15437
+ catch (_b) {
15438
+ // skip
15439
+ }
15440
+ }
15441
+ if (!hasWork) {
15442
+ debugLog('Eager download: No blobs remaining, exiting');
15443
+ return;
15444
+ }
15445
+ setDownloadingState(downloading$, true);
15446
+ try {
15447
+ debugLog(`Eager download: Found ${syncedTables.length} syncable tables: ${syncedTables.map(t => t.name).join(', ')}`);
15448
+ for (const table of syncedTables) {
15449
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted)
15450
+ ;
15451
+ try {
15452
+ // Check if table has _hasBlobRefs index
15453
+ const hasIndex = table.schema.indexes.some(idx => idx.name === '_hasBlobRefs');
15454
+ if (!hasIndex)
15455
+ continue;
15456
+ // Query objects with _hasBlobRefs marker
15457
+ const unresolvedObjects = yield table
15458
+ .where('_hasBlobRefs')
15459
+ .equals(1)
15460
+ .toArray();
15461
+ debugLog(`Eager download: Table ${table.name} has ${unresolvedObjects.length} unresolved objects`);
15462
+ const databaseUrl = (_a = db.cloud.options) === null || _a === void 0 ? void 0 : _a.databaseUrl;
15463
+ if (!databaseUrl)
15464
+ throw new Error('Database URL is required to download blobs');
15465
+ // Download up to MAX_CONCURRENT blobs in parallel
15466
+ const MAX_CONCURRENT = 6;
15467
+ const primaryKey = table.schema.primKey;
15468
+ // Filter to actionable objects first
15469
+ const pending = unresolvedObjects.filter(obj => {
15470
+ if (!hasUnresolvedBlobRefs(obj))
15471
+ return false;
15472
+ const key = primaryKey.keyPath
15473
+ ? Dexie.getByKeyPath(obj, primaryKey.keyPath)
15474
+ : undefined;
15475
+ return key !== undefined;
15476
+ });
15477
+ // Process in parallel with concurrency limit
15478
+ let i = 0;
15479
+ const runNext = () => __awaiter(this, void 0, void 0, function* () {
15480
+ while (i < pending.length) {
15481
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted)
15482
+ ;
15483
+ const obj = pending[i++];
15484
+ const key = Dexie.getByKeyPath(obj, primaryKey.keyPath);
15485
+ try {
15486
+ // Refresh token per object — cheap (returns cached) but ensures
15487
+ // we pick up renewed tokens during long download sessions.
15488
+ const resolvedBlobs = [];
15489
+ yield resolveAllBlobRefs(obj, databaseUrl, resolvedBlobs, '', new WeakMap(), db.blobDownloadTracker);
15490
+ const updateSpec = {
15491
+ _hasBlobRefs: undefined,
15492
+ };
15493
+ for (const blob of resolvedBlobs) {
15494
+ updateSpec[blob.keyPath] = blob.data;
15495
+ }
15496
+ debugLog(`Eager download: Updating ${table.name}:${key} with ${resolvedBlobs.length} blobs`);
15497
+ yield table.update(key, updateSpec);
15498
+ // liveQuery in blobProgress.ts auto-detects this change
15499
+ }
15500
+ catch (err) {
15501
+ console.error(`Failed to download blobs for ${table.name}:${key}:`, err);
15502
+ }
15503
+ }
15504
+ });
15505
+ // Launch up to MAX_CONCURRENT workers
15506
+ const workers = [];
15507
+ for (let w = 0; w < Math.min(MAX_CONCURRENT, pending.length); w++) {
15508
+ workers.push(runNext());
15509
+ }
15510
+ yield Promise.all(workers);
15511
+ }
15512
+ catch (err) {
15513
+ // Table might not have _hasBlobRefs index or other issues - skip silently
15514
+ }
15515
+ }
15516
+ }
15517
+ finally {
15518
+ setDownloadingState(downloading$, false);
15519
+ }
15520
+ });
15521
+ }
15522
+
14525
15523
  // Emulate true-private property db. Why? So it's not stored in DB.
14526
15524
  const wm$1 = new WeakMap();
14527
15525
  class AuthPersistedContext {
@@ -15480,7 +16478,7 @@
15480
16478
  }
15481
16479
  if (mode === 'readwrite') {
15482
16480
  // Give each transaction a globally unique id.
15483
- tx.txid = randomString$1(16);
16481
+ tx.txid = randomString(16);
15484
16482
  tx.opCount = 0;
15485
16483
  // Introduce the concept of current user that lasts through the entire transaction.
15486
16484
  // This is important because the tracked mutations must be connected to the user.
@@ -15778,6 +16776,318 @@
15778
16776
  };
15779
16777
  }
15780
16778
 
16779
+ /**
16780
+ * BlobSavingQueue - Queues resolved blobs for saving back to IndexedDB
16781
+ *
16782
+ * Uses setTimeout(fn, 0) instead of queueMicrotask to completely isolate
16783
+ * from Dexie's Promise.PSD context. This prevents the save operation
16784
+ * from inheriting any ongoing transaction.
16785
+ *
16786
+ * Each blob is saved atomically using downCore transaction with the specific
16787
+ * keyPath to avoid race conditions with other property changes.
16788
+ */
16789
+ class BlobSavingQueue {
16790
+ constructor(db) {
16791
+ this.queue = [];
16792
+ this.isProcessing = false;
16793
+ this.db = db;
16794
+ }
16795
+ /**
16796
+ * Queue a resolved blob for saving.
16797
+ * Only the specific blob property will be updated atomically.
16798
+ */
16799
+ saveBlobs(tableName, primaryKey, resolvedBlobs) {
16800
+ this.queue.push({ tableName, primaryKey, resolvedBlobs });
16801
+ this.startConsumer();
16802
+ }
16803
+ /**
16804
+ * Start the consumer if not already processing.
16805
+ * Uses setTimeout(fn, 0) to completely break out of any
16806
+ * Dexie transaction context (Promise.PSD).
16807
+ */
16808
+ startConsumer() {
16809
+ if (this.isProcessing)
16810
+ return;
16811
+ this.isProcessing = true;
16812
+ // Use setTimeout to completely isolate from Dexie's PSD context
16813
+ // queueMicrotask would risk inheriting the current transaction
16814
+ setTimeout(() => {
16815
+ this.processQueue();
16816
+ }, 0);
16817
+ }
16818
+ /**
16819
+ * Process all queued blobs.
16820
+ * Runs in a completely isolated context (no inherited transaction).
16821
+ * Uses atomic updates to avoid race conditions.
16822
+ */
16823
+ processQueue() {
16824
+ const item = this.queue.shift();
16825
+ if (!item) {
16826
+ this.isProcessing = false;
16827
+ return;
16828
+ }
16829
+ // Atomic update of just the blob property
16830
+ this.db.transaction('rw', item.tableName, (tx) => {
16831
+ const trans = tx.idbtrans;
16832
+ trans.disableChangeTracking = true; // Don't regard this as a change for sync purposes
16833
+ trans.disableAccessControl = true; // Bypass any access control checks since this is an internal operation
16834
+ trans.disableBlobResolve = true; // Custom flag to skip blob resolve middleware during this transaction
16835
+ const updateSpec = {};
16836
+ for (const blob of item.resolvedBlobs) {
16837
+ updateSpec[blob.keyPath] = blob.data;
16838
+ }
16839
+ tx.table(item.tableName).update(item.primaryKey, obj => {
16840
+ // Check that object still has the same unresolved blob refs before applying update (i.e. it hasn't been modified since we read it)
16841
+ for (const blob of item.resolvedBlobs) {
16842
+ // 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.
16843
+ const currentValue = Dexie.getByKeyPath(obj, blob.keyPath);
16844
+ if (currentValue === undefined) {
16845
+ // Blob property was removed - skip updating this blob
16846
+ continue;
16847
+ }
16848
+ if (!isBlobRef(currentValue)) {
16849
+ // Blob property was modified to a non-blob-ref value - skip updating this blob
16850
+ continue;
16851
+ }
16852
+ if (currentValue.ref !== blob.ref) {
16853
+ // Blob property was modified - skip updating this blob
16854
+ return; // Stop. Another items has been queued to fully fix the object.
16855
+ }
16856
+ Dexie.setByKeyPath(obj, blob.keyPath, blob.data);
16857
+ }
16858
+ delete obj._hasBlobRefs; // Clear the _hasBlobRefs marker if all refs was resolved.
16859
+ });
16860
+ }).catch((error) => {
16861
+ console.error(`Error saving resolved blobs on ${item.tableName}:${item.primaryKey}:`, error);
16862
+ }).finally(() => {
16863
+ // Process next item in the queue
16864
+ return this.processQueue();
16865
+ });
16866
+ }
16867
+ }
16868
+
16869
+ /**
16870
+ * DBCore Middleware for resolving BlobRefs on read
16871
+ *
16872
+ * This middleware intercepts read operations and resolves any BlobRefs
16873
+ * found in objects marked with _hasBlobRefs.
16874
+ *
16875
+ * Important: Avoids async/await to preserve Dexie's Promise.PSD context.
16876
+ * Uses Dexie.waitFor() only for explicit rw transactions to keep them alive.
16877
+ * For readonly or implicit transactions, resolves directly (no waitFor needed).
16878
+ *
16879
+ * Resolved blobs are queued for saving via BlobSavingQueue, which uses
16880
+ * setTimeout(fn, 0) to completely isolate from Dexie's transaction context.
16881
+ * Each blob is saved atomically using Table.update() with its keyPath to
16882
+ * avoid race conditions with other property changes.
16883
+ *
16884
+ * Blob downloads use Authorization header (same as sync) via the server
16885
+ * proxy endpoint: GET /blob/{ref}
16886
+ */
16887
+ function createBlobResolveMiddleware(db) {
16888
+ return {
16889
+ stack: 'dbcore',
16890
+ name: 'blobResolve',
16891
+ level: -2, // Run below other middlewares and after sync and caching middlewares
16892
+ create(downlevelDatabase) {
16893
+ // Create a single queue instance for this database
16894
+ const blobSavingQueue = new BlobSavingQueue(db);
16895
+ return Object.assign(Object.assign({}, downlevelDatabase), { table(tableName) {
16896
+ var _a;
16897
+ if (!db.cloud) {
16898
+ // db.cloud not yet initialized - skip blob resolution
16899
+ // Fall through to downlevel table to avoid crash
16900
+ return downlevelDatabase.table(tableName);
16901
+ }
16902
+ const dbUrl = (_a = db.cloud.options) === null || _a === void 0 ? void 0 : _a.databaseUrl;
16903
+ const downlevelTable = downlevelDatabase.table(tableName);
16904
+ // Skip internal tables
16905
+ if (tableName.startsWith('$')) {
16906
+ return downlevelTable;
16907
+ }
16908
+ return Object.assign(Object.assign({}, downlevelTable), { get(req) {
16909
+ var _a;
16910
+ if ((_a = req.trans) === null || _a === void 0 ? void 0 : _a.disableBlobResolve) {
16911
+ return downlevelTable.get(req);
16912
+ }
16913
+ return downlevelTable.get(req).then(result => {
16914
+ if (result && hasUnresolvedBlobRefs(result)) {
16915
+ return resolveAndSave(downlevelTable, req.trans, req.key, result, blobSavingQueue, db);
16916
+ }
16917
+ return result;
16918
+ });
16919
+ },
16920
+ getMany(req) {
16921
+ var _a;
16922
+ if ((_a = req.trans) === null || _a === void 0 ? void 0 : _a.disableBlobResolve) {
16923
+ return downlevelTable.getMany(req);
16924
+ }
16925
+ return downlevelTable.getMany(req).then(results => {
16926
+ // Check if any results need resolution
16927
+ const needsResolution = results.some(r => r && hasUnresolvedBlobRefs(r));
16928
+ if (!needsResolution)
16929
+ return results;
16930
+ return Dexie.Promise.all(results.map((result, index) => {
16931
+ if (result && hasUnresolvedBlobRefs(result)) {
16932
+ return resolveAndSave(downlevelTable, req.trans, req.keys[index], result, blobSavingQueue, db);
16933
+ }
16934
+ return result;
16935
+ }));
16936
+ });
16937
+ },
16938
+ query(req) {
16939
+ var _a;
16940
+ if ((_a = req.trans) === null || _a === void 0 ? void 0 : _a.disableBlobResolve) {
16941
+ return downlevelTable.query(req);
16942
+ }
16943
+ return downlevelTable.query(req).then(result => {
16944
+ if (!result.result || !Array.isArray(result.result))
16945
+ return result;
16946
+ // Check if any results need resolution
16947
+ const needsResolution = result.result.some(r => r && hasUnresolvedBlobRefs(r));
16948
+ if (!needsResolution)
16949
+ return result;
16950
+ return Dexie.Promise.all(result.result.map(item => {
16951
+ if (item && hasUnresolvedBlobRefs(item)) {
16952
+ return resolveAndSave(downlevelTable, req.trans, undefined, item, blobSavingQueue, db);
16953
+ }
16954
+ return item;
16955
+ })).then(resolved => (Object.assign(Object.assign({}, result), { result: resolved })));
16956
+ });
16957
+ },
16958
+ openCursor(req) {
16959
+ var _a;
16960
+ if ((_a = req.trans) === null || _a === void 0 ? void 0 : _a.disableBlobResolve) {
16961
+ return downlevelTable.openCursor(req);
16962
+ }
16963
+ return downlevelTable.openCursor(req).then(cursor => {
16964
+ if (!cursor)
16965
+ return cursor; // No results, so no resolution needed
16966
+ if (!req.values)
16967
+ return cursor; // No values requested, so no resolution needed
16968
+ if (!dbUrl)
16969
+ return cursor; // No database URL configured, can't resolve blobs
16970
+ return createBlobResolvingCursor(cursor, downlevelTable, blobSavingQueue, db);
16971
+ });
16972
+ } });
16973
+ } });
16974
+ },
16975
+ };
16976
+ }
16977
+ /**
16978
+ * Create a cursor wrapper that resolves BlobRefs in values synchronously.
16979
+ *
16980
+ * Uses Object.create() to inherit all cursor methods, only overriding:
16981
+ * - start(): Resolves BlobRefs before calling the callback
16982
+ * - value: Getter that returns the resolved value
16983
+ *
16984
+ * Returns the cursor synchronously. Resolution happens in start() before
16985
+ * each onNext callback, ensuring cursor.value is always available.
16986
+ */
16987
+ function createBlobResolvingCursor(cursor, table, blobSavingQueue, db) {
16988
+ // Create wrapped cursor using Object.create() - inherits everything
16989
+ const wrappedCursor = Object.create(cursor, {
16990
+ value: {
16991
+ value: cursor.value,
16992
+ enumerable: true,
16993
+ writable: true
16994
+ },
16995
+ start: {
16996
+ value(onNext) {
16997
+ // Override start to resolve BlobRefs before each callback
16998
+ return cursor.start(() => {
16999
+ const rawValue = cursor.value;
17000
+ if (!rawValue || !hasUnresolvedBlobRefs(rawValue)) {
17001
+ onNext();
17002
+ return;
17003
+ }
17004
+ resolveAndSave(table, cursor.trans, cursor.primaryKey, rawValue, blobSavingQueue, db, true).then(resolved => {
17005
+ wrappedCursor.value = resolved;
17006
+ onNext();
17007
+ }, err => {
17008
+ console.error('Failed to resolve BlobRefs for cursor value:', err);
17009
+ onNext();
17010
+ });
17011
+ });
17012
+ }
17013
+ }
17014
+ });
17015
+ return wrappedCursor;
17016
+ }
17017
+ /**
17018
+ * Resolve BlobRefs in an object and queue each blob for atomic saving.
17019
+ *
17020
+ * Uses Dexie.waitFor() only when needed:
17021
+ * - Skip waitFor for readonly ('r') transactions
17022
+ * - Skip waitFor for implicit transactions (most common in liveQuery)
17023
+ * - Use waitFor only for explicit rw transactions that need to stay alive
17024
+ *
17025
+ * Each resolved blob is queued individually with its keyPath for atomic
17026
+ * update using downCore transaction with the specific keyPath - this avoids race conditions.
17027
+ *
17028
+ * Returns Dexie.Promise to preserve PSD context.
17029
+ */
17030
+ function resolveAndSave(table, trans, pKey, // optional. If missing, tries to extract from object using primary key path
17031
+ obj, blobSavingQueue, db, isCursorValue = false // Flag to indicate if we're resolving a cursor value (which may not have a primary key)
17032
+ ) {
17033
+ var _a;
17034
+ try {
17035
+ // Determine if we need waitFor:
17036
+ // Skip waitFor ONLY if BOTH conditions are met:
17037
+ // 1. readonly transaction
17038
+ // 2. implicit (non-explicit) transaction
17039
+ //
17040
+ // Transaction.explicit is true when user called db.transaction() explicitly.
17041
+ // For implicit transactions (auto-created for single operations),
17042
+ // Dexie handles async automatically so no waitFor needed.
17043
+ const currentTx = Dexie.currentTransaction;
17044
+ const isReadonly = (currentTx === null || currentTx === void 0 ? void 0 : currentTx.mode) === 'readonly';
17045
+ const isExplicit = (currentTx === null || currentTx === void 0 ? void 0 : currentTx.explicit) === true;
17046
+ // Skip waitFor only for implicit readonly (most common case: liveQuery)
17047
+ const skipWaitFor = isReadonly && !isExplicit && !isCursorValue;
17048
+ const needsWaitFor = currentTx && !skipWaitFor;
17049
+ const dbUrl = ((_a = db.cloud.options) === null || _a === void 0 ? void 0 : _a.databaseUrl) || '';
17050
+ // Collect resolved blobs with their keyPaths
17051
+ const resolvedBlobs = [];
17052
+ // Create the resolution promise with auth info
17053
+ const resolutionPromise = resolveAllBlobRefs(obj, dbUrl, resolvedBlobs, '', new WeakMap(), db.blobDownloadTracker);
17054
+ // Wrap with waitFor to keep transaction alive during fetch
17055
+ const resolvePromise = needsWaitFor
17056
+ ? Dexie.waitFor(resolutionPromise)
17057
+ : Dexie.Promise.resolve(resolutionPromise);
17058
+ return resolvePromise.then(resolved => {
17059
+ // Get primary key from the object
17060
+ const primaryKey = table.schema.primaryKey;
17061
+ const key = pKey !== undefined ? pKey : primaryKey.keyPath
17062
+ ? Dexie.getByKeyPath(obj, primaryKey.keyPath)
17063
+ : undefined;
17064
+ if (key !== undefined) {
17065
+ // Queue each resolved blob individually for atomic update
17066
+ // This uses setTimeout(fn, 0) to completely isolate from
17067
+ // Dexie's transaction context (avoids inheriting PSD)
17068
+ if (isReadonly) {
17069
+ blobSavingQueue.saveBlobs(table.name, key, resolvedBlobs);
17070
+ }
17071
+ else {
17072
+ // For rw transactions, we can save directly without queueing
17073
+ // since we're still in the same transaction context
17074
+ table.mutate({ type: 'put', keys: [key], values: [resolved], trans }).catch(err => {
17075
+ console.error(`Failed to save resolved blob on ${table.name}:${key}:`, err);
17076
+ });
17077
+ }
17078
+ }
17079
+ return resolved;
17080
+ }).catch(err => {
17081
+ console.error(`[dexie-cloud:blobResolve] Failed to resolve BlobRefs on ${table.name}:`, err);
17082
+ return obj; // Return original object on error - never block the read pipeline
17083
+ });
17084
+ }
17085
+ catch (err) {
17086
+ console.error(`[dexie-cloud:blobResolve] Sync error in resolveAndSave on ${table.name}:`, err);
17087
+ return Dexie.Promise.resolve(obj); // Never block reads
17088
+ }
17089
+ }
17090
+
15781
17091
  function overrideParseStoresSpec(origFunc, dexie) {
15782
17092
  return function (stores, dbSchema) {
15783
17093
  var _a;
@@ -15830,6 +17140,11 @@
15830
17140
  if (!/^\$/.test(tableName)) {
15831
17141
  storesClone[`$${tableName}_mutations`] = '++rev';
15832
17142
  cloudTableSchema.markedForSync = true;
17143
+ // Add sparse index for _hasBlobRefs (for BlobRef resolution tracking)
17144
+ // IndexedDB sparse indexes have zero overhead when the property doesn't exist
17145
+ if (!storesClone[tableName].includes('_hasBlobRefs')) {
17146
+ storesClone[tableName] += ',_hasBlobRefs';
17147
+ }
15833
17148
  }
15834
17149
  if (cloudTableSchema.deleted) {
15835
17150
  cloudTableSchema.deleted = false;
@@ -16731,7 +18046,6 @@
16731
18046
  yield checkSyncRateLimitDelay(db);
16732
18047
  yield performGuardedJob(db, CURRENT_SYNC_WORKER, () => sync(db, cloudOptions, cloudSchema, options));
16733
18048
  ongoingSyncs.delete(db);
16734
- console.debug('Done sync');
16735
18049
  }
16736
18050
  catch (error) {
16737
18051
  ongoingSyncs.delete(db);
@@ -16746,8 +18060,6 @@
16746
18060
  }
16747
18061
  }
16748
18062
 
16749
- const SECONDS = 1000;
16750
-
16751
18063
  function LocalSyncWorker(db, cloudOptions, cloudSchema) {
16752
18064
  let localSyncEventSubscription = null;
16753
18065
  let cancelToken = { cancelled: false };
@@ -17084,6 +18396,38 @@
17084
18396
  color: "#374151",
17085
18397
  transition: "all 0.2s ease",
17086
18398
  gap: "12px"
18399
+ },
18400
+ // Copy button for alerts with copyText
18401
+ CopyButton: {
18402
+ display: "inline-flex",
18403
+ alignItems: "center",
18404
+ gap: "4px",
18405
+ padding: "4px 10px",
18406
+ marginTop: "8px",
18407
+ border: "1px solid #d1d5db",
18408
+ borderRadius: "4px",
18409
+ backgroundColor: "#f9fafb",
18410
+ cursor: "pointer",
18411
+ fontSize: "12px",
18412
+ fontWeight: "500",
18413
+ color: "#374151",
18414
+ transition: "all 0.15s ease",
18415
+ fontFamily: "monospace"
18416
+ },
18417
+ CopyButtonCopied: {
18418
+ display: "inline-flex",
18419
+ alignItems: "center",
18420
+ gap: "4px",
18421
+ padding: "4px 10px",
18422
+ marginTop: "8px",
18423
+ border: "1px solid #22c55e",
18424
+ borderRadius: "4px",
18425
+ backgroundColor: "#f0fdf4",
18426
+ cursor: "default",
18427
+ fontSize: "12px",
18428
+ fontWeight: "500",
18429
+ color: "#16a34a",
18430
+ fontFamily: "monospace"
17087
18431
  }};
17088
18432
 
17089
18433
  function Dialog({ children, className }) {
@@ -17194,7 +18538,9 @@
17194
18538
  return (_$1(Dialog, { className: "dxc-login-dlg" },
17195
18539
  _$1(k$1, null,
17196
18540
  _$1("h3", { style: Styles.WindowHeader }, title),
17197
- alerts.map((alert, idx) => (_$1("p", { key: idx, style: Styles.Alert[alert.type] }, resolveText(alert)))),
18541
+ alerts.map((alert, idx) => (_$1("div", { key: idx },
18542
+ _$1("p", { style: Styles.Alert[alert.type] }, resolveText(alert)),
18543
+ alert.copyText && _$1(CopyButton, { text: alert.copyText })))),
17198
18544
  hasOptions && (_$1("div", { class: "dxc-options" }, hasMultipleGroups ? (
17199
18545
  // Render with dividers between groups
17200
18546
  Array.from(optionGroups.entries()).map(([groupName, groupOptions], groupIdx) => (_$1(k$1, { key: groupName },
@@ -17233,6 +18579,50 @@
17233
18579
  return value;
17234
18580
  }
17235
18581
  }
18582
+ function CopyButton({ text }) {
18583
+ const [copied, setCopied] = d(false);
18584
+ const timeoutRef = A(null);
18585
+ // Cleanup timeout on unmount
18586
+ _(() => {
18587
+ return () => {
18588
+ if (timeoutRef.current !== null)
18589
+ clearTimeout(timeoutRef.current);
18590
+ };
18591
+ }, []);
18592
+ const scheduleCopiedReset = () => {
18593
+ if (timeoutRef.current !== null)
18594
+ clearTimeout(timeoutRef.current);
18595
+ setCopied(true);
18596
+ timeoutRef.current = setTimeout(() => {
18597
+ timeoutRef.current = null;
18598
+ setCopied(false);
18599
+ }, 2000);
18600
+ };
18601
+ const handleClick = () => {
18602
+ var _a;
18603
+ if (typeof navigator !== 'undefined' && ((_a = navigator.clipboard) === null || _a === void 0 ? void 0 : _a.writeText)) {
18604
+ navigator.clipboard.writeText(text).then(scheduleCopiedReset).catch(() => {
18605
+ fallbackCopy(text, scheduleCopiedReset);
18606
+ });
18607
+ }
18608
+ else {
18609
+ fallbackCopy(text, scheduleCopiedReset);
18610
+ }
18611
+ };
18612
+ return (_$1("button", { type: "button", style: copied ? Styles.CopyButtonCopied : Styles.CopyButton, onClick: handleClick, title: "Copy to clipboard" }, copied ? '✓ Copied!' : `📋 ${text}`));
18613
+ }
18614
+ function fallbackCopy(text, onSuccess) {
18615
+ const textarea = document.createElement('textarea');
18616
+ textarea.value = text;
18617
+ textarea.style.position = 'fixed';
18618
+ textarea.style.opacity = '0';
18619
+ document.body.appendChild(textarea);
18620
+ textarea.select();
18621
+ const success = document.execCommand('copy');
18622
+ document.body.removeChild(textarea);
18623
+ if (success)
18624
+ onSuccess();
18625
+ }
17236
18626
 
17237
18627
  class LoginGui extends x {
17238
18628
  constructor(props) {
@@ -17931,9 +19321,10 @@
17931
19321
  currentUserEmitter.next(UNAUTHORIZED_USER);
17932
19322
  });
17933
19323
  const syncComplete = new rxjs.Subject();
19324
+ const downloading$ = createDownloadingState();
17934
19325
  dexie.cloud = {
17935
19326
  // @ts-ignore
17936
- version: "4.3.9",
19327
+ version: "4.4.1",
17937
19328
  options: Object.assign({}, DEFAULT_OPTIONS),
17938
19329
  schema: null,
17939
19330
  get currentUserId() {
@@ -17948,6 +19339,7 @@
17948
19339
  syncComplete,
17949
19340
  },
17950
19341
  persistedSyncState: new rxjs.BehaviorSubject(undefined),
19342
+ blobProgress: observeBlobProgress(DexieCloudDB(dexie), downloading$),
17951
19343
  userInteraction: new rxjs.BehaviorSubject(undefined),
17952
19344
  webSocketStatus: new rxjs.BehaviorSubject('not-started'),
17953
19345
  login(hint) {
@@ -17960,6 +19352,16 @@
17960
19352
  invites: getInvitesObservable(dexie),
17961
19353
  roles: getGlobalRolesObservable(dexie),
17962
19354
  configure(options) {
19355
+ // Validate maxStringLength — Infinity disables offloading, otherwise must be
19356
+ // a finite positive number not exceeding the server limit (32768).
19357
+ const MAX_SERVER_STRING_LENGTH = 32768;
19358
+ if (options.maxStringLength !== undefined &&
19359
+ options.maxStringLength !== Infinity &&
19360
+ (!Number.isFinite(options.maxStringLength) ||
19361
+ options.maxStringLength < 0 ||
19362
+ options.maxStringLength > MAX_SERVER_STRING_LENGTH)) {
19363
+ throw new Error(`maxStringLength must be Infinity or a finite number in [0, ${MAX_SERVER_STRING_LENGTH}]. Got: ${options.maxStringLength}`);
19364
+ }
17963
19365
  options = dexie.cloud.options = Object.assign(Object.assign({}, dexie.cloud.options), options);
17964
19366
  configuredProgramatically = true;
17965
19367
  if (options.databaseUrl && options.nameSuffix) {
@@ -18061,6 +19463,7 @@
18061
19463
  var _a, _b;
18062
19464
  return ((_b = (_a = this.db.cloud.schema) === null || _a === void 0 ? void 0 : _a[this.name]) === null || _b === void 0 ? void 0 : _b.idPrefix) || '';
18063
19465
  };
19466
+ dexie.use(createBlobResolveMiddleware(DexieCloudDB(dexie)));
18064
19467
  dexie.use(createMutationTrackingMiddleware({
18065
19468
  currentUserObservable: dexie.cloud.currentUser,
18066
19469
  db: DexieCloudDB(dexie),
@@ -18069,7 +19472,7 @@
18069
19472
  dexie.use(createIdGenerationMiddleware(DexieCloudDB(dexie)));
18070
19473
  function onDbReady(dexie) {
18071
19474
  return __awaiter(this, void 0, void 0, function* () {
18072
- var _a, _b, _c, _d, _e, _f, _g;
19475
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
18073
19476
  closed = false; // As Dexie calls us, we are not closed anymore. Maybe reopened? Remember db.ready event is registered with sticky flag!
18074
19477
  const db = DexieCloudDB(dexie);
18075
19478
  // Setup default GUI:
@@ -18083,6 +19486,25 @@
18083
19486
  }
18084
19487
  // Forward db.syncCompleteEvent to be publicly consumable via db.cloud.events.syncComplete:
18085
19488
  subscriptions.push(db.syncCompleteEvent.subscribe(syncComplete));
19489
+ // Eager blob download: When blobMode='eager' (default), download unresolved blobs after sync
19490
+ const blobMode = (_c = (_b = db.cloud.options) === null || _b === void 0 ? void 0 : _b.blobMode) !== null && _c !== void 0 ? _c : 'eager';
19491
+ if (blobMode === 'eager') {
19492
+ let eagerBlobDownloadInFlight = null;
19493
+ const downloadBlobs = () => {
19494
+ if (eagerBlobDownloadInFlight)
19495
+ return;
19496
+ eagerBlobDownloadInFlight = Dexie.ignoreTransaction(() => downloadUnresolvedBlobs(db, downloading$))
19497
+ .catch(err => {
19498
+ console.error('[dexie-cloud] Eager blob download failed:', err);
19499
+ })
19500
+ .finally(() => {
19501
+ eagerBlobDownloadInFlight = null;
19502
+ });
19503
+ };
19504
+ setTimeout(downloadBlobs, 0); // Don't block ready event. Start downloading blobs in the background right after.
19505
+ // And also after every sync completes:
19506
+ subscriptions.push(db.syncCompleteEvent.subscribe(downloadBlobs));
19507
+ }
18086
19508
  //verifyConfig(db.cloud.options); Not needed (yet at least!)
18087
19509
  // Verify the user has allowed version increment.
18088
19510
  if (!db.tables.every((table) => table.core)) {
@@ -18238,7 +19660,7 @@
18238
19660
  // Continue with normal flow - user can try again
18239
19661
  }
18240
19662
  }
18241
- const requireAuth = (_b = db.cloud.options) === null || _b === void 0 ? void 0 : _b.requireAuth;
19663
+ const requireAuth = (_d = db.cloud.options) === null || _d === void 0 ? void 0 : _d.requireAuth;
18242
19664
  if (requireAuth) {
18243
19665
  if (db.cloud.isServiceWorkerDB) {
18244
19666
  // If this is a service worker DB, we can't do authentication here,
@@ -18275,20 +19697,20 @@
18275
19697
  localSyncWorker.stop();
18276
19698
  localSyncWorker = null;
18277
19699
  throwIfClosed();
18278
- const doInitialSync = ((_c = db.cloud.options) === null || _c === void 0 ? void 0 : _c.databaseUrl) && (!initiallySynced || changedUser);
19700
+ const doInitialSync = ((_e = db.cloud.options) === null || _e === void 0 ? void 0 : _e.databaseUrl) && (!initiallySynced || changedUser);
18279
19701
  if (doInitialSync) {
18280
19702
  // Do the initial sync directly in the browser thread no matter if we are using service worker or not.
18281
19703
  yield performInitialSync(db, db.cloud.options, db.cloud.schema);
18282
19704
  db.setInitiallySynced(true);
18283
19705
  }
18284
19706
  throwIfClosed();
18285
- if (db.cloud.usingServiceWorker && ((_d = db.cloud.options) === null || _d === void 0 ? void 0 : _d.databaseUrl)) {
19707
+ if (db.cloud.usingServiceWorker && ((_f = db.cloud.options) === null || _f === void 0 ? void 0 : _f.databaseUrl)) {
18286
19708
  if (!doInitialSync) {
18287
19709
  registerSyncEvent(db, 'push').catch(() => { });
18288
19710
  }
18289
19711
  registerPeriodicSyncEvent(db).catch(() => { });
18290
19712
  }
18291
- else if (((_e = db.cloud.options) === null || _e === void 0 ? void 0 : _e.databaseUrl) &&
19713
+ else if (((_g = db.cloud.options) === null || _g === void 0 ? void 0 : _g.databaseUrl) &&
18292
19714
  db.cloud.schema &&
18293
19715
  !db.cloud.isServiceWorkerDB) {
18294
19716
  // There's no SW. Start SyncWorker instead.
@@ -18317,8 +19739,8 @@
18317
19739
  }));
18318
19740
  }
18319
19741
  // Connect WebSocket unless we are in a service worker or websocket is disabled.
18320
- if (((_f = db.cloud.options) === null || _f === void 0 ? void 0 : _f.databaseUrl) &&
18321
- !((_g = db.cloud.options) === null || _g === void 0 ? void 0 : _g.disableWebSocket) &&
19742
+ if (((_h = db.cloud.options) === null || _h === void 0 ? void 0 : _h.databaseUrl) &&
19743
+ !((_j = db.cloud.options) === null || _j === void 0 ? void 0 : _j.disableWebSocket) &&
18322
19744
  !IS_SERVICE_WORKER) {
18323
19745
  subscriptions.push(connectWebSocket(db));
18324
19746
  }
@@ -18326,7 +19748,7 @@
18326
19748
  }
18327
19749
  }
18328
19750
  // @ts-ignore
18329
- dexieCloud.version = "4.3.9";
19751
+ dexieCloud.version = "4.4.1";
18330
19752
  Dexie.Cloud = dexieCloud;
18331
19753
 
18332
19754
  // In case the SW lives for a while, let it reuse already opened connections: