Support deleting databases

This commit is contained in:
Simon Binder 2023-08-09 18:13:45 +02:00
parent b774290b3a
commit 9d4d3cd2a4
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
9 changed files with 246 additions and 67 deletions

View File

@ -23,6 +23,7 @@ import 'package:sqlite3/wasm.dart';
import 'broadcast_stream_queries.dart';
import 'channel.dart';
import 'wasm_setup/shared.dart';
import 'wasm_setup/protocol.dart';
/// Whether the `crossOriginIsolated` JavaScript property is true in the current
@ -46,10 +47,9 @@ class WasmDatabaseOpener {
final List<WasmStorageImplementation> availableImplementations = [
WasmStorageImplementation.inMemory,
];
final Set<(DatabaseLocation, String)> existingDatabases = {};
final Set<ExistingDatabase> existingDatabases = {};
MessagePort? _sharedWorker;
Worker? _dedicatedWorker;
_DriftWorker? _sharedWorker, _dedicatedWorker;
WasmDatabaseOpener(
this.sqlite3WasmUri,
@ -78,10 +78,10 @@ class WasmDatabaseOpener {
// database name and can interpret the opfsExists and indexedDbExists
// fields we're getting from older workers accordingly.
if (result.opfsExists) {
existingDatabases.add((DatabaseLocation.opfs, databaseName));
existingDatabases.add((WebStorageApi.opfs, databaseName));
}
if (result.indexedDbExists) {
existingDatabases.add((DatabaseLocation.indexedDb, databaseName));
existingDatabases.add((WebStorageApi.indexedDb, databaseName));
}
}
}
@ -96,7 +96,7 @@ class WasmDatabaseOpener {
try {
await _probeDedicated();
} on Object {
_dedicatedWorker?.terminate();
_dedicatedWorker?.close();
_dedicatedWorker = null;
}
@ -106,14 +106,11 @@ class WasmDatabaseOpener {
Future<void> _probeDedicated() async {
if (supportsWorkers) {
final dedicatedWorker =
_dedicatedWorker = Worker(driftWorkerUri.toString());
_createCompatibilityCheck().sendToWorker(dedicatedWorker);
final dedicatedWorker = _dedicatedWorker =
_DriftWorker.dedicated(Worker(driftWorkerUri.toString()));
_createCompatibilityCheck().sendTo(dedicatedWorker.send);
final workerMessages = StreamQueue(
_readMessages(dedicatedWorker.onMessage, dedicatedWorker.onError));
final status = await workerMessages.nextNoError
final status = await dedicatedWorker.workerMessages.nextNoError
as DedicatedWorkerCompatibilityResult;
_handleCompatibilityResult(status);
@ -136,16 +133,13 @@ class WasmDatabaseOpener {
if (supportsSharedWorkers) {
final sharedWorker =
SharedWorker(driftWorkerUri.toString(), 'drift worker');
final port = _sharedWorker = sharedWorker.port!;
final sharedMessages =
StreamQueue(_readMessages(port.onMessage, sharedWorker.onError));
final port = sharedWorker.port!;
final shared = _sharedWorker = _DriftWorker.shared(sharedWorker, port);
// First, the shared worker will tell us which features it supports.
_createCompatibilityCheck().sendToPort(port);
final sharedFeatures =
await sharedMessages.nextNoError as SharedWorkerCompatibilityResult;
await sharedMessages.cancel();
final sharedFeatures = await shared.workerMessages.nextNoError
as SharedWorkerCompatibilityResult;
_handleCompatibilityResult(sharedFeatures);
@ -164,12 +158,50 @@ class WasmDatabaseOpener {
}
}
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);
}
}
void close() {
workerMessages.cancel();
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<(DatabaseLocation, String)> existingDatabases;
final List<ExistingDatabase> existingDatabases;
@override
final Set<MissingBrowserFeature> missingFeatures;
@ -208,16 +240,12 @@ final class _ProbeResult implements WasmProbeResult {
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.
@ -291,10 +319,21 @@ final class _ProbeResult implements WasmProbeResult {
}
@override
Future<void> deleteDatabase(
DatabaseLocation implementation, String name) async {
// TODO: implement deleteDatabase
throw UnimplementedError();
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);
await dedicated.workerMessages.nextNoError;
} else {
throw StateError(
'No dedicated worker available to delete OPFS database');
}
}
}
}

View File

@ -48,11 +48,11 @@ class DedicatedDriftWorker {
final existingServer = _servers.servers[dbName];
var indexedDbExists = false, opfsExists = false;
final existingDatabases = <(DatabaseLocation, String)>[];
final existingDatabases = <ExistingDatabase>[];
if (supportsOpfs) {
for (final database in await opfsDatabases()) {
existingDatabases.add((DatabaseLocation.opfs, database));
existingDatabases.add((WebStorageApi.opfs, database));
if (database == dbName) {
opfsExists = true;
@ -83,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;
}

View File

@ -30,6 +30,7 @@ sealed class WasmInitializationMessage {
DedicatedWorkerCompatibilityResult.fromJsPayload(payload!),
SharedWorkerCompatibilityResult.type =>
SharedWorkerCompatibilityResult.fromJsPayload(payload!),
DeleteDatabase.type => DeleteDatabase.fromJsPayload(payload!),
_ => throw ArgumentError('Unknown type $type'),
};
}
@ -62,7 +63,7 @@ sealed class CompatibilityResult extends WasmInitializationMessage {
/// 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<(DatabaseLocation, String)> existingDatabases;
final List<ExistingDatabase> existingDatabases;
final bool indexedDbExists;
final bool opfsExists;
@ -101,7 +102,7 @@ final class SharedWorkerCompatibilityResult extends CompatibilityResult {
final asList = payload as List;
final asBooleans = asList.cast<bool>();
final List<(DatabaseLocation, String)> existingDatabases;
final List<ExistingDatabase> existingDatabases;
if (asList.length > 5) {
existingDatabases =
EncodeLocations.readFromJs(asList[5] as List<dynamic>);
@ -251,7 +252,7 @@ final class DedicatedWorkerCompatibilityResult extends CompatibilityResult {
});
factory DedicatedWorkerCompatibilityResult.fromJsPayload(Object payload) {
final existingDatabases = <(DatabaseLocation, String)>[];
final existingDatabases = <ExistingDatabase>[];
if (hasProperty(payload, 'existing')) {
existingDatabases
@ -300,13 +301,51 @@ final class DedicatedWorkerCompatibilityResult extends CompatibilityResult {
}
}
extension EncodeLocations on List<(DatabaseLocation, String)> {
static List<(DatabaseLocation, String)> readFromJs(List<Object?> object) {
final existing = <(DatabaseLocation, String)>[];
final class StartFileSystemServer extends WasmInitializationMessage {
static const type = 'StartFileSystemServer';
final WorkerOptions sqlite3Options;
StartFileSystemServer(this.sqlite3Options);
factory StartFileSystemServer.fromJsPayload(Object payload) {
return StartFileSystemServer(payload as WorkerOptions);
}
@override
void sendTo(PostMessage sender) {
sender.sendTyped(type, sqlite3Options);
}
}
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((
DatabaseLocation.byName[getProperty(entry as Object, 'l')]!,
WebStorageApi.byName[getProperty(entry as Object, 'l')]!,
getProperty(entry, 'n'),
));
}
@ -328,23 +367,6 @@ extension EncodeLocations on List<(DatabaseLocation, String)> {
}
}
final class StartFileSystemServer extends WasmInitializationMessage {
static const type = 'StartFileSystemServer';
final WorkerOptions sqlite3Options;
StartFileSystemServer(this.sqlite3Options);
factory StartFileSystemServer.fromJsPayload(Object payload) {
return StartFileSystemServer(payload as WorkerOptions);
}
@override
void sendTo(PostMessage sender) {
sender.sendTyped(type, sqlite3Options);
}
}
extension on PostMessage {
void sendTyped(String type, Object? payload, [List<Object>? transfer]) {
final object = newObject<Object>();

View File

@ -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,6 +108,14 @@ 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) {
@ -132,6 +141,22 @@ Future<List<String>> opfsDatabases() async {
];
}
/// 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
@ -151,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 ||
@ -190,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());

View File

@ -82,11 +82,17 @@ enum WasmStorageImplementation {
inMemory,
}
enum DatabaseLocation {
/// 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;
static final byName = DatabaseLocation.values.asNameMap();
/// Cached [EnumByName.asNameMap] for [values].
static final byName = WebStorageApi.values.asNameMap();
}
/// An enumeration of features not supported by the current browsers.
@ -127,8 +133,20 @@ enum MissingBrowserFeature {
sharedArrayBuffers,
}
typedef ExistingDatabase = (DatabaseLocation, String);
/// 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.
@ -150,13 +168,21 @@ abstract interface class WasmProbeResult {
/// 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,
});
Future<void> deleteDatabase(DatabaseLocation implementation, String name);
/// Deletes an [ExistingDatabase] from storage.
///
/// This method should not be called while a connection to the database is
/// opened.
Future<void> deleteDatabase(ExistingDatabase database);
}
/// The result of opening a WASM database with default options.

View File

@ -108,11 +108,11 @@ class WasmDatabase extends DelegatedDatabase {
for (final (location, name) in probed.existingDatabases) {
if (name == databaseName) {
final implementationsForStorage = switch (location) {
DatabaseLocation.indexedDb => const [
WebStorageApi.indexedDb => const [
WasmStorageImplementation.sharedIndexedDb,
WasmStorageImplementation.unsafeIndexedDb
],
DatabaseLocation.opfs => const [
WebStorageApi.opfs => const [
WasmStorageImplementation.opfsShared,
WasmStorageImplementation.opfsLocks,
],

View File

@ -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]),
]);
}
}

View File

@ -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 ',
() {

View File

@ -29,6 +29,18 @@ 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');
@ -104,6 +116,7 @@ Future<String> _detectImplementations(String? _) async {
return json.encode({
'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(),
});
}