@@ -16,6 +16,7 @@ import {
1616 createErrorResponse ,
1717 RPC_TIMEOUT_MS ,
1818 SHARED_WORKER_PATH ,
19+ PROVIDER_LOCK ,
1920} from '../shared' ;
2021
2122export interface DbClientOptions {
@@ -47,6 +48,13 @@ export class DbClient {
4748 { clientId : string ; requestId : RequestId }
4849 > ( ) ;
4950
51+ // Track whether database has been opened (for failover recovery)
52+ private databaseOpened = false ;
53+
54+ // Provider polling interval (for detecting provider loss)
55+ private providerPollInterval : ReturnType < typeof setInterval > | null = null ;
56+ private static readonly PROVIDER_POLL_INTERVAL_MS = 1000 ;
57+
5058 readonly dbName : string ;
5159
5260 constructor ( options : DbClientOptions ) {
@@ -67,8 +75,19 @@ export class DbClient {
6775
6876 this . port . onmessage = ( event ) => this . handleMessage ( event . data ) ;
6977 this . port . start ( ) ;
78+
79+ // Register unload handler to notify SharedWorker when tab closes
80+ this . handleBeforeUnload = ( ) => {
81+ console . log ( '[DbClient] Tab closing, sending disconnect message' ) ;
82+ this . port ?. postMessage ( { type : 'disconnect' } ) ;
83+ } ;
84+ globalThis . addEventListener ( 'beforeunload' , this . handleBeforeUnload ) ;
85+ // Also use pagehide which is more reliable on mobile
86+ globalThis . addEventListener ( 'pagehide' , this . handleBeforeUnload ) ;
7087 }
7188
89+ private handleBeforeUnload : ( ) => void = ( ) => { } ;
90+
7291 private handleMessage ( msg : unknown ) {
7392 const message = msg as {
7493 type : string ;
@@ -93,10 +112,19 @@ export class DbClient {
93112 console . log ( '[DbClient] Provider elected. Am I provider?' , this . isProvider ) ;
94113 if ( this . isProvider ) {
95114 this . initializeProviderWorker ( ) ;
115+ this . stopProviderPolling ( ) ;
116+ } else {
117+ // Start polling in case provider dies without sending disconnect
118+ this . startProviderPolling ( ) ;
96119 }
97120 this . resolveReady ( ) ;
98121 break ;
99122
123+ case 'try-become-provider' :
124+ console . log ( '[DbClient] Asked to try becoming provider' ) ;
125+ this . tryAcquireProviderLock ( ) ;
126+ break ;
127+
100128 case 'forward-request' :
101129 // Only provider should receive this
102130 if ( this . isProvider && message . clientId && message . request ) {
@@ -121,6 +149,68 @@ export class DbClient {
121149 }
122150 }
123151
152+ /**
153+ * Start polling for provider lock availability.
154+ * This handles the case where the provider dies without sending a disconnect message.
155+ */
156+ private startProviderPolling ( ) {
157+ if ( this . providerPollInterval ) return ; // Already polling
158+
159+ console . log ( '[DbClient] Starting provider polling' ) ;
160+ this . providerPollInterval = setInterval ( ( ) => {
161+ if ( ! this . isProvider ) {
162+ this . tryAcquireProviderLock ( ) ;
163+ }
164+ } , DbClient . PROVIDER_POLL_INTERVAL_MS ) ;
165+ }
166+
167+ /**
168+ * Stop polling for provider lock availability.
169+ */
170+ private stopProviderPolling ( ) {
171+ if ( this . providerPollInterval ) {
172+ console . log ( '[DbClient] Stopping provider polling' ) ;
173+ clearInterval ( this . providerPollInterval ) ;
174+ this . providerPollInterval = null ;
175+ }
176+ }
177+
178+ /**
179+ * Attempt to acquire the provider Web Lock.
180+ * If successful, notify the coordinator that we are now the provider.
181+ * The lock is held for the lifetime of the tab - when the tab closes,
182+ * the browser automatically releases the lock.
183+ */
184+ private async tryAcquireProviderLock ( ) {
185+ const lockName = PROVIDER_LOCK ( this . dbName ) ;
186+ console . log ( '[DbClient] Trying to acquire provider lock:' , lockName ) ;
187+
188+ try {
189+ await navigator . locks . request (
190+ lockName ,
191+ { mode : 'exclusive' , ifAvailable : true } ,
192+ async ( lock ) => {
193+ if ( lock ) {
194+ console . log ( '[DbClient] Acquired provider lock!' ) ;
195+ // Notify SharedWorker that we are now the provider
196+ this . port ?. postMessage ( { type : 'became-provider' } ) ;
197+
198+ // Hold the lock indefinitely by never resolving
199+ // The lock is automatically released when the tab closes
200+ await new Promise < void > ( ( ) => {
201+ // Never resolves - holds lock until tab closes
202+ } ) ;
203+ } else {
204+ console . log ( '[DbClient] Provider lock not available' ) ;
205+ // Another tab already has the lock - we'll receive provider-elected message
206+ }
207+ }
208+ ) ;
209+ } catch ( e ) {
210+ console . error ( '[DbClient] Failed to acquire provider lock:' , e ) ;
211+ }
212+ }
213+
124214 /**
125215 * Initialize the dedicated worker for database operations (provider only)
126216 */
@@ -136,6 +226,21 @@ export class DbClient {
136226 this . providerWorker . onerror = ( error ) => {
137227 console . error ( '[DbClient] Provider worker error:' , error ) ;
138228 } ;
229+
230+ // If database was previously opened, re-open it for failover recovery
231+ // This ensures the new provider can serve queries immediately
232+ if ( this . databaseOpened ) {
233+ console . log ( '[DbClient] Re-opening database after failover:' , this . dbName ) ;
234+ // Need to wait for worker to be ready before sending open request
235+ // Use a small delay to ensure worker message handler is set up
236+ setTimeout ( ( ) => {
237+ this . sendRequestToLocalWorker (
238+ createRequest ( 'open' , crypto . randomUUID ( ) , { dbName : this . dbName } )
239+ ) . catch ( ( e ) => {
240+ console . error ( '[DbClient] Failed to re-open database after failover:' , e ) ;
241+ } ) ;
242+ } , 0 ) ;
243+ }
139244 }
140245
141246 /**
@@ -285,12 +390,14 @@ export class DbClient {
285390 await this . sendRequest (
286391 createRequest ( 'open' , crypto . randomUUID ( ) , { dbName : this . dbName } )
287392 ) ;
393+ this . databaseOpened = true ;
288394 }
289395
290396 async close ( ) : Promise < void > {
291397 await this . sendRequest (
292398 createRequest ( 'close' , crypto . randomUUID ( ) , { dbName : this . dbName } )
293399 ) ;
400+ this . databaseOpened = false ;
294401 }
295402
296403 async exec ( sql : string , bind ?: unknown [ ] ) : Promise < { changes : number } > {
@@ -313,6 +420,13 @@ export class DbClient {
313420 }
314421
315422 disconnect ( ) {
423+ // Stop provider polling
424+ this . stopProviderPolling ( ) ;
425+ // Remove unload listeners
426+ globalThis . removeEventListener ( 'beforeunload' , this . handleBeforeUnload ) ;
427+ globalThis . removeEventListener ( 'pagehide' , this . handleBeforeUnload ) ;
428+ // Notify SharedWorker
429+ this . port ?. postMessage ( { type : 'disconnect' } ) ;
316430 this . port ?. close ( ) ;
317431 this . worker = null ;
318432 this . port = null ;
0 commit comments