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
  *
@@ -3903,6 +3903,194 @@
3903
3903
  });
3904
3904
  }
3905
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
+ }
3906
4094
  function applyServerChanges(changes, db) {
3907
4095
  return __awaiter(this, void 0, void 0, function* () {
3908
4096
  console.debug('Applying server changes', changes, Dexie.currentTransaction);
@@ -3938,6 +4126,7 @@
3938
4126
  const keys = mut.keys.map(keyDecoder);
3939
4127
  switch (mut.type) {
3940
4128
  case 'insert':
4129
+ mut.values.forEach(markIfHasBlobRefs);
3941
4130
  if (primaryKey.outbound) {
3942
4131
  yield table.bulkAdd(mut.values, keys);
3943
4132
  }
@@ -3950,6 +4139,7 @@
3950
4139
  }
3951
4140
  break;
3952
4141
  case 'upsert':
4142
+ mut.values.forEach(markIfHasBlobRefs);
3953
4143
  if (primaryKey.outbound) {
3954
4144
  yield table.bulkPut(mut.values, keys);
3955
4145
  }
@@ -13368,7 +13558,7 @@
13368
13558
  *
13369
13559
  * ==========================================================================
13370
13560
  *
13371
- * Version 4.4.0, Wed Mar 18 2026
13561
+ * Version 4.4.0, Thu Mar 19 2026
13372
13562
  *
13373
13563
  * https://dexie.org
13374
13564
  *
@@ -13865,143 +14055,6 @@
13865
14055
  });
13866
14056
  }
13867
14057
 
13868
- /**
13869
- * Check if a value is a BlobRef (offloaded binary data)
13870
- * A BlobRef has _bt (type), ref (blob ID), but no v (inline data)
13871
- */
13872
- function isBlobRef(value) {
13873
- if (typeof value !== 'object' || value === null)
13874
- return false;
13875
- const obj = value;
13876
- return (typeof obj._bt === 'string' &&
13877
- typeof obj.ref === 'string' &&
13878
- obj.v === undefined // No inline data = it's a reference
13879
- );
13880
- }
13881
- /**
13882
- * Check if a value is a serialized TSONRef (after IndexedDB storage)
13883
- * Has 'type' instead of '$t', and no Symbol marker
13884
- */
13885
- function isSerializedTSONRef(value) {
13886
- if (typeof value !== 'object' || value === null)
13887
- return false;
13888
- const obj = value;
13889
- return (typeof obj.type === 'string' &&
13890
- typeof obj.ref === 'string' &&
13891
- typeof obj.size === 'number' &&
13892
- obj._bt === undefined // Not a raw BlobRef
13893
- );
13894
- }
13895
- /**
13896
- * Convert downloaded Uint8Array to the original type specified in BlobRef
13897
- */
13898
- function convertToOriginalType(data, ref) {
13899
- // Get the underlying ArrayBuffer (handle shared buffer case)
13900
- const buffer = data.buffer.byteLength === data.byteLength
13901
- ? data.buffer
13902
- : data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
13903
- switch (ref._bt) {
13904
- case 'Blob':
13905
- return new Blob([new Uint8Array(buffer)], { type: ref.ct || '' });
13906
- case 'ArrayBuffer':
13907
- return buffer;
13908
- case 'Uint8Array':
13909
- return data;
13910
- case 'Int8Array':
13911
- return new Int8Array(buffer);
13912
- case 'Uint8ClampedArray':
13913
- return new Uint8ClampedArray(buffer);
13914
- case 'Int16Array':
13915
- return new Int16Array(buffer);
13916
- case 'Uint16Array':
13917
- return new Uint16Array(buffer);
13918
- case 'Int32Array':
13919
- return new Int32Array(buffer);
13920
- case 'Uint32Array':
13921
- return new Uint32Array(buffer);
13922
- case 'Float32Array':
13923
- return new Float32Array(buffer);
13924
- case 'Float64Array':
13925
- return new Float64Array(buffer);
13926
- case 'BigInt64Array':
13927
- return new BigInt64Array(buffer);
13928
- case 'BigUint64Array':
13929
- return new BigUint64Array(buffer);
13930
- case 'DataView':
13931
- return new DataView(buffer);
13932
- default:
13933
- // Fallback to Uint8Array for unknown types
13934
- return data;
13935
- }
13936
- }
13937
- /**
13938
- * Recursively resolve all BlobRefs in an object and collect them for queueing.
13939
- * Returns a new object with BlobRefs replaced by their original type data,
13940
- * and populates the resolvedBlobs array with keyPath info for each blob.
13941
- *
13942
- * @param obj - Object to resolve
13943
- * @param dbUrl - Base URL for the database
13944
- * @param accessToken - Access token for blob downloads
13945
- * @param resolvedBlobs - Array to collect resolved blob info
13946
- * @param currentPath - Current property path (for tracking)
13947
- * @param visited - WeakMap for circular reference detection
13948
- */
13949
- function resolveAllBlobRefs(obj_1, dbUrl_1) {
13950
- return __awaiter(this, arguments, void 0, function* (obj, dbUrl, resolvedBlobs = [], currentPath = '', visited = new WeakMap(), tracker) {
13951
- if (obj == null) { // null or undefined
13952
- return obj;
13953
- }
13954
- // Check if this is a BlobRef - resolve it and track it
13955
- if (isBlobRef(obj)) {
13956
- const rawData = yield tracker.download(obj, dbUrl);
13957
- const data = convertToOriginalType(rawData, obj);
13958
- resolvedBlobs.push({ keyPath: currentPath, data, ref: obj.ref });
13959
- return data;
13960
- }
13961
- // Handle arrays
13962
- if (Array.isArray(obj)) {
13963
- // Avoid circular references - check and set BEFORE iterating
13964
- if (visited.has(obj)) {
13965
- return visited.get(obj);
13966
- }
13967
- const result = [];
13968
- visited.set(obj, result); // Set before iterating to handle self-references
13969
- for (let i = 0; i < obj.length; i++) {
13970
- const itemPath = currentPath ? `${currentPath}.${i}` : `${i}`;
13971
- result.push(yield resolveAllBlobRefs(obj[i], dbUrl, resolvedBlobs, itemPath, visited, tracker));
13972
- }
13973
- return result;
13974
- }
13975
- // Handle POJO objects only (not Date, RegExp, Blob, ArrayBuffer, etc.)
13976
- if (typeof obj === 'object' && obj.constructor === Object) {
13977
- // Avoid circular references
13978
- if (visited.has(obj)) {
13979
- return visited.get(obj);
13980
- }
13981
- const result = {};
13982
- visited.set(obj, result);
13983
- for (const [propName, value] of Object.entries(obj)) {
13984
- // Skip the _hasBlobRefs marker itself
13985
- if (propName === '_hasBlobRefs') {
13986
- continue;
13987
- }
13988
- const propPath = currentPath ? `${currentPath}.${propName}` : propName;
13989
- result[propName] = yield resolveAllBlobRefs(value, dbUrl, resolvedBlobs, propPath, visited, tracker);
13990
- }
13991
- return result;
13992
- }
13993
- return obj;
13994
- });
13995
- }
13996
- /**
13997
- * Check if an object has unresolved BlobRefs
13998
- */
13999
- function hasUnresolvedBlobRefs(obj) {
14000
- return (typeof obj === 'object' &&
14001
- obj !== null &&
14002
- obj._hasBlobRefs === 1);
14003
- }
14004
-
14005
14058
  /**
14006
14059
  * Blob Offloading for Dexie Cloud
14007
14060
  *
@@ -14010,6 +14063,8 @@
14010
14063
  */
14011
14064
  // Blobs >= 4KB are offloaded to blob storage
14012
14065
  const BLOB_OFFLOAD_THRESHOLD = 4096;
14066
+ // Default max string length before offloading (32KB characters)
14067
+ const DEFAULT_MAX_STRING_LENGTH = 32768;
14013
14068
  // Cache: once we know the server doesn't support blob storage, skip future uploads.
14014
14069
  // Maps databaseUrl → boolean (true = supported, false = not supported).
14015
14070
  const blobEndpointSupported = new Map();
@@ -14150,10 +14205,10 @@
14150
14205
  );
14151
14206
  });
14152
14207
  }
14153
- function offloadBlobsAndMarkDirty(obj, databaseUrl, getCachedAccessToken) {
14154
- return __awaiter(this, void 0, void 0, function* () {
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) {
14155
14210
  const dirtyFlag = { dirty: false };
14156
- const result = yield offloadBlobs(obj, databaseUrl, getCachedAccessToken, dirtyFlag);
14211
+ const result = yield offloadBlobs(obj, databaseUrl, getCachedAccessToken, maxStringLength, dirtyFlag);
14157
14212
  // Mark the object as dirty for sync if any blobs were offloaded
14158
14213
  if (dirtyFlag.dirty && typeof result === 'object' && result !== null && result.constructor === Object) {
14159
14214
  result._hasBlobRefs = 1;
@@ -14166,10 +14221,26 @@
14166
14221
  * Returns a new object with blobs replaced by BlobRefs
14167
14222
  */
14168
14223
  function offloadBlobs(obj_1, databaseUrl_1, getCachedAccessToken_1) {
14169
- return __awaiter(this, arguments, void 0, function* (obj, databaseUrl, getCachedAccessToken, dirtyFlag = { dirty: false }, visited = new WeakSet()) {
14224
+ return __awaiter(this, arguments, void 0, function* (obj, databaseUrl, getCachedAccessToken, maxStringLength = DEFAULT_MAX_STRING_LENGTH, dirtyFlag = { dirty: false }, visited = new WeakSet()) {
14170
14225
  if (obj === null || obj === undefined) {
14171
14226
  return obj;
14172
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
+ }
14173
14244
  // Check if this is a blob that should be offloaded
14174
14245
  if (shouldOffloadBlob(obj)) {
14175
14246
  if (blobEndpointSupported.get(databaseUrl) === false) {
@@ -14198,7 +14269,7 @@
14198
14269
  if (Array.isArray(obj)) {
14199
14270
  const result = [];
14200
14271
  for (const item of obj) {
14201
- result.push(yield offloadBlobs(item, databaseUrl, getCachedAccessToken, dirtyFlag, visited));
14272
+ result.push(yield offloadBlobs(item, databaseUrl, getCachedAccessToken, maxStringLength, dirtyFlag, visited));
14202
14273
  }
14203
14274
  return result;
14204
14275
  }
@@ -14210,7 +14281,7 @@
14210
14281
  }
14211
14282
  const result = {};
14212
14283
  for (const [key, value] of Object.entries(obj)) {
14213
- result[key] = yield offloadBlobs(value, databaseUrl, getCachedAccessToken, dirtyFlag, visited);
14284
+ result[key] = yield offloadBlobs(value, databaseUrl, getCachedAccessToken, maxStringLength, dirtyFlag, visited);
14214
14285
  }
14215
14286
  return result;
14216
14287
  });
@@ -14219,13 +14290,13 @@
14219
14290
  * Process a DBOperationsSet and offload any large blobs
14220
14291
  * Returns a new DBOperationsSet with blobs replaced by BlobRefs
14221
14292
  */
14222
- function offloadBlobsInOperations(operations, databaseUrl, getCachedAccessToken) {
14223
- return __awaiter(this, void 0, void 0, function* () {
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) {
14224
14295
  const result = [];
14225
14296
  for (const tableOps of operations) {
14226
14297
  const processedMuts = [];
14227
14298
  for (const mut of tableOps.muts) {
14228
- const processedMut = yield offloadBlobsInOperation(mut, databaseUrl, getCachedAccessToken);
14299
+ const processedMut = yield offloadBlobsInOperation(mut, databaseUrl, getCachedAccessToken, maxStringLength);
14229
14300
  processedMuts.push(processedMut);
14230
14301
  }
14231
14302
  result.push({
@@ -14236,20 +14307,20 @@
14236
14307
  return result;
14237
14308
  });
14238
14309
  }
14239
- function offloadBlobsInOperation(op, databaseUrl, getCachedAccessToken) {
14240
- return __awaiter(this, void 0, void 0, function* () {
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) {
14241
14312
  switch (op.type) {
14242
14313
  case 'insert':
14243
14314
  case 'upsert': {
14244
- const processedValues = yield Promise.all(op.values.map(value => offloadBlobsAndMarkDirty(value, databaseUrl, getCachedAccessToken)));
14315
+ const processedValues = yield Promise.all(op.values.map(value => offloadBlobsAndMarkDirty(value, databaseUrl, getCachedAccessToken, maxStringLength)));
14245
14316
  return Object.assign(Object.assign({}, op), { values: processedValues });
14246
14317
  }
14247
14318
  case 'update': {
14248
- const processedChangeSpecs = yield Promise.all(op.changeSpecs.map(spec => offloadBlobsAndMarkDirty(spec, databaseUrl, getCachedAccessToken)));
14319
+ const processedChangeSpecs = yield Promise.all(op.changeSpecs.map(spec => offloadBlobsAndMarkDirty(spec, databaseUrl, getCachedAccessToken, maxStringLength)));
14249
14320
  return Object.assign(Object.assign({}, op), { changeSpecs: processedChangeSpecs });
14250
14321
  }
14251
14322
  case 'modify': {
14252
- const processedChangeSpec = yield offloadBlobsAndMarkDirty(op.changeSpec, databaseUrl, getCachedAccessToken);
14323
+ const processedChangeSpec = yield offloadBlobsAndMarkDirty(op.changeSpec, databaseUrl, getCachedAccessToken, maxStringLength);
14253
14324
  return Object.assign(Object.assign({}, op), { changeSpec: processedChangeSpec });
14254
14325
  }
14255
14326
  case 'delete':
@@ -14264,33 +14335,37 @@
14264
14335
  * Check if there are any large blobs in the operations that need offloading
14265
14336
  * This is a quick check to avoid unnecessary processing
14266
14337
  */
14267
- function hasLargeBlobsInOperations(operations) {
14338
+ function hasLargeBlobsInOperations(operations, maxStringLength = DEFAULT_MAX_STRING_LENGTH) {
14268
14339
  for (const tableOps of operations) {
14269
14340
  for (const mut of tableOps.muts) {
14270
- if (hasLargeBlobsInOperation(mut)) {
14341
+ if (hasLargeBlobsInOperation(mut, maxStringLength)) {
14271
14342
  return true;
14272
14343
  }
14273
14344
  }
14274
14345
  }
14275
14346
  return false;
14276
14347
  }
14277
- function hasLargeBlobsInOperation(op) {
14348
+ function hasLargeBlobsInOperation(op, maxStringLength) {
14278
14349
  switch (op.type) {
14279
14350
  case 'insert':
14280
14351
  case 'upsert':
14281
- return op.values.some(value => hasLargeBlobs(value));
14352
+ return op.values.some(value => hasLargeBlobs(value, maxStringLength));
14282
14353
  case 'update':
14283
- return op.changeSpecs.some(spec => hasLargeBlobs(spec));
14354
+ return op.changeSpecs.some(spec => hasLargeBlobs(spec, maxStringLength));
14284
14355
  case 'modify':
14285
- return hasLargeBlobs(op.changeSpec);
14356
+ return hasLargeBlobs(op.changeSpec, maxStringLength);
14286
14357
  default:
14287
14358
  return false;
14288
14359
  }
14289
14360
  }
14290
- function hasLargeBlobs(obj, visited = new WeakSet()) {
14361
+ function hasLargeBlobs(obj, maxStringLength, visited = new WeakSet()) {
14291
14362
  if (obj === null || obj === undefined) {
14292
14363
  return false;
14293
14364
  }
14365
+ // Check long strings
14366
+ if (typeof obj === 'string' && obj.length > maxStringLength && maxStringLength !== Infinity) {
14367
+ return true;
14368
+ }
14294
14369
  if (shouldOffloadBlob(obj)) {
14295
14370
  return true;
14296
14371
  }
@@ -14303,13 +14378,13 @@
14303
14378
  }
14304
14379
  visited.add(obj);
14305
14380
  if (Array.isArray(obj)) {
14306
- return obj.some(item => hasLargeBlobs(item, visited));
14381
+ return obj.some(item => hasLargeBlobs(item, maxStringLength, visited));
14307
14382
  }
14308
14383
  // Traverse plain objects (POJO-like) - use duck typing since IndexedDB
14309
14384
  // may return objects where constructor !== Object
14310
14385
  const proto = Object.getPrototypeOf(obj);
14311
14386
  if (proto === Object.prototype || proto === null) {
14312
- return Object.values(obj).some(value => hasLargeBlobs(value, visited));
14387
+ return Object.values(obj).some(value => hasLargeBlobs(value, maxStringLength, visited));
14313
14388
  }
14314
14389
  return false;
14315
14390
  }
@@ -14570,7 +14645,7 @@
14570
14645
  return __awaiter(this, arguments, void 0, function* (db, options, schema, { isInitialSync, cancelToken, justCheckIfNeeded, purpose } = {
14571
14646
  isInitialSync: false,
14572
14647
  }) {
14573
- var _a;
14648
+ var _a, _b, _c;
14574
14649
  if (!justCheckIfNeeded) {
14575
14650
  console.debug('SYNC STARTED', { isInitialSync, purpose });
14576
14651
  }
@@ -14644,9 +14719,10 @@
14644
14719
  // Offload large blobs to blob storage before sync
14645
14720
  //
14646
14721
  let processedChangeSet = clientChangeSet;
14647
- const hasLargeBlobs = hasLargeBlobsInOperations(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);
14648
14724
  if (hasLargeBlobs) {
14649
- processedChangeSet = yield offloadBlobsInOperations(clientChangeSet, databaseUrl, () => loadCachedAccessToken(db));
14725
+ processedChangeSet = yield offloadBlobsInOperations(clientChangeSet, databaseUrl, () => loadCachedAccessToken(db), maxStringLength);
14650
14726
  }
14651
14727
  //
14652
14728
  // Push changes to server
@@ -19248,7 +19324,7 @@
19248
19324
  const downloading$ = createDownloadingState();
19249
19325
  dexie.cloud = {
19250
19326
  // @ts-ignore
19251
- version: "4.4.0",
19327
+ version: "4.4.1",
19252
19328
  options: Object.assign({}, DEFAULT_OPTIONS),
19253
19329
  schema: null,
19254
19330
  get currentUserId() {
@@ -19276,6 +19352,16 @@
19276
19352
  invites: getInvitesObservable(dexie),
19277
19353
  roles: getGlobalRolesObservable(dexie),
19278
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
+ }
19279
19365
  options = dexie.cloud.options = Object.assign(Object.assign({}, dexie.cloud.options), options);
19280
19366
  configuredProgramatically = true;
19281
19367
  if (options.databaseUrl && options.nameSuffix) {
@@ -19662,7 +19748,7 @@
19662
19748
  }
19663
19749
  }
19664
19750
  // @ts-ignore
19665
- dexieCloud.version = "4.4.0";
19751
+ dexieCloud.version = "4.4.1";
19666
19752
  Dexie.Cloud = dexieCloud;
19667
19753
 
19668
19754
  // In case the SW lives for a while, let it reuse already opened connections: