Add some protocol messages for wasm init

This commit is contained in:
Simon Binder 2023-05-18 22:22:01 +02:00
parent 0bb7d6ade6
commit 5db8d58b72
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
4 changed files with 284 additions and 4 deletions

View File

@ -1,9 +1,128 @@
/// This file is responsible for opening a suitable WASM sqlite3 database based
/// on the features available in the browsing context we're in.
///
/// The main challenge of hosting a sqlite3 database in the browser is the
/// implementation of a persistence solution. Being a C library, sqlite3 expects
/// synchronous access to a file system, which is tricky to implement with
/// asynchronous
library;
import 'dart:html';
import 'package:drift/wasm.dart';
import 'package:js/js.dart';
import 'package:js/js_util.dart';
// ignore: implementation_imports
import 'package:sqlite3/src/wasm/js_interop/file_system_access.dart';
@JS()
@anonymous
class WorkerInitializationMessage {
external String get type;
external Object get payload;
external factory WorkerInitializationMessage(
{required String type, required Object payload});
}
@JS()
@anonymous
class SharedWorkerSupportedFlags {
static const type = 'shared-supported';
external bool get canSpawnDedicatedWorkers;
external bool get dedicatedCanUseOpfs;
external bool get canUseIndexedDb;
external factory SharedWorkerSupportedFlags({
required bool canSpawnDedicatedWorkers,
required bool dedicatedCanUseOpfs,
required bool canUseIndexedDb,
});
}
@JS()
@anonymous
class DedicatedWorkerPurpose {
static const type = 'dedicated-worker-purpose';
static const purposeSharedOpfs = 'opfs-shared';
external String get purpose;
external factory DedicatedWorkerPurpose({required String purpose});
}
@JS()
@anonymous
class WorkerSetupError {
static const type = 'worker-error';
}
@JS()
external bool get crossOriginIsolated;
bool get supportsSharedWorkers => hasProperty(globalThis, 'SharedWorker');
Future<WasmDatabaseResult> openWasmDatabase({
required Uri sqlite3WasmUri,
required Uri driftWorkerUri,
required String databaseName,
}) async {
// First, let's see if we can spawn dedicated workers in shared workers, which
// would enable us to efficiently share a OPFS database.
if (supportsSharedWorkers) {
final sharedWorker =
SharedWorker(driftWorkerUri.toString(), 'drift worker');
final port = sharedWorker.port!;
}
throw 'todo';
}
/// Checks whether the OPFS API is likely to be correctly implemented in the
/// current browser.
///
/// Since OPFS uses the synchronous file system access API, this method can only
/// return true when called in a dedicated worker.
Future<bool> checkOpfsSupport() async {
final storage = storageManager;
if (storage == null) return false;
final opfsRoot = await storage.directory;
const testFileName = '_drift_feature_detection';
FileSystemFileHandle? fileHandle;
FileSystemSyncAccessHandle? openedFile;
try {
fileHandle = await opfsRoot.openFile(testFileName, create: true);
openedFile = await fileHandle.createSyncAccessHandle();
// In earlier versions of the OPFS standard, some methods like `getSize()`
// on a sync file handle have actually been asynchronous. We don't support
// Browsers that implement the outdated spec.
final getSizeResult = callMethod<Object?>(openedFile, 'getSize', []);
if (typeofEquals<Object?>(getSizeResult, 'object')) {
// Returned a promise, that's no good.
await promiseToFuture<Object?>(getSizeResult!);
return false;
}
return true;
} on Object {
return false;
} finally {
if (openedFile != null) {
openedFile.close();
}
if (fileHandle != null) {
await opfsRoot.removeEntry(testFileName);
}
}
}
Future<bool> checkIndexedDbSupport() async {
return true;
}

View File

@ -132,10 +132,75 @@ class _WasmDelegate extends Sqlite3Delegate<CommonDatabase> {
}
}
/// The storage implementation used by the `drift` and `sqlite3` packages to
/// emulate a synchronous file system on the web, used by the sqlite3 C library
/// to store databases.
///
/// As persistence APIs exposed by Browsers are usually asynchronous, faking
/// a synchronous file system
enum WasmStorageImplementation {
/// Uses the [Origin private file system APIs][OPFS] provided my modern
/// browsers to persist data.
///
/// In this storage mode, drift will host a single shared worker between tabs.
/// As the file system API is only available in dedicated workers, the shared
/// worker will spawn an internal dedicated worker which is thus shared between
/// tabs as well.
///
/// The OPFS API allows synchronous access to files, but only after they have
/// been opened asynchronously. Since sqlite3 only needs access to two files
/// for a database files, we can just open them once and keep them open while
/// the database is in use.
///
/// This mode is a very reliable and efficient approach to access sqlite3
/// on the web, and the preferred mode used by drift.
///
/// While the relevant specifications allow shared workers to spawn nested
/// workers, this is only implemented in Firefox at the time of writing.
/// Chrome (https://crbug.com/1088481) and Safari don't support this yet.
///
/// [OPFS]: https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API#origin_private_file_system
opfsShared,
/// Uses the [Origin private file system APIs][OPFS] provided my modern
/// browsers to persist data.
///
/// Unlike [opfsShared], this storage implementation does not use a shared
/// worker (either because it is not available or because the current browser
/// doesn't allow shared workers to spawn dedicated workers). Instead, each
/// tab spawns two dedicated workers that use `Atomics.wait` and `Atomics.notify`
/// to turn the asynchronous OPFS API into a synchronous file system
/// implementation.
///
/// While being less efficient than [opfsShared], this mode is also very
/// reliably and used by the official WASM builds of the sqlite3 project as
/// well.
///
/// It requires [cross-origin isolation], which needs to be enabled by serving
/// your app with special headers:
///
/// ```
/// Cross-Origin-Opener-Policy: same-origin
/// Cross-Origin-Embedder-Policy: require-corp
/// ```
///
/// [OPFS]: https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API#origin_private_file_system´
/// [cross-origin isolation]: https://developer.mozilla.org/en-US/docs/Web/API/crossOriginIsolated
opfsLocks,
sharedIndexedDb,
/// Uses the asynchronous IndexedDB API outside of any worker to persist data.
///
/// Unlike [opfsShared] or [opfsLocks], this storage implementation can't
/// prevent two tabs from accessing the same data.
unsafeIndexedDb,
/// A fallback storage implementation that doesn't store anything.
///
/// This implementation is chosen when none of the features needed for other
/// storage implementations are supported by the current browser. In this case,
/// [WasmDatabaseResult.missingFeatures] enumerates missing browser features.
inMemory,
}
@ -145,8 +210,8 @@ enum MissingBrowserFeature {
nestedDedicatedWorkers,
fileSystemAccess,
indexedDb,
atomics,
sharedArrayBuffers,
notCrossOriginIsolated,
}
class WasmDatabaseResult {

View File

@ -1,3 +1,99 @@
import 'package:drift/src/web/wasm_setup.dart';
import 'dart:async';
import 'dart:html';
Future<void> main() async {}
import 'package:drift/wasm.dart';
import 'package:drift/src/web/wasm_setup.dart';
import 'package:js/js_util.dart';
Future<void> main() async {
final self = WorkerGlobalScope.instance;
if (self is SharedWorkerGlobalScope) {
// This is a shared worker. It responds
_SharedDriftServer(self).start();
} else {}
}
class _SharedDriftServer {
final SharedWorkerGlobalScope self;
/// If we end up using [WasmStorageImplementation.opfsShared], this is the
/// "shared-dedicated" worker hosting the database.
Worker? _dedicatedWorker;
Future<SharedWorkerSupportedFlags>? _featureDetection;
_SharedDriftServer(this.self);
void start() {
const event = EventStreamProvider<MessageEvent>('connect');
event.forTarget(self).listen(_newConnection);
}
void _newConnection(MessageEvent event) async {
// Start a feature detection run and inform the client about what we can do.
final detectionFuture = (_featureDetection ??= _startFeatureDetection());
final responsePort = event.ports[0];
try {
final result = await detectionFuture;
responsePort.postMessage(WorkerInitializationMessage(
type: SharedWorkerSupportedFlags.type, payload: result));
} catch (e) {
responsePort.postMessage(WorkerInitializationMessage(
type: WorkerSetupError.type, payload: WorkerSetupError()));
}
}
Future<SharedWorkerSupportedFlags> _startFeatureDetection() async {
// First, let's see if this shared worker can spawn dedicated workers.
final hasWorker = hasProperty(self, 'Worker');
final canUseIndexedDb = await checkIndexedDbSupport();
if (!hasWorker) {
return SharedWorkerSupportedFlags(
canSpawnDedicatedWorkers: false,
dedicatedCanUseOpfs: false,
canUseIndexedDb: canUseIndexedDb,
);
} else {
final worker = _dedicatedWorker = Worker(Uri.base.toString());
// Tell the worker that we want to use it to host a shared OPFS database.
// It will respond with whether it can do that.
worker.postMessage(WorkerInitializationMessage(
type: DedicatedWorkerPurpose.type,
payload: DedicatedWorkerPurpose(
purpose: DedicatedWorkerPurpose.purposeSharedOpfs,
),
));
final completer = Completer<SharedWorkerSupportedFlags>();
StreamSubscription? messageSubscription, errorSubscription;
void result(bool result) {
if (!completer.isCompleted) {
completer.complete(SharedWorkerSupportedFlags(
canSpawnDedicatedWorkers: true,
dedicatedCanUseOpfs: result,
canUseIndexedDb: canUseIndexedDb,
));
messageSubscription?.cancel();
errorSubscription?.cancel();
}
}
messageSubscription = worker.onMessage.listen((event) {
result(event.data as bool);
});
errorSubscription = worker.onError.listen((event) {
result(false);
worker.terminate();
_dedicatedWorker = null;
});
return completer.future;
}
}
}

View File

@ -31,7 +31,7 @@ dependencies:
# Drift-specific analysis and apis
drift: '>=2.8.0 <2.9.0'
sqlite3: '>=0.1.6 <2.0.0'
sqlite3: '>=0.1.6 <3.0.0'
sqlparser: '^0.30.0'
# Dart analysis