mirror of https://github.com/AMT-Cheif/drift.git
Merge branch 'list-wasm-databases' into develop
This commit is contained in:
commit
08fa63069c
|
@ -13,7 +13,6 @@ import 'dart:async';
|
|||
import 'dart:html';
|
||||
|
||||
import 'package:async/async.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/remote.dart';
|
||||
import 'package:drift/wasm.dart';
|
||||
|
@ -24,8 +23,8 @@ import 'package:sqlite3/wasm.dart';
|
|||
|
||||
import 'broadcast_stream_queries.dart';
|
||||
import 'channel.dart';
|
||||
import 'wasm_setup/protocol.dart';
|
||||
import 'wasm_setup/shared.dart';
|
||||
import 'wasm_setup/protocol.dart';
|
||||
|
||||
/// Whether the `crossOriginIsolated` JavaScript property is true in the current
|
||||
/// context.
|
||||
|
@ -41,28 +40,53 @@ bool get supportsWorkers => hasProperty(globalThis, 'Worker');
|
|||
class WasmDatabaseOpener {
|
||||
final Uri sqlite3WasmUri;
|
||||
final Uri driftWorkerUri;
|
||||
final String databaseName;
|
||||
FutureOr<Uint8List?> Function()? initializeDatabase;
|
||||
|
||||
final String? databaseName;
|
||||
|
||||
final Set<MissingBrowserFeature> missingFeatures = {};
|
||||
final List<WasmStorageImplementation> availableImplementations = [
|
||||
WasmStorageImplementation.inMemory,
|
||||
];
|
||||
final Set<ExistingDatabase> existingDatabases = {};
|
||||
|
||||
bool _existsInIndexedDb = false;
|
||||
bool _existsInOpfs = false;
|
||||
_DriftWorker? _sharedWorker, _dedicatedWorker;
|
||||
|
||||
MessagePort? _sharedWorker;
|
||||
Worker? _dedicatedWorker;
|
||||
WasmDatabaseOpener(
|
||||
this.sqlite3WasmUri,
|
||||
this.driftWorkerUri,
|
||||
this.databaseName,
|
||||
);
|
||||
|
||||
WasmDatabaseOpener({
|
||||
required this.sqlite3WasmUri,
|
||||
required this.driftWorkerUri,
|
||||
required this.databaseName,
|
||||
this.initializeDatabase,
|
||||
});
|
||||
RequestCompatibilityCheck _createCompatibilityCheck() {
|
||||
return RequestCompatibilityCheck(databaseName ?? 'driftCompatibilityCheck');
|
||||
}
|
||||
|
||||
Future<void> probe() async {
|
||||
void _handleCompatibilityResult(CompatibilityResult result) {
|
||||
missingFeatures.addAll(result.missingFeatures);
|
||||
|
||||
final databaseName = this.databaseName;
|
||||
|
||||
// Note that existingDatabases are only sent from workers shipped with drift
|
||||
// 2.11 or later. Later drift versions need to be able to talk to newer
|
||||
// workers though.
|
||||
if (result.existingDatabases.isNotEmpty) {
|
||||
existingDatabases.addAll(result.existingDatabases);
|
||||
}
|
||||
|
||||
if (databaseName != null) {
|
||||
// If this opener has been created for WasmDatabase.open, we have a
|
||||
// database name and can interpret the opfsExists and indexedDbExists
|
||||
// fields we're getting from older workers accordingly.
|
||||
if (result.opfsExists) {
|
||||
existingDatabases.add((WebStorageApi.opfs, databaseName));
|
||||
}
|
||||
if (result.indexedDbExists) {
|
||||
existingDatabases.add((WebStorageApi.indexedDb, databaseName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<WasmProbeResult> probe() async {
|
||||
try {
|
||||
await _probeShared();
|
||||
} on Object {
|
||||
|
@ -72,99 +96,167 @@ class WasmDatabaseOpener {
|
|||
try {
|
||||
await _probeDedicated();
|
||||
} on Object {
|
||||
_dedicatedWorker?.terminate();
|
||||
_dedicatedWorker?.close();
|
||||
_dedicatedWorker = null;
|
||||
}
|
||||
|
||||
if (_dedicatedWorker == null) {
|
||||
// Something is wrong with web workers, let's see if we can get things
|
||||
// running without a worker.
|
||||
if (await checkIndexedDbSupport()) {
|
||||
availableImplementations.add(WasmStorageImplementation.unsafeIndexedDb);
|
||||
_existsInIndexedDb = await checkIndexedDbExists(databaseName);
|
||||
return _ProbeResult(availableImplementations, existingDatabases.toList(),
|
||||
missingFeatures, this);
|
||||
}
|
||||
|
||||
Future<void> _probeDedicated() async {
|
||||
if (supportsWorkers) {
|
||||
final dedicatedWorker = _dedicatedWorker =
|
||||
_DriftWorker.dedicated(Worker(driftWorkerUri.toString()));
|
||||
_createCompatibilityCheck().sendTo(dedicatedWorker.send);
|
||||
|
||||
final status = await dedicatedWorker.workerMessages.nextNoError
|
||||
as DedicatedWorkerCompatibilityResult;
|
||||
|
||||
_handleCompatibilityResult(status);
|
||||
|
||||
if (status.supportsNestedWorkers &&
|
||||
status.canAccessOpfs &&
|
||||
status.supportsSharedArrayBuffers) {
|
||||
availableImplementations.add(WasmStorageImplementation.opfsLocks);
|
||||
}
|
||||
|
||||
if (status.supportsIndexedDb) {
|
||||
availableImplementations.add(WasmStorageImplementation.unsafeIndexedDb);
|
||||
}
|
||||
} else {
|
||||
missingFeatures.add(MissingBrowserFeature.dedicatedWorkers);
|
||||
}
|
||||
}
|
||||
|
||||
Future<WasmDatabaseResult> open() async {
|
||||
await probe();
|
||||
Future<void> _probeShared() async {
|
||||
if (supportsSharedWorkers) {
|
||||
final sharedWorker =
|
||||
SharedWorker(driftWorkerUri.toString(), 'drift worker');
|
||||
final port = sharedWorker.port!;
|
||||
final shared = _sharedWorker = _DriftWorker.shared(sharedWorker, port);
|
||||
|
||||
// If we have an existing database in storage, we want to keep using that
|
||||
// format to avoid data loss (e.g. after a browser update that enables a
|
||||
// otherwise preferred storage implementation). In the future, we might want
|
||||
// to consider migrating between storage implementations as well.
|
||||
if (_existsInIndexedDb &&
|
||||
(availableImplementations
|
||||
.contains(WasmStorageImplementation.sharedIndexedDb) ||
|
||||
availableImplementations
|
||||
.contains(WasmStorageImplementation.unsafeIndexedDb))) {
|
||||
availableImplementations.removeWhere((element) =>
|
||||
element != WasmStorageImplementation.sharedIndexedDb &&
|
||||
element != WasmStorageImplementation.unsafeIndexedDb);
|
||||
} else if (_existsInOpfs &&
|
||||
(availableImplementations
|
||||
.contains(WasmStorageImplementation.opfsShared) ||
|
||||
availableImplementations
|
||||
.contains(WasmStorageImplementation.opfsLocks))) {
|
||||
availableImplementations.removeWhere((element) =>
|
||||
element != WasmStorageImplementation.opfsShared &&
|
||||
element != WasmStorageImplementation.opfsLocks);
|
||||
// First, the shared worker will tell us which features it supports.
|
||||
_createCompatibilityCheck().sendToPort(port);
|
||||
final sharedFeatures = await shared.workerMessages.nextNoError
|
||||
as SharedWorkerCompatibilityResult;
|
||||
|
||||
_handleCompatibilityResult(sharedFeatures);
|
||||
|
||||
// Prefer to use the shared worker to host the database if it supports the
|
||||
// necessary APIs.
|
||||
if (sharedFeatures.canSpawnDedicatedWorkers &&
|
||||
sharedFeatures.dedicatedWorkersCanUseOpfs) {
|
||||
availableImplementations.add(WasmStorageImplementation.opfsShared);
|
||||
}
|
||||
if (sharedFeatures.canUseIndexedDb) {
|
||||
availableImplementations.add(WasmStorageImplementation.sharedIndexedDb);
|
||||
}
|
||||
} else {
|
||||
missingFeatures.add(MissingBrowserFeature.sharedWorkers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enum values are ordered by preferrability, so just pick the best option
|
||||
// left.
|
||||
availableImplementations.sortBy<num>((element) => element.index);
|
||||
return await _connect(availableImplementations.firstOrNull ??
|
||||
WasmStorageImplementation.inMemory);
|
||||
final class _DriftWorker {
|
||||
final AbstractWorker worker;
|
||||
|
||||
/// The message port to communicate with the worker, if it's a shared worker.
|
||||
final MessagePort? portForShared;
|
||||
|
||||
final StreamQueue<WasmInitializationMessage> workerMessages;
|
||||
|
||||
_DriftWorker.dedicated(Worker this.worker)
|
||||
: portForShared = null,
|
||||
workerMessages =
|
||||
StreamQueue(_readMessages(worker.onMessage, worker.onError));
|
||||
|
||||
_DriftWorker.shared(SharedWorker this.worker, this.portForShared)
|
||||
: workerMessages =
|
||||
StreamQueue(_readMessages(worker.port!.onMessage, worker.onError));
|
||||
|
||||
void send(Object? msg, [List<Object>? transfer]) {
|
||||
switch (worker) {
|
||||
case final Worker worker:
|
||||
worker.postMessage(msg, transfer);
|
||||
case SharedWorker():
|
||||
portForShared!.postMessage(msg, transfer);
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens a database with the given [storage] implementation, bypassing the
|
||||
/// feature detection. Must be called after [probe].
|
||||
Future<WasmDatabaseResult> openWith(WasmStorageImplementation storage) async {
|
||||
return await _connect(storage);
|
||||
}
|
||||
void close() {
|
||||
workerMessages.cancel();
|
||||
|
||||
Future<WasmDatabaseResult> _connect(WasmStorageImplementation storage) async {
|
||||
switch (worker) {
|
||||
case final Worker dedicated:
|
||||
dedicated.terminate();
|
||||
case SharedWorker():
|
||||
portForShared!.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class _ProbeResult implements WasmProbeResult {
|
||||
@override
|
||||
final List<WasmStorageImplementation> availableStorages;
|
||||
|
||||
@override
|
||||
final List<ExistingDatabase> existingDatabases;
|
||||
|
||||
@override
|
||||
final Set<MissingBrowserFeature> missingFeatures;
|
||||
|
||||
final WasmDatabaseOpener opener;
|
||||
|
||||
_ProbeResult(
|
||||
this.availableStorages,
|
||||
this.existingDatabases,
|
||||
this.missingFeatures,
|
||||
this.opener,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<DatabaseConnection> open(
|
||||
WasmStorageImplementation implementation,
|
||||
String name, {
|
||||
FutureOr<Uint8List?> Function()? initializeDatabase,
|
||||
}) async {
|
||||
final channel = MessageChannel();
|
||||
final initializer = initializeDatabase;
|
||||
final initChannel = initializer != null ? MessageChannel() : null;
|
||||
final local = channel.port1.channel();
|
||||
|
||||
final message = ServeDriftDatabase(
|
||||
sqlite3WasmUri: sqlite3WasmUri,
|
||||
sqlite3WasmUri: opener.sqlite3WasmUri,
|
||||
port: channel.port2,
|
||||
storage: storage,
|
||||
databaseName: databaseName,
|
||||
storage: implementation,
|
||||
databaseName: name,
|
||||
initializationPort: initChannel?.port2,
|
||||
);
|
||||
|
||||
final sharedWorker = _sharedWorker;
|
||||
final dedicatedWorker = _dedicatedWorker;
|
||||
final sharedWorker = opener._sharedWorker;
|
||||
final dedicatedWorker = opener._dedicatedWorker;
|
||||
|
||||
switch (storage) {
|
||||
switch (implementation) {
|
||||
case WasmStorageImplementation.opfsShared:
|
||||
case WasmStorageImplementation.sharedIndexedDb:
|
||||
// These are handled by the shared worker, so we can close the dedicated
|
||||
// worker used for feature detection.
|
||||
dedicatedWorker?.terminate();
|
||||
message.sendToPort(sharedWorker!);
|
||||
// Forward connection request to shared worker.
|
||||
message.sendTo(sharedWorker!.send);
|
||||
case WasmStorageImplementation.opfsLocks:
|
||||
case WasmStorageImplementation.unsafeIndexedDb:
|
||||
sharedWorker?.close();
|
||||
|
||||
if (dedicatedWorker != null) {
|
||||
message.sendToWorker(dedicatedWorker);
|
||||
message.sendTo(dedicatedWorker.send);
|
||||
} else {
|
||||
// Workers seem to be broken, but we don't need them with this storage
|
||||
// mode.
|
||||
return _hostDatabaseLocally(
|
||||
storage, await IndexedDbFileSystem.open(dbName: databaseName));
|
||||
return _hostDatabaseLocally(implementation,
|
||||
await IndexedDbFileSystem.open(dbName: name), initializeDatabase);
|
||||
}
|
||||
|
||||
case WasmStorageImplementation.inMemory:
|
||||
// Nothing works on this browser, so we'll fall back to an in-memory
|
||||
// database.
|
||||
return _hostDatabaseLocally(storage, InMemoryFileSystem());
|
||||
return _hostDatabaseLocally(
|
||||
implementation, InMemoryFileSystem(), initializeDatabase);
|
||||
}
|
||||
|
||||
initChannel?.port1.onMessage.listen((event) async {
|
||||
|
@ -181,7 +273,7 @@ class WasmDatabaseOpener {
|
|||
});
|
||||
|
||||
var connection = await connectToRemoteAndInitialize(local);
|
||||
if (storage == WasmStorageImplementation.opfsLocks) {
|
||||
if (implementation == WasmStorageImplementation.opfsLocks) {
|
||||
// We want stream queries to update for writes in other tabs. For the
|
||||
// implementations backed by a shared worker, the worker takes care of
|
||||
// that.
|
||||
|
@ -193,20 +285,21 @@ class WasmDatabaseOpener {
|
|||
connection = DatabaseConnection(
|
||||
connection.executor,
|
||||
connectionData: connection.connectionData,
|
||||
streamQueries: BroadcastStreamQueryStore(databaseName),
|
||||
streamQueries: BroadcastStreamQueryStore(name),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return WasmDatabaseResult(connection, storage, missingFeatures);
|
||||
return connection;
|
||||
}
|
||||
|
||||
/// Returns a database connection that doesn't use web workers.
|
||||
Future<WasmDatabaseResult> _hostDatabaseLocally(
|
||||
WasmStorageImplementation storage, VirtualFileSystem vfs) async {
|
||||
final initializer = initializeDatabase;
|
||||
|
||||
final sqlite3 = await WasmSqlite3.loadFromUrl(sqlite3WasmUri);
|
||||
Future<DatabaseConnection> _hostDatabaseLocally(
|
||||
WasmStorageImplementation storage,
|
||||
VirtualFileSystem vfs,
|
||||
FutureOr<Uint8List?> Function()? initializer,
|
||||
) async {
|
||||
final sqlite3 = await WasmSqlite3.loadFromUrl(opener.sqlite3WasmUri);
|
||||
sqlite3.registerVirtualFileSystem(vfs);
|
||||
|
||||
if (initializer != null) {
|
||||
|
@ -220,75 +313,26 @@ class WasmDatabaseOpener {
|
|||
}
|
||||
}
|
||||
|
||||
return WasmDatabaseResult(
|
||||
DatabaseConnection(
|
||||
WasmDatabase(sqlite3: sqlite3, path: '/database'),
|
||||
),
|
||||
storage,
|
||||
missingFeatures,
|
||||
return DatabaseConnection(
|
||||
WasmDatabase(sqlite3: sqlite3, path: '/database'),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _probeShared() async {
|
||||
if (supportsSharedWorkers) {
|
||||
final sharedWorker =
|
||||
SharedWorker(driftWorkerUri.toString(), 'drift worker');
|
||||
final port = _sharedWorker = sharedWorker.port!;
|
||||
@override
|
||||
Future<void> deleteDatabase(ExistingDatabase database) async {
|
||||
switch (database.$1) {
|
||||
case WebStorageApi.indexedDb:
|
||||
await deleteDatabaseInIndexedDb(database.$2);
|
||||
case WebStorageApi.opfs:
|
||||
final dedicated = opener._dedicatedWorker;
|
||||
if (dedicated != null) {
|
||||
DeleteDatabase(database).sendTo(dedicated.send);
|
||||
|
||||
final sharedMessages =
|
||||
StreamQueue(_readMessages(port.onMessage, sharedWorker.onError));
|
||||
|
||||
// First, the shared worker will tell us which features it supports.
|
||||
RequestCompatibilityCheck(databaseName).sendToPort(port);
|
||||
final sharedFeatures =
|
||||
await sharedMessages.nextNoError as SharedWorkerCompatibilityResult;
|
||||
await sharedMessages.cancel();
|
||||
missingFeatures.addAll(sharedFeatures.missingFeatures);
|
||||
|
||||
_existsInOpfs |= sharedFeatures.opfsExists;
|
||||
_existsInIndexedDb |= sharedFeatures.indexedDbExists;
|
||||
|
||||
// Prefer to use the shared worker to host the database if it supports the
|
||||
// necessary APIs.
|
||||
if (sharedFeatures.canSpawnDedicatedWorkers &&
|
||||
sharedFeatures.dedicatedWorkersCanUseOpfs) {
|
||||
availableImplementations.add(WasmStorageImplementation.opfsShared);
|
||||
}
|
||||
if (sharedFeatures.canUseIndexedDb) {
|
||||
availableImplementations.add(WasmStorageImplementation.sharedIndexedDb);
|
||||
}
|
||||
} else {
|
||||
missingFeatures.add(MissingBrowserFeature.sharedWorkers);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _probeDedicated() async {
|
||||
if (supportsWorkers) {
|
||||
final dedicatedWorker =
|
||||
_dedicatedWorker = Worker(driftWorkerUri.toString());
|
||||
RequestCompatibilityCheck(databaseName).sendToWorker(dedicatedWorker);
|
||||
|
||||
final workerMessages = StreamQueue(
|
||||
_readMessages(dedicatedWorker.onMessage, dedicatedWorker.onError));
|
||||
|
||||
final status = await workerMessages.nextNoError
|
||||
as DedicatedWorkerCompatibilityResult;
|
||||
missingFeatures.addAll(status.missingFeatures);
|
||||
|
||||
_existsInOpfs |= status.opfsExists;
|
||||
_existsInIndexedDb |= status.indexedDbExists;
|
||||
|
||||
if (status.supportsNestedWorkers &&
|
||||
status.canAccessOpfs &&
|
||||
status.supportsSharedArrayBuffers) {
|
||||
availableImplementations.add(WasmStorageImplementation.opfsLocks);
|
||||
}
|
||||
|
||||
if (status.supportsIndexedDb) {
|
||||
availableImplementations.add(WasmStorageImplementation.unsafeIndexedDb);
|
||||
}
|
||||
} else {
|
||||
missingFeatures.add(MissingBrowserFeature.dedicatedWorkers);
|
||||
await dedicated.workerMessages.nextNoError;
|
||||
} else {
|
||||
throw StateError(
|
||||
'No dedicated worker available to delete OPFS database');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,11 +3,9 @@
|
|||
import 'dart:async';
|
||||
import 'dart:html';
|
||||
|
||||
import 'package:drift/wasm.dart';
|
||||
import 'package:js/js_util.dart';
|
||||
import 'package:sqlite3/wasm.dart';
|
||||
// ignore: implementation_imports
|
||||
import 'package:sqlite3/src/wasm/js_interop/file_system_access.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../../utils/synchronized.dart';
|
||||
import 'protocol.dart';
|
||||
|
@ -48,32 +46,25 @@ class DedicatedDriftWorker {
|
|||
});
|
||||
|
||||
final existingServer = _servers.servers[dbName];
|
||||
|
||||
var indexedDbExists = false, opfsExists = false;
|
||||
final existingDatabases = <ExistingDatabase>[];
|
||||
|
||||
if (supportsOpfs) {
|
||||
for (final database in await opfsDatabases()) {
|
||||
existingDatabases.add((WebStorageApi.opfs, database));
|
||||
|
||||
if (database == dbName) {
|
||||
opfsExists = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (existingServer != null) {
|
||||
indexedDbExists = existingServer.storage.isIndexedDbBased;
|
||||
opfsExists = existingServer.storage.isOpfsBased;
|
||||
} else {
|
||||
if (supportsIndexedDb) {
|
||||
indexedDbExists = await checkIndexedDbExists(dbName);
|
||||
}
|
||||
|
||||
if (supportsOpfs) {
|
||||
final storage = storageManager!;
|
||||
final pathSegments = p.url.split(pathForOpfs(dbName));
|
||||
|
||||
var directory = await storage.directory;
|
||||
opfsExists = true;
|
||||
|
||||
for (final segment in pathSegments) {
|
||||
try {
|
||||
directory = await directory.getDirectory(segment);
|
||||
} on Object {
|
||||
opfsExists = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (supportsIndexedDb) {
|
||||
indexedDbExists = await checkIndexedDbExists(dbName);
|
||||
}
|
||||
|
||||
DedicatedWorkerCompatibilityResult(
|
||||
|
@ -84,6 +75,7 @@ class DedicatedDriftWorker {
|
|||
hasProperty(globalThis, 'SharedArrayBuffer'),
|
||||
opfsExists: opfsExists,
|
||||
indexedDbExists: indexedDbExists,
|
||||
existingDatabases: existingDatabases,
|
||||
).sendToClient(self);
|
||||
case ServeDriftDatabase():
|
||||
_servers.serve(message);
|
||||
|
@ -91,6 +83,22 @@ class DedicatedDriftWorker {
|
|||
final worker = await VfsWorker.create(options);
|
||||
self.postMessage(true);
|
||||
await worker.start();
|
||||
case DeleteDatabase(database: (final storage, final name)):
|
||||
try {
|
||||
switch (storage) {
|
||||
case WebStorageApi.indexedDb:
|
||||
await deleteDatabaseInIndexedDb(name);
|
||||
case WebStorageApi.opfs:
|
||||
await deleteDatabaseInOpfs(name);
|
||||
}
|
||||
|
||||
// Send the request back to indicate a successful delete.
|
||||
message.sendToClient(self);
|
||||
} catch (e) {
|
||||
WorkerError(e.toString()).sendToClient(self);
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'dart:html';
|
||||
import 'dart:js';
|
||||
|
||||
import 'package:js/js_util.dart';
|
||||
import 'package:sqlite3/wasm.dart';
|
||||
|
@ -29,6 +30,7 @@ sealed class WasmInitializationMessage {
|
|||
DedicatedWorkerCompatibilityResult.fromJsPayload(payload!),
|
||||
SharedWorkerCompatibilityResult.type =>
|
||||
SharedWorkerCompatibilityResult.fromJsPayload(payload!),
|
||||
DeleteDatabase.type => DeleteDatabase.fromJsPayload(payload!),
|
||||
_ => throw ArgumentError('Unknown type $type'),
|
||||
};
|
||||
}
|
||||
|
@ -55,38 +57,66 @@ sealed class WasmInitializationMessage {
|
|||
}
|
||||
}
|
||||
|
||||
sealed class CompatibilityResult extends WasmInitializationMessage {
|
||||
/// All existing databases.
|
||||
///
|
||||
/// This list is only reported by the drift worker shipped with drift 2.11.
|
||||
/// When an older worker is used, only [indexedDbExists] and [opfsExists] can
|
||||
/// be used to check whether the database exists.
|
||||
final List<ExistingDatabase> existingDatabases;
|
||||
|
||||
final bool indexedDbExists;
|
||||
final bool opfsExists;
|
||||
|
||||
Iterable<MissingBrowserFeature> get missingFeatures;
|
||||
|
||||
CompatibilityResult({
|
||||
required this.existingDatabases,
|
||||
required this.indexedDbExists,
|
||||
required this.opfsExists,
|
||||
});
|
||||
}
|
||||
|
||||
/// A message used by the shared worker to report compatibility results.
|
||||
///
|
||||
/// It describes the features available from the shared worker, which the tab
|
||||
/// can use to infer a desired storage implementation, or whether the shared
|
||||
/// worker should be used at all.
|
||||
final class SharedWorkerCompatibilityResult extends WasmInitializationMessage {
|
||||
final class SharedWorkerCompatibilityResult extends CompatibilityResult {
|
||||
static const type = 'SharedWorkerCompatibilityResult';
|
||||
|
||||
final bool canSpawnDedicatedWorkers;
|
||||
final bool dedicatedWorkersCanUseOpfs;
|
||||
final bool canUseIndexedDb;
|
||||
|
||||
final bool indexedDbExists;
|
||||
final bool opfsExists;
|
||||
|
||||
SharedWorkerCompatibilityResult({
|
||||
required this.canSpawnDedicatedWorkers,
|
||||
required this.dedicatedWorkersCanUseOpfs,
|
||||
required this.canUseIndexedDb,
|
||||
required this.indexedDbExists,
|
||||
required this.opfsExists,
|
||||
required super.indexedDbExists,
|
||||
required super.opfsExists,
|
||||
required super.existingDatabases,
|
||||
});
|
||||
|
||||
factory SharedWorkerCompatibilityResult.fromJsPayload(Object payload) {
|
||||
final data = (payload as List).cast<bool>();
|
||||
final asList = payload as List;
|
||||
final asBooleans = asList.cast<bool>();
|
||||
|
||||
final List<ExistingDatabase> existingDatabases;
|
||||
if (asList.length > 5) {
|
||||
existingDatabases =
|
||||
EncodeLocations.readFromJs(asList[5] as List<dynamic>);
|
||||
} else {
|
||||
existingDatabases = const [];
|
||||
}
|
||||
|
||||
return SharedWorkerCompatibilityResult(
|
||||
canSpawnDedicatedWorkers: data[0],
|
||||
dedicatedWorkersCanUseOpfs: data[1],
|
||||
canUseIndexedDb: data[2],
|
||||
indexedDbExists: data[3],
|
||||
opfsExists: data[4],
|
||||
canSpawnDedicatedWorkers: asBooleans[0],
|
||||
dedicatedWorkersCanUseOpfs: asBooleans[1],
|
||||
canUseIndexedDb: asBooleans[2],
|
||||
indexedDbExists: asBooleans[3],
|
||||
opfsExists: asBooleans[4],
|
||||
existingDatabases: existingDatabases,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -98,9 +128,11 @@ final class SharedWorkerCompatibilityResult extends WasmInitializationMessage {
|
|||
canUseIndexedDb,
|
||||
indexedDbExists,
|
||||
opfsExists,
|
||||
existingDatabases.encodeToJs(),
|
||||
]);
|
||||
}
|
||||
|
||||
@override
|
||||
Iterable<MissingBrowserFeature> get missingFeatures sync* {
|
||||
if (!canSpawnDedicatedWorkers) {
|
||||
yield MissingBrowserFeature.dedicatedWorkersInSharedWorkers;
|
||||
|
@ -183,6 +215,10 @@ final class ServeDriftDatabase extends WasmInitializationMessage {
|
|||
final class RequestCompatibilityCheck extends WasmInitializationMessage {
|
||||
static const type = 'RequestCompatibilityCheck';
|
||||
|
||||
/// The database name to check when reporting whether it exists already.
|
||||
///
|
||||
/// Older versions of the drif worker only support checking a single database
|
||||
/// name. On newer workers, this field is ignored.
|
||||
final String databaseName;
|
||||
|
||||
RequestCompatibilityCheck(this.databaseName);
|
||||
|
@ -197,8 +233,7 @@ final class RequestCompatibilityCheck extends WasmInitializationMessage {
|
|||
}
|
||||
}
|
||||
|
||||
final class DedicatedWorkerCompatibilityResult
|
||||
extends WasmInitializationMessage {
|
||||
final class DedicatedWorkerCompatibilityResult extends CompatibilityResult {
|
||||
static const type = 'DedicatedWorkerCompatibilityResult';
|
||||
|
||||
final bool supportsNestedWorkers;
|
||||
|
@ -206,22 +241,24 @@ final class DedicatedWorkerCompatibilityResult
|
|||
final bool supportsSharedArrayBuffers;
|
||||
final bool supportsIndexedDb;
|
||||
|
||||
/// Whether an IndexedDb database under the desired name exists already.
|
||||
final bool indexedDbExists;
|
||||
|
||||
/// Whether an OPFS database under the desired name exists already.
|
||||
final bool opfsExists;
|
||||
|
||||
DedicatedWorkerCompatibilityResult({
|
||||
required this.supportsNestedWorkers,
|
||||
required this.canAccessOpfs,
|
||||
required this.supportsSharedArrayBuffers,
|
||||
required this.supportsIndexedDb,
|
||||
required this.indexedDbExists,
|
||||
required this.opfsExists,
|
||||
required super.indexedDbExists,
|
||||
required super.opfsExists,
|
||||
required super.existingDatabases,
|
||||
});
|
||||
|
||||
factory DedicatedWorkerCompatibilityResult.fromJsPayload(Object payload) {
|
||||
final existingDatabases = <ExistingDatabase>[];
|
||||
|
||||
if (hasProperty(payload, 'existing')) {
|
||||
existingDatabases
|
||||
.addAll(EncodeLocations.readFromJs(getProperty(payload, 'existing')));
|
||||
}
|
||||
|
||||
return DedicatedWorkerCompatibilityResult(
|
||||
supportsNestedWorkers: getProperty(payload, 'supportsNestedWorkers'),
|
||||
canAccessOpfs: getProperty(payload, 'canAccessOpfs'),
|
||||
|
@ -230,6 +267,7 @@ final class DedicatedWorkerCompatibilityResult
|
|||
supportsIndexedDb: getProperty(payload, 'supportsIndexedDb'),
|
||||
indexedDbExists: getProperty(payload, 'indexedDbExists'),
|
||||
opfsExists: getProperty(payload, 'opfsExists'),
|
||||
existingDatabases: existingDatabases,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -244,10 +282,12 @@ final class DedicatedWorkerCompatibilityResult
|
|||
object, 'supportsSharedArrayBuffers', supportsSharedArrayBuffers);
|
||||
setProperty(object, 'indexedDbExists', indexedDbExists);
|
||||
setProperty(object, 'opfsExists', opfsExists);
|
||||
setProperty(object, 'existing', existingDatabases.encodeToJs());
|
||||
|
||||
sender.sendTyped(type, object);
|
||||
}
|
||||
|
||||
@override
|
||||
Iterable<MissingBrowserFeature> get missingFeatures sync* {
|
||||
if (!canAccessOpfs) {
|
||||
yield MissingBrowserFeature.fileSystemAccess;
|
||||
|
@ -278,6 +318,55 @@ final class StartFileSystemServer extends WasmInitializationMessage {
|
|||
}
|
||||
}
|
||||
|
||||
final class DeleteDatabase extends WasmInitializationMessage {
|
||||
static const type = 'DeleteDatabase';
|
||||
|
||||
final ExistingDatabase database;
|
||||
|
||||
DeleteDatabase(this.database);
|
||||
|
||||
factory DeleteDatabase.fromJsPayload(Object payload) {
|
||||
final asList = payload as List<Object?>;
|
||||
return DeleteDatabase((
|
||||
WebStorageApi.byName[asList[0] as String]!,
|
||||
asList[1] as String,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
void sendTo(PostMessage sender) {
|
||||
sender.sendTyped(type, [database.$1.name, database.$2]);
|
||||
}
|
||||
}
|
||||
|
||||
extension EncodeLocations on List<ExistingDatabase> {
|
||||
static List<ExistingDatabase> readFromJs(List<Object?> object) {
|
||||
final existing = <ExistingDatabase>[];
|
||||
|
||||
for (final entry in object) {
|
||||
existing.add((
|
||||
WebStorageApi.byName[getProperty(entry as Object, 'l')]!,
|
||||
getProperty(entry, 'n'),
|
||||
));
|
||||
}
|
||||
|
||||
return existing;
|
||||
}
|
||||
|
||||
Object encodeToJs() {
|
||||
final existing = JsArray<Object>();
|
||||
for (final entry in this) {
|
||||
final object = newObject<Object>();
|
||||
setProperty(object, 'l', entry.$1.name);
|
||||
setProperty(object, 'n', entry.$2);
|
||||
|
||||
existing.add(object);
|
||||
}
|
||||
|
||||
return existing;
|
||||
}
|
||||
}
|
||||
|
||||
extension on PostMessage {
|
||||
void sendTyped(String type, Object? payload, [List<Object>? transfer]) {
|
||||
final object = newObject<Object>();
|
||||
|
|
|
@ -87,7 +87,7 @@ Future<bool> checkIndexedDbExists(String databaseName) async {
|
|||
try {
|
||||
final idb = getProperty<IdbFactory>(globalThis, 'indexedDB');
|
||||
|
||||
await idb.open(
|
||||
final database = await idb.open(
|
||||
databaseName,
|
||||
// Current schema version used by the [IndexedDbFileSystem]
|
||||
version: 1,
|
||||
|
@ -100,6 +100,7 @@ Future<bool> checkIndexedDbExists(String databaseName) async {
|
|||
);
|
||||
|
||||
indexedDbExists ??= true;
|
||||
database.close();
|
||||
} catch (_) {
|
||||
// May throw due to us aborting in the upgrade callback.
|
||||
}
|
||||
|
@ -107,12 +108,55 @@ Future<bool> checkIndexedDbExists(String databaseName) async {
|
|||
return indexedDbExists ?? false;
|
||||
}
|
||||
|
||||
/// Deletes a database from IndexedDb if supported.
|
||||
Future<void> deleteDatabaseInIndexedDb(String databaseName) async {
|
||||
final idb = window.indexedDB;
|
||||
if (idb != null) {
|
||||
await idb.deleteDatabase(databaseName);
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs the path used by drift to store a database in the origin-private
|
||||
/// section of the agent's file system.
|
||||
String pathForOpfs(String databaseName) {
|
||||
return 'drift_db/$databaseName';
|
||||
}
|
||||
|
||||
/// Collects all drift OPFS databases.
|
||||
Future<List<String>> opfsDatabases() async {
|
||||
final storage = storageManager;
|
||||
if (storage == null) return const [];
|
||||
|
||||
var directory = await storage.directory;
|
||||
try {
|
||||
directory = await directory.getDirectory('drift_db');
|
||||
} on Object {
|
||||
// The drift_db folder doesn't exist, so there aren't any databases.
|
||||
return const [];
|
||||
}
|
||||
|
||||
return [
|
||||
await for (final entry in directory.list())
|
||||
if (entry.isDirectory) entry.name,
|
||||
];
|
||||
}
|
||||
|
||||
/// Deletes the OPFS folder storing a database with the given [databaseName] if
|
||||
/// such folder exists.
|
||||
Future<void> deleteDatabaseInOpfs(String databaseName) async {
|
||||
final storage = storageManager;
|
||||
if (storage == null) return;
|
||||
|
||||
var directory = await storage.directory;
|
||||
try {
|
||||
directory = await directory.getDirectory('drift_db');
|
||||
await directory.removeEntry(databaseName, recursive: true);
|
||||
} on Object {
|
||||
// fine, an error probably means that the database didn't exist in the first
|
||||
// place.
|
||||
}
|
||||
}
|
||||
|
||||
/// Manages drift servers.
|
||||
///
|
||||
/// When using a shared worker, multiple clients may want to use different drift
|
||||
|
@ -132,7 +176,8 @@ class DriftServerController {
|
|||
|
||||
final vfs = await switch (message.storage) {
|
||||
WasmStorageImplementation.opfsShared =>
|
||||
SimpleOpfsFileSystem.loadFromStorage(message.databaseName),
|
||||
SimpleOpfsFileSystem.loadFromStorage(
|
||||
pathForOpfs(message.databaseName)),
|
||||
WasmStorageImplementation.opfsLocks =>
|
||||
_loadLockedWasmVfs(message.databaseName),
|
||||
WasmStorageImplementation.unsafeIndexedDb ||
|
||||
|
@ -171,7 +216,7 @@ class DriftServerController {
|
|||
Future<WasmVfs> _loadLockedWasmVfs(String databaseName) async {
|
||||
// Create SharedArrayBuffers to synchronize requests
|
||||
final options = WasmVfs.createOptions(
|
||||
root: 'drift_db/$databaseName/',
|
||||
root: pathForOpfs(databaseName),
|
||||
);
|
||||
final worker = Worker(Uri.base.toString());
|
||||
|
||||
|
|
|
@ -76,6 +76,7 @@ class SharedDriftWorker {
|
|||
canUseIndexedDb: canUseIndexedDb,
|
||||
indexedDbExists: indexedDbExists,
|
||||
opfsExists: false,
|
||||
existingDatabases: const [],
|
||||
);
|
||||
} else {
|
||||
final worker = _dedicatedWorker ??= Worker(Uri.base.toString());
|
||||
|
@ -86,7 +87,12 @@ class SharedDriftWorker {
|
|||
final completer = Completer<SharedWorkerCompatibilityResult>();
|
||||
StreamSubscription? messageSubscription, errorSubscription;
|
||||
|
||||
void result(bool opfsAvailable, bool opfsExists, bool indexedDbExists) {
|
||||
void result(
|
||||
bool opfsAvailable,
|
||||
bool opfsExists,
|
||||
bool indexedDbExists,
|
||||
List<ExistingDatabase> databases,
|
||||
) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(SharedWorkerCompatibilityResult(
|
||||
canSpawnDedicatedWorkers: true,
|
||||
|
@ -94,6 +100,7 @@ class SharedDriftWorker {
|
|||
canUseIndexedDb: canUseIndexedDb,
|
||||
indexedDbExists: indexedDbExists,
|
||||
opfsExists: opfsExists,
|
||||
existingDatabases: databases,
|
||||
));
|
||||
|
||||
messageSubscription?.cancel();
|
||||
|
@ -110,11 +117,12 @@ class SharedDriftWorker {
|
|||
compatibilityResult.canAccessOpfs,
|
||||
compatibilityResult.opfsExists,
|
||||
compatibilityResult.indexedDbExists,
|
||||
compatibilityResult.existingDatabases,
|
||||
);
|
||||
});
|
||||
|
||||
errorSubscription = worker.onError.listen((event) {
|
||||
result(false, false, false);
|
||||
result(false, false, false, const []);
|
||||
worker.terminate();
|
||||
_dedicatedWorker = null;
|
||||
});
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
/// integration tests on a Dart VM (`extras/integration_tests/web_wasm`).
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
/// The storage implementation used by the `drift` and `sqlite3` packages to
|
||||
|
@ -80,6 +82,19 @@ enum WasmStorageImplementation {
|
|||
inMemory,
|
||||
}
|
||||
|
||||
/// The storage API used by drift to store a database.
|
||||
enum WebStorageApi {
|
||||
/// The database is stored in the origin-private section of the user's file
|
||||
/// system via the FileSystem Access API.
|
||||
opfs,
|
||||
|
||||
/// The database is stored in IndexedDb.
|
||||
indexedDb;
|
||||
|
||||
/// Cached [EnumByName.asNameMap] for [values].
|
||||
static final byName = WebStorageApi.values.asNameMap();
|
||||
}
|
||||
|
||||
/// An enumeration of features not supported by the current browsers.
|
||||
///
|
||||
/// While this information may not be useful to end users, it can be used to
|
||||
|
@ -118,8 +133,64 @@ enum MissingBrowserFeature {
|
|||
sharedArrayBuffers,
|
||||
}
|
||||
|
||||
/// The result of opening a WASM database.
|
||||
class WasmDatabaseResult {
|
||||
/// Information about an existing web database, consisting of its
|
||||
/// storage API ([WebStorageApi]) and its name.
|
||||
typedef ExistingDatabase = (WebStorageApi, String);
|
||||
|
||||
/// The result of probing the current browser for wasm compatibility.
|
||||
///
|
||||
/// This reports available storage implementations ([availableStorages]) and
|
||||
/// [missingFeatures] that contributed to some storage implementations not being
|
||||
/// available.
|
||||
///
|
||||
/// In addition, [existingDatabases] reports a list of existing databases. Note
|
||||
/// that databases stored in IndexedDb can't be listed reliably. Only databases
|
||||
/// with the name given in [WasmDatabase.probe] are listed. Databases stored in
|
||||
/// OPFS are always listed.
|
||||
abstract interface class WasmProbeResult {
|
||||
/// All available [WasmStorageImplementation]s supported by the current
|
||||
/// browsing context.
|
||||
///
|
||||
/// Depending on the features available in the browser your app runs on and
|
||||
/// whether your app is served with the required headers for shared array
|
||||
/// buffers, different implementations might be available.
|
||||
///
|
||||
/// You can see the [WasmStorageImplementation]s and
|
||||
/// [the web documentation](https://drift.simonbinder.eu/web/#storages) to
|
||||
/// learn more about which implementations drift can use.
|
||||
List<WasmStorageImplementation> get availableStorages;
|
||||
|
||||
/// For every storage found, drift also reports existing drift databases.
|
||||
List<ExistingDatabase> get existingDatabases;
|
||||
|
||||
/// An enumeration of missing browser features probed by drift.
|
||||
///
|
||||
/// Missing browser features limit the available storage implementations.
|
||||
Set<MissingBrowserFeature> get missingFeatures;
|
||||
|
||||
/// Opens a connection to a database via the chosen [implementation].
|
||||
///
|
||||
/// When this database doesn't exist, [initializeDatabase] is invoked to
|
||||
/// optionally return the initial bytes of the database.
|
||||
Future<DatabaseConnection> open(
|
||||
WasmStorageImplementation implementation,
|
||||
String name, {
|
||||
FutureOr<Uint8List?> Function()? initializeDatabase,
|
||||
});
|
||||
|
||||
/// Deletes an [ExistingDatabase] from storage.
|
||||
///
|
||||
/// This method should not be called while a connection to the database is
|
||||
/// opened.
|
||||
///
|
||||
/// This method is only supported when using the drift worker shipped with the
|
||||
/// drift 2.11 release or later. This method will not work when using an older
|
||||
/// worker.
|
||||
Future<void> deleteDatabase(ExistingDatabase database);
|
||||
}
|
||||
|
||||
/// The result of opening a WASM database with default options.
|
||||
final class WasmDatabaseResult {
|
||||
/// The drift database connection to pass to the [GeneratedDatabase.new]
|
||||
/// constructor of your database class to use the opened database.
|
||||
final DatabaseConnection resolvedExecutor;
|
||||
|
|
|
@ -10,11 +10,12 @@ import 'dart:async';
|
|||
import 'dart:html';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/src/web/wasm_setup.dart';
|
||||
import 'package:sqlite3/wasm.dart';
|
||||
|
||||
import 'backends.dart';
|
||||
import 'src/sqlite3/database.dart';
|
||||
import 'src/web/wasm_setup.dart';
|
||||
import 'src/web/wasm_setup/dedicated_worker.dart';
|
||||
import 'src/web/wasm_setup/shared_worker.dart';
|
||||
import 'src/web/wasm_setup/types.dart';
|
||||
|
@ -90,13 +91,87 @@ class WasmDatabase extends DelegatedDatabase {
|
|||
required Uri sqlite3Uri,
|
||||
required Uri driftWorkerUri,
|
||||
FutureOr<Uint8List?> Function()? initializeDatabase,
|
||||
}) {
|
||||
return WasmDatabaseOpener(
|
||||
databaseName: databaseName,
|
||||
sqlite3WasmUri: sqlite3Uri,
|
||||
}) async {
|
||||
final probed = await probe(
|
||||
sqlite3Uri: sqlite3Uri,
|
||||
driftWorkerUri: driftWorkerUri,
|
||||
initializeDatabase: initializeDatabase,
|
||||
).open();
|
||||
databaseName: databaseName,
|
||||
);
|
||||
|
||||
// If we have an existing database in storage, we want to keep using that
|
||||
// format to avoid data loss (e.g. after a browser update that enables a
|
||||
// otherwise preferred storage implementation). In the future, we might want
|
||||
// to consider migrating between storage implementations as well.
|
||||
final availableImplementations = probed.availableStorages.toList();
|
||||
|
||||
checkExisting:
|
||||
for (final (location, name) in probed.existingDatabases) {
|
||||
if (name == databaseName) {
|
||||
final implementationsForStorage = switch (location) {
|
||||
WebStorageApi.indexedDb => const [
|
||||
WasmStorageImplementation.sharedIndexedDb,
|
||||
WasmStorageImplementation.unsafeIndexedDb
|
||||
],
|
||||
WebStorageApi.opfs => const [
|
||||
WasmStorageImplementation.opfsShared,
|
||||
WasmStorageImplementation.opfsLocks,
|
||||
],
|
||||
};
|
||||
|
||||
// If any of the implementations for this location is still availalable,
|
||||
// we want to use it instead of another location.
|
||||
if (implementationsForStorage.any(availableImplementations.contains)) {
|
||||
availableImplementations
|
||||
.removeWhere((i) => !implementationsForStorage.contains(i));
|
||||
break checkExisting;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enum values are ordered by preferrability, so just pick the best option
|
||||
// left.
|
||||
availableImplementations.sortBy<num>((element) => element.index);
|
||||
|
||||
final bestImplementation = availableImplementations.firstOrNull ??
|
||||
WasmStorageImplementation.inMemory;
|
||||
final connection = await probed.open(bestImplementation, databaseName);
|
||||
|
||||
return WasmDatabaseResult(
|
||||
connection, bestImplementation, probed.missingFeatures);
|
||||
}
|
||||
|
||||
/// Probes for:
|
||||
///
|
||||
/// - available storage implementations based on supported web APIs.
|
||||
/// - APIs not currently supported by the browser.
|
||||
/// - existing drift databases in the current browsing context.
|
||||
///
|
||||
/// This information can be used to control whether to open a drift database,
|
||||
/// or whether the current browser is unsuitable for the persistence
|
||||
/// requirements of your app.
|
||||
/// For most apps, using [open] directly is easier. It calls [probe]
|
||||
/// internally and uses the best storage implementation available.
|
||||
///
|
||||
/// The [databaseName] option is not strictly required. But drift can't list
|
||||
/// databases stored in IndexedDb, they are not part of
|
||||
/// [WasmProbeResult.existingDatabases] by default. This is because drift
|
||||
/// databases can't be distinguished from other IndexedDb databases without
|
||||
/// opening them, which might disturb the running operation of them. When a
|
||||
/// [databaseName] is passed, drift will explicitly probe whether a database
|
||||
/// with that name exists in IndexedDb and whether it is a drift database.
|
||||
/// Drift is always able to list databases stored in OPFS, regardless of
|
||||
/// whether [databaseName] is passed or not.
|
||||
///
|
||||
/// Note that this method is only fully supported when using the drift worker
|
||||
/// shipped with the drift 2.11 release. Older workers are only supported when
|
||||
/// [databaseName] is non-null.
|
||||
static Future<WasmProbeResult> probe({
|
||||
required Uri sqlite3Uri,
|
||||
required Uri driftWorkerUri,
|
||||
String? databaseName,
|
||||
}) async {
|
||||
return await WasmDatabaseOpener(sqlite3Uri, driftWorkerUri, databaseName)
|
||||
.probe();
|
||||
}
|
||||
|
||||
/// The entrypoint for a web worker suitable for use with [open].
|
||||
|
|
|
@ -1,18 +1,7 @@
|
|||
Integration tests for `package:drift/native.dart`.
|
||||
|
||||
To test persistence, we need to instrument a browsers in a way not covered by the normal
|
||||
`test` package. For instance, we need to reload pages to ensure data is still there.
|
||||
To run the tests automatically (with us managing a browser driver), just run `dart test`.
|
||||
|
||||
## Running tests with Firefox
|
||||
|
||||
```
|
||||
geckodriver &
|
||||
dart run tool/drift_wasm_test.dart firefox http://localhost:4444
|
||||
```
|
||||
|
||||
## Running tests with Chrome
|
||||
|
||||
```
|
||||
chromedriver --port=4444 --url-base=wd/hub &
|
||||
dart run tool/drift_wasm_test.dart chrome http://localhost:4444/wd/hub/
|
||||
```
|
||||
To manually debug issues, it might make sense to trigger some functionality manually.
|
||||
You can run `dart run tool/server_manually.dart` to start a web server hosting the test
|
||||
content on http://localhost:8080.
|
|
@ -105,6 +105,7 @@ class DriftWebDriver {
|
|||
({
|
||||
Set<WasmStorageImplementation> storages,
|
||||
Set<MissingBrowserFeature> missingFeatures,
|
||||
List<ExistingDatabase> existing,
|
||||
})> probeImplementations() async {
|
||||
final rawResult = await driver
|
||||
.executeAsync('detectImplementations("", arguments[0])', []);
|
||||
|
@ -119,6 +120,13 @@ class DriftWebDriver {
|
|||
for (final entry in result['missing'])
|
||||
MissingBrowserFeature.values.byName(entry)
|
||||
},
|
||||
existing: <ExistingDatabase>[
|
||||
for (final entry in result['existing'])
|
||||
(
|
||||
WebStorageApi.byName[entry[0] as String]!,
|
||||
entry[1] as String,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -149,4 +157,10 @@ class DriftWebDriver {
|
|||
throw 'Could not set initialization mode';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteDatabase(WebStorageApi storageApi, String name) async {
|
||||
await driver.executeAsync('delete_database(arguments[0], arguments[1])', [
|
||||
json.encode([storageApi.name, name]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -118,6 +118,29 @@ void main() {
|
|||
}
|
||||
});
|
||||
|
||||
if (entry != WasmStorageImplementation.inMemory) {
|
||||
test('delete', () async {
|
||||
final impl = await driver.probeImplementations();
|
||||
expect(impl.existing, isEmpty);
|
||||
|
||||
await driver.openDatabase(entry);
|
||||
await driver.insertIntoDatabase();
|
||||
await driver.waitForTableUpdate();
|
||||
|
||||
await driver.driver.refresh(); // Reset JS state
|
||||
|
||||
final newImpls = await driver.probeImplementations();
|
||||
expect(newImpls.existing, hasLength(1));
|
||||
final existing = newImpls.existing[0];
|
||||
await driver.deleteDatabase(existing.$1, existing.$2);
|
||||
|
||||
await driver.driver.refresh();
|
||||
|
||||
final finalImpls = await driver.probeImplementations();
|
||||
expect(finalImpls.existing, isEmpty);
|
||||
});
|
||||
}
|
||||
|
||||
group(
|
||||
'initialization from ',
|
||||
() {
|
||||
|
|
|
@ -5,14 +5,15 @@ import 'dart:js_util';
|
|||
import 'package:async/async.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/wasm.dart';
|
||||
// ignore: invalid_use_of_internal_member
|
||||
import 'package:drift/src/web/wasm_setup.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:web_wasm/initialization_mode.dart';
|
||||
import 'package:web_wasm/src/database.dart';
|
||||
import 'package:sqlite3/wasm.dart';
|
||||
|
||||
const dbName = 'drift_test';
|
||||
final sqlite3WasmUri = Uri.parse('/sqlite3.wasm');
|
||||
final driftWorkerUri = Uri.parse('/worker.dart.js');
|
||||
|
||||
TestDatabase? openedDatabase;
|
||||
StreamQueue<void>? tableUpdates;
|
||||
|
||||
|
@ -28,10 +29,27 @@ void main() {
|
|||
initializationMode = InitializationMode.values.byName(arg!);
|
||||
return true;
|
||||
});
|
||||
_addCallbackForWebDriver('delete_database', (arg) async {
|
||||
final result = await WasmDatabase.probe(
|
||||
sqlite3Uri: sqlite3WasmUri,
|
||||
driftWorkerUri: driftWorkerUri,
|
||||
);
|
||||
|
||||
final decoded = json.decode(arg!);
|
||||
|
||||
await result.deleteDatabase(
|
||||
(WebStorageApi.byName[decoded[0] as String]!, decoded[1] as String),
|
||||
);
|
||||
});
|
||||
|
||||
document.getElementById('selfcheck')?.onClick.listen((event) async {
|
||||
print('starting');
|
||||
final database = await _opener.open();
|
||||
final database = await WasmDatabase.open(
|
||||
databaseName: dbName,
|
||||
sqlite3Uri: sqlite3WasmUri,
|
||||
driftWorkerUri: driftWorkerUri,
|
||||
initializeDatabase: _initializeDatabase,
|
||||
);
|
||||
|
||||
print('selected storage: ${database.chosenImplementation}');
|
||||
print('missing features: ${database.missingFeatures}');
|
||||
|
@ -54,77 +72,81 @@ void _addCallbackForWebDriver(String name, Future Function(String?) impl) {
|
|||
}));
|
||||
}
|
||||
|
||||
WasmDatabaseOpener get _opener {
|
||||
Future<Uint8List> Function()? initializeDatabase;
|
||||
|
||||
Future<Uint8List?> _initializeDatabase() async {
|
||||
switch (initializationMode) {
|
||||
case InitializationMode.loadAsset:
|
||||
initializeDatabase = () async {
|
||||
final response = await http.get(Uri.parse('/initial.db'));
|
||||
return response.bodyBytes;
|
||||
};
|
||||
final response = await http.get(Uri.parse('/initial.db'));
|
||||
return response.bodyBytes;
|
||||
|
||||
case InitializationMode.migrateCustomWasmDatabase:
|
||||
initializeDatabase = () async {
|
||||
// Let's first open a custom WasmDatabase, the way it would have been
|
||||
// done before WasmDatabase.open.
|
||||
final sqlite3 =
|
||||
await WasmSqlite3.loadFromUrl(Uri.parse('/sqlite3.wasm'));
|
||||
final fs = await IndexedDbFileSystem.open(dbName: dbName);
|
||||
sqlite3.registerVirtualFileSystem(fs, makeDefault: true);
|
||||
|
||||
final wasmDb = WasmDatabase(sqlite3: sqlite3, path: '/app.db');
|
||||
final db = TestDatabase(wasmDb);
|
||||
await db
|
||||
.into(db.testTable)
|
||||
.insert(TestTableCompanion.insert(content: 'from old database'));
|
||||
await db.close();
|
||||
// Let's first open a custom WasmDatabase, the way it would have been
|
||||
// done before WasmDatabase.open.
|
||||
final sqlite3 = await WasmSqlite3.loadFromUrl(Uri.parse('/sqlite3.wasm'));
|
||||
final fs = await IndexedDbFileSystem.open(dbName: dbName);
|
||||
sqlite3.registerVirtualFileSystem(fs, makeDefault: true);
|
||||
|
||||
final (file: file, outFlags: _) =
|
||||
fs.xOpen(Sqlite3Filename('/app.db'), 0);
|
||||
final blob = Uint8List(file.xFileSize());
|
||||
file.xRead(blob, 0);
|
||||
file.xClose();
|
||||
fs.xDelete('/app.db', 0);
|
||||
await fs.close();
|
||||
final wasmDb = WasmDatabase(sqlite3: sqlite3, path: '/app.db');
|
||||
final db = TestDatabase(wasmDb);
|
||||
await db
|
||||
.into(db.testTable)
|
||||
.insert(TestTableCompanion.insert(content: 'from old database'));
|
||||
await db.close();
|
||||
|
||||
return blob;
|
||||
};
|
||||
break;
|
||||
final (file: file, outFlags: _) = fs.xOpen(Sqlite3Filename('/app.db'), 0);
|
||||
final blob = Uint8List(file.xFileSize());
|
||||
file.xRead(blob, 0);
|
||||
file.xClose();
|
||||
fs.xDelete('/app.db', 0);
|
||||
await fs.close();
|
||||
|
||||
return blob;
|
||||
case InitializationMode.none:
|
||||
break;
|
||||
return null;
|
||||
}
|
||||
|
||||
return WasmDatabaseOpener(
|
||||
databaseName: dbName,
|
||||
sqlite3WasmUri: Uri.parse('/sqlite3.wasm'),
|
||||
driftWorkerUri: Uri.parse('/worker.dart.js'),
|
||||
initializeDatabase: initializeDatabase,
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> _detectImplementations(String? _) async {
|
||||
final opener = _opener;
|
||||
await opener.probe();
|
||||
final result = await WasmDatabase.probe(
|
||||
sqlite3Uri: sqlite3WasmUri,
|
||||
driftWorkerUri: driftWorkerUri,
|
||||
databaseName: dbName,
|
||||
);
|
||||
|
||||
return json.encode({
|
||||
'impls': opener.availableImplementations.map((r) => r.name).toList(),
|
||||
'missing': opener.missingFeatures.map((r) => r.name).toList(),
|
||||
'impls': result.availableStorages.map((r) => r.name).toList(),
|
||||
'missing': result.missingFeatures.map((r) => r.name).toList(),
|
||||
'existing': result.existingDatabases.map((r) => [r.$1.name, r.$2]).toList(),
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _open(String? implementationName) async {
|
||||
final opener = _opener;
|
||||
WasmDatabaseResult result;
|
||||
DatabaseConnection connection;
|
||||
|
||||
if (implementationName != null) {
|
||||
await opener.probe();
|
||||
result = await opener
|
||||
.openWith(WasmStorageImplementation.values.byName(implementationName));
|
||||
final probeResult = await WasmDatabase.probe(
|
||||
sqlite3Uri: sqlite3WasmUri,
|
||||
driftWorkerUri: driftWorkerUri,
|
||||
databaseName: dbName,
|
||||
);
|
||||
|
||||
connection = await probeResult.open(
|
||||
WasmStorageImplementation.values.byName(implementationName),
|
||||
dbName,
|
||||
initializeDatabase: _initializeDatabase,
|
||||
);
|
||||
} else {
|
||||
result = await opener.open();
|
||||
final result = await WasmDatabase.open(
|
||||
databaseName: dbName,
|
||||
sqlite3Uri: sqlite3WasmUri,
|
||||
driftWorkerUri: driftWorkerUri,
|
||||
initializeDatabase: _initializeDatabase,
|
||||
);
|
||||
|
||||
connection = result.resolvedExecutor;
|
||||
}
|
||||
|
||||
final db = openedDatabase = TestDatabase(result.resolvedExecutor);
|
||||
final db = openedDatabase = TestDatabase(connection);
|
||||
|
||||
// Make sure it works!
|
||||
await db.customSelect('SELECT 1').get();
|
||||
|
|
Loading…
Reference in New Issue