diff --git a/examples/multiplatform/db/sqlite3/web.dart b/examples/multiplatform/db/sqlite3/web.dart index 8f269223..e0adbdb2 100644 --- a/examples/multiplatform/db/sqlite3/web.dart +++ b/examples/multiplatform/db/sqlite3/web.dart @@ -5,7 +5,7 @@ Future openSqliteDb() async { const name = 'my_app'; // Please download `sqlite3.wasm` from https://github.com/simolus3/sqlite3.dart/releases // into the `web/` dir of your Flutter app. See `README.md` for details. - final sqlite = await WasmSqlite3.loadFromUrl(Uri.parse('sqlite3.wasm')); + final sqlite = await WasmSqlite3.loadFromUrlString('sqlite3.wasm'); final fileSystem = await IndexedDbFileSystem.open(dbName: name); sqlite.registerVirtualFileSystem(fileSystem, makeDefault: true); return sqlite.open(name); diff --git a/sqlite3/CHANGELOG.md b/sqlite3/CHANGELOG.md index a0c25052..92417ee9 100644 --- a/sqlite3/CHANGELOG.md +++ b/sqlite3/CHANGELOG.md @@ -2,6 +2,8 @@ - Support `native_toolchain_c` versions `0.18.x`. - Upgrade to SQLite version 3.53.1. +- Allow disabling `BigInt` support via `-Dsqlite3.dartbigints=false`, which can be used to + reduce code size on the web if nothing else uses `BigInt` values. ## 3.3.1 diff --git a/sqlite3/README.md b/sqlite3/README.md index e9b56a2e..30da44ae 100644 --- a/sqlite3/README.md +++ b/sqlite3/README.md @@ -51,7 +51,9 @@ databases. The [hook options page](./doc/hook.md) describe this setup. When binding parameters to queries, the supported types are `ìnt`, `double`, `String`, `List` (for `BLOB`) and `null`. Result sets will use the same set of types. -On the web (but only on the web), `BigInt` is supported as well. + +On the web (when compiled with `dart2js`), `BigInt` is supported as well to represent 64bit integers. +Support for this can be disabled with `-Dsqlite3.dartbigints=false`. ## WASM (web support) @@ -85,7 +87,7 @@ import 'package:sqlite3/common.dart'; import 'package:sqlite3/wasm.dart'; Future loadSqlite() async { - final sqlite = await WasmSqlite3.loadFromUrl(Uri.parse('sqlite3.wasm')); + final sqlite = await WasmSqlite3.loadFromUrlString('sqlite3.wasm'); final fileSystem = await IndexedDbFileSystem.open(dbName: 'my_app'); sqlite.registerVirtualFileSystem(fileSystem, makeDefault: true); return sqlite; @@ -123,7 +125,7 @@ To test the encryption integration, download `sqlite3mc.wasm` from the [releases of this package and use that as a URL to load sqlite3 on the web: ```dart -final sqlite3 = await WasmSqlite3.loadFromUrl(Uri.parse('sqlite3mc.wasm')); +final sqlite3 = await WasmSqlite3.loadFromUrlString('sqlite3mc.wasm'); sqlite3.registerVirtualFileSystem(InMemoryFileSystem(), makeDefault: true); final database = sqlite3.open('/database') diff --git a/sqlite3/doc/common.md b/sqlite3/doc/common.md index 66dc7f8e..79987285 100644 --- a/sqlite3/doc/common.md +++ b/sqlite3/doc/common.md @@ -38,7 +38,7 @@ Future openDatabase() async { import 'package:sqlite3/wasm.dart'; Future openDatabase() async { - final sqlite = await WasmSqlite3.loadFromUrl(Uri.parse('sqlite3.wasm')); + final sqlite = await WasmSqlite3.loadFromUrlString('sqlite3.wasm'); final fs = await IndexedDbFileSystem.open(dbName: 'app.db'); sqlite.registerVirtualFileSystem(fs, makeDefault: true); return sqlite.open('/app.db'); diff --git a/sqlite3/example/web/main.dart b/sqlite3/example/web/main.dart index d165ab5b..6567b8b2 100644 --- a/sqlite3/example/web/main.dart +++ b/sqlite3/example/web/main.dart @@ -11,9 +11,7 @@ Future main() async { startIndexedDb.onClick.listen((_) async { startIndexedDb.remove(); - final sqlite3 = await WasmSqlite3.loadFromUrl( - Uri.parse('sqlite3.debug.wasm'), - ); + final sqlite3 = await WasmSqlite3.loadFromUrlString('sqlite3.debug.wasm'); print(sqlite3.version); diff --git a/sqlite3/lib/src/compile_options.dart b/sqlite3/lib/src/compile_options.dart new file mode 100644 index 00000000..b795bfba --- /dev/null +++ b/sqlite3/lib/src/compile_options.dart @@ -0,0 +1,15 @@ +/// Whether to support binding [BigInt] values. +/// +/// This is enabled by default, and disabling it is experimental. This is mainly +/// relevant for web workers, which don't really benefit from using [BigInt] +/// values internally: +/// +/// - Over the wire, we receive values as `JSBigInt`. +/// - To bind values to SQLite, we need to use `JSBigInt` as well. +/// +/// By removing support for [BigInt]s and their conversion to `JSBigInt`, we +/// save around 20kb from the default worker with `dart compile js -O4`. +const supportDartBigInts = bool.fromEnvironment( + 'sqlite3.dartbigints', + defaultValue: true, +); diff --git a/sqlite3/lib/src/constants.dart b/sqlite3/lib/src/constants.dart index 77b8069e..2addb14e 100644 --- a/sqlite3/lib/src/constants.dart +++ b/sqlite3/lib/src/constants.dart @@ -544,9 +544,6 @@ const SQLITE_DELETE = 9; const SQLITE_INSERT = 18; const SQLITE_UPDATE = 23; -final bigIntMinValue64 = BigInt.parse('-9223372036854775808'); -final bigIntMaxValue64 = BigInt.parse('9223372036854775807'); - // Connection config options https://www.sqlite.org/c3ref/c_dbconfig_defensive.html const SQLITE_DBCONFIG_DQS_DML = 1013; const SQLITE_DBCONFIG_DQS_DDL = 1014; diff --git a/sqlite3/lib/src/implementation/statement.dart b/sqlite3/lib/src/implementation/statement.dart index 9c65e51f..0cceabaf 100644 --- a/sqlite3/lib/src/implementation/statement.dart +++ b/sqlite3/lib/src/implementation/statement.dart @@ -1,3 +1,4 @@ +import '../compile_options.dart'; import '../constants.dart'; import '../result_set.dart'; import '../statement.dart'; @@ -210,7 +211,10 @@ base class StatementImplementation extends CommonPreparedStatement { final rc = switch (param) { null => statement.sqlite3_bind_null(i), int() => statement.sqlite3_bind_int64(i, param), - BigInt() => statement.sqlite3_bind_int64BigInt(i, param.checkRange), + BigInt() when supportDartBigInts => statement.sqlite3_bind_int64BigInt( + i, + param.checkRange, + ), bool() => statement.sqlite3_bind_int64(i, param ? 1 : 0), double() => statement.sqlite3_bind_double(i, param), String() => statement.sqlite3_bind_text(i, param), diff --git a/sqlite3/lib/src/implementation/utils.dart b/sqlite3/lib/src/implementation/utils.dart index d62229f8..6b6cfe3f 100644 --- a/sqlite3/lib/src/implementation/utils.dart +++ b/sqlite3/lib/src/implementation/utils.dart @@ -8,6 +8,9 @@ extension BigIntRangeCheck on BigInt { } return this; } + + static final bigIntMinValue64 = -(BigInt.one << 63); + static final bigIntMaxValue64 = (BigInt.one << 63) - BigInt.one; } int eTextRep(bool deterministic, bool directOnly, bool subtype) { diff --git a/sqlite3/lib/src/in_memory_vfs.dart b/sqlite3/lib/src/in_memory_vfs.dart index dcf9de36..b0bf3e28 100644 --- a/sqlite3/lib/src/in_memory_vfs.dart +++ b/sqlite3/lib/src/in_memory_vfs.dart @@ -1,10 +1,10 @@ import 'dart:math'; import 'dart:typed_data'; -import 'package:path/path.dart' as p; import 'package:typed_data/typed_buffers.dart'; import 'constants.dart'; +import 'platform/platform.dart'; import 'vfs.dart'; import 'utils.dart'; @@ -32,7 +32,7 @@ final class InMemoryFileSystem extends BaseVirtualFileSystem { @override String xFullPathName(String path) { - return p.url.normalize('/$path'); + return pathToAbsoluteAndNormalize(path); } @override diff --git a/sqlite3/lib/src/platform/fallback.dart b/sqlite3/lib/src/platform/fallback.dart new file mode 100644 index 00000000..20fcb5e2 --- /dev/null +++ b/sqlite3/lib/src/platform/fallback.dart @@ -0,0 +1,5 @@ +import 'package:path/path.dart' as p; + +String pathToAbsoluteAndNormalize(String source) { + return p.url.normalize('/$source'); +} diff --git a/sqlite3/lib/src/platform/platform.dart b/sqlite3/lib/src/platform/platform.dart new file mode 100644 index 00000000..e28950d2 --- /dev/null +++ b/sqlite3/lib/src/platform/platform.dart @@ -0,0 +1 @@ +export 'fallback.dart' if (dart.library.js_interop) 'web.dart'; diff --git a/sqlite3/lib/src/platform/web.dart b/sqlite3/lib/src/platform/web.dart new file mode 100644 index 00000000..6c9db275 --- /dev/null +++ b/sqlite3/lib/src/platform/web.dart @@ -0,0 +1,36 @@ +/// Helpers implemented with direct interop to web APIs instead of +/// cross-platform Dart packages or the SDK. +/// +/// In compiled web workers, Dart's [Uri.parse] implementation takes up a good +/// chunk of the total file size. By using [URL] directly (which is good enough +/// for our use case), we can avoid that implementation. +library; + +import 'dart:js_interop'; + +import 'package:web/web.dart'; + +import '../implementation/statement.dart'; +import '../statement.dart'; +import '../wasm/bindings.dart'; + +String pathToAbsoluteAndNormalize(String source) { + return URL(source, 'file:///').pathname; +} + +Iterable pathComponents(String path) { + return URL(path, 'file:///').pathname.split('/').where((e) => e.isNotEmpty); +} + +final class BoxedJavaScriptBigInt implements CustomStatementParameter { + final JSBigInt value; + + BoxedJavaScriptBigInt(this.value); + + @override + void applyTo(CommonPreparedStatement statement, int index) { + final raw = + (statement as StatementImplementation).statement as WasmStatement; + raw.bindings.sqlite3_bind_int64(raw.stmt, index, value); + } +} diff --git a/sqlite3/lib/src/wasm/bindings.dart b/sqlite3/lib/src/wasm/bindings.dart index 23609cf6..af62ef2d 100644 --- a/sqlite3/lib/src/wasm/bindings.dart +++ b/sqlite3/lib/src/wasm/bindings.dart @@ -10,6 +10,7 @@ import '../functions.dart'; import '../implementation/bindings.dart'; import '../implementation/exception.dart'; import 'injected_values.dart'; +import 'js_interop/core.dart'; import 'wasm_interop.dart' as wasm; import 'sqlite3_wasm.g.dart'; import 'wasm_interop.dart'; @@ -473,7 +474,11 @@ final class WasmStatement implements RawSqliteStatement { @override int sqlite3_bind_int64BigInt(int index, BigInt value) { - return bindings.sqlite3_bind_int64(stmt, index, value); + return bindings.sqlite3_bind_int64( + stmt, + index, + JsBigInt.fromBigInt(value).jsObject, + ); } @override diff --git a/sqlite3/lib/src/wasm/js_interop/core.dart b/sqlite3/lib/src/wasm/js_interop/core.dart index 9285547d..861faf3d 100644 --- a/sqlite3/lib/src/wasm/js_interop/core.dart +++ b/sqlite3/lib/src/wasm/js_interop/core.dart @@ -2,6 +2,9 @@ import 'dart:async'; import 'dart:js_interop'; import 'dart:js_interop_unsafe'; +import '../../compile_options.dart'; +import '../../platform/web.dart'; + @JS('BigInt') external JSBigInt _bigInt(JSAny? s); @@ -27,7 +30,13 @@ extension type JsBigInt(JSBigInt _jsBigInt) implements JSBigInt { int get asDartInt => _number(_jsBigInt).toDartInt; - BigInt get asDartBigInt => BigInt.parse(jsToString()); + Object get asDartBigInt { + if (!supportDartBigInts) { + return BoxedJavaScriptBigInt(this); + } + + return BigInt.parse(jsToString()); + } JSBigInt get jsObject => _jsBigInt; diff --git a/sqlite3/lib/src/wasm/sqlite3.dart b/sqlite3/lib/src/wasm/sqlite3.dart index e3de81a1..1ce132a7 100644 --- a/sqlite3/lib/src/wasm/sqlite3.dart +++ b/sqlite3/lib/src/wasm/sqlite3.dart @@ -51,6 +51,23 @@ final class WasmSqlite3 extends Sqlite3Implementation { Uri uri, { Map? headers, WasmModuleLoader? loader, + }) { + return loadFromUrlString(uri.toString()); + } + + /// Loads a web version of the sqlite3 libraries. + /// + /// The native wasm library for sqlite3 is loaded from the [url] with the + /// desired [headers] through a `fetch` request. + /// + /// Using this over [loadFromUrl] might reduce compiled JS sizes for apps + /// which otherwise don't use URLs. + /// + /// [pgk release]: https://github.com/simolus3/sqlite3.dart/releases + static Future loadFromUrlString( + String url, { + Map? headers, + WasmModuleLoader? loader, }) async { web.RequestInit? options; @@ -63,9 +80,7 @@ final class WasmSqlite3 extends Sqlite3Implementation { options = web.RequestInit(headers: headersJs); } - final jsUri = uri.isAbsolute - ? web.URL(uri.toString()) - : web.URL(uri.toString(), Uri.base.toString()); + final jsUri = web.URL(url, (globalContext['location'] as web.URL).href); final response = await fetch(jsUri, options).toDart; return _load(response, loader); } diff --git a/sqlite3/lib/src/wasm/vfs/async_opfs/client.dart b/sqlite3/lib/src/wasm/vfs/async_opfs/client.dart index 537da34e..d995a4aa 100644 --- a/sqlite3/lib/src/wasm/vfs/async_opfs/client.dart +++ b/sqlite3/lib/src/wasm/vfs/async_opfs/client.dart @@ -3,7 +3,7 @@ import 'dart:js_interop_unsafe'; import 'dart:math'; import 'dart:typed_data'; -import 'package:path/path.dart' as p; +import 'package:web/web.dart' show URL; import '../../../constants.dart'; import '../../../vfs.dart'; @@ -42,7 +42,6 @@ final class WasmVfs extends BaseVirtualFileSystem { final MessageSerializer serializer; final String chroot; - final p.Context pathContext; WasmVfs({ super.random, @@ -53,7 +52,6 @@ final class WasmVfs extends BaseVirtualFileSystem { workerOptions.synchronizationBuffer, ), serializer = MessageSerializer(workerOptions.communicationBuffer), - pathContext = p.Context(style: p.Style.url, current: chroot), super(name: vfsName); Res _runInWorker( @@ -89,12 +87,13 @@ final class WasmVfs extends BaseVirtualFileSystem { @override String xFullPathName(String path) { - final resolved = pathContext.absolute(path); - if (!p.isWithin(chroot, resolved)) { + final normalized = URL(path, 'file://$chroot').pathname; + + if (!normalized.startsWith(chroot)) { throw const VfsException(SqlError.SQLITE_CANTOPEN); } - return resolved; + return normalized; } @override diff --git a/sqlite3/lib/src/wasm/vfs/async_opfs/sync_channel.dart b/sqlite3/lib/src/wasm/vfs/async_opfs/sync_channel.dart index 5c02d76a..742127cb 100644 --- a/sqlite3/lib/src/wasm/vfs/async_opfs/sync_channel.dart +++ b/sqlite3/lib/src/wasm/vfs/async_opfs/sync_channel.dart @@ -121,7 +121,12 @@ class MessageSerializer { String _readString(int offset) { final length = dataView.getInt32(offset); - return utf8.decode(buffer.asUint8ListSlice(offset + 4, length)); + // Browsers don't allow TextDecoder.decode on shared array buffers + // (https://github.com/whatwg/encoding/issues/172). So, we copy. + final originalSlice = buffer.asUint8ListSlice(offset + 4, length); + final copy = Uint8List.fromList(originalSlice); + + return utf8.decode(copy); } void _writeString(int offset, String data) { diff --git a/sqlite3/lib/src/wasm/vfs/async_opfs/worker.dart b/sqlite3/lib/src/wasm/vfs/async_opfs/worker.dart index 614d0f9b..f6ced312 100644 --- a/sqlite3/lib/src/wasm/vfs/async_opfs/worker.dart +++ b/sqlite3/lib/src/wasm/vfs/async_opfs/worker.dart @@ -4,7 +4,6 @@ library; import 'dart:js_interop'; -import 'package:path/path.dart' as p show url; import 'package:web/web.dart' show FileSystemDirectoryHandle, @@ -13,6 +12,7 @@ import 'package:web/web.dart' FileSystemReadWriteOptions; import '../../../constants.dart'; +import '../../../platform/web.dart'; import '../../../vfs.dart'; import '../../js_interop.dart'; import 'sync_channel.dart'; @@ -61,12 +61,12 @@ extension type WorkerOptions._raw(JSObject _) implements JSObject { } class _ResolvedPath { - final String fullPath; + final String debugPath; final FileSystemDirectoryHandle directory; final String filename; - _ResolvedPath(this.fullPath, this.directory, this.filename); + _ResolvedPath(this.debugPath, this.directory, this.filename); Future openFile({bool create = false}) { return directory.openFile(filename, create: create); @@ -101,7 +101,7 @@ class VfsWorker { static Future create(WorkerOptions options) async { var root = await storageManager!.directory; - final split = p.url.split(options.root); + final split = pathComponents(options.root); for (final directory in split) { root = await root.getDirectory(directory, create: true); @@ -114,8 +114,7 @@ class VfsWorker { String absolutePath, { bool createDirectories = false, }) async { - final fullPath = p.url.relative(absolutePath, from: '/'); - final [...directories, file] = p.url.split(fullPath); + final [...directories, file] = [...pathComponents(absolutePath)]; var dirHandle = root; for (final entry in directories) { @@ -125,7 +124,7 @@ class VfsWorker { ); } - return _ResolvedPath(fullPath, dirHandle, file); + return _ResolvedPath(absolutePath, dirHandle, file); } Future _xAccess(NameAndInt32Flags flags) async { @@ -169,7 +168,7 @@ class VfsWorker { final opened = _OpenedFileHandle( fd: _fdCounter++, directory: resolved.directory, - fullPath: resolved.fullPath, + debugPath: resolved.debugPath, filename: resolved.filename, file: fileHandle, deleteOnClose: (flags & SqlFlag.SQLITE_OPEN_DELETEONCLOSE) != 0, @@ -422,7 +421,7 @@ class VfsWorker { // across requests. if (!file.explicitlyLocked) { _implicitlyHeldLocks.add(file); - _log('Acquired implicit lock for ${file.fullPath}'); + _log('Acquired implicit lock for ${file.debugPath}'); } return handle; } catch (e) { @@ -447,7 +446,7 @@ class VfsWorker { void _closeSyncHandle(_OpenedFileHandle handle) { final syncHandle = handle.syncHandle; if (syncHandle != null) { - _log('Closing sync handle for ${handle.fullPath}'); + _log('Closing sync handle for ${handle.debugPath}'); handle.syncHandle = null; _implicitlyHeldLocks.remove(handle); handle.explicitlyLocked = false; @@ -461,7 +460,7 @@ class _OpenedFileHandle { final bool readonly; final bool deleteOnClose; - final String fullPath; + final String debugPath; final FileSystemDirectoryHandle directory; final String filename; final FileSystemFileHandle file; @@ -473,7 +472,7 @@ class _OpenedFileHandle { required this.fd, required this.readonly, required this.deleteOnClose, - required this.fullPath, + required this.debugPath, required this.directory, required this.filename, required this.file, diff --git a/sqlite3/lib/src/wasm/vfs/simple_opfs.dart b/sqlite3/lib/src/wasm/vfs/simple_opfs.dart index 7c027a02..6672c471 100644 --- a/sqlite3/lib/src/wasm/vfs/simple_opfs.dart +++ b/sqlite3/lib/src/wasm/vfs/simple_opfs.dart @@ -2,7 +2,6 @@ import 'dart:js_interop'; import 'dart:typed_data'; import 'package:meta/meta.dart'; -import 'package:path/path.dart' as p; import 'package:web/web.dart' show FileSystemDirectoryHandle, @@ -15,6 +14,7 @@ import '../../constants.dart'; import '../../vfs.dart'; import '../js_interop.dart'; import '../../in_memory_vfs.dart'; +import '../../platform/web.dart'; @internal enum FileType { @@ -82,7 +82,7 @@ final class SimpleOpfsFileSystem extends BaseVirtualFileSystem { FileSystemDirectoryHandle? parent; var opfsDirectory = await storage.directory; - for (final segment in p.split(path)) { + for (final segment in pathComponents(path)) { parent = opfsDirectory; opfsDirectory = await opfsDirectory.getDirectory(segment, create: create); } @@ -127,13 +127,19 @@ final class SimpleOpfsFileSystem extends BaseVirtualFileSystem { try { (parent, handle) = await _resolveDir(path, create: false); - } on DOMException catch (e) { - if (e.name == 'NotFoundError' || e.name == 'TypeMismatchError') { - // Directory doesn't exist, ignore. - return; - } else { - rethrow; + // ignore: invalid_runtime_check_with_js_interop_types + } on JSAny catch (e) { + // TODO: Remove type clause (needs Dart 3.12 as a minimum version) + if (e.isA()) { + final asDomException = e as DOMException; + if (asDomException.name == 'NotFoundError' || + asDomException.name == 'TypeMismatchError') { + // Directory doesn't exist, ignore. + return; + } } + + rethrow; } if (parent != null) { @@ -210,7 +216,7 @@ final class SimpleOpfsFileSystem extends BaseVirtualFileSystem { @override String xFullPathName(String path) { - return p.url.normalize('/$path'); + return pathToAbsoluteAndNormalize(path); } @override diff --git a/sqlite3/lib/src/wasm/wasm_interop.dart b/sqlite3/lib/src/wasm/wasm_interop.dart index b1cdd4d7..2b3f09f9 100644 --- a/sqlite3/lib/src/wasm/wasm_interop.dart +++ b/sqlite3/lib/src/wasm/wasm_interop.dart @@ -164,12 +164,8 @@ class WasmBindings { return sqlite3.sqlite3_bind_null(stmt, index); } - int sqlite3_bind_int64(Pointer stmt, int index, BigInt value) { - return sqlite3.sqlite3_bind_int64( - stmt, - index, - JsBigInt.fromBigInt(value).jsObject, - ); + int sqlite3_bind_int64(Pointer stmt, int index, JSBigInt value) { + return sqlite3.sqlite3_bind_int64(stmt, index, value); } int sqlite3_bind_int(Pointer stmt, int index, int value) { diff --git a/sqlite3/pubspec.yaml b/sqlite3/pubspec.yaml index dab25c83..f20cba06 100644 --- a/sqlite3/pubspec.yaml +++ b/sqlite3/pubspec.yaml @@ -1,6 +1,6 @@ name: sqlite3 description: Provides lightweight yet convenient bindings to SQLite by using dart:ffi -version: 3.3.2-wip +version: 3.3.2 homepage: https://github.com/simolus3/sqlite3.dart/tree/main/sqlite3 issue_tracker: https://github.com/simolus3/sqlite3.dart/issues resolution: workspace diff --git a/sqlite3_web/CHANGELOG.md b/sqlite3_web/CHANGELOG.md index e4f588bc..c8b32b53 100644 --- a/sqlite3_web/CHANGELOG.md +++ b/sqlite3_web/CHANGELOG.md @@ -1,6 +1,9 @@ -## 0.7.2 (unreleased) +## 0.8.0 (unreleased) - Remove `stream_channel` dependency with custom implementation, slightly decreasing compiled size. +- Use strings instead of URLs to reduce code size. +- Custom Dart objects can no longer be serialized via `serializeParameters` and `serializeResultSet`. + Only SQLite values are supported. ## 0.7.1 diff --git a/sqlite3_web/README.md b/sqlite3_web/README.md index a1ec4daa..7903e3e3 100644 --- a/sqlite3_web/README.md +++ b/sqlite3_web/README.md @@ -63,8 +63,8 @@ that are transparently hosted in the worker: ```dart Future connectToDatabase() async { final sqlite = await WebSqlite.open( - workers: WorkerConnector.defaultWorkers(Uri.parse('worker.dart.js')), - wasmModule: Uri.parse('sqlite3.wasm'), + workers: WorkerConnector.defaultWorkers('worker.dart.js'), + wasmModule: 'sqlite3.wasm', ); final features = await sqlite.runFeatureDetection(); diff --git a/sqlite3_web/benchmark/benchmark.dart b/sqlite3_web/benchmark/benchmark.dart index 236e4774..32f898c9 100644 --- a/sqlite3_web/benchmark/benchmark.dart +++ b/sqlite3_web/benchmark/benchmark.dart @@ -237,7 +237,7 @@ final class ClientStateNotifier extends Notifier { final sqlite3 = Provider((ref) { return WebSqlite.open( workers: _EncapsulatedWorkerConnector(), - wasmModule: Uri.parse('sqlite3.wasm'), + wasmModule: 'sqlite3.wasm', ); }); @@ -252,7 +252,7 @@ final selectedTarget = StateProvider((ref) { final class _EncapsulatedWorkerConnector implements WorkerConnector { final WorkerConnector _inner = WorkerConnector.defaultWorkers( - Uri.parse('worker.dart.js'), + 'worker.dart.js', ); @override diff --git a/sqlite3_web/benchmark/worker.dart b/sqlite3_web/benchmark/worker.dart index a54402b4..271b516c 100644 --- a/sqlite3_web/benchmark/worker.dart +++ b/sqlite3_web/benchmark/worker.dart @@ -14,15 +14,16 @@ void main() { WorkerEnvironment environment; if (globalContext.instanceOfString('SharedWorkerGlobalScope')) { + final scope = globalContext as SharedWorkerGlobalScope; + // This shared worker is used both to hand out database access and to // coordinate multiple tabs running concurrency benchmarks. We encapsulate // messages from the sqlite3_async package in a WorkerMessage struct, which // allows this worker to speak both protocols. final fakeEnvironment = environment = FakeWorkerEnvironment( - WorkerConnector.defaultWorkers(Uri.base), + WorkerConnector.defaultWorkers(scope.location.href), ); - final scope = globalContext as SharedWorkerGlobalScope; final clients = ClientsDriver(); clients.run(); diff --git a/sqlite3_web/build.yaml b/sqlite3_web/build.yaml index 4b223f6b..3cf86c28 100644 --- a/sqlite3_web/build.yaml +++ b/sqlite3_web/build.yaml @@ -24,7 +24,9 @@ targets: generate_for: - "**/worker.dart" options: - compiler: dart2js + compilers: + dart2js: + args: [-O4, -Dsqlite3.dartbigints=false] build_web_compilers:dart2js_archive_extractor: enabled: false diff --git a/sqlite3_web/example/main.dart b/sqlite3_web/example/main.dart index 83b94d70..918a04f3 100644 --- a/sqlite3_web/example/main.dart +++ b/sqlite3_web/example/main.dart @@ -5,8 +5,8 @@ import 'package:sqlite3_web/sqlite3_web.dart'; void main() async { final sqlite = WebSqlite.open( - workers: WorkerConnector.defaultWorkers(Uri.parse('worker.dart.js')), - wasmModule: Uri.parse('sqlite3.wasm'), + workers: WorkerConnector.defaultWorkers('worker.dart.js'), + wasmModule: 'sqlite3.wasm', ); final features = await sqlite.runFeatureDetection(); diff --git a/sqlite3_web/lib/src/client.dart b/sqlite3_web/lib/src/client.dart index b76b55c4..e680217b 100644 --- a/sqlite3_web/lib/src/client.dart +++ b/sqlite3_web/lib/src/client.dart @@ -369,7 +369,7 @@ final class WorkerConnection extends ProtocolChannel { } Future requestDatabase({ - required Uri wasmUri, + required String wasmUri, required String databaseName, required DatabaseImplementation implementation, required bool onlyOpenVfs, @@ -378,7 +378,7 @@ final class WorkerConnection extends ProtocolChannel { final response = await sendRequest( newOpenRequest( requestId: 0, - wasmUri: wasmUri.toString(), + wasmUri: wasmUri, databaseName: databaseName, storageMode: implementation.resolveToVfs().toJS, onlyOpenVfs: onlyOpenVfs, @@ -401,7 +401,7 @@ final class WorkerConnection extends ProtocolChannel { final class DatabaseClient implements WebSqlite { final WorkerConnector workers; - final Uri wasmUri; + final String wasmUri; final DatabaseController _localController; final Future Function(JSAny?) _handleCustomRequest; diff --git a/sqlite3_web/lib/src/database.dart b/sqlite3_web/lib/src/database.dart index dd16bb0d..7f069996 100644 --- a/sqlite3_web/lib/src/database.dart +++ b/sqlite3_web/lib/src/database.dart @@ -17,10 +17,10 @@ abstract base class DatabaseController { /// Loads a wasm module from the given [uri] with the specified [headers]. Future loadWasmModule( - Uri uri, { + String uri, { Map? headers, }) async { - return WasmSqlite3.loadFromUrl(uri, headers: headers); + return WasmSqlite3.loadFromUrlString(uri, headers: headers); } /// Opens a database in the pre-configured [sqlite3] instance under the @@ -358,7 +358,7 @@ abstract class WebSqlite { /// If it's absent, the default is to throw an exception when called. static WebSqlite open({ required WorkerConnector workers, - required Uri wasmModule, + required String wasmModule, DatabaseController? controller, Future Function(JSAny?)? handleCustomRequest, }) { @@ -391,7 +391,7 @@ abstract class WebSqlite { }) { final client = DatabaseClient( const WorkerConnector.unsupported(), - Uri.base, + (globalContext as Window).location.href, const _DefaultDatabaseController(), handleCustomRequest, ); diff --git a/sqlite3_web/lib/src/protocol/extensions.dart b/sqlite3_web/lib/src/protocol/extensions.dart index 3f0a1a0c..15a3fcaa 100644 --- a/sqlite3_web/lib/src/protocol/extensions.dart +++ b/sqlite3_web/lib/src/protocol/extensions.dart @@ -146,14 +146,14 @@ extension RowsResponseUtils on RowsResponse { required bool autoCommit, required int lastInsertRowId, }) { - final jsRows = []; + final jsRows = JSArray>.withLength(resultSet.length); final columns = resultSet.columnNames.length; final typeVector = Uint8List(resultSet.length * columns); for (var i = 0; i < resultSet.length; i++) { final row = resultSet.rows[i]; assert(row.length == columns); - final jsRow = List.filled(row.length, null); + final jsRow = JSArray.withLength(row.length); for (var j = 0; j < columns; j++) { final (code, value) = TypeCode.encodeValue(row[j]); @@ -162,14 +162,9 @@ extension RowsResponseUtils on RowsResponse { typeVector[i * columns + j] = code.index; } - jsRows.add(jsRow.toJS); + jsRows[i] = jsRow; } - final rows = [ - for (final row in resultSet.rows) - [for (final column in row) column.jsify()].toJS, - ].toJS; - JSArray? tableNames; if (resultSet.tableNames case var dartTableNames?) { tableNames = [ @@ -183,7 +178,7 @@ extension RowsResponseUtils on RowsResponse { ].toJS, tableNames: tableNames, typeVector: typeVector.buffer.toJS, - rows: rows, + rows: jsRows, autoCommit: autoCommit, lastInsertRowId: lastInsertRowId, requestId: requestId, diff --git a/sqlite3_web/lib/src/protocol/helper.g.dart b/sqlite3_web/lib/src/protocol/helper.g.dart index 1662ae9c..189f3796 100644 --- a/sqlite3_web/lib/src/protocol/helper.g.dart +++ b/sqlite3_web/lib/src/protocol/helper.g.dart @@ -687,8 +687,7 @@ SharedCompatibilityCheck newSharedCompatibilityCheck({ @anonymous extension type _DedicatedInSharedCompatibilityCheck._( DedicatedInSharedCompatibilityCheck _ -) - implements DedicatedInSharedCompatibilityCheck { +) implements DedicatedInSharedCompatibilityCheck { external factory _DedicatedInSharedCompatibilityCheck({ @JS('d') required String? databaseName, @JS('i') required int requestId, diff --git a/sqlite3_web/lib/src/protocol/messages.dart b/sqlite3_web/lib/src/protocol/messages.dart index 6527c0f7..d7ea12b6 100644 --- a/sqlite3_web/lib/src/protocol/messages.dart +++ b/sqlite3_web/lib/src/protocol/messages.dart @@ -4,6 +4,10 @@ import 'dart:typed_data'; import 'package:sqlite3/wasm.dart'; // ignore: implementation_imports import 'package:sqlite3/src/wasm/js_interop/core.dart'; +// ignore: implementation_imports +import 'package:sqlite3/src/platform/web.dart'; +// ignore: implementation_imports +import 'package:sqlite3/src/compile_options.dart'; import '../channel.dart'; import 'dsl.dart'; @@ -208,7 +212,7 @@ enum TypeCode { const hasNativeInts = !identical(0, 0.0); return switch (this) { - TypeCode.unknown => column.dartify(), + TypeCode.unknown => throw ArgumentError('Unsupported type code'), TypeCode.integer => (column as JSNumber).toDartInt, TypeCode.bigInt => hasNativeInts @@ -242,9 +246,12 @@ enum TypeCode { case final int integer: value = integer.toJS; code = TypeCode.integer; - case final BigInt bi: + case final BigInt bi when supportDartBigInts: value = JsBigInt.fromBigInt(bi); code = TypeCode.bigInt; + case final BoxedJavaScriptBigInt bi when !supportDartBigInts: + value = bi.value; + code = TypeCode.bigInt; case final double d: value = d.toJS; code = TypeCode.float; @@ -257,9 +264,8 @@ enum TypeCode { case final bool boolean: value = boolean.toJS; code = TypeCode.boolean; - case final other: - value = other.jsify(); - code = TypeCode.unknown; + default: + throw ArgumentError('Unsupported value: $dart'); } return (code, value); diff --git a/sqlite3_web/lib/src/worker.dart b/sqlite3_web/lib/src/worker.dart index 98b638e4..544236c0 100644 --- a/sqlite3_web/lib/src/worker.dart +++ b/sqlite3_web/lib/src/worker.dart @@ -5,13 +5,14 @@ import 'dart:typed_data'; import 'package:sqlite3/wasm.dart'; import 'package:web/web.dart' show + AbortController, AbortSignal, DedicatedWorkerGlobalScope, EventStreamProviders, FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemSyncAccessHandle, - AbortController; + URL; // ignore: implementation_imports import 'package:sqlite3/src/wasm/js_interop/new_file_system_access.dart'; @@ -265,7 +266,7 @@ final class _ClientConnection extends ProtocolChannel AbortSignal abortSignal, ) async { return await _runner.openLock.withCriticalSection(() async { - await _runner.loadWasmModule(Uri.parse(request.wasmUri)); + await _runner.loadWasmModule(request.wasmUri); DatabaseState? database; _ConnectionDatabase? connectionDatabase; @@ -723,7 +724,7 @@ final class WorkerRunner { var _nextDatabaseId = 0; Future? _sqlite3; - Uri? _wasmUri; + String? _wasmUri; final Mutex _compatibilityCheck = Mutex(); @@ -839,18 +840,20 @@ final class WorkerRunner { }); } - Future loadWasmModule(Uri uri) async { + Future loadWasmModule(String url) async { + final resolved = URL(url, (globalContext['location'] as URL).href).href; + if (_sqlite3 != null) { - if (_wasmUri != uri) { + if (_wasmUri != resolved) { throw StateError( 'Workers only support a single sqlite3 wasm module, provided ' - 'different URI (has $_wasmUri, got $uri)', + 'different URI (has $_wasmUri, got $resolved)', ); } await _sqlite3; } else { - final future = _sqlite3 = _controller.loadWasmModule(uri).onError(( + final future = _sqlite3 = _controller.loadWasmModule(resolved).onError(( error, stackTrace, ) { @@ -858,7 +861,7 @@ final class WorkerRunner { throw error!; }); await future; - _wasmUri = uri; + _wasmUri = resolved; } } diff --git a/sqlite3_web/lib/src/worker_connector.dart b/sqlite3_web/lib/src/worker_connector.dart index f2b04c6e..035ed29b 100644 --- a/sqlite3_web/lib/src/worker_connector.dart +++ b/sqlite3_web/lib/src/worker_connector.dart @@ -39,7 +39,9 @@ abstract class _WorkerEnvironment final T scope; @override - final WorkerConnector connector = WorkerConnector.defaultWorkers(Uri.base); + final WorkerConnector connector = WorkerConnector.defaultWorkers( + (globalContext as WorkerGlobalScope).location.href, + ); _WorkerEnvironment() : scope = globalContext as T; } @@ -141,7 +143,7 @@ abstract interface class WorkerConnector { /// /// The [uri] must point to a compiled Dart program using /// [WebSqlite.workerEntrypoint] to receive messages. - factory WorkerConnector.defaultWorkers(Uri uri) { + factory WorkerConnector.defaultWorkers(String uri) { return _DefaultWorkerConnector(uri); } @@ -167,9 +169,9 @@ abstract interface class WorkerHandle { } final class _DefaultWorkerConnector implements WorkerConnector { - final Uri _worker; + final String _workerUrl; - _DefaultWorkerConnector(this._worker); + _DefaultWorkerConnector(this._workerUrl); @override WorkerHandle? spawnDedicatedWorker() { @@ -178,7 +180,7 @@ final class _DefaultWorkerConnector implements WorkerConnector { } return _DedicatedWorker( - Worker(_worker.toString().toJS, WorkerOptions(name: 'sqlite3_worker')), + Worker(_workerUrl.toJS, WorkerOptions(name: 'sqlite3_worker')), ); } @@ -188,7 +190,7 @@ final class _DefaultWorkerConnector implements WorkerConnector { return null; } - final worker = SharedWorker(_worker.toString().toJS); + final worker = SharedWorker(_workerUrl.toJS); worker.port.start(); return _SharedWorker(worker); } diff --git a/sqlite3_web/pubspec.yaml b/sqlite3_web/pubspec.yaml index 9ce43596..716ac495 100644 --- a/sqlite3_web/pubspec.yaml +++ b/sqlite3_web/pubspec.yaml @@ -10,7 +10,7 @@ environment: dependencies: meta: ^1.11.0 - sqlite3: ^3.0.0 + sqlite3: ^3.3.2 web: ^1.0.0 dev_dependencies: diff --git a/sqlite3_web/test/integration_test.dart b/sqlite3_web/test/integration_test.dart index 96a573ab..6bbe2bfb 100644 --- a/sqlite3_web/test/integration_test.dart +++ b/sqlite3_web/test/integration_test.dart @@ -83,6 +83,12 @@ void main() { setUpAll(() async { final process = driverProcess = await browser.spawnDriver(); + + // On macOS, geckodriver seems to time out if there's no consumer on + // these. + process.stderr.listen((_) {}); + process.stdout.listen((_) {}); + process.exitCode.then((code) { if (!isStoppingProcess) { throw 'Webdriver stopped (code $code) before tearing down tests.'; diff --git a/sqlite3_web/test/protocol_test.dart b/sqlite3_web/test/protocol_test.dart index 26a9eabf..834458a2 100644 --- a/sqlite3_web/test/protocol_test.dart +++ b/sqlite3_web/test/protocol_test.dart @@ -30,15 +30,7 @@ void main() { group('TypeCode', () { test('is compatible with dartify()', () { - for (final value in [ - 1, - 3.4, - true, - null, - {'custom': 'object'}, - 'string', - Uint8List(10), - ]) { + for (final value in [1, 3.4, true, null, 'string', Uint8List(10)]) { final (_, jsified) = TypeCode.encodeValue(value); expect(jsified.dartify(), value); } @@ -59,7 +51,6 @@ void main() { isA().having((e) => e.length, 'length', 10), isDart2Wasm ? 100 : BigInt.from(100), null, - {'custom': 'object'}, ]); if (isDart2Wasm) { // Make sure we don't loose type information in the js conversion across @@ -83,7 +74,6 @@ void main() { Uint8List(10), BigInt.from(100), null, - {'custom': 'object'}, ]); await client.sendRequest( newRunQuery( diff --git a/sqlite3_web/test/worker_test.dart b/sqlite3_web/test/worker_test.dart index d215083f..3933f22f 100644 --- a/sqlite3_web/test/worker_test.dart +++ b/sqlite3_web/test/worker_test.dart @@ -11,13 +11,13 @@ import 'package:sqlite3_web/src/client.dart'; import 'package:test/test.dart'; void main() { - late Uri sqlite3WasmUri; + late String sqlite3WasmUri; late FakeWorkerEnvironment fakeWorkers; setUpAll(() async { final channel = spawnHybridUri('/test/asset_server.dart'); final port = (await channel.stream.first as double).toInt(); - sqlite3WasmUri = Uri.parse('http://localhost:$port/web/sqlite3.wasm'); + sqlite3WasmUri = 'http://localhost:$port/web/sqlite3.wasm'; }); setUp(() { diff --git a/sqlite3_web/web/main.dart b/sqlite3_web/web/main.dart index 86e2f6db..c672369a 100644 --- a/sqlite3_web/web/main.dart +++ b/sqlite3_web/web/main.dart @@ -9,8 +9,8 @@ import 'package:web/web.dart'; import 'controller.dart'; -final sqlite3WasmUri = Uri.parse('sqlite3.wasm'); -final workerUri = Uri.parse('worker.dart.js'); +const sqlite3WasmUri = 'sqlite3.wasm'; +const workerUri = 'worker.dart.js'; const databaseName = 'database'; const additionalOptions = 'test-additional-options';