@@ -9,26 +9,55 @@ import type { HttpAdapter } from './adapters.js';
99import type { BlobHandling , BlobRef } from './types.js' ;
1010import { DexieCloudError } from './types.js' ;
1111
12+ /**
13+ * Minimum byte size for offloading a binary to blob storage.
14+ * Binaries smaller than this threshold are kept inline (as base64).
15+ * Must match the server-side threshold.
16+ */
17+ export const BLOB_THRESHOLD = 4096 ;
18+
19+ /**
20+ * Maximum number of concurrent blob downloads in _walkForRead.
21+ * Mirrors the client-side MAX_CONCURRENT pattern.
22+ */
23+ const MAX_CONCURRENT_DOWNLOADS = 6 ;
24+
1225/** Generate a unique blob ID */
1326function generateBlobId ( ) : string {
1427 if ( typeof crypto !== 'undefined' && typeof crypto . randomUUID === 'function' ) {
1528 return crypto . randomUUID ( ) . replace ( / - / g, '' ) ;
1629 }
17- // Fallback: timestamp + random hex
18- return Date . now ( ) . toString ( 16 ) + Math . random ( ) . toString ( 16 ) . slice ( 2 ) ;
30+ // Fallback: use getRandomValues for strong entropy
31+ if ( typeof crypto !== 'undefined' && typeof crypto . getRandomValues === 'function' ) {
32+ const bytes = new Uint8Array ( 16 ) ;
33+ crypto . getRandomValues ( bytes ) ;
34+ return Array . from ( bytes )
35+ . map ( ( b ) => b . toString ( 16 ) . padStart ( 2 , '0' ) )
36+ . join ( '' ) ;
37+ }
38+ // Last resort (non-browser, non-Node env): still better than Math.random alone
39+ const ts = Date . now ( ) . toString ( 16 ) ;
40+ const rand = Math . floor ( Math . random ( ) * 0xffffffff ) . toString ( 16 ) . padStart ( 8 , '0' ) ;
41+ return ts + rand ;
1942}
2043
21- /** Convert Blob/ArrayBuffer/TypedArray to Uint8Array */
22- async function toUint8Array ( data : Uint8Array | Blob | ArrayBuffer ) : Promise < Uint8Array > {
44+ /**
45+ * Convert Blob/ArrayBuffer/TypedArray/DataView to Uint8Array.
46+ * Accepts any ArrayBufferView (TypedArrays + DataView) as well as
47+ * Uint8Array, ArrayBuffer, and Blob.
48+ */
49+ async function toUint8Array (
50+ data : Uint8Array | Blob | ArrayBuffer | ArrayBufferView
51+ ) : Promise < Uint8Array > {
2352 if ( data instanceof Uint8Array ) return data ;
2453 if ( data instanceof ArrayBuffer ) return new Uint8Array ( data ) ;
2554 if ( typeof Blob !== 'undefined' && data instanceof Blob ) {
2655 const buf = await data . arrayBuffer ( ) ;
2756 return new Uint8Array ( buf ) ;
2857 }
29- // TypedArray (e.g. Int8Array, etc.)
58+ // Handles all TypedArrays ( Int8Array, Float32Array, etc.) and DataView
3059 if ( ArrayBuffer . isView ( data ) ) {
31- return new Uint8Array ( ( data as ArrayBufferView ) . buffer ) ;
60+ return new Uint8Array ( data . buffer , data . byteOffset , data . byteLength ) ;
3261 }
3362 throw new TypeError ( 'Unsupported data type for blob upload' ) ;
3463}
@@ -92,7 +121,7 @@ export class BlobManager {
92121 * Returns the blob ref (e.g. "1:abc123...").
93122 */
94123 async upload (
95- data : Uint8Array | Blob | ArrayBuffer ,
124+ data : Uint8Array | Blob | ArrayBuffer | ArrayBufferView ,
96125 token : string ,
97126 contentType = 'application/octet-stream'
98127 ) : Promise < string > {
@@ -125,14 +154,20 @@ export class BlobManager {
125154 const parsed = JSON . parse ( text ) ;
126155 if ( parsed ?. ref ) return parsed . ref as string ;
127156 } catch {
128- // ignore parse errors, construct ref ourselves
157+ // ignore parse errors, fall through
129158 }
130159 // If server returned "version:blobId" directly
131160 if ( text . includes ( ':' ) ) return text . trim ( ) ;
132161 }
133162
134- // Fallback: assume version 1
135- return `1:${ blobId } ` ;
163+ // Server response was unparseable — we cannot safely construct a ref
164+ // because we don't know the server-assigned version.
165+ throw new DexieCloudError (
166+ `Blob upload succeeded (HTTP ${ response . status } ) but server returned no parseable ref. ` +
167+ `Cannot construct a safe blob reference without the server-assigned version.` ,
168+ response . status ,
169+ text
170+ ) ;
136171 }
137172
138173 /**
@@ -160,8 +195,9 @@ export class BlobManager {
160195 }
161196
162197 /**
163- * Process an object before uploading: find inline blobs, upload them,
164- * replace with BlobRefs. Only active in 'auto' mode.
198+ * Process an object before uploading: find inline blobs large enough to
199+ * offload (≥ BLOB_THRESHOLD bytes), upload them, replace with BlobRefs.
200+ * Small binaries are left inline. Only active in 'auto' mode.
165201 */
166202 async processForUpload ( obj : any , token : string ) : Promise < any > {
167203 if ( this . mode !== 'auto' ) return obj ;
@@ -179,8 +215,13 @@ export class BlobManager {
179215
180216 private async _walkForUpload ( val : any , token : string ) : Promise < any > {
181217 if ( isInlineBlob ( val ) ) {
182- // Upload inline blob, replace with BlobRef
183218 const bytes = base64ToUint8Array ( val . v ) ;
219+ // Only offload to blob storage if the binary meets the size threshold.
220+ // Small binaries are cheaper to keep inline than to round-trip through
221+ // the blob endpoint.
222+ if ( bytes . length < BLOB_THRESHOLD ) {
223+ return val ; // keep as-is
224+ }
184225 const contentType = val . ct ?? 'application/octet-stream' ;
185226 const ref = await this . upload ( bytes , token , contentType ) ;
186227 const blobRef : BlobRef = {
@@ -193,27 +234,21 @@ export class BlobManager {
193234 }
194235
195236 if ( Array . isArray ( val ) ) {
196- const results : any [ ] = [ ] ;
197- for ( const item of val ) {
198- results . push ( await this . _walkForUpload ( item , token ) ) ;
199- }
200- return results ;
237+ return Promise . all ( val . map ( ( item ) => this . _walkForUpload ( item , token ) ) ) ;
201238 }
202239
203240 if ( val !== null && typeof val === 'object' ) {
204- const result : Record < string , any > = { } ;
205- for ( const [ k , v ] of Object . entries ( val ) ) {
206- result [ k ] = await this . _walkForUpload ( v , token ) ;
207- }
208- return result ;
241+ const entries = await Promise . all (
242+ Object . entries ( val ) . map ( async ( [ k , v ] ) => [ k , await this . _walkForUpload ( v , token ) ] as const )
243+ ) ;
244+ return Object . fromEntries ( entries ) ;
209245 }
210246
211247 return val ;
212248 }
213249
214250 private async _walkForRead ( val : any , token : string ) : Promise < any > {
215251 if ( isBlobRef ( val ) ) {
216- // Download and replace with inline
217252 const { data, contentType } = await this . download ( val . ref , token ) ;
218253 return {
219254 _bt : val . _bt ,
@@ -223,21 +258,48 @@ export class BlobManager {
223258 }
224259
225260 if ( Array . isArray ( val ) ) {
226- const results : any [ ] = [ ] ;
227- for ( const item of val ) {
228- results . push ( await this . _walkForRead ( item , token ) ) ;
229- }
230- return results ;
261+ // Download up to MAX_CONCURRENT_DOWNLOADS blobs in parallel
262+ return this . _parallelMap ( val , ( item ) => this . _walkForRead ( item , token ) ) ;
231263 }
232264
233265 if ( val !== null && typeof val === 'object' ) {
266+ const keys = Object . keys ( val ) ;
267+ const resolvedValues = await this . _parallelMap (
268+ keys ,
269+ ( k ) => this . _walkForRead ( val [ k ] , token )
270+ ) ;
234271 const result : Record < string , any > = { } ;
235- for ( const [ k , v ] of Object . entries ( val ) ) {
236- result [ k ] = await this . _walkForRead ( v , token ) ;
272+ for ( let i = 0 ; i < keys . length ; i ++ ) {
273+ result [ keys [ i ] ! ] = resolvedValues [ i ] ;
237274 }
238275 return result ;
239276 }
240277
241278 return val ;
242279 }
280+
281+ /**
282+ * Like Promise.all but with a concurrency cap.
283+ */
284+ private async _parallelMap < T , R > (
285+ items : T [ ] ,
286+ fn : ( item : T ) => Promise < R >
287+ ) : Promise < R [ ] > {
288+ const results : R [ ] = new Array ( items . length ) ;
289+ let index = 0 ;
290+
291+ async function worker ( ) {
292+ while ( index < items . length ) {
293+ const i = index ++ ;
294+ results [ i ] = await fn ( items [ i ] ! ) ;
295+ }
296+ }
297+
298+ const workers = Array . from (
299+ { length : Math . min ( MAX_CONCURRENT_DOWNLOADS , items . length ) } ,
300+ ( ) => worker ( )
301+ ) ;
302+ await Promise . all ( workers ) ;
303+ return results ;
304+ }
243305}
0 commit comments