From f830d9edd70186f24d316795f8b85ec074ac80c5 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sat, 23 Nov 2024 18:33:34 +0100 Subject: [PATCH] IndexedDb: Store blobs as array buffers if necessary --- sqlite3/lib/src/wasm/vfs/indexed_db.dart | 83 +++++++++++++++++++++--- 1 file changed, 75 insertions(+), 8 deletions(-) diff --git a/sqlite3/lib/src/wasm/vfs/indexed_db.dart b/sqlite3/lib/src/wasm/vfs/indexed_db.dart index 01533d7..2cc3a99 100644 --- a/sqlite3/lib/src/wasm/vfs/indexed_db.dart +++ b/sqlite3/lib/src/wasm/vfs/indexed_db.dart @@ -46,6 +46,14 @@ class AsynchronousIndexedDbFileSystem { web.IDBDatabase? _database; final String _dbName; + /// Whether to store chunks as [web.Blob]s instead of array buffers. + /// + /// It seems like loading blobs concurrently may be more efficient, but not + /// all browsers support storing blobs in IndexedDB. We support both blobs + /// and array buffers on the read path. For writes, we run a feature detection + /// after opening the file system to determine whether to store blobs. + bool _storeBlobs = true; + AsynchronousIndexedDbFileSystem(this._dbName); bool get _isClosed => _database == null; @@ -79,6 +87,44 @@ class AsynchronousIndexedDbFileSystem { final openFuture = openRequest.completeOrBlocked(); completer.complete(openFuture); _database = await completer.future; + + _storeBlobs = await _supportsStoringBlobs(); + } + + /// Probes whether the IndexedDB implementation supports storing [web.Blob] + /// instances. + /// + /// Safari in private windows does not support storing blobs, but allows + /// storing array buffers directly. Our read paths support reading blobs and + /// array buffers, so we use this to determine which format to use for writes. + Future _supportsStoringBlobs() async { + final transaction = + _database!.transaction([_blocksStore.toJS].toJS, 'readwrite'); + + web.Blob blob; + + try { + final blocks = transaction.objectStore(_blocksStore); + + final request = blocks.add( + web.Blob([Uint8List(4096).buffer.toJS].toJS), + ['test'.toJS].toJS, + ); + final key = await request.complete(); + + blob = await blocks.get(key).complete(); + } on Object { + return false; + } finally { + transaction.abort(); + } + + try { + await blob.byteBuffer(); + return true; + } on Object { + return false; + } } void close() { @@ -163,7 +209,12 @@ class AsynchronousIndexedDbFileSystem { // We can't have an async suspension in here because that would close the // transaction. Launch the reader now and wait for all reads later. readOperations.add(Future.sync(() async { - final data = await (row.value as web.Blob).byteBuffer(); + ByteBuffer data; + if (row.value.instanceOfString('Blob')) { + data = await (row.value as web.Blob).byteBuffer(); + } else { + data = (row.value as JSArrayBuffer).toDart; + } result.setAll(rowOffset, data.asUint8List(0, length)); })); } @@ -191,8 +242,13 @@ class AsynchronousIndexedDbFileSystem { final key = (row.key as JSArray).toDart; final rowOffset = (key[1] as JSNumber).toDartInt; - final blob = row.value as web.Blob; - final dataLength = min(blob.size, file.length - rowOffset); + final value = row.value; + final isBlob = value.instanceOfString('Blob'); + final valueSize = isBlob + ? (value as web.Blob).size + : (value as _JSArrayBuffer).byteLength; + + final dataLength = min(valueSize, file.length - rowOffset); if (rowOffset < offset) { // This block starts before the section that we're interested in, so cut @@ -204,7 +260,9 @@ class AsynchronousIndexedDbFileSystem { // Do the reading async because we loose the transaction on the first // suspension. readOperations.add(Future.sync(() async { - final data = await blob.byteBuffer(); + final data = isBlob + ? await (value as web.Blob).byteBuffer() + : (value as _JSArrayBuffer).toDart; target.setRange( 0, @@ -226,7 +284,9 @@ class AsynchronousIndexedDbFileSystem { bytesRead += lengthToCopy; readOperations.add(Future.sync(() async { - final data = await blob.byteBuffer(); + final data = isBlob + ? await (value as web.Blob).byteBuffer() + : (value as _JSArrayBuffer).toDart; target.setAll(startInTarget, data.asUint8List(0, lengthToCopy)); })); @@ -253,15 +313,17 @@ class AsynchronousIndexedDbFileSystem { final cursor = await blocks .openCursor(web.IDBKeyRange.only([fileId.toJS, blockStart.toJS].toJS)) .complete(); - final blob = web.Blob([block.toJS].toJS); + + final value = + _storeBlobs ? web.Blob([block.toJS].toJS) : block.buffer.toJS; if (cursor == null) { // There isn't, let's write a new block await blocks - .put(blob, [fileId.toJS, blockStart.toJS].toJS) + .put(value, [fileId.toJS, blockStart.toJS].toJS) .complete(); } else { - await cursor.update(blob).complete(); + await cursor.update(value).complete(); } } @@ -828,3 +890,8 @@ final class _WriteFileWorkItem extends _IndexedDbWorkItem { ._write(await fileSystem._fileId(path), request); } } + +@JS('ArrayBuffer') +extension type _JSArrayBuffer(JSArrayBuffer _) implements JSArrayBuffer { + external int get byteLength; +}