From 9486ae1420e8681047e4a0c355e2c00f93e0883c Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 3 Jun 2025 10:55:01 +0200 Subject: [PATCH 1/2] Shared worker: Release mutex when tab closes --- packages/sqlite_async/build.yaml | 9 ++++ packages/sqlite_async/example/web/index.html | 29 +++++++++++++ packages/sqlite_async/example/web/main.dart | 41 +++++++++++++++++++ packages/sqlite_async/example/web/worker.dart | 6 +++ .../lib/src/web/worker/worker_utils.dart | 26 ++++++++++++ 5 files changed, 111 insertions(+) create mode 100644 packages/sqlite_async/build.yaml create mode 100644 packages/sqlite_async/example/web/index.html create mode 100644 packages/sqlite_async/example/web/main.dart create mode 100644 packages/sqlite_async/example/web/worker.dart diff --git a/packages/sqlite_async/build.yaml b/packages/sqlite_async/build.yaml new file mode 100644 index 0000000..91e9a3d --- /dev/null +++ b/packages/sqlite_async/build.yaml @@ -0,0 +1,9 @@ +targets: + $default: + builders: + build_web_compilers:entrypoint: + options: + # Workers can't be compiled with dartdevc, so use dart2js for the example + compiler: dart2js + generate_for: + - example/web/** diff --git a/packages/sqlite_async/example/web/index.html b/packages/sqlite_async/example/web/index.html new file mode 100644 index 0000000..4bc69de --- /dev/null +++ b/packages/sqlite_async/example/web/index.html @@ -0,0 +1,29 @@ + + + + + + sqlite_async web demo + + + + + +

sqlite_async demo

+ +
+This page is used to test the sqlite_async package on the web. +Use the console to open and interact with databases. + +
+
+const db = await open('test.db');
+const lock = await write_lock(db);
+release_lock(lock);
+
+
+
+ + + + diff --git a/packages/sqlite_async/example/web/main.dart b/packages/sqlite_async/example/web/main.dart new file mode 100644 index 0000000..95bd1f1 --- /dev/null +++ b/packages/sqlite_async/example/web/main.dart @@ -0,0 +1,41 @@ +import 'dart:async'; +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; + +import 'package:sqlite_async/sqlite_async.dart'; + +void main() { + globalContext['open'] = (String path) { + return Future(() async { + final db = SqliteDatabase( + path: path, + options: SqliteOptions( + webSqliteOptions: WebSqliteOptions( + wasmUri: + 'https://cdn.jsdelivr.net/npm/@powersync/dart-wasm-bundles@latest/dist/sqlite3.wasm', + workerUri: 'worker.dart.js', + ), + ), + ); + await db.initialize(); + return db.toJSBox; + }).toJS; + }.toJS; + + globalContext['write_lock'] = (JSBoxedDartObject db) { + final hasLock = Completer(); + final completer = Completer(); + + (db.toDart as SqliteDatabase).writeLock((_) async { + print('has write lock!'); + hasLock.complete(); + await completer.future; + }); + + return hasLock.future.then((_) => completer.toJSBox).toJS; + }.toJS; + + globalContext['release_lock'] = (JSBoxedDartObject db) { + (db.toDart as Completer).complete(); + }.toJS; +} diff --git a/packages/sqlite_async/example/web/worker.dart b/packages/sqlite_async/example/web/worker.dart new file mode 100644 index 0000000..481455d --- /dev/null +++ b/packages/sqlite_async/example/web/worker.dart @@ -0,0 +1,6 @@ +import 'package:sqlite_async/sqlite3_web.dart'; +import 'package:sqlite_async/sqlite3_web_worker.dart'; + +void main() { + WebSqlite.workerEntrypoint(controller: AsyncSqliteController()); +} diff --git a/packages/sqlite_async/lib/src/web/worker/worker_utils.dart b/packages/sqlite_async/lib/src/web/worker/worker_utils.dart index af39747..3ecb257 100644 --- a/packages/sqlite_async/lib/src/web/worker/worker_utils.dart +++ b/packages/sqlite_async/lib/src/web/worker/worker_utils.dart @@ -56,9 +56,27 @@ class AsyncSqliteDatabase extends WorkerDatabase { // these requests for shared workers, so we can assume each database is only // opened once and we don't need web locks here. final mutex = ReadWriteMutex(); + final Map _state = {}; AsyncSqliteDatabase({required this.database}); + _ConnectionState _findState(ClientConnection connection) { + return _state.putIfAbsent(connection, _ConnectionState.new); + } + + void _markHoldsMutex(ClientConnection connection) { + final state = _findState(connection); + state.holdsMutex = true; + if (!state.hasOnCloseListener) { + state.hasOnCloseListener = true; + connection.closed.then((_) { + if (state.holdsMutex) { + mutex.release(); + } + }); + } + } + @override Future handleCustomRequest( ClientConnection connection, JSAny? request) async { @@ -67,9 +85,12 @@ class AsyncSqliteDatabase extends WorkerDatabase { switch (message.kind) { case CustomDatabaseMessageKind.requestSharedLock: await mutex.acquireRead(); + _markHoldsMutex(connection); case CustomDatabaseMessageKind.requestExclusiveLock: await mutex.acquireWrite(); + _markHoldsMutex(connection); case CustomDatabaseMessageKind.releaseLock: + _findState(connection).holdsMutex = false; mutex.release(); case CustomDatabaseMessageKind.lockObtained: throw UnsupportedError('This is a response, not a request'); @@ -123,3 +144,8 @@ class AsyncSqliteDatabase extends WorkerDatabase { return resultSetMap; } } + +final class _ConnectionState { + bool hasOnCloseListener = false; + bool holdsMutex = false; +} From cfdb89c74e6da505702dbcc706dd06df5364793e Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 3 Jun 2025 11:10:24 +0200 Subject: [PATCH 2/2] Actually we want to compile tests too --- packages/sqlite_async/build.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/sqlite_async/build.yaml b/packages/sqlite_async/build.yaml index 91e9a3d..0774cc7 100644 --- a/packages/sqlite_async/build.yaml +++ b/packages/sqlite_async/build.yaml @@ -5,5 +5,3 @@ targets: options: # Workers can't be compiled with dartdevc, so use dart2js for the example compiler: dart2js - generate_for: - - example/web/**