Skip to content

Commit b4c38b7

Browse files
[Feature] Web Navigator Locks (#54)
Added Navigator locks for web mutexes
1 parent 0e9e9d2 commit b4c38b7

File tree

15 files changed

+355
-68
lines changed

15 files changed

+355
-68
lines changed

.github/workflows/release.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ name: Compile Assets and Create Draft Release
44
on:
55
push:
66
tags:
7-
# Trigger on tags beginning with 'v'
8-
- 'v*'
7+
# Trigger on sqlite_async tags
8+
- 'sqlite_async-v*'
99

1010
jobs:
1111
release:

CHANGELOG.md

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Change Log
2+
3+
All notable changes to this project will be documented in this file.
4+
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5+
6+
## 2024-07-10
7+
8+
### Changes
9+
10+
---
11+
12+
Packages with breaking changes:
13+
14+
- There are no breaking changes in this release.
15+
16+
Packages with other changes:
17+
18+
- [`sqlite_async` - `v0.8.1`](#sqlite_async---v081)
19+
- [`drift_sqlite_async` - `v0.1.0-alpha.3`](#drift_sqlite_async---v010-alpha3)
20+
21+
Packages with dependency updates only:
22+
23+
> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.
24+
25+
- `drift_sqlite_async` - `v0.1.0-alpha.3`
26+
27+
---
28+
29+
#### `sqlite_async` - `v0.8.1`
30+
31+
- **FEAT**: use navigator locks.
32+

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ This monorepo uses [melos](https://melos.invertase.dev/) to handle command and p
1313

1414
To configure the monorepo for development run `melos prepare` after cloning.
1515

16-
For detailed usage, check out the inner [sqlite_async](https://github.com/powersync-ja/sqlite_async.dart/tree/main/packages/sqlite_async) and [drift_sqlite_async](https://github.com/powersync-ja/sqlite_async.dart/tree/main/packages/drift_sqlite_async) packages.
16+
For detailed usage, check out the inner [sqlite_async](https://github.com/powersync-ja/sqlite_async.dart/tree/main/packages/sqlite_async) and [drift_sqlite_async](https://github.com/powersync-ja/sqlite_async.dart/tree/main/packages/drift_sqlite_async) packages.

packages/drift_sqlite_async/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.1.0-alpha.3
2+
3+
- Update a dependency to the latest release.
4+
15
## 0.1.0-alpha.2
26

37
- Update dependency `sqlite_async` to version 0.8.0.

packages/drift_sqlite_async/pubspec.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: drift_sqlite_async
2-
version: 0.1.0-alpha.2
2+
version: 0.1.0-alpha.3
33
homepage: https://github.com/powersync-ja/sqlite_async.dart
44
repository: https://github.com/powersync-ja/sqlite_async.dart
55
description: Use Drift with a sqlite_async database, allowing both to be used in the same application.
@@ -15,7 +15,7 @@ environment:
1515
sdk: ">=3.0.0 <4.0.0"
1616
dependencies:
1717
drift: ^2.15.0
18-
sqlite_async: ^0.8.0
18+
sqlite_async: ^0.8.1
1919
dev_dependencies:
2020
build_runner: ^2.4.8
2121
drift_dev: ^2.15.0

packages/sqlite_async/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.8.1
2+
3+
- Added Navigator locks for web `Mutex`s.
4+
15
## 0.8.0
26

37
- Added web support (web functionality is in beta)

packages/sqlite_async/lib/src/common/mutex.dart

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import 'package:sqlite_async/src/impl/mutex_impl.dart';
22

33
abstract class Mutex {
4-
factory Mutex() {
5-
return MutexImpl();
4+
factory Mutex(
5+
{
6+
/// An optional identifier for this Mutex instance.
7+
/// This could be used for platform specific logic or debugging purposes.
8+
/// Currently this is not used on native platforms.
9+
/// On web this will be used for the lock name if Navigator locks are available.
10+
String? identifier}) {
11+
return MutexImpl(identifier: identifier);
612
}
713

814
/// timeout is a timeout for acquiring the lock, not for the callback

packages/sqlite_async/lib/src/impl/stub_mutex.dart

+4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import 'package:sqlite_async/src/common/mutex.dart';
22

33
class MutexImpl implements Mutex {
4+
String? identifier;
5+
6+
MutexImpl({this.identifier});
7+
48
@override
59
Future<void> close() {
610
throw UnimplementedError();

packages/sqlite_async/lib/src/native/native_isolate_mutex.dart

+4-3
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import 'package:sqlite_async/src/common/mutex.dart';
77
import 'package:sqlite_async/src/common/port_channel.dart';
88

99
abstract class MutexImpl implements Mutex {
10-
factory MutexImpl() {
11-
return SimpleMutex();
10+
factory MutexImpl({String? identifier}) {
11+
return SimpleMutex(identifier: identifier);
1212
}
1313
}
1414

@@ -19,12 +19,13 @@ class SimpleMutex implements MutexImpl {
1919
// Adapted from https://github.com/tekartik/synchronized.dart/blob/master/synchronized/lib/src/basic_lock.dart
2020

2121
Future<dynamic>? last;
22+
String? identifier;
2223

2324
// Hack to make sure the Mutex is not copied to another isolate.
2425
// ignore: unused_field
2526
final Finalizer _f = Finalizer((_) {});
2627

27-
SimpleMutex();
28+
SimpleMutex({this.identifier});
2829

2930
bool get locked => last != null;
3031

Original file line numberDiff line numberDiff line change
@@ -1,13 +1,37 @@
1+
import 'dart:async';
2+
import 'dart:math';
3+
4+
import 'package:meta/meta.dart';
15
import 'package:mutex/mutex.dart' as mutex;
6+
import 'dart:js_interop';
7+
import 'dart:js_util' as js_util;
8+
// This allows for checking things like hasProperty without the need for depending on the `js` package
9+
import 'dart:js_interop_unsafe';
10+
import 'package:web/web.dart';
11+
212
import 'package:sqlite_async/src/common/mutex.dart';
313

14+
@JS('navigator')
15+
external Navigator get _navigator;
16+
417
/// Web implementation of [Mutex]
5-
/// This should use `navigator.locks` in future
618
class MutexImpl implements Mutex {
7-
late final mutex.Mutex m;
19+
late final mutex.Mutex fallback;
20+
String? identifier;
21+
final String _resolvedIdentifier;
822

9-
MutexImpl() {
10-
m = mutex.Mutex();
23+
MutexImpl({this.identifier})
24+
25+
/// On web a lock name is required for Navigator locks.
26+
/// Having exclusive Mutex instances requires a somewhat unique lock name.
27+
/// This provides a best effort unique identifier, if no identifier is provided.
28+
/// This should be fine for most use cases:
29+
/// - The uuid package could be added for better uniqueness if required.
30+
/// This would add another package dependency to `sqlite_async` which is potentially unnecessary at this point.
31+
/// An identifier should be supplied for better exclusion.
32+
: _resolvedIdentifier = identifier ??
33+
"${DateTime.now().microsecondsSinceEpoch}-${Random().nextDouble()}" {
34+
fallback = mutex.Mutex();
1135
}
1236

1337
@override
@@ -17,12 +41,111 @@ class MutexImpl implements Mutex {
1741

1842
@override
1943
Future<T> lock<T>(Future<T> Function() callback, {Duration? timeout}) {
20-
// Note this lock is only valid in a single web tab
21-
return m.protect(callback);
44+
if ((_navigator as JSObject).hasProperty('locks'.toJS).toDart) {
45+
return _webLock(callback, timeout: timeout);
46+
} else {
47+
return _fallbackLock(callback, timeout: timeout);
48+
}
49+
}
50+
51+
/// Locks the callback with a standard Mutex from the `mutex` package
52+
Future<T> _fallbackLock<T>(Future<T> Function() callback,
53+
{Duration? timeout}) {
54+
final completer = Completer<T>();
55+
// Need to implement timeout manually for this
56+
bool isTimedOut = false;
57+
Timer? timer;
58+
if (timeout != null) {
59+
timer = Timer(timeout, () {
60+
isTimedOut = true;
61+
completer
62+
.completeError(TimeoutException('Failed to acquire lock', timeout));
63+
});
64+
}
65+
66+
fallback.protect(() async {
67+
try {
68+
if (isTimedOut) {
69+
// Don't actually run logic
70+
return;
71+
}
72+
timer?.cancel();
73+
final result = await callback();
74+
completer.complete(result);
75+
} catch (ex) {
76+
completer.completeError(ex);
77+
}
78+
});
79+
80+
return completer.future;
81+
}
82+
83+
/// Locks the callback with web Navigator locks
84+
Future<T> _webLock<T>(Future<T> Function() callback,
85+
{Duration? timeout}) async {
86+
final lock = await _getWebLock(timeout);
87+
try {
88+
final result = await callback();
89+
return result;
90+
} finally {
91+
lock.release();
92+
}
93+
}
94+
95+
/// Passing the Dart callback directly to the JS Navigator can cause some weird
96+
/// context related bugs. Instead the JS lock callback will return a hold on the lock
97+
/// which is represented as a [HeldLock]. This hold can be used when wrapping the Dart
98+
/// callback to manage the JS lock.
99+
/// This is inspired and adapted from https://github.com/simolus3/sqlite3.dart/blob/7bdca77afd7be7159dbef70fd1ac5aa4996211a9/sqlite3_web/lib/src/locks.dart#L6
100+
Future<HeldLock> _getWebLock(Duration? timeout) {
101+
final gotLock = Completer<HeldLock>.sync();
102+
// Navigator locks can be timed out by using an AbortSignal
103+
final controller = AbortController();
104+
105+
Timer? timer;
106+
107+
if (timeout != null) {
108+
timer = Timer(timeout, () {
109+
gotLock
110+
.completeError(TimeoutException('Failed to acquire lock', timeout));
111+
controller.abort('Timeout'.toJS);
112+
});
113+
}
114+
115+
// If timeout occurred before the lock is available, then this callback should not be called.
116+
JSPromise jsCallback(JSAny lock) {
117+
timer?.cancel();
118+
119+
// Give the Held lock something to mark this Navigator lock as completed
120+
final jsCompleter = Completer.sync();
121+
gotLock.complete(HeldLock._(jsCompleter));
122+
return jsCompleter.future.toJS;
123+
}
124+
125+
final lockOptions = JSObject();
126+
lockOptions['signal'] = controller.signal;
127+
final promise = _navigator.locks
128+
.request(_resolvedIdentifier, lockOptions, jsCallback.toJS);
129+
// A timeout abort will throw an exception which needs to be handled.
130+
// There should not be any other unhandled lock errors.
131+
js_util.promiseToFuture(promise).catchError((error) {});
132+
return gotLock.future;
22133
}
23134

24135
@override
25136
Mutex open() {
26137
return this;
27138
}
28139
}
140+
141+
/// This represents a hold on an active Navigator lock.
142+
/// This is created inside the Navigator lock callback function and is used to release the lock
143+
/// from an external source.
144+
@internal
145+
class HeldLock {
146+
final Completer<void> _completer;
147+
148+
HeldLock._(this._completer);
149+
150+
void release() => _completer.complete();
151+
}

packages/sqlite_async/lib/src/web/web_sqlite_open_factory.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class DefaultSqliteOpenFactory
5555
// cases, we need to implement a mutex locally.
5656
final mutex = connection.access == AccessMode.throughSharedWorker
5757
? null
58-
: MutexImpl();
58+
: MutexImpl(identifier: path); // Use the DB path as a mutex identifier
5959

6060
return WebDatabase(connection.database, options.mutex ?? mutex);
6161
}

packages/sqlite_async/pubspec.yaml

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: sqlite_async
22
description: High-performance asynchronous interface for SQLite on Dart and Flutter.
3-
version: 0.8.0
3+
version: 0.8.1
44
repository: https://github.com/powersync-ja/sqlite_async.dart
55
environment:
66
sdk: ">=3.4.0 <4.0.0"
@@ -18,6 +18,7 @@ dependencies:
1818
collection: ^1.17.0
1919
mutex: ^3.1.0
2020
meta: ^1.10.0
21+
web: ^0.5.1
2122

2223
dev_dependencies:
2324
dcli: ^4.0.0

0 commit comments

Comments
 (0)