mirror of https://github.com/AMT-Cheif/drift.git
Add some protocol messages for wasm init
This commit is contained in:
parent
0bb7d6ade6
commit
5db8d58b72
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue