@@ -76,6 +76,10 @@ export type WrappedSyncPort = {
7676 db ?: DBAdapter ;
7777 currentSubscriptions : SubscribedStream [ ] ;
7878 closeListeners : ( ( ) => void | Promise < void > ) [ ] ;
79+ /**
80+ * If we can use Navigator locks to detect if the client has closed.
81+ */
82+ isProtectedFromClose : boolean ;
7983} ;
8084
8185/**
@@ -142,7 +146,9 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
142146 createSyncImplementation : async ( ) => {
143147 return this . portMutex . runExclusive ( async ( ) => {
144148 await this . waitForReady ( ) ;
149+ this . logger . debug ( 'Creating sync implementation' ) ;
145150 if ( ! this . dbAdapter ) {
151+ this . logger . debug ( 'Opening internal DB' ) ;
146152 await this . openInternalDB ( ) ;
147153 }
148154
@@ -171,6 +177,19 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
171177 return this . connectionManager . syncStreamImplementation ?. isConnected ?? false ;
172178 }
173179
180+ /**
181+ * Gets the last client port which we know is safe from unexpected closes.
182+ */
183+ protected get lastWrappedPort ( ) : WrappedSyncPort | undefined {
184+ // Find the last port which is protected from close
185+ for ( let i = this . ports . length - 1 ; i >= 0 ; i -- ) {
186+ if ( this . ports [ i ] . isProtectedFromClose ) {
187+ return this . ports [ i ] ;
188+ }
189+ }
190+ return ;
191+ }
192+
174193 async waitForStatus ( status : SyncStatusOptions ) : Promise < void > {
175194 return this . withSyncImplementation ( async ( sync ) => {
176195 return sync . waitForStatus ( status ) ;
@@ -276,7 +295,8 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
276295 port,
277296 clientProvider : Comlink . wrap < AbstractSharedSyncClientProvider > ( port ) ,
278297 currentSubscriptions : [ ] ,
279- closeListeners : [ ]
298+ closeListeners : [ ] ,
299+ isProtectedFromClose : false
280300 } satisfies WrappedSyncPort ;
281301 this . ports . push ( portProvider ) ;
282302
@@ -298,11 +318,11 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
298318 // Remove the port within a mutex context.
299319 // Warns if the port is not found. This should not happen in practice.
300320 // We return early if the port is not found.
301- const { trackedPort , shouldReconnect } = await this . portMutex . runExclusive ( async ( ) => {
321+ return await this . portMutex . runExclusive ( async ( ) => {
302322 const index = this . ports . findIndex ( ( p ) => p == port ) ;
303323 if ( index < 0 ) {
304324 this . logger . warn ( `Could not remove port ${ port } since it is not present in active ports.` ) ;
305- return { } ;
325+ return ( ) => { } ;
306326 }
307327
308328 const trackedPort = this . ports [ index ] ;
@@ -322,7 +342,6 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
322342 } ) ;
323343
324344 const shouldReconnect = ! ! this . connectionManager . syncStreamImplementation && this . ports . length > 0 ;
325-
326345 /**
327346 * If the current database adapter is the one that is being closed, we need to disconnect from the backend.
328347 * We can disconnect in the portMutex lock. This ensures the disconnect is not affected by potential other
@@ -338,30 +357,25 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
338357 . catch ( ( ex ) => this . logger . warn ( 'Error while disconnecting. Will attempt to reconnect.' , ex ) ) ;
339358 }
340359
341- return {
342- shouldReconnect,
343- trackedPort
344- } ;
345- } ) ;
346-
347- if ( ! trackedPort ) {
348- // We could not find the port to remove
349- return ( ) => { } ;
350- }
351-
352- for ( const closeListener of trackedPort . closeListeners ) {
353- await closeListener ( ) ;
354- }
360+ for ( const closeListener of trackedPort . closeListeners ) {
361+ await closeListener ( ) ;
362+ }
355363
356- if ( shouldReconnect ) {
357- await this . connectionManager . connect ( CONNECTOR_PLACEHOLDER , this . lastConnectOptions ?? { } ) ;
358- }
364+ try {
365+ await trackedPort . db ?. close ( ) ;
366+ } catch ( ex ) {
367+ this . logger . warn ( 'error closing database' , ex ) ;
368+ }
359369
360- // Re-index subscriptions, the subscriptions of the removed port would no longer be considered.
361- this . collectActiveSubscriptions ( ) ;
370+ // Re-index subscriptions, the subscriptions of the removed port would no longer be considered.
371+ this . collectActiveSubscriptions ( ) ;
372+ if ( shouldReconnect ) {
373+ // The internals of this needs a port mutex lock. It should be safe to start this operation here, but we cannot and don't need to await it.
374+ this . connectionManager . connect ( CONNECTOR_PLACEHOLDER , this . lastConnectOptions ?? { } ) ;
375+ }
362376
363- // Release proxy
364- return ( ) => trackedPort . clientProvider [ Comlink . releaseProxy ] ( ) ;
377+ return ( ) => trackedPort . clientProvider [ Comlink . releaseProxy ] ( ) ;
378+ } ) ;
365379 }
366380
367381 triggerCrudUpload ( ) {
@@ -410,7 +424,10 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
410424 remote : new WebRemote (
411425 {
412426 invalidateCredentials : async ( ) => {
413- const lastPort = this . ports [ this . ports . length - 1 ] ;
427+ const lastPort = this . lastWrappedPort ;
428+ if ( ! lastPort ) {
429+ throw new Error ( 'No client port found to invalidate credentials' ) ;
430+ }
414431 try {
415432 this . logger . log ( 'calling the last port client provider to invalidate credentials' ) ;
416433 lastPort . clientProvider . invalidateCredentials ( ) ;
@@ -419,7 +436,10 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
419436 }
420437 } ,
421438 fetchCredentials : async ( ) => {
422- const lastPort = this . ports [ this . ports . length - 1 ] ;
439+ const lastPort = this . lastWrappedPort ;
440+ if ( ! lastPort ) {
441+ throw new Error ( 'No client port found to fetch credentials' ) ;
442+ }
423443 return new Promise ( async ( resolve , reject ) => {
424444 const abortController = new AbortController ( ) ;
425445 this . fetchCredentialsController = {
@@ -442,7 +462,10 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
442462 this . logger
443463 ) ,
444464 uploadCrud : async ( ) => {
445- const lastPort = this . ports [ this . ports . length - 1 ] ;
465+ const lastPort = this . lastWrappedPort ;
466+ if ( ! lastPort ) {
467+ throw new Error ( 'No client port found to upload crud' ) ;
468+ }
446469
447470 return new Promise ( async ( resolve , reject ) => {
448471 const abortController = new AbortController ( ) ;
@@ -470,7 +493,7 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
470493 }
471494
472495 protected async openInternalDB ( ) {
473- const lastClient = this . ports [ this . ports . length - 1 ] ;
496+ const lastClient = this . lastWrappedPort ;
474497 if ( ! lastClient ) {
475498 // Should not really happen in practice
476499 throw new Error ( `Could not open DB connection since no client is connected.` ) ;
@@ -481,6 +504,7 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
481504 const db = await remote ( this . syncParams ! . dbParams ) ;
482505 const locked = new LockedAsyncDatabaseAdapter ( {
483506 name : identifier ,
507+ defaultLockTimeoutMs : 20_000 , // Max wait time for a lock request (we will retry failed attempts)
484508 openConnection : async ( ) => {
485509 const wrapped = new WorkerWrappedAsyncDatabaseConnection ( {
486510 remote,
0 commit comments