@@ -5,6 +5,7 @@ import { getNavigatorLocks } from '../../shared/navigator.js';
55import { RawSqliteConnection , RawWaSqliteDatabaseOptions } from '../../db/adapters/wa-sqlite/RawSqliteConnection.js' ;
66import { ConcurrentSqliteConnection } from '../../db/adapters/wa-sqlite/ConcurrentConnection.js' ;
77import { WASQLiteVFS } from '../../db/adapters/wa-sqlite/vfs.js' ;
8+ import { Mutex } from '@powersync/shared-internals' ;
89
910const 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 */
2021export 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