@@ -14,6 +14,7 @@ import { SqliteEncryptionError } from './errors.js';
1414import { SQLiteOPFSAztecMap } from './map.js' ;
1515import type { ResultRow , SqlValue , WorkerRequest , WorkerResponse } from './messages.js' ;
1616import { SQLiteOPFSAztecMultiMap } from './multi_map.js' ;
17+ import { type PoolLockLease , acquirePoolLock , normalizePoolDirectory } from './pool_lock.js' ;
1718import { SQLiteOPFSAztecSet } from './set.js' ;
1819import { SQLiteOPFSAztecSingleton } from './singleton.js' ;
1920
@@ -36,12 +37,14 @@ export class AztecSQLiteOPFSStore implements AztecAsyncKVStore {
3637 #nextId = 0 ;
3738 #inTx = false ;
3839 #closed = false ;
40+ #workerFailed = false ;
3941
4042 private constructor (
4143 worker : Worker ,
4244 name : string ,
4345 log : Logger ,
4446 public readonly isEphemeral : boolean ,
47+ private readonly poolLock ?: PoolLockLease ,
4548 ) {
4649 this . #worker = worker ;
4750 this . #name = name ;
@@ -57,6 +60,7 @@ export class AztecSQLiteOPFSStore implements AztecAsyncKVStore {
5760 handler . resolve ( ev . data ) ;
5861 } ;
5962 this . #worker. onerror = ev => {
63+ this . #workerFailed = true ;
6064 this . #log. error ( `SQLite worker crashed: ${ ev . message } ` ) ;
6165 this . #rejectPending( `SQLite worker crashed: ${ ev . message } ` ) ;
6266 } ;
@@ -70,6 +74,10 @@ export class AztecSQLiteOPFSStore implements AztecAsyncKVStore {
7074 * required when multiple stores coexist in the same tab, because the SAH Pool holds
7175 * an exclusive lock on its directory.
7276 *
77+ * Persistent stores hold an origin-wide Web Lock for the pool directory until close
78+ * or delete. If another store instance already owns it, open fails immediately with
79+ * `SqlitePoolBusyError`.
80+ *
7381 * Pass `encryptionKey` (exactly 32 bytes) to enable at-rest encryption via sqlite3mc's
7482 * ChaCha20 page cipher. The key buffer is **transferred** to the worker — its
7583 * ArrayBuffer detaches on the caller side after `postMessage`. This is intentional:
@@ -102,22 +110,26 @@ export class AztecSQLiteOPFSStore implements AztecAsyncKVStore {
102110 log . debug (
103111 `Opening SQLite-OPFS ${ ephemeral ? 'ephemeral ' : '' } ${ encryptionKey ? 'encrypted ' : '' } database ${ dbName } ` ,
104112 ) ;
105- const worker = new Worker ( new URL ( './worker.js' , import . meta. url ) , { type : 'module' } ) ;
106- const store = new AztecSQLiteOPFSStore ( worker , dbName , log , ephemeral ) ;
107- // Transfer (not clone) the key buffer to the worker so we don't leave a
108- // second copy on the main thread. Caveat: this detaches the caller's
109- // encryptionKey.buffer — subsequent reads from the same Uint8Array are empty.
110- const transfer = encryptionKey ? [ encryptionKey . buffer as ArrayBuffer ] : undefined ;
113+ const effectivePoolDirectory = ephemeral ? undefined : normalizePoolDirectory ( poolDirectory ) ;
114+ const poolLock = effectivePoolDirectory ? await acquirePoolLock ( effectivePoolDirectory ) : undefined ;
115+ let worker : Worker | undefined ;
111116 try {
117+ worker = new Worker ( new URL ( './worker.js' , import . meta. url ) , { type : 'module' } ) ;
118+ const store = new AztecSQLiteOPFSStore ( worker , dbName , log , ephemeral , poolLock ) ;
119+ // Transfer (not clone) the key buffer to the worker so we don't leave a
120+ // second copy on the main thread. Caveat: this detaches the caller's
121+ // encryptionKey.buffer — subsequent reads from the same Uint8Array are empty.
122+ const transfer = encryptionKey ? [ encryptionKey . buffer as ArrayBuffer ] : undefined ;
112123 await store . #sendRequest(
113- { type : 'init' , id : store . #allocId( ) , dbName, ephemeral, poolDirectory, encryptionKey } ,
124+ { type : 'init' , id : store . #allocId( ) , dbName, ephemeral, poolDirectory : effectivePoolDirectory , encryptionKey } ,
114125 transfer ,
115126 ) ;
127+ return store ;
116128 } catch ( err ) {
117- worker . terminate ( ) ;
129+ worker ?. terminate ( ) ;
130+ await poolLock ?. release ( ) ;
118131 throw err ;
119132 }
120- return store ;
121133 }
122134
123135 openMap < K extends Key , V extends Value > ( name : string ) : AztecAsyncMap < K , V > {
@@ -179,12 +191,16 @@ export class AztecSQLiteOPFSStore implements AztecAsyncKVStore {
179191 return ;
180192 }
181193 this . #closed = true ;
182- await this . #txQueue. end ( ) ;
183- await this . #sendRequest( { type : 'deleteDb' , id : this . #allocId( ) , dbName : this . #name } ) . catch ( err =>
184- this . #log. warn ( `SQLite deleteDb failed: ${ err instanceof Error ? err . message : err } ` ) ,
185- ) ;
186- this . #worker. terminate ( ) ;
187- this . #rejectPending( 'SQLite store deleted' ) ;
194+ try {
195+ await this . #txQueue. end ( ) ;
196+ await this . #sendRequest( { type : 'deleteDb' , id : this . #allocId( ) , dbName : this . #name } ) . catch ( err =>
197+ this . #log. warn ( `SQLite deleteDb failed: ${ err instanceof Error ? err . message : err } ` ) ,
198+ ) ;
199+ } finally {
200+ this . #worker. terminate ( ) ;
201+ this . #rejectPending( 'SQLite store deleted' ) ;
202+ await this . poolLock ?. release ( ) ;
203+ }
188204 }
189205
190206 /**
@@ -204,10 +220,14 @@ export class AztecSQLiteOPFSStore implements AztecAsyncKVStore {
204220 return ;
205221 }
206222 this . #closed = true ;
207- await this . #txQueue. end ( ) ;
208- await this . #sendRequest( { type : 'close' , id : this . #allocId( ) } ) . catch ( ( ) => { } ) ;
209- this . #worker. terminate ( ) ;
210- this . #rejectPending( 'SQLite store closed' ) ;
223+ try {
224+ await this . #txQueue. end ( ) ;
225+ await this . #sendRequest( { type : 'close' , id : this . #allocId( ) } ) . catch ( ( ) => { } ) ;
226+ } finally {
227+ this . #worker. terminate ( ) ;
228+ this . #rejectPending( 'SQLite store closed' ) ;
229+ await this . poolLock ?. release ( ) ;
230+ }
211231 }
212232
213233 backupTo ( _dstPath : string , _compact ?: boolean ) : Promise < void > {
@@ -268,6 +288,9 @@ export class AztecSQLiteOPFSStore implements AztecAsyncKVStore {
268288 }
269289
270290 #sendRequest( req : WorkerRequest , transfer ?: Transferable [ ] ) : Promise < WorkerResponse > {
291+ if ( this . #workerFailed) {
292+ return Promise . reject ( new Error ( 'SQLite worker has crashed' ) ) ;
293+ }
271294 return new Promise < WorkerResponse > ( ( resolve , reject ) => {
272295 this . #pending. set ( req . id , {
273296 resolve : resp => {
0 commit comments