@@ -46,6 +46,14 @@ class AsynchronousIndexedDbFileSystem {
4646 web.IDBDatabase ? _database;
4747 final String _dbName;
4848
49+ /// Whether to store chunks as [web.Blob] s instead of array buffers.
50+ ///
51+ /// It seems like loading blobs concurrently may be more efficient, but not
52+ /// all browsers support storing blobs in IndexedDB. We support both blobs
53+ /// and array buffers on the read path. For writes, we run a feature detection
54+ /// after opening the file system to determine whether to store blobs.
55+ bool _storeBlobs = true ;
56+
4957 AsynchronousIndexedDbFileSystem (this ._dbName);
5058
5159 bool get _isClosed => _database == null ;
@@ -79,6 +87,44 @@ class AsynchronousIndexedDbFileSystem {
7987 final openFuture = openRequest.completeOrBlocked< web.IDBDatabase > ();
8088 completer.complete (openFuture);
8189 _database = await completer.future;
90+
91+ _storeBlobs = await _supportsStoringBlobs ();
92+ }
93+
94+ /// Probes whether the IndexedDB implementation supports storing [web.Blob]
95+ /// instances.
96+ ///
97+ /// Safari in private windows does not support storing blobs, but allows
98+ /// storing array buffers directly. Our read paths support reading blobs and
99+ /// array buffers, so we use this to determine which format to use for writes.
100+ Future <bool > _supportsStoringBlobs () async {
101+ final transaction =
102+ _database! .transaction ([_blocksStore.toJS].toJS, 'readwrite' );
103+
104+ web.Blob blob;
105+
106+ try {
107+ final blocks = transaction.objectStore (_blocksStore);
108+
109+ final request = blocks.add (
110+ web.Blob ([Uint8List (4096 ).buffer.toJS].toJS),
111+ ['test' .toJS].toJS,
112+ );
113+ final key = await request.complete ();
114+
115+ blob = await blocks.get (key).complete< web.Blob > ();
116+ } on Object {
117+ return false ;
118+ } finally {
119+ transaction.abort ();
120+ }
121+
122+ try {
123+ await blob.byteBuffer ();
124+ return true ;
125+ } on Object {
126+ return false ;
127+ }
82128 }
83129
84130 void close () {
@@ -163,7 +209,12 @@ class AsynchronousIndexedDbFileSystem {
163209 // We can't have an async suspension in here because that would close the
164210 // transaction. Launch the reader now and wait for all reads later.
165211 readOperations.add (Future .sync (() async {
166- final data = await (row.value as web.Blob ).byteBuffer ();
212+ ByteBuffer data;
213+ if (row.value.instanceOfString ('Blob' )) {
214+ data = await (row.value as web.Blob ).byteBuffer ();
215+ } else {
216+ data = (row.value as JSArrayBuffer ).toDart;
217+ }
167218 result.setAll (rowOffset, data.asUint8List (0 , length));
168219 }));
169220 }
@@ -191,8 +242,13 @@ class AsynchronousIndexedDbFileSystem {
191242
192243 final key = (row.key as JSArray ).toDart;
193244 final rowOffset = (key[1 ] as JSNumber ).toDartInt;
194- final blob = row.value as web.Blob ;
195- final dataLength = min (blob.size, file.length - rowOffset);
245+ final value = row.value;
246+ final isBlob = value.instanceOfString ('Blob' );
247+ final valueSize = isBlob
248+ ? (value as web.Blob ).size
249+ : (value as _JSArrayBuffer ).byteLength;
250+
251+ final dataLength = min (valueSize, file.length - rowOffset);
196252
197253 if (rowOffset < offset) {
198254 // This block starts before the section that we're interested in, so cut
@@ -204,7 +260,9 @@ class AsynchronousIndexedDbFileSystem {
204260 // Do the reading async because we loose the transaction on the first
205261 // suspension.
206262 readOperations.add (Future .sync (() async {
207- final data = await blob.byteBuffer ();
263+ final data = isBlob
264+ ? await (value as web.Blob ).byteBuffer ()
265+ : (value as _JSArrayBuffer ).toDart;
208266
209267 target.setRange (
210268 0 ,
@@ -226,7 +284,9 @@ class AsynchronousIndexedDbFileSystem {
226284
227285 bytesRead += lengthToCopy;
228286 readOperations.add (Future .sync (() async {
229- final data = await blob.byteBuffer ();
287+ final data = isBlob
288+ ? await (value as web.Blob ).byteBuffer ()
289+ : (value as _JSArrayBuffer ).toDart;
230290
231291 target.setAll (startInTarget, data.asUint8List (0 , lengthToCopy));
232292 }));
@@ -253,15 +313,17 @@ class AsynchronousIndexedDbFileSystem {
253313 final cursor = await blocks
254314 .openCursor (web.IDBKeyRange .only ([fileId.toJS, blockStart.toJS].toJS))
255315 .complete< web.IDBCursorWithValue ? > ();
256- final blob = web.Blob ([block.toJS].toJS);
316+
317+ final value =
318+ _storeBlobs ? web.Blob ([block.toJS].toJS) : block.buffer.toJS;
257319
258320 if (cursor == null ) {
259321 // There isn't, let's write a new block
260322 await blocks
261- .put (blob , [fileId.toJS, blockStart.toJS].toJS)
323+ .put (value , [fileId.toJS, blockStart.toJS].toJS)
262324 .complete <JSAny ?>();
263325 } else {
264- await cursor.update (blob ).complete <JSAny ?>();
326+ await cursor.update (value ).complete <JSAny ?>();
265327 }
266328 }
267329
@@ -828,3 +890,8 @@ final class _WriteFileWorkItem extends _IndexedDbWorkItem {
828890 ._write (await fileSystem._fileId (path), request);
829891 }
830892}
893+
894+ @JS ('ArrayBuffer' )
895+ extension type _JSArrayBuffer (JSArrayBuffer _) implements JSArrayBuffer {
896+ external int get byteLength;
897+ }
0 commit comments