Skip to content

Commit 0738be7

Browse files
committed
Allow opening workers in parallel
1 parent 9ad2aa5 commit 0738be7

3 files changed

Lines changed: 37 additions & 23 deletions

File tree

.changeset/dirty-trees-add.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/web': patch
3+
---
4+
5+
Allow opening read workers for the OPFS WriteAhead file system in parallel, improving startup performance.

packages/web/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,12 @@ export class WASQLiteOpenFactory implements SQLOpenFactory {
150150
// This VFS supports concurrent reads, so we can open additional workers to host read-only connections for
151151
// concurrent reads / writes.
152152
const additionalReadersCount = this.options.additionalReaders ?? 1;
153+
154+
const additionalReaderPromises: Promise<DatabaseClient>[] = [];
153155
for (let i = 0; i < additionalReadersCount; i++) {
154-
const reader = await openDatabaseWorker(true);
155-
additionalReaders.push(reader);
156+
additionalReaderPromises.push(openDatabaseWorker(true));
156157
}
158+
additionalReaders.push(...(await Promise.all(additionalReaderPromises)));
157159
}
158160
} else {
159161
// Don't use a web worker. Instead, open the MultiDatabaseServer a worker would use locally.

packages/web/src/worker/db/MultiDatabaseServer.ts

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { getNavigatorLocks } from '../../shared/navigator.js';
55
import { RawSqliteConnection, RawWaSqliteDatabaseOptions } from '../../db/adapters/wa-sqlite/RawSqliteConnection.js';
66
import { ConcurrentSqliteConnection } from '../../db/adapters/wa-sqlite/ConcurrentConnection.js';
77
import { WASQLiteVFS } from '../../db/adapters/wa-sqlite/vfs.js';
8+
import { Mutex } from '@powersync/shared-internals';
89

910
const OPEN_DB_LOCK = 'open-wasqlite-db';
1011

@@ -18,7 +19,8 @@ export interface ConnectToMultiDatabaseServerOptions {
1819
* Shared state to manage multiple database connections hosted by a worker.
1920
*/
2021
export class MultiDatabaseServer {
21-
private activeDatabases = new Map<string, DatabaseServer>();
22+
readonly #activeDatabases = new Map<string, DatabaseServer>();
23+
readonly #localOpenLock = new Mutex();
2224

2325
constructor(readonly logger: PowerSyncLogger) {}
2426

@@ -38,7 +40,7 @@ export class MultiDatabaseServer {
3840

3941
async connectToExisting(name: string, lockName: string): Promise<ClientConnectionView> {
4042
return getNavigatorLocks().request(OPEN_DB_LOCK, async () => {
41-
const server = this.activeDatabases.get(name);
43+
const server = this.#activeDatabases.get(name);
4244
if (server == null) {
4345
throw new Error(`connectToExisting(${name}) failed because the worker doesn't own a database with that name.`);
4446
}
@@ -55,7 +57,7 @@ export class MultiDatabaseServer {
5557

5658
for (let count = 0; count < maxAttempts - 1; count++) {
5759
try {
58-
server = await this.databaseOpenAttempt(logger, options);
60+
server = await this.#databaseOpenAttempt(logger, options);
5961
} catch (error) {
6062
this.logger.log({
6163
level: LogLevels.warn,
@@ -67,24 +69,22 @@ export class MultiDatabaseServer {
6769
}
6870

6971
// Final attempt if we haven't been able to open the server - rethrow errors if we still can't open.
70-
server ??= await this.databaseOpenAttempt(logger, options);
72+
server ??= await this.#databaseOpenAttempt(logger, options);
7173
return server.connect(lockName);
7274
}
7375

74-
private async databaseOpenAttempt(
75-
logger: PowerSyncLogger,
76-
options: RawWaSqliteDatabaseOptions
77-
): Promise<DatabaseServer> {
78-
return getNavigatorLocks().request(OPEN_DB_LOCK, async () => {
79-
const { filename, readonly, vfs } = options;
80-
81-
let server: DatabaseServer | undefined = this.activeDatabases.get(filename);
76+
async #databaseOpenAttempt(logger: PowerSyncLogger, options: RawWaSqliteDatabaseOptions): Promise<DatabaseServer> {
77+
const { filename, readonly, vfs } = options;
78+
// We don't need navigator locks for shared workers because all queries run in this shared worker exclusively.
79+
// For read-only connections, we use a VFS that supports concurrent reads (so a single lock on the connection is
80+
// fine). In-memory databases either run in a shared worker or aren't shared across tabs at all, so the internal
81+
// lock is enough.
82+
const needsNavigatorLocks = !(isSharedWorker || readonly || vfs == WASQLiteVFS.InMemoryVfs);
83+
const activeDatabases = this.#activeDatabases;
84+
85+
async function openDatabase() {
86+
let server: DatabaseServer | undefined = activeDatabases.get(filename);
8287
if (server == null) {
83-
// We don't need navigator locks for shared workers because all queries run in this shared worker exclusively.
84-
// For read-only connections, we use a VFS that supports concurrent reads (so a single lock on the connection is
85-
// fine). In-memory databases either run in a shared worker or aren't shared across tabs at all, so the internal
86-
// lock is enough.
87-
const needsNavigatorLocks = !(isSharedWorker || readonly || vfs == WASQLiteVFS.InMemoryVfs);
8888
const connection = new RawSqliteConnection(options);
8989
const withSafeConcurrency = new ConcurrentSqliteConnection(connection, needsNavigatorLocks);
9090

@@ -101,21 +101,28 @@ export class MultiDatabaseServer {
101101
}
102102
returnLease();
103103

104-
const onClose = () => this.activeDatabases.delete(filename);
104+
const onClose = () => activeDatabases.delete(filename);
105105
server = new DatabaseServer({
106106
inner: withSafeConcurrency,
107107
logger,
108108
onClose
109109
});
110-
this.activeDatabases.set(filename, server);
110+
activeDatabases.set(filename, server);
111111
}
112112

113113
return server;
114-
});
114+
}
115+
116+
if (needsNavigatorLocks) {
117+
return getNavigatorLocks().request(OPEN_DB_LOCK, openDatabase);
118+
} else {
119+
// Even if we don't need navigator locks, this avoids a race between
120+
return this.#localOpenLock.runExclusive(openDatabase);
121+
}
115122
}
116123

117124
closeAll() {
118-
const existingDatabases = [...this.activeDatabases.values()];
125+
const existingDatabases = [...this.#activeDatabases.values()];
119126
return Promise.all(
120127
existingDatabases.map((db) => {
121128
db.forceClose();

0 commit comments

Comments
 (0)