Skip to content

Commit f830d9e

Browse files
committed
IndexedDb: Store blobs as array buffers if necessary
1 parent 0ab242b commit f830d9e

File tree

1 file changed

+75
-8
lines changed

1 file changed

+75
-8
lines changed

sqlite3/lib/src/wasm/vfs/indexed_db.dart

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)