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: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({
|
Future<WasmDatabaseResult> openWasmDatabase({
|
||||||
required Uri sqlite3WasmUri,
|
required Uri sqlite3WasmUri,
|
||||||
required Uri driftWorkerUri,
|
required Uri driftWorkerUri,
|
||||||
required String databaseName,
|
required String databaseName,
|
||||||
}) async {
|
}) 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';
|
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 {
|
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,
|
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,
|
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,
|
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,
|
inMemory,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,8 +210,8 @@ enum MissingBrowserFeature {
|
||||||
nestedDedicatedWorkers,
|
nestedDedicatedWorkers,
|
||||||
fileSystemAccess,
|
fileSystemAccess,
|
||||||
indexedDb,
|
indexedDb,
|
||||||
atomics,
|
|
||||||
sharedArrayBuffers,
|
sharedArrayBuffers,
|
||||||
|
notCrossOriginIsolated,
|
||||||
}
|
}
|
||||||
|
|
||||||
class WasmDatabaseResult {
|
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-specific analysis and apis
|
||||||
drift: '>=2.8.0 <2.9.0'
|
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'
|
sqlparser: '^0.30.0'
|
||||||
|
|
||||||
# Dart analysis
|
# Dart analysis
|
||||||
|
|
Loading…
Reference in New Issue