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.
- package/dist/modern/DexieCloudOptions.d.ts +10 -0
- package/dist/modern/dexie-cloud-addon.js +115 -29
- package/dist/modern/dexie-cloud-addon.js.map +1 -1
- package/dist/modern/dexie-cloud-addon.min.js +1 -1
- package/dist/modern/dexie-cloud-addon.min.js.map +1 -1
- package/dist/modern/service-worker.js +252 -166
- package/dist/modern/service-worker.js.map +1 -1
- package/dist/modern/service-worker.min.js +1 -1
- package/dist/modern/service-worker.min.js.map +1 -1
- package/dist/modern/sync/blobOffloading.d.ts +5 -4
- package/dist/modern/sync/blobResolve.d.ts +3 -3
- package/dist/umd/dexie-cloud-addon.js +116 -30
- package/dist/umd/dexie-cloud-addon.js.map +1 -1
- package/dist/umd/dexie-cloud-addon.min.js +1 -1
- package/dist/umd/dexie-cloud-addon.min.js.map +1 -1
- package/dist/umd/service-worker.js +253 -167
- package/dist/umd/service-worker.js.map +1 -1
- package/dist/umd/service-worker.min.js +1 -1
- package/dist/umd/service-worker.min.js.map +1 -1
- package/package.json +1 -1
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* ==========================================================================
|
|
10
10
|
*
|
|
11
|
-
* Version 4.4.
|
|
11
|
+
* Version 4.4.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(
|
|
3186
|
-
return __awaiter(this,
|
|
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(
|
|
3255
|
-
return __awaiter(this,
|
|
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(
|
|
3272
|
-
return __awaiter(this,
|
|
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
|
|
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.
|
|
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.
|
|
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:
|