-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathweb_mutex.dart
149 lines (127 loc) · 4.75 KB
/
web_mutex.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import 'dart:async';
import 'dart:math';
import 'package:meta/meta.dart';
import 'package:mutex/mutex.dart' as mutex;
import 'dart:js_interop';
// This allows for checking things like hasProperty without the need for depending on the `js` package
import 'dart:js_interop_unsafe';
import 'package:web/web.dart';
import 'package:sqlite_async/src/common/mutex.dart';
@JS('navigator')
external Navigator get _navigator;
/// Web implementation of [Mutex]
class MutexImpl implements Mutex {
late final mutex.Mutex fallback;
final String resolvedIdentifier;
MutexImpl({String? identifier})
/// On web a lock name is required for Navigator locks.
/// Having exclusive Mutex instances requires a somewhat unique lock name.
/// This provides a best effort unique identifier, if no identifier is provided.
/// This should be fine for most use cases:
/// - The uuid package could be added for better uniqueness if required.
/// This would add another package dependency to `sqlite_async` which is potentially unnecessary at this point.
/// An identifier should be supplied for better exclusion.
: resolvedIdentifier = identifier ??
"${DateTime.now().microsecondsSinceEpoch}-${Random().nextDouble()}" {
fallback = mutex.Mutex();
}
@override
Future<void> close() async {
// This isn't relevant for web at the moment.
}
@override
Future<T> lock<T>(Future<T> Function() callback, {Duration? timeout}) {
if (_navigator.has('locks')) {
return _webLock(callback, timeout: timeout);
} else {
return _fallbackLock(callback, timeout: timeout);
}
}
/// Locks the callback with a standard Mutex from the `mutex` package
Future<T> _fallbackLock<T>(Future<T> Function() callback,
{Duration? timeout}) {
final completer = Completer<T>();
// Need to implement timeout manually for this
bool isTimedOut = false;
Timer? timer;
if (timeout != null) {
timer = Timer(timeout, () {
isTimedOut = true;
completer
.completeError(TimeoutException('Failed to acquire lock', timeout));
});
}
fallback.protect(() async {
try {
if (isTimedOut) {
// Don't actually run logic
return;
}
timer?.cancel();
final result = await callback();
completer.complete(result);
} catch (ex) {
completer.completeError(ex);
}
});
return completer.future;
}
/// Locks the callback with web Navigator locks
Future<T> _webLock<T>(Future<T> Function() callback,
{Duration? timeout}) async {
final lock = await _getWebLock(timeout);
try {
final result = await callback();
return result;
} finally {
lock.release();
}
}
/// Passing the Dart callback directly to the JS Navigator can cause some weird
/// context related bugs. Instead the JS lock callback will return a hold on the lock
/// which is represented as a [HeldLock]. This hold can be used when wrapping the Dart
/// callback to manage the JS lock.
/// This is inspired and adapted from https://github.com/simolus3/sqlite3.dart/blob/7bdca77afd7be7159dbef70fd1ac5aa4996211a9/sqlite3_web/lib/src/locks.dart#L6
Future<HeldLock> _getWebLock(Duration? timeout) {
final gotLock = Completer<HeldLock>.sync();
// Navigator locks can be timed out by using an AbortSignal
final controller = AbortController();
Timer? timer;
if (timeout != null) {
timer = Timer(timeout, () {
gotLock
.completeError(TimeoutException('Failed to acquire lock', timeout));
controller.abort('Timeout'.toJS);
});
}
// If timeout occurred before the lock is available, then this callback should not be called.
JSPromise jsCallback(JSAny lock) {
timer?.cancel();
// Give the Held lock something to mark this Navigator lock as completed
final jsCompleter = Completer.sync();
gotLock.complete(HeldLock._(jsCompleter));
return jsCompleter.future.toJS;
}
final lockOptions = JSObject();
lockOptions['signal'] = controller.signal;
final promise = _navigator.locks
.request(resolvedIdentifier, lockOptions, jsCallback.toJS);
// A timeout abort will throw an exception which needs to be handled.
// There should not be any other unhandled lock errors.
promise.toDart.catchError((error) => null);
return gotLock.future;
}
@override
Mutex open() {
return this;
}
}
/// This represents a hold on an active Navigator lock.
/// This is created inside the Navigator lock callback function and is used to release the lock
/// from an external source.
@internal
class HeldLock {
final Completer<void> _completer;
HeldLock._(this._completer);
void release() => _completer.complete();
}