Skip to content

Commit b899c96

Browse files
authored
Merge pull request simolus3#275 from powersync-ja/sqlite3-web-without-workers
`sqlite3_web`: Support opening databases without workers
2 parents f4a9140 + d433d19 commit b899c96

File tree

6 files changed

+144
-27
lines changed

6 files changed

+144
-27
lines changed

sqlite3_web/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
## 0.2.1
22

33
- Add `WebSqlite.deleteDatabase` to delete databases.
4+
- Support opening databases without workers.
45

56
## 0.2.0
67

sqlite3_web/lib/src/client.dart

+69-22
Original file line numberDiff line numberDiff line change
@@ -227,15 +227,19 @@ final class WorkerConnection extends ProtocolChannel {
227227
final class DatabaseClient implements WebSqlite {
228228
final Uri workerUri;
229229
final Uri wasmUri;
230+
final DatabaseController _localController;
230231

231232
final Lock _startWorkersLock = Lock();
232233
bool _startedWorkers = false;
233234
WorkerConnection? _connectionToDedicated;
234235
WorkerConnection? _connectionToShared;
235236
WorkerConnection? _connectionToDedicatedInShared;
237+
238+
WorkerConnection? _connectionToLocal;
239+
236240
final Set<MissingBrowserFeature> _missingFeatures = {};
237241

238-
DatabaseClient(this.workerUri, this.wasmUri);
242+
DatabaseClient(this.workerUri, this.wasmUri, this._localController);
239243

240244
Future<void> startWorkers() {
241245
return _startWorkersLock.synchronized(() async {
@@ -244,36 +248,52 @@ final class DatabaseClient implements WebSqlite {
244248
}
245249
_startedWorkers = true;
246250

247-
if (globalContext.has('Worker')) {
248-
final dedicated = Worker(
251+
await _startDedicated();
252+
await _startShared();
253+
});
254+
}
255+
256+
Future<void> _startDedicated() async {
257+
if (globalContext.has('Worker')) {
258+
final Worker dedicated;
259+
try {
260+
dedicated = Worker(
249261
workerUri.toString().toJS,
250262
WorkerOptions(name: 'sqlite3_worker'),
251263
);
252-
253-
final (endpoint, channel) = await createChannel();
254-
ConnectRequest(endpoint: endpoint, requestId: 0)
255-
.sendToWorker(dedicated);
256-
257-
_connectionToDedicated =
258-
WorkerConnection(channel.injectErrorsFrom(dedicated));
259-
} else {
264+
} on Object {
260265
_missingFeatures.add(MissingBrowserFeature.dedicatedWorkers);
266+
return;
261267
}
262268

263-
if (globalContext.has('SharedWorker')) {
264-
final shared = SharedWorker(workerUri.toString().toJS);
265-
shared.port.start();
269+
final (endpoint, channel) = await createChannel();
270+
ConnectRequest(endpoint: endpoint, requestId: 0).sendToWorker(dedicated);
266271

267-
final (endpoint, channel) = await createChannel();
268-
ConnectRequest(endpoint: endpoint, requestId: 0)
269-
.sendToPort(shared.port);
272+
_connectionToDedicated =
273+
WorkerConnection(channel.injectErrorsFrom(dedicated));
274+
} else {
275+
_missingFeatures.add(MissingBrowserFeature.dedicatedWorkers);
276+
}
277+
}
270278

271-
_connectionToShared =
272-
WorkerConnection(channel.injectErrorsFrom(shared));
273-
} else {
279+
Future<void> _startShared() async {
280+
if (globalContext.has('SharedWorker')) {
281+
final SharedWorker shared;
282+
try {
283+
shared = SharedWorker(workerUri.toString().toJS);
284+
shared.port.start();
285+
} on Object {
274286
_missingFeatures.add(MissingBrowserFeature.sharedWorkers);
287+
return;
275288
}
276-
});
289+
290+
final (endpoint, channel) = await createChannel();
291+
ConnectRequest(endpoint: endpoint, requestId: 0).sendToPort(shared.port);
292+
293+
_connectionToShared = WorkerConnection(channel.injectErrorsFrom(shared));
294+
} else {
295+
_missingFeatures.add(MissingBrowserFeature.sharedWorkers);
296+
}
277297
}
278298

279299
Future<WorkerConnection> _connectToDedicatedInShared() {
@@ -291,6 +311,22 @@ final class DatabaseClient implements WebSqlite {
291311
});
292312
}
293313

314+
Future<WorkerConnection> _connectToLocal() async {
315+
return _startWorkersLock.synchronized(() async {
316+
if (_connectionToLocal case final conn?) {
317+
return conn;
318+
}
319+
320+
final local = Local();
321+
final (endpoint, channel) = await createChannel();
322+
WorkerRunner(_localController, environment: local).handleRequests();
323+
local
324+
.addTopLevelMessage(ConnectRequest(requestId: 0, endpoint: endpoint));
325+
326+
return _connectionToLocal = WorkerConnection(channel);
327+
});
328+
}
329+
294330
@override
295331
Future<void> deleteDatabase(
296332
{required String name, required StorageMode storage}) async {
@@ -310,6 +346,7 @@ final class DatabaseClient implements WebSqlite {
310346

311347
final existing = <ExistingDatabase>{};
312348
final available = <(StorageMode, AccessMode)>[];
349+
var workersReportedIndexedDbSupport = false;
313350

314351
if (_connectionToDedicated case final connection?) {
315352
final response = await connection.sendRequest(
@@ -327,6 +364,8 @@ final class DatabaseClient implements WebSqlite {
327364
if (result.canUseIndexedDb) {
328365
available
329366
.add((StorageMode.indexedDb, AccessMode.throughDedicatedWorker));
367+
368+
workersReportedIndexedDbSupport = true;
330369
} else {
331370
_missingFeatures.add(MissingBrowserFeature.indexedDb);
332371
}
@@ -363,6 +402,7 @@ final class DatabaseClient implements WebSqlite {
363402
final result = CompatibilityResult.fromJS(response.response as JSObject);
364403

365404
if (result.canUseIndexedDb) {
405+
workersReportedIndexedDbSupport = true;
366406
available.add((StorageMode.indexedDb, AccessMode.throughSharedWorker));
367407
} else {
368408
_missingFeatures.add(MissingBrowserFeature.indexedDb);
@@ -383,6 +423,12 @@ final class DatabaseClient implements WebSqlite {
383423
}
384424
}
385425

426+
available.add((StorageMode.inMemory, AccessMode.inCurrentContext));
427+
if (workersReportedIndexedDbSupport || await checkIndexedDbSupport()) {
428+
// If the workers can use IndexedDb, so can we.
429+
available.add((StorageMode.indexedDb, AccessMode.inCurrentContext));
430+
}
431+
386432
return FeatureDetectionResult(
387433
missingFeatures: _missingFeatures.toList(),
388434
existingDatabases: existing.toList(),
@@ -425,7 +471,8 @@ final class DatabaseClient implements WebSqlite {
425471
connection = _connectionToDedicated!;
426472
shared = false;
427473
case AccessMode.inCurrentContext:
428-
throw UnimplementedError('todo: Open database locally');
474+
connection = await _connectToLocal();
475+
shared = false;
429476
}
430477

431478
final response = await connection.sendRequest(

sqlite3_web/lib/src/database.dart

+41-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import 'worker.dart';
99

1010
/// A controller responsible for opening databases in the worker.
1111
abstract base class DatabaseController {
12+
/// Constant base constructor.
13+
const DatabaseController();
14+
1215
/// Loads a wasm module from the given [uri] with the specified [headers].
1316
Future<WasmSqlite3> loadWasmModule(Uri uri,
1417
{Map<String, String>? headers}) async {
@@ -199,11 +202,17 @@ abstract class WebSqlite {
199202

200203
/// Opens a [WebSqlite] instance by connecting to the given [worker] and
201204
/// using the [wasmModule] url to load sqlite3.
205+
///
206+
/// The [controller] is used when connecting to a sqlite3 database without
207+
/// using workers. It should typically be the same implementation as the one
208+
/// passed to [workerEntrypoint].
202209
static WebSqlite open({
203210
required Uri worker,
204211
required Uri wasmModule,
212+
DatabaseController? controller,
205213
}) {
206-
return DatabaseClient(worker, wasmModule);
214+
return DatabaseClient(
215+
worker, wasmModule, controller ?? const _DefaultDatabaseController());
207216
}
208217

209218
/// Connects to an endpoint previously obtained with [Database.additionalConnection].
@@ -218,7 +227,37 @@ abstract class WebSqlite {
218227
/// was called. This limitation does not exist for databases hosted by shared
219228
/// workers.
220229
static Future<Database> connectToPort(SqliteWebEndpoint endpoint) {
221-
final client = DatabaseClient(Uri.base, Uri.base);
230+
final client =
231+
DatabaseClient(Uri.base, Uri.base, const _DefaultDatabaseController());
222232
return client.connectToExisting(endpoint);
223233
}
224234
}
235+
236+
final class _DefaultDatabaseController extends DatabaseController {
237+
const _DefaultDatabaseController();
238+
239+
@override
240+
Future<JSAny?> handleCustomRequest(
241+
ClientConnection connection, JSAny? request) {
242+
throw UnimplementedError();
243+
}
244+
245+
@override
246+
Future<WorkerDatabase> openDatabase(
247+
WasmSqlite3 sqlite3, String path, String vfs) async {
248+
return _DefaultWorkerDatabase(sqlite3.open(path, vfs: vfs));
249+
}
250+
}
251+
252+
final class _DefaultWorkerDatabase extends WorkerDatabase {
253+
@override
254+
final CommonDatabase database;
255+
256+
_DefaultWorkerDatabase(this.database);
257+
258+
@override
259+
Future<JSAny?> handleCustomRequest(
260+
ClientConnection connection, JSAny? request) {
261+
throw UnimplementedError();
262+
}
263+
}

sqlite3_web/lib/src/worker.dart

+23-1
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,27 @@ final class Shared extends WorkerEnvironment {
105105
}
106106
}
107107

108+
/// A fake worker environment running in the same context as the main
109+
/// application.
110+
///
111+
/// This allows using a communication channel based on message ports regardless
112+
/// of where the database is hosted. While that adds overhead, a local
113+
/// environment is only used as a fallback if workers are unavailable.
114+
final class Local extends WorkerEnvironment {
115+
final StreamController<Message> _messages = StreamController();
116+
117+
Local() : super._();
118+
119+
void addTopLevelMessage(Message message) {
120+
_messages.add(message);
121+
}
122+
123+
@override
124+
Stream<Message> get topLevelRequests {
125+
return _messages.stream;
126+
}
127+
}
128+
108129
/// A database opened by a client.
109130
final class _ConnectionDatabase {
110131
final DatabaseState database;
@@ -429,7 +450,8 @@ final class WorkerRunner {
429450
/// a shared context that can use synchronous JS APIs.
430451
Worker? _innerWorker;
431452

432-
WorkerRunner(this._controller) : _environment = WorkerEnvironment();
453+
WorkerRunner(this._controller, {WorkerEnvironment? environment})
454+
: _environment = environment ?? WorkerEnvironment();
433455

434456
void handleRequests() async {
435457
await for (final message in _environment.topLevelRequests) {

sqlite3_web/test/integration_test.dart

+7-2
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,13 @@ enum Browser {
4545
final available = <(StorageMode, AccessMode)>{};
4646
for (final storage in StorageMode.values) {
4747
for (final access in AccessMode.values) {
48-
if (access != AccessMode.inCurrentContext &&
49-
!unsupportedImplementations.contains((storage, access))) {
48+
if (access == AccessMode.inCurrentContext &&
49+
storage == StorageMode.opfs) {
50+
// OPFS access is only available in workers.
51+
continue;
52+
}
53+
54+
if (!unsupportedImplementations.contains((storage, access))) {
5055
available.add((storage, access));
5156
}
5257
}

sqlite3_web/web/main.dart

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import 'dart:typed_data';
66

77
import 'package:sqlite3_web/sqlite3_web.dart';
88

9+
import 'controller.dart';
10+
911
final sqlite3WasmUri = Uri.parse('sqlite3.wasm');
1012
final workerUri = Uri.parse('worker.dart.js');
1113
const databaseName = 'database';
@@ -127,6 +129,7 @@ WebSqlite initializeSqlite() {
127129
return webSqlite ??= WebSqlite.open(
128130
worker: workerUri,
129131
wasmModule: sqlite3WasmUri,
132+
controller: ExampleController(isInWorker: false),
130133
);
131134
}
132135

0 commit comments

Comments
 (0)