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.
@@ -50,4 +50,14 @@ export interface DexieCloudOptions {
50
50
  * Best for apps with large media that may not all be needed offline.
51
51
  */
52
52
  blobMode?: 'eager' | 'lazy';
53
+ /** Maximum string length (in characters) before offloading to blob storage during sync.
54
+ *
55
+ * Strings longer than this threshold are uploaded as blobs during sync,
56
+ * reducing sync payload size. The original string is kept intact in IndexedDB.
57
+ *
58
+ * Set to `Infinity` to disable string offloading.
59
+ *
60
+ * @default 32768
61
+ */
62
+ maxStringLength?: number;
53
63
  }
@@ -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
  *
@@ -1365,10 +1365,48 @@ function isSerializedTSONRef(value) {
1365
1365
  obj._bt === undefined // Not a raw BlobRef
1366
1366
  );
1367
1367
  }
1368
+ /**
1369
+ * Recursively check if an object contains any BlobRefs
1370
+ */
1371
+ function hasBlobRefs(obj, visited = new WeakSet()) {
1372
+ if (obj === null || obj === undefined) {
1373
+ return false;
1374
+ }
1375
+ if (isBlobRef(obj)) {
1376
+ return true;
1377
+ }
1378
+ if (typeof obj !== 'object') {
1379
+ return false;
1380
+ }
1381
+ // Avoid circular references - check BEFORE processing
1382
+ if (visited.has(obj)) {
1383
+ return false;
1384
+ }
1385
+ visited.add(obj);
1386
+ // Skip special objects that can't contain BlobRefs
1387
+ if (obj instanceof Date || obj instanceof RegExp || obj instanceof Blob) {
1388
+ return false;
1389
+ }
1390
+ if (obj instanceof ArrayBuffer || ArrayBuffer.isView(obj)) {
1391
+ return false;
1392
+ }
1393
+ if (Array.isArray(obj)) {
1394
+ return obj.some(item => hasBlobRefs(item, visited));
1395
+ }
1396
+ // Only traverse POJOs
1397
+ if (obj.constructor === Object) {
1398
+ return Object.values(obj).some(value => hasBlobRefs(value, visited));
1399
+ }
1400
+ return false;
1401
+ }
1368
1402
  /**
1369
1403
  * Convert downloaded Uint8Array to the original type specified in BlobRef
1370
1404
  */
1371
1405
  function convertToOriginalType(data, ref) {
1406
+ // String type: decode UTF-8 back to string
1407
+ if (ref._bt === 'string') {
1408
+ return new TextDecoder().decode(data);
1409
+ }
1372
1410
  // Get the underlying ArrayBuffer (handle shared buffer case)
1373
1411
  const buffer = data.buffer.byteLength === data.byteLength
1374
1412
  ? data.buffer
@@ -3541,6 +3579,19 @@ function bulkUpdate(table, keys, changeSpecs) {
3541
3579
  });
3542
3580
  }
3543
3581
 
3582
+ /**
3583
+ * If the incoming value contains BlobRefs (e.g. offloaded strings or binaries),
3584
+ * mark it with _hasBlobRefs = 1 so the blobResolveMiddleware will resolve them
3585
+ * on the next read.
3586
+ */
3587
+ function markIfHasBlobRefs(obj) {
3588
+ if (obj !== null &&
3589
+ typeof obj === 'object' &&
3590
+ obj.constructor === Object &&
3591
+ hasBlobRefs(obj)) {
3592
+ obj._hasBlobRefs = 1;
3593
+ }
3594
+ }
3544
3595
  function applyServerChanges(changes, db) {
3545
3596
  return __awaiter(this, void 0, void 0, function* () {
3546
3597
  console.debug('Applying server changes', changes, Dexie.currentTransaction);
@@ -3576,6 +3627,7 @@ function applyServerChanges(changes, db) {
3576
3627
  const keys = mut.keys.map(keyDecoder);
3577
3628
  switch (mut.type) {
3578
3629
  case 'insert':
3630
+ mut.values.forEach(markIfHasBlobRefs);
3579
3631
  if (primaryKey.outbound) {
3580
3632
  yield table.bulkAdd(mut.values, keys);
3581
3633
  }
@@ -3588,6 +3640,7 @@ function applyServerChanges(changes, db) {
3588
3640
  }
3589
3641
  break;
3590
3642
  case 'upsert':
3643
+ mut.values.forEach(markIfHasBlobRefs);
3591
3644
  if (primaryKey.outbound) {
3592
3645
  yield table.bulkPut(mut.values, keys);
3593
3646
  }
@@ -3843,6 +3896,8 @@ function applyYServerMessages(yMessages, db) {
3843
3896
  */
3844
3897
  // Blobs >= 4KB are offloaded to blob storage
3845
3898
  const BLOB_OFFLOAD_THRESHOLD = 4096;
3899
+ // Default max string length before offloading (32KB characters)
3900
+ const DEFAULT_MAX_STRING_LENGTH = 32768;
3846
3901
  // Cache: once we know the server doesn't support blob storage, skip future uploads.
3847
3902
  // Maps databaseUrl → boolean (true = supported, false = not supported).
3848
3903
  const blobEndpointSupported = new Map();
@@ -3983,10 +4038,10 @@ function uploadBlob(databaseUrl, getCachedAccessToken, blob) {
3983
4038
  );
3984
4039
  });
3985
4040
  }
3986
- function offloadBlobsAndMarkDirty(obj, databaseUrl, getCachedAccessToken) {
3987
- return __awaiter(this, void 0, void 0, function* () {
4041
+ function offloadBlobsAndMarkDirty(obj_1, databaseUrl_1, getCachedAccessToken_1) {
4042
+ return __awaiter(this, arguments, void 0, function* (obj, databaseUrl, getCachedAccessToken, maxStringLength = DEFAULT_MAX_STRING_LENGTH) {
3988
4043
  const dirtyFlag = { dirty: false };
3989
- const result = yield offloadBlobs(obj, databaseUrl, getCachedAccessToken, dirtyFlag);
4044
+ const result = yield offloadBlobs(obj, databaseUrl, getCachedAccessToken, maxStringLength, dirtyFlag);
3990
4045
  // Mark the object as dirty for sync if any blobs were offloaded
3991
4046
  if (dirtyFlag.dirty && typeof result === 'object' && result !== null && result.constructor === Object) {
3992
4047
  result._hasBlobRefs = 1;
@@ -3999,10 +4054,26 @@ function offloadBlobsAndMarkDirty(obj, databaseUrl, getCachedAccessToken) {
3999
4054
  * Returns a new object with blobs replaced by BlobRefs
4000
4055
  */
4001
4056
  function offloadBlobs(obj_1, databaseUrl_1, getCachedAccessToken_1) {
4002
- return __awaiter(this, arguments, void 0, function* (obj, databaseUrl, getCachedAccessToken, dirtyFlag = { dirty: false }, visited = new WeakSet()) {
4057
+ return __awaiter(this, arguments, void 0, function* (obj, databaseUrl, getCachedAccessToken, maxStringLength = DEFAULT_MAX_STRING_LENGTH, dirtyFlag = { dirty: false }, visited = new WeakSet()) {
4003
4058
  if (obj === null || obj === undefined) {
4004
4059
  return obj;
4005
4060
  }
4061
+ // Check if this is a long string that should be offloaded
4062
+ if (typeof obj === 'string' && obj.length > maxStringLength && maxStringLength !== Infinity) {
4063
+ if (blobEndpointSupported.get(databaseUrl) === false) {
4064
+ return obj;
4065
+ }
4066
+ const blob = new Blob([obj], { type: 'text/plain;charset=utf-8' });
4067
+ const blobRef = yield uploadBlob(databaseUrl, getCachedAccessToken, blob);
4068
+ if (blobRef === null) {
4069
+ blobEndpointSupported.set(databaseUrl, false);
4070
+ return obj;
4071
+ }
4072
+ blobEndpointSupported.set(databaseUrl, true);
4073
+ dirtyFlag.dirty = true;
4074
+ // Mark as string type so it's resolved back to string, not Blob
4075
+ return Object.assign(Object.assign({}, blobRef), { _bt: 'string' });
4076
+ }
4006
4077
  // Check if this is a blob that should be offloaded
4007
4078
  if (shouldOffloadBlob(obj)) {
4008
4079
  if (blobEndpointSupported.get(databaseUrl) === false) {
@@ -4031,7 +4102,7 @@ function offloadBlobs(obj_1, databaseUrl_1, getCachedAccessToken_1) {
4031
4102
  if (Array.isArray(obj)) {
4032
4103
  const result = [];
4033
4104
  for (const item of obj) {
4034
- result.push(yield offloadBlobs(item, databaseUrl, getCachedAccessToken, dirtyFlag, visited));
4105
+ result.push(yield offloadBlobs(item, databaseUrl, getCachedAccessToken, maxStringLength, dirtyFlag, visited));
4035
4106
  }
4036
4107
  return result;
4037
4108
  }
@@ -4043,7 +4114,7 @@ function offloadBlobs(obj_1, databaseUrl_1, getCachedAccessToken_1) {
4043
4114
  }
4044
4115
  const result = {};
4045
4116
  for (const [key, value] of Object.entries(obj)) {
4046
- result[key] = yield offloadBlobs(value, databaseUrl, getCachedAccessToken, dirtyFlag, visited);
4117
+ result[key] = yield offloadBlobs(value, databaseUrl, getCachedAccessToken, maxStringLength, dirtyFlag, visited);
4047
4118
  }
4048
4119
  return result;
4049
4120
  });
@@ -4052,13 +4123,13 @@ function offloadBlobs(obj_1, databaseUrl_1, getCachedAccessToken_1) {
4052
4123
  * Process a DBOperationsSet and offload any large blobs
4053
4124
  * Returns a new DBOperationsSet with blobs replaced by BlobRefs
4054
4125
  */
4055
- function offloadBlobsInOperations(operations, databaseUrl, getCachedAccessToken) {
4056
- return __awaiter(this, void 0, void 0, function* () {
4126
+ function offloadBlobsInOperations(operations_1, databaseUrl_1, getCachedAccessToken_1) {
4127
+ return __awaiter(this, arguments, void 0, function* (operations, databaseUrl, getCachedAccessToken, maxStringLength = DEFAULT_MAX_STRING_LENGTH) {
4057
4128
  const result = [];
4058
4129
  for (const tableOps of operations) {
4059
4130
  const processedMuts = [];
4060
4131
  for (const mut of tableOps.muts) {
4061
- const processedMut = yield offloadBlobsInOperation(mut, databaseUrl, getCachedAccessToken);
4132
+ const processedMut = yield offloadBlobsInOperation(mut, databaseUrl, getCachedAccessToken, maxStringLength);
4062
4133
  processedMuts.push(processedMut);
4063
4134
  }
4064
4135
  result.push({
@@ -4069,20 +4140,20 @@ function offloadBlobsInOperations(operations, databaseUrl, getCachedAccessToken)
4069
4140
  return result;
4070
4141
  });
4071
4142
  }
4072
- function offloadBlobsInOperation(op, databaseUrl, getCachedAccessToken) {
4073
- return __awaiter(this, void 0, void 0, function* () {
4143
+ function offloadBlobsInOperation(op_1, databaseUrl_1, getCachedAccessToken_1) {
4144
+ return __awaiter(this, arguments, void 0, function* (op, databaseUrl, getCachedAccessToken, maxStringLength = DEFAULT_MAX_STRING_LENGTH) {
4074
4145
  switch (op.type) {
4075
4146
  case 'insert':
4076
4147
  case 'upsert': {
4077
- const processedValues = yield Promise.all(op.values.map(value => offloadBlobsAndMarkDirty(value, databaseUrl, getCachedAccessToken)));
4148
+ const processedValues = yield Promise.all(op.values.map(value => offloadBlobsAndMarkDirty(value, databaseUrl, getCachedAccessToken, maxStringLength)));
4078
4149
  return Object.assign(Object.assign({}, op), { values: processedValues });
4079
4150
  }
4080
4151
  case 'update': {
4081
- const processedChangeSpecs = yield Promise.all(op.changeSpecs.map(spec => offloadBlobsAndMarkDirty(spec, databaseUrl, getCachedAccessToken)));
4152
+ const processedChangeSpecs = yield Promise.all(op.changeSpecs.map(spec => offloadBlobsAndMarkDirty(spec, databaseUrl, getCachedAccessToken, maxStringLength)));
4082
4153
  return Object.assign(Object.assign({}, op), { changeSpecs: processedChangeSpecs });
4083
4154
  }
4084
4155
  case 'modify': {
4085
- const processedChangeSpec = yield offloadBlobsAndMarkDirty(op.changeSpec, databaseUrl, getCachedAccessToken);
4156
+ const processedChangeSpec = yield offloadBlobsAndMarkDirty(op.changeSpec, databaseUrl, getCachedAccessToken, maxStringLength);
4086
4157
  return Object.assign(Object.assign({}, op), { changeSpec: processedChangeSpec });
4087
4158
  }
4088
4159
  case 'delete':
@@ -4097,33 +4168,37 @@ function offloadBlobsInOperation(op, databaseUrl, getCachedAccessToken) {
4097
4168
  * Check if there are any large blobs in the operations that need offloading
4098
4169
  * This is a quick check to avoid unnecessary processing
4099
4170
  */
4100
- function hasLargeBlobsInOperations(operations) {
4171
+ function hasLargeBlobsInOperations(operations, maxStringLength = DEFAULT_MAX_STRING_LENGTH) {
4101
4172
  for (const tableOps of operations) {
4102
4173
  for (const mut of tableOps.muts) {
4103
- if (hasLargeBlobsInOperation(mut)) {
4174
+ if (hasLargeBlobsInOperation(mut, maxStringLength)) {
4104
4175
  return true;
4105
4176
  }
4106
4177
  }
4107
4178
  }
4108
4179
  return false;
4109
4180
  }
4110
- function hasLargeBlobsInOperation(op) {
4181
+ function hasLargeBlobsInOperation(op, maxStringLength) {
4111
4182
  switch (op.type) {
4112
4183
  case 'insert':
4113
4184
  case 'upsert':
4114
- return op.values.some(value => hasLargeBlobs(value));
4185
+ return op.values.some(value => hasLargeBlobs(value, maxStringLength));
4115
4186
  case 'update':
4116
- return op.changeSpecs.some(spec => hasLargeBlobs(spec));
4187
+ return op.changeSpecs.some(spec => hasLargeBlobs(spec, maxStringLength));
4117
4188
  case 'modify':
4118
- return hasLargeBlobs(op.changeSpec);
4189
+ return hasLargeBlobs(op.changeSpec, maxStringLength);
4119
4190
  default:
4120
4191
  return false;
4121
4192
  }
4122
4193
  }
4123
- function hasLargeBlobs(obj, visited = new WeakSet()) {
4194
+ function hasLargeBlobs(obj, maxStringLength, visited = new WeakSet()) {
4124
4195
  if (obj === null || obj === undefined) {
4125
4196
  return false;
4126
4197
  }
4198
+ // Check long strings
4199
+ if (typeof obj === 'string' && obj.length > maxStringLength && maxStringLength !== Infinity) {
4200
+ return true;
4201
+ }
4127
4202
  if (shouldOffloadBlob(obj)) {
4128
4203
  return true;
4129
4204
  }
@@ -4136,13 +4211,13 @@ function hasLargeBlobs(obj, visited = new WeakSet()) {
4136
4211
  }
4137
4212
  visited.add(obj);
4138
4213
  if (Array.isArray(obj)) {
4139
- return obj.some(item => hasLargeBlobs(item, visited));
4214
+ return obj.some(item => hasLargeBlobs(item, maxStringLength, visited));
4140
4215
  }
4141
4216
  // Traverse plain objects (POJO-like) - use duck typing since IndexedDB
4142
4217
  // may return objects where constructor !== Object
4143
4218
  const proto = Object.getPrototypeOf(obj);
4144
4219
  if (proto === Object.prototype || proto === null) {
4145
- return Object.values(obj).some(value => hasLargeBlobs(value, visited));
4220
+ return Object.values(obj).some(value => hasLargeBlobs(value, maxStringLength, visited));
4146
4221
  }
4147
4222
  return false;
4148
4223
  }
@@ -4403,7 +4478,7 @@ function _sync(db_1, options_1, schema_1) {
4403
4478
  return __awaiter(this, arguments, void 0, function* (db, options, schema, { isInitialSync, cancelToken, justCheckIfNeeded, purpose } = {
4404
4479
  isInitialSync: false,
4405
4480
  }) {
4406
- var _a;
4481
+ var _a, _b, _c;
4407
4482
  if (!justCheckIfNeeded) {
4408
4483
  console.debug('SYNC STARTED', { isInitialSync, purpose });
4409
4484
  }
@@ -4477,9 +4552,10 @@ function _sync(db_1, options_1, schema_1) {
4477
4552
  // Offload large blobs to blob storage before sync
4478
4553
  //
4479
4554
  let processedChangeSet = clientChangeSet;
4480
- const hasLargeBlobs = hasLargeBlobsInOperations(clientChangeSet);
4555
+ const maxStringLength = (_c = (_b = db.cloud.options) === null || _b === void 0 ? void 0 : _b.maxStringLength) !== null && _c !== void 0 ? _c : 32768;
4556
+ const hasLargeBlobs = hasLargeBlobsInOperations(clientChangeSet, maxStringLength);
4481
4557
  if (hasLargeBlobs) {
4482
- processedChangeSet = yield offloadBlobsInOperations(clientChangeSet, databaseUrl, () => loadCachedAccessToken(db));
4558
+ processedChangeSet = yield offloadBlobsInOperations(clientChangeSet, databaseUrl, () => loadCachedAccessToken(db), maxStringLength);
4483
4559
  }
4484
4560
  //
4485
4561
  // Push changes to server
@@ -8189,7 +8265,7 @@ function dexieCloud(dexie) {
8189
8265
  const downloading$ = createDownloadingState();
8190
8266
  dexie.cloud = {
8191
8267
  // @ts-ignore
8192
- version: "4.4.0",
8268
+ version: "4.4.1",
8193
8269
  options: Object.assign({}, DEFAULT_OPTIONS),
8194
8270
  schema: null,
8195
8271
  get currentUserId() {
@@ -8217,6 +8293,16 @@ function dexieCloud(dexie) {
8217
8293
  invites: getInvitesObservable(dexie),
8218
8294
  roles: getGlobalRolesObservable(dexie),
8219
8295
  configure(options) {
8296
+ // Validate maxStringLength — Infinity disables offloading, otherwise must be
8297
+ // a finite positive number not exceeding the server limit (32768).
8298
+ const MAX_SERVER_STRING_LENGTH = 32768;
8299
+ if (options.maxStringLength !== undefined &&
8300
+ options.maxStringLength !== Infinity &&
8301
+ (!Number.isFinite(options.maxStringLength) ||
8302
+ options.maxStringLength < 0 ||
8303
+ options.maxStringLength > MAX_SERVER_STRING_LENGTH)) {
8304
+ throw new Error(`maxStringLength must be Infinity or a finite number in [0, ${MAX_SERVER_STRING_LENGTH}]. Got: ${options.maxStringLength}`);
8305
+ }
8220
8306
  options = dexie.cloud.options = Object.assign(Object.assign({}, dexie.cloud.options), options);
8221
8307
  configuredProgramatically = true;
8222
8308
  if (options.databaseUrl && options.nameSuffix) {
@@ -8603,7 +8689,7 @@ function dexieCloud(dexie) {
8603
8689
  }
8604
8690
  }
8605
8691
  // @ts-ignore
8606
- dexieCloud.version = "4.4.0";
8692
+ dexieCloud.version = "4.4.1";
8607
8693
  Dexie.Cloud = dexieCloud;
8608
8694
 
8609
8695
  export { dexieCloud as default, defineYDocTrigger, dexieCloud, getTiedObjectId, getTiedRealmId, resolveText };