dexie-cloud-addon 4.4.0 → 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.
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * ==========================================================================
10
10
  *
11
- * Version 4.4.0, Wed Mar 18 2026
11
+ * Version 4.4.1, Thu Mar 19 2026
12
12
  *
13
13
  * https://dexie.org
14
14
  *
@@ -2603,6 +2603,194 @@ function bulkUpdate(table, keys, changeSpecs) {
2603
2603
  });
2604
2604
  }
2605
2605
 
2606
+ /**
2607
+ * Check if a value is a BlobRef (offloaded binary data)
2608
+ * A BlobRef has _bt (type), ref (blob ID), but no v (inline data)
2609
+ */
2610
+ function isBlobRef(value) {
2611
+ if (typeof value !== 'object' || value === null)
2612
+ return false;
2613
+ const obj = value;
2614
+ return (typeof obj._bt === 'string' &&
2615
+ typeof obj.ref === 'string' &&
2616
+ obj.v === undefined // No inline data = it's a reference
2617
+ );
2618
+ }
2619
+ /**
2620
+ * Check if a value is a serialized TSONRef (after IndexedDB storage)
2621
+ * Has 'type' instead of '$t', and no Symbol marker
2622
+ */
2623
+ function isSerializedTSONRef(value) {
2624
+ if (typeof value !== 'object' || value === null)
2625
+ return false;
2626
+ const obj = value;
2627
+ return (typeof obj.type === 'string' &&
2628
+ typeof obj.ref === 'string' &&
2629
+ typeof obj.size === 'number' &&
2630
+ obj._bt === undefined // Not a raw BlobRef
2631
+ );
2632
+ }
2633
+ /**
2634
+ * Recursively check if an object contains any BlobRefs
2635
+ */
2636
+ function hasBlobRefs(obj, visited = new WeakSet()) {
2637
+ if (obj === null || obj === undefined) {
2638
+ return false;
2639
+ }
2640
+ if (isBlobRef(obj)) {
2641
+ return true;
2642
+ }
2643
+ if (typeof obj !== 'object') {
2644
+ return false;
2645
+ }
2646
+ // Avoid circular references - check BEFORE processing
2647
+ if (visited.has(obj)) {
2648
+ return false;
2649
+ }
2650
+ visited.add(obj);
2651
+ // Skip special objects that can't contain BlobRefs
2652
+ if (obj instanceof Date || obj instanceof RegExp || obj instanceof Blob) {
2653
+ return false;
2654
+ }
2655
+ if (obj instanceof ArrayBuffer || ArrayBuffer.isView(obj)) {
2656
+ return false;
2657
+ }
2658
+ if (Array.isArray(obj)) {
2659
+ return obj.some(item => hasBlobRefs(item, visited));
2660
+ }
2661
+ // Only traverse POJOs
2662
+ if (obj.constructor === Object) {
2663
+ return Object.values(obj).some(value => hasBlobRefs(value, visited));
2664
+ }
2665
+ return false;
2666
+ }
2667
+ /**
2668
+ * Convert downloaded Uint8Array to the original type specified in BlobRef
2669
+ */
2670
+ function convertToOriginalType(data, ref) {
2671
+ // String type: decode UTF-8 back to string
2672
+ if (ref._bt === 'string') {
2673
+ return new TextDecoder().decode(data);
2674
+ }
2675
+ // Get the underlying ArrayBuffer (handle shared buffer case)
2676
+ const buffer = data.buffer.byteLength === data.byteLength
2677
+ ? data.buffer
2678
+ : data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
2679
+ switch (ref._bt) {
2680
+ case 'Blob':
2681
+ return new Blob([new Uint8Array(buffer)], { type: ref.ct || '' });
2682
+ case 'ArrayBuffer':
2683
+ return buffer;
2684
+ case 'Uint8Array':
2685
+ return data;
2686
+ case 'Int8Array':
2687
+ return new Int8Array(buffer);
2688
+ case 'Uint8ClampedArray':
2689
+ return new Uint8ClampedArray(buffer);
2690
+ case 'Int16Array':
2691
+ return new Int16Array(buffer);
2692
+ case 'Uint16Array':
2693
+ return new Uint16Array(buffer);
2694
+ case 'Int32Array':
2695
+ return new Int32Array(buffer);
2696
+ case 'Uint32Array':
2697
+ return new Uint32Array(buffer);
2698
+ case 'Float32Array':
2699
+ return new Float32Array(buffer);
2700
+ case 'Float64Array':
2701
+ return new Float64Array(buffer);
2702
+ case 'BigInt64Array':
2703
+ return new BigInt64Array(buffer);
2704
+ case 'BigUint64Array':
2705
+ return new BigUint64Array(buffer);
2706
+ case 'DataView':
2707
+ return new DataView(buffer);
2708
+ default:
2709
+ // Fallback to Uint8Array for unknown types
2710
+ return data;
2711
+ }
2712
+ }
2713
+ /**
2714
+ * Recursively resolve all BlobRefs in an object and collect them for queueing.
2715
+ * Returns a new object with BlobRefs replaced by their original type data,
2716
+ * and populates the resolvedBlobs array with keyPath info for each blob.
2717
+ *
2718
+ * @param obj - Object to resolve
2719
+ * @param dbUrl - Base URL for the database
2720
+ * @param accessToken - Access token for blob downloads
2721
+ * @param resolvedBlobs - Array to collect resolved blob info
2722
+ * @param currentPath - Current property path (for tracking)
2723
+ * @param visited - WeakMap for circular reference detection
2724
+ */
2725
+ function resolveAllBlobRefs(obj_1, dbUrl_1) {
2726
+ return __awaiter(this, arguments, void 0, function* (obj, dbUrl, resolvedBlobs = [], currentPath = '', visited = new WeakMap(), tracker) {
2727
+ if (obj == null) { // null or undefined
2728
+ return obj;
2729
+ }
2730
+ // Check if this is a BlobRef - resolve it and track it
2731
+ if (isBlobRef(obj)) {
2732
+ const rawData = yield tracker.download(obj, dbUrl);
2733
+ const data = convertToOriginalType(rawData, obj);
2734
+ resolvedBlobs.push({ keyPath: currentPath, data, ref: obj.ref });
2735
+ return data;
2736
+ }
2737
+ // Handle arrays
2738
+ if (Array.isArray(obj)) {
2739
+ // Avoid circular references - check and set BEFORE iterating
2740
+ if (visited.has(obj)) {
2741
+ return visited.get(obj);
2742
+ }
2743
+ const result = [];
2744
+ visited.set(obj, result); // Set before iterating to handle self-references
2745
+ for (let i = 0; i < obj.length; i++) {
2746
+ const itemPath = currentPath ? `${currentPath}.${i}` : `${i}`;
2747
+ result.push(yield resolveAllBlobRefs(obj[i], dbUrl, resolvedBlobs, itemPath, visited, tracker));
2748
+ }
2749
+ return result;
2750
+ }
2751
+ // Handle POJO objects only (not Date, RegExp, Blob, ArrayBuffer, etc.)
2752
+ if (typeof obj === 'object' && obj.constructor === Object) {
2753
+ // Avoid circular references
2754
+ if (visited.has(obj)) {
2755
+ return visited.get(obj);
2756
+ }
2757
+ const result = {};
2758
+ visited.set(obj, result);
2759
+ for (const [propName, value] of Object.entries(obj)) {
2760
+ // Skip the _hasBlobRefs marker itself
2761
+ if (propName === '_hasBlobRefs') {
2762
+ continue;
2763
+ }
2764
+ const propPath = currentPath ? `${currentPath}.${propName}` : propName;
2765
+ result[propName] = yield resolveAllBlobRefs(value, dbUrl, resolvedBlobs, propPath, visited, tracker);
2766
+ }
2767
+ return result;
2768
+ }
2769
+ return obj;
2770
+ });
2771
+ }
2772
+ /**
2773
+ * Check if an object has unresolved BlobRefs
2774
+ */
2775
+ function hasUnresolvedBlobRefs(obj) {
2776
+ return (typeof obj === 'object' &&
2777
+ obj !== null &&
2778
+ obj._hasBlobRefs === 1);
2779
+ }
2780
+
2781
+ /**
2782
+ * If the incoming value contains BlobRefs (e.g. offloaded strings or binaries),
2783
+ * mark it with _hasBlobRefs = 1 so the blobResolveMiddleware will resolve them
2784
+ * on the next read.
2785
+ */
2786
+ function markIfHasBlobRefs(obj) {
2787
+ if (obj !== null &&
2788
+ typeof obj === 'object' &&
2789
+ obj.constructor === Object &&
2790
+ hasBlobRefs(obj)) {
2791
+ obj._hasBlobRefs = 1;
2792
+ }
2793
+ }
2606
2794
  function applyServerChanges(changes, db) {
2607
2795
  return __awaiter(this, void 0, void 0, function* () {
2608
2796
  console.debug('Applying server changes', changes, Dexie.currentTransaction);
@@ -2638,6 +2826,7 @@ function applyServerChanges(changes, db) {
2638
2826
  const keys = mut.keys.map(keyDecoder);
2639
2827
  switch (mut.type) {
2640
2828
  case 'insert':
2829
+ mut.values.forEach(markIfHasBlobRefs);
2641
2830
  if (primaryKey.outbound) {
2642
2831
  yield table.bulkAdd(mut.values, keys);
2643
2832
  }
@@ -2650,6 +2839,7 @@ function applyServerChanges(changes, db) {
2650
2839
  }
2651
2840
  break;
2652
2841
  case 'upsert':
2842
+ mut.values.forEach(markIfHasBlobRefs);
2653
2843
  if (primaryKey.outbound) {
2654
2844
  yield table.bulkPut(mut.values, keys);
2655
2845
  }
@@ -2897,143 +3087,6 @@ function applyYServerMessages(yMessages, db) {
2897
3087
  });
2898
3088
  }
2899
3089
 
2900
- /**
2901
- * Check if a value is a BlobRef (offloaded binary data)
2902
- * A BlobRef has _bt (type), ref (blob ID), but no v (inline data)
2903
- */
2904
- function isBlobRef(value) {
2905
- if (typeof value !== 'object' || value === null)
2906
- return false;
2907
- const obj = value;
2908
- return (typeof obj._bt === 'string' &&
2909
- typeof obj.ref === 'string' &&
2910
- obj.v === undefined // No inline data = it's a reference
2911
- );
2912
- }
2913
- /**
2914
- * Check if a value is a serialized TSONRef (after IndexedDB storage)
2915
- * Has 'type' instead of '$t', and no Symbol marker
2916
- */
2917
- function isSerializedTSONRef(value) {
2918
- if (typeof value !== 'object' || value === null)
2919
- return false;
2920
- const obj = value;
2921
- return (typeof obj.type === 'string' &&
2922
- typeof obj.ref === 'string' &&
2923
- typeof obj.size === 'number' &&
2924
- obj._bt === undefined // Not a raw BlobRef
2925
- );
2926
- }
2927
- /**
2928
- * Convert downloaded Uint8Array to the original type specified in BlobRef
2929
- */
2930
- function convertToOriginalType(data, ref) {
2931
- // Get the underlying ArrayBuffer (handle shared buffer case)
2932
- const buffer = data.buffer.byteLength === data.byteLength
2933
- ? data.buffer
2934
- : data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
2935
- switch (ref._bt) {
2936
- case 'Blob':
2937
- return new Blob([new Uint8Array(buffer)], { type: ref.ct || '' });
2938
- case 'ArrayBuffer':
2939
- return buffer;
2940
- case 'Uint8Array':
2941
- return data;
2942
- case 'Int8Array':
2943
- return new Int8Array(buffer);
2944
- case 'Uint8ClampedArray':
2945
- return new Uint8ClampedArray(buffer);
2946
- case 'Int16Array':
2947
- return new Int16Array(buffer);
2948
- case 'Uint16Array':
2949
- return new Uint16Array(buffer);
2950
- case 'Int32Array':
2951
- return new Int32Array(buffer);
2952
- case 'Uint32Array':
2953
- return new Uint32Array(buffer);
2954
- case 'Float32Array':
2955
- return new Float32Array(buffer);
2956
- case 'Float64Array':
2957
- return new Float64Array(buffer);
2958
- case 'BigInt64Array':
2959
- return new BigInt64Array(buffer);
2960
- case 'BigUint64Array':
2961
- return new BigUint64Array(buffer);
2962
- case 'DataView':
2963
- return new DataView(buffer);
2964
- default:
2965
- // Fallback to Uint8Array for unknown types
2966
- return data;
2967
- }
2968
- }
2969
- /**
2970
- * Recursively resolve all BlobRefs in an object and collect them for queueing.
2971
- * Returns a new object with BlobRefs replaced by their original type data,
2972
- * and populates the resolvedBlobs array with keyPath info for each blob.
2973
- *
2974
- * @param obj - Object to resolve
2975
- * @param dbUrl - Base URL for the database
2976
- * @param accessToken - Access token for blob downloads
2977
- * @param resolvedBlobs - Array to collect resolved blob info
2978
- * @param currentPath - Current property path (for tracking)
2979
- * @param visited - WeakMap for circular reference detection
2980
- */
2981
- function resolveAllBlobRefs(obj_1, dbUrl_1) {
2982
- return __awaiter(this, arguments, void 0, function* (obj, dbUrl, resolvedBlobs = [], currentPath = '', visited = new WeakMap(), tracker) {
2983
- if (obj == null) { // null or undefined
2984
- return obj;
2985
- }
2986
- // Check if this is a BlobRef - resolve it and track it
2987
- if (isBlobRef(obj)) {
2988
- const rawData = yield tracker.download(obj, dbUrl);
2989
- const data = convertToOriginalType(rawData, obj);
2990
- resolvedBlobs.push({ keyPath: currentPath, data, ref: obj.ref });
2991
- return data;
2992
- }
2993
- // Handle arrays
2994
- if (Array.isArray(obj)) {
2995
- // Avoid circular references - check and set BEFORE iterating
2996
- if (visited.has(obj)) {
2997
- return visited.get(obj);
2998
- }
2999
- const result = [];
3000
- visited.set(obj, result); // Set before iterating to handle self-references
3001
- for (let i = 0; i < obj.length; i++) {
3002
- const itemPath = currentPath ? `${currentPath}.${i}` : `${i}`;
3003
- result.push(yield resolveAllBlobRefs(obj[i], dbUrl, resolvedBlobs, itemPath, visited, tracker));
3004
- }
3005
- return result;
3006
- }
3007
- // Handle POJO objects only (not Date, RegExp, Blob, ArrayBuffer, etc.)
3008
- if (typeof obj === 'object' && obj.constructor === Object) {
3009
- // Avoid circular references
3010
- if (visited.has(obj)) {
3011
- return visited.get(obj);
3012
- }
3013
- const result = {};
3014
- visited.set(obj, result);
3015
- for (const [propName, value] of Object.entries(obj)) {
3016
- // Skip the _hasBlobRefs marker itself
3017
- if (propName === '_hasBlobRefs') {
3018
- continue;
3019
- }
3020
- const propPath = currentPath ? `${currentPath}.${propName}` : propName;
3021
- result[propName] = yield resolveAllBlobRefs(value, dbUrl, resolvedBlobs, propPath, visited, tracker);
3022
- }
3023
- return result;
3024
- }
3025
- return obj;
3026
- });
3027
- }
3028
- /**
3029
- * Check if an object has unresolved BlobRefs
3030
- */
3031
- function hasUnresolvedBlobRefs(obj) {
3032
- return (typeof obj === 'object' &&
3033
- obj !== null &&
3034
- obj._hasBlobRefs === 1);
3035
- }
3036
-
3037
3090
  /**
3038
3091
  * Blob Offloading for Dexie Cloud
3039
3092
  *
@@ -3042,6 +3095,8 @@ function hasUnresolvedBlobRefs(obj) {
3042
3095
  */
3043
3096
  // Blobs >= 4KB are offloaded to blob storage
3044
3097
  const BLOB_OFFLOAD_THRESHOLD = 4096;
3098
+ // Default max string length before offloading (32KB characters)
3099
+ const DEFAULT_MAX_STRING_LENGTH = 32768;
3045
3100
  // Cache: once we know the server doesn't support blob storage, skip future uploads.
3046
3101
  // Maps databaseUrl → boolean (true = supported, false = not supported).
3047
3102
  const blobEndpointSupported = new Map();
@@ -3182,10 +3237,10 @@ function uploadBlob(databaseUrl, getCachedAccessToken, blob) {
3182
3237
  );
3183
3238
  });
3184
3239
  }
3185
- function offloadBlobsAndMarkDirty(obj, databaseUrl, getCachedAccessToken) {
3186
- return __awaiter(this, void 0, void 0, function* () {
3240
+ function offloadBlobsAndMarkDirty(obj_1, databaseUrl_1, getCachedAccessToken_1) {
3241
+ return __awaiter(this, arguments, void 0, function* (obj, databaseUrl, getCachedAccessToken, maxStringLength = DEFAULT_MAX_STRING_LENGTH) {
3187
3242
  const dirtyFlag = { dirty: false };
3188
- const result = yield offloadBlobs(obj, databaseUrl, getCachedAccessToken, dirtyFlag);
3243
+ const result = yield offloadBlobs(obj, databaseUrl, getCachedAccessToken, maxStringLength, dirtyFlag);
3189
3244
  // Mark the object as dirty for sync if any blobs were offloaded
3190
3245
  if (dirtyFlag.dirty && typeof result === 'object' && result !== null && result.constructor === Object) {
3191
3246
  result._hasBlobRefs = 1;
@@ -3198,10 +3253,26 @@ function offloadBlobsAndMarkDirty(obj, databaseUrl, getCachedAccessToken) {
3198
3253
  * Returns a new object with blobs replaced by BlobRefs
3199
3254
  */
3200
3255
  function offloadBlobs(obj_1, databaseUrl_1, getCachedAccessToken_1) {
3201
- return __awaiter(this, arguments, void 0, function* (obj, databaseUrl, getCachedAccessToken, dirtyFlag = { dirty: false }, visited = new WeakSet()) {
3256
+ return __awaiter(this, arguments, void 0, function* (obj, databaseUrl, getCachedAccessToken, maxStringLength = DEFAULT_MAX_STRING_LENGTH, dirtyFlag = { dirty: false }, visited = new WeakSet()) {
3202
3257
  if (obj === null || obj === undefined) {
3203
3258
  return obj;
3204
3259
  }
3260
+ // Check if this is a long string that should be offloaded
3261
+ if (typeof obj === 'string' && obj.length > maxStringLength && maxStringLength !== Infinity) {
3262
+ if (blobEndpointSupported.get(databaseUrl) === false) {
3263
+ return obj;
3264
+ }
3265
+ const blob = new Blob([obj], { type: 'text/plain;charset=utf-8' });
3266
+ const blobRef = yield uploadBlob(databaseUrl, getCachedAccessToken, blob);
3267
+ if (blobRef === null) {
3268
+ blobEndpointSupported.set(databaseUrl, false);
3269
+ return obj;
3270
+ }
3271
+ blobEndpointSupported.set(databaseUrl, true);
3272
+ dirtyFlag.dirty = true;
3273
+ // Mark as string type so it's resolved back to string, not Blob
3274
+ return Object.assign(Object.assign({}, blobRef), { _bt: 'string' });
3275
+ }
3205
3276
  // Check if this is a blob that should be offloaded
3206
3277
  if (shouldOffloadBlob(obj)) {
3207
3278
  if (blobEndpointSupported.get(databaseUrl) === false) {
@@ -3230,7 +3301,7 @@ function offloadBlobs(obj_1, databaseUrl_1, getCachedAccessToken_1) {
3230
3301
  if (Array.isArray(obj)) {
3231
3302
  const result = [];
3232
3303
  for (const item of obj) {
3233
- result.push(yield offloadBlobs(item, databaseUrl, getCachedAccessToken, dirtyFlag, visited));
3304
+ result.push(yield offloadBlobs(item, databaseUrl, getCachedAccessToken, maxStringLength, dirtyFlag, visited));
3234
3305
  }
3235
3306
  return result;
3236
3307
  }
@@ -3242,7 +3313,7 @@ function offloadBlobs(obj_1, databaseUrl_1, getCachedAccessToken_1) {
3242
3313
  }
3243
3314
  const result = {};
3244
3315
  for (const [key, value] of Object.entries(obj)) {
3245
- result[key] = yield offloadBlobs(value, databaseUrl, getCachedAccessToken, dirtyFlag, visited);
3316
+ result[key] = yield offloadBlobs(value, databaseUrl, getCachedAccessToken, maxStringLength, dirtyFlag, visited);
3246
3317
  }
3247
3318
  return result;
3248
3319
  });
@@ -3251,13 +3322,13 @@ function offloadBlobs(obj_1, databaseUrl_1, getCachedAccessToken_1) {
3251
3322
  * Process a DBOperationsSet and offload any large blobs
3252
3323
  * Returns a new DBOperationsSet with blobs replaced by BlobRefs
3253
3324
  */
3254
- function offloadBlobsInOperations(operations, databaseUrl, getCachedAccessToken) {
3255
- return __awaiter(this, void 0, void 0, function* () {
3325
+ function offloadBlobsInOperations(operations_1, databaseUrl_1, getCachedAccessToken_1) {
3326
+ return __awaiter(this, arguments, void 0, function* (operations, databaseUrl, getCachedAccessToken, maxStringLength = DEFAULT_MAX_STRING_LENGTH) {
3256
3327
  const result = [];
3257
3328
  for (const tableOps of operations) {
3258
3329
  const processedMuts = [];
3259
3330
  for (const mut of tableOps.muts) {
3260
- const processedMut = yield offloadBlobsInOperation(mut, databaseUrl, getCachedAccessToken);
3331
+ const processedMut = yield offloadBlobsInOperation(mut, databaseUrl, getCachedAccessToken, maxStringLength);
3261
3332
  processedMuts.push(processedMut);
3262
3333
  }
3263
3334
  result.push({
@@ -3268,20 +3339,20 @@ function offloadBlobsInOperations(operations, databaseUrl, getCachedAccessToken)
3268
3339
  return result;
3269
3340
  });
3270
3341
  }
3271
- function offloadBlobsInOperation(op, databaseUrl, getCachedAccessToken) {
3272
- return __awaiter(this, void 0, void 0, function* () {
3342
+ function offloadBlobsInOperation(op_1, databaseUrl_1, getCachedAccessToken_1) {
3343
+ return __awaiter(this, arguments, void 0, function* (op, databaseUrl, getCachedAccessToken, maxStringLength = DEFAULT_MAX_STRING_LENGTH) {
3273
3344
  switch (op.type) {
3274
3345
  case 'insert':
3275
3346
  case 'upsert': {
3276
- const processedValues = yield Promise.all(op.values.map(value => offloadBlobsAndMarkDirty(value, databaseUrl, getCachedAccessToken)));
3347
+ const processedValues = yield Promise.all(op.values.map(value => offloadBlobsAndMarkDirty(value, databaseUrl, getCachedAccessToken, maxStringLength)));
3277
3348
  return Object.assign(Object.assign({}, op), { values: processedValues });
3278
3349
  }
3279
3350
  case 'update': {
3280
- const processedChangeSpecs = yield Promise.all(op.changeSpecs.map(spec => offloadBlobsAndMarkDirty(spec, databaseUrl, getCachedAccessToken)));
3351
+ const processedChangeSpecs = yield Promise.all(op.changeSpecs.map(spec => offloadBlobsAndMarkDirty(spec, databaseUrl, getCachedAccessToken, maxStringLength)));
3281
3352
  return Object.assign(Object.assign({}, op), { changeSpecs: processedChangeSpecs });
3282
3353
  }
3283
3354
  case 'modify': {
3284
- const processedChangeSpec = yield offloadBlobsAndMarkDirty(op.changeSpec, databaseUrl, getCachedAccessToken);
3355
+ const processedChangeSpec = yield offloadBlobsAndMarkDirty(op.changeSpec, databaseUrl, getCachedAccessToken, maxStringLength);
3285
3356
  return Object.assign(Object.assign({}, op), { changeSpec: processedChangeSpec });
3286
3357
  }
3287
3358
  case 'delete':
@@ -3296,33 +3367,37 @@ function offloadBlobsInOperation(op, databaseUrl, getCachedAccessToken) {
3296
3367
  * Check if there are any large blobs in the operations that need offloading
3297
3368
  * This is a quick check to avoid unnecessary processing
3298
3369
  */
3299
- function hasLargeBlobsInOperations(operations) {
3370
+ function hasLargeBlobsInOperations(operations, maxStringLength = DEFAULT_MAX_STRING_LENGTH) {
3300
3371
  for (const tableOps of operations) {
3301
3372
  for (const mut of tableOps.muts) {
3302
- if (hasLargeBlobsInOperation(mut)) {
3373
+ if (hasLargeBlobsInOperation(mut, maxStringLength)) {
3303
3374
  return true;
3304
3375
  }
3305
3376
  }
3306
3377
  }
3307
3378
  return false;
3308
3379
  }
3309
- function hasLargeBlobsInOperation(op) {
3380
+ function hasLargeBlobsInOperation(op, maxStringLength) {
3310
3381
  switch (op.type) {
3311
3382
  case 'insert':
3312
3383
  case 'upsert':
3313
- return op.values.some(value => hasLargeBlobs(value));
3384
+ return op.values.some(value => hasLargeBlobs(value, maxStringLength));
3314
3385
  case 'update':
3315
- return op.changeSpecs.some(spec => hasLargeBlobs(spec));
3386
+ return op.changeSpecs.some(spec => hasLargeBlobs(spec, maxStringLength));
3316
3387
  case 'modify':
3317
- return hasLargeBlobs(op.changeSpec);
3388
+ return hasLargeBlobs(op.changeSpec, maxStringLength);
3318
3389
  default:
3319
3390
  return false;
3320
3391
  }
3321
3392
  }
3322
- function hasLargeBlobs(obj, visited = new WeakSet()) {
3393
+ function hasLargeBlobs(obj, maxStringLength, visited = new WeakSet()) {
3323
3394
  if (obj === null || obj === undefined) {
3324
3395
  return false;
3325
3396
  }
3397
+ // Check long strings
3398
+ if (typeof obj === 'string' && obj.length > maxStringLength && maxStringLength !== Infinity) {
3399
+ return true;
3400
+ }
3326
3401
  if (shouldOffloadBlob(obj)) {
3327
3402
  return true;
3328
3403
  }
@@ -3335,13 +3410,13 @@ function hasLargeBlobs(obj, visited = new WeakSet()) {
3335
3410
  }
3336
3411
  visited.add(obj);
3337
3412
  if (Array.isArray(obj)) {
3338
- return obj.some(item => hasLargeBlobs(item, visited));
3413
+ return obj.some(item => hasLargeBlobs(item, maxStringLength, visited));
3339
3414
  }
3340
3415
  // Traverse plain objects (POJO-like) - use duck typing since IndexedDB
3341
3416
  // may return objects where constructor !== Object
3342
3417
  const proto = Object.getPrototypeOf(obj);
3343
3418
  if (proto === Object.prototype || proto === null) {
3344
- return Object.values(obj).some(value => hasLargeBlobs(value, visited));
3419
+ return Object.values(obj).some(value => hasLargeBlobs(value, maxStringLength, visited));
3345
3420
  }
3346
3421
  return false;
3347
3422
  }
@@ -3602,7 +3677,7 @@ function _sync(db_1, options_1, schema_1) {
3602
3677
  return __awaiter(this, arguments, void 0, function* (db, options, schema, { isInitialSync, cancelToken, justCheckIfNeeded, purpose } = {
3603
3678
  isInitialSync: false,
3604
3679
  }) {
3605
- var _a;
3680
+ var _a, _b, _c;
3606
3681
  if (!justCheckIfNeeded) {
3607
3682
  console.debug('SYNC STARTED', { isInitialSync, purpose });
3608
3683
  }
@@ -3676,9 +3751,10 @@ function _sync(db_1, options_1, schema_1) {
3676
3751
  // Offload large blobs to blob storage before sync
3677
3752
  //
3678
3753
  let processedChangeSet = clientChangeSet;
3679
- const hasLargeBlobs = hasLargeBlobsInOperations(clientChangeSet);
3754
+ const maxStringLength = (_c = (_b = db.cloud.options) === null || _b === void 0 ? void 0 : _b.maxStringLength) !== null && _c !== void 0 ? _c : 32768;
3755
+ const hasLargeBlobs = hasLargeBlobsInOperations(clientChangeSet, maxStringLength);
3680
3756
  if (hasLargeBlobs) {
3681
- processedChangeSet = yield offloadBlobsInOperations(clientChangeSet, databaseUrl, () => loadCachedAccessToken(db));
3757
+ processedChangeSet = yield offloadBlobsInOperations(clientChangeSet, databaseUrl, () => loadCachedAccessToken(db), maxStringLength);
3682
3758
  }
3683
3759
  //
3684
3760
  // Push changes to server
@@ -8018,7 +8094,7 @@ function dexieCloud(dexie) {
8018
8094
  const downloading$ = createDownloadingState();
8019
8095
  dexie.cloud = {
8020
8096
  // @ts-ignore
8021
- version: "4.4.0",
8097
+ version: "4.4.1",
8022
8098
  options: Object.assign({}, DEFAULT_OPTIONS),
8023
8099
  schema: null,
8024
8100
  get currentUserId() {
@@ -8046,6 +8122,16 @@ function dexieCloud(dexie) {
8046
8122
  invites: getInvitesObservable(dexie),
8047
8123
  roles: getGlobalRolesObservable(dexie),
8048
8124
  configure(options) {
8125
+ // Validate maxStringLength — Infinity disables offloading, otherwise must be
8126
+ // a finite positive number not exceeding the server limit (32768).
8127
+ const MAX_SERVER_STRING_LENGTH = 32768;
8128
+ if (options.maxStringLength !== undefined &&
8129
+ options.maxStringLength !== Infinity &&
8130
+ (!Number.isFinite(options.maxStringLength) ||
8131
+ options.maxStringLength < 0 ||
8132
+ options.maxStringLength > MAX_SERVER_STRING_LENGTH)) {
8133
+ throw new Error(`maxStringLength must be Infinity or a finite number in [0, ${MAX_SERVER_STRING_LENGTH}]. Got: ${options.maxStringLength}`);
8134
+ }
8049
8135
  options = dexie.cloud.options = Object.assign(Object.assign({}, dexie.cloud.options), options);
8050
8136
  configuredProgramatically = true;
8051
8137
  if (options.databaseUrl && options.nameSuffix) {
@@ -8432,7 +8518,7 @@ function dexieCloud(dexie) {
8432
8518
  }
8433
8519
  }
8434
8520
  // @ts-ignore
8435
- dexieCloud.version = "4.4.0";
8521
+ dexieCloud.version = "4.4.1";
8436
8522
  Dexie.Cloud = dexieCloud;
8437
8523
 
8438
8524
  // In case the SW lives for a while, let it reuse already opened connections: