@@ -42,13 +42,40 @@ const SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
4242export class RemoteTransport {
4343 private client : Client | null = null ;
4444
45+ /**
46+ * Factory function for lazy (re)connection.
47+ *
48+ * When set, `ensureConnected()` will invoke this factory to create a
49+ * @libsql /client instance on-demand — recovering from cold-start failures,
50+ * transient network errors, or serverless recycling without requiring the
51+ * caller to explicitly call `connect()` again.
52+ */
53+ private connectFactory : ( ( ) => Promise < Client > ) | null = null ;
54+
55+ /**
56+ * Tracks whether a lazy-connect attempt is already in progress to prevent
57+ * concurrent reconnection storms under high concurrency.
58+ */
59+ private connectPromise : Promise < Client > | null = null ;
60+
4561 /**
4662 * Set the @libsql/client instance used for all queries.
4763 */
4864 setClient ( client : Client ) : void {
4965 this . client = client ;
5066 }
5167
68+ /**
69+ * Register a factory function for lazy (re)connection.
70+ *
71+ * TursoDriver calls this during construction so that the transport can
72+ * self-heal when the initial `connect()` call fails or when the client
73+ * becomes unavailable (e.g., serverless cold-start, transient error).
74+ */
75+ setConnectFactory ( factory : ( ) => Promise < Client > ) : void {
76+ this . connectFactory = factory ;
77+ }
78+
5279 /**
5380 * Get the current @libsql/client instance.
5481 */
@@ -71,9 +98,9 @@ export class RemoteTransport {
7198 // ===================================
7299
73100 async checkHealth ( ) : Promise < boolean > {
74- if ( ! this . client ) return false ;
75101 try {
76- await this . client . execute ( 'SELECT 1' ) ;
102+ const client = await this . ensureConnected ( ) ;
103+ await client . execute ( 'SELECT 1' ) ;
77104 return true ;
78105 } catch {
79106 return false ;
@@ -85,7 +112,7 @@ export class RemoteTransport {
85112 // ===================================
86113
87114 async execute ( command : unknown , params ?: unknown [ ] ) : Promise < unknown > {
88- this . ensureClient ( ) ;
115+ await this . ensureConnected ( ) ;
89116 if ( typeof command !== 'string' ) return command ;
90117
91118 const stmt : InStatement = params && params . length > 0
@@ -101,7 +128,7 @@ export class RemoteTransport {
101128 // ===================================
102129
103130 async find ( object : string , query : any ) : Promise < Record < string , unknown > [ ] > {
104- this . ensureClient ( ) ;
131+ await this . ensureConnected ( ) ;
105132
106133 const { sql, args } = this . buildSelectSQL ( object , query ) ;
107134
@@ -123,7 +150,7 @@ export class RemoteTransport {
123150 async findOne ( object : string , query : any ) : Promise < Record < string , unknown > | null > {
124151 // When called with a string/number id fall back gracefully
125152 if ( typeof query === 'string' || typeof query === 'number' ) {
126- this . ensureClient ( ) ;
153+ await this . ensureConnected ( ) ;
127154 const result = await this . client ! . execute ( {
128155 sql : `SELECT * FROM "${ object } " WHERE "id" = ? LIMIT 1` ,
129156 args : [ query ] ,
@@ -148,7 +175,7 @@ export class RemoteTransport {
148175 }
149176
150177 async create ( object : string , data : Record < string , unknown > ) : Promise < Record < string , unknown > > {
151- this . ensureClient ( ) ;
178+ await this . ensureConnected ( ) ;
152179
153180 const { _id, ...rest } = data as any ;
154181 const toInsert = { ...rest } ;
@@ -176,7 +203,7 @@ export class RemoteTransport {
176203 }
177204
178205 async update ( object : string , id : string | number , data : Record < string , unknown > ) : Promise < Record < string , unknown > > {
179- this . ensureClient ( ) ;
206+ await this . ensureConnected ( ) ;
180207
181208 const columns = Object . keys ( data ) ;
182209 const setClauses = columns . map ( ( col ) => `"${ col } " = ?` ) . join ( ', ' ) ;
@@ -195,7 +222,7 @@ export class RemoteTransport {
195222 }
196223
197224 async upsert ( object : string , data : Record < string , unknown > , conflictKeys ?: string [ ] ) : Promise < Record < string , unknown > > {
198- this . ensureClient ( ) ;
225+ await this . ensureConnected ( ) ;
199226
200227 const { _id, ...rest } = data as any ;
201228 const toUpsert = { ...rest } ;
@@ -235,7 +262,7 @@ export class RemoteTransport {
235262 }
236263
237264 async delete ( object : string , id : string | number ) : Promise < boolean > {
238- this . ensureClient ( ) ;
265+ await this . ensureConnected ( ) ;
239266 const result = await this . client ! . execute ( {
240267 sql : `DELETE FROM "${ object } " WHERE "id" = ?` ,
241268 args : [ id ] ,
@@ -244,7 +271,7 @@ export class RemoteTransport {
244271 }
245272
246273 async count ( object : string , query ?: any ) : Promise < number > {
247- this . ensureClient ( ) ;
274+ await this . ensureConnected ( ) ;
248275
249276 const { whereClauses, args } = this . buildWhereSQL ( query ?. where ) ;
250277 let sql = `SELECT COUNT(*) as count FROM "${ object } "` ;
@@ -283,7 +310,7 @@ export class RemoteTransport {
283310 }
284311
285312 async bulkDelete ( object : string , ids : Array < string | number > ) : Promise < void > {
286- this . ensureClient ( ) ;
313+ await this . ensureConnected ( ) ;
287314 if ( ids . length === 0 ) return ;
288315
289316 const placeholders = ids . map ( ( ) => '?' ) . join ( ', ' ) ;
@@ -294,7 +321,7 @@ export class RemoteTransport {
294321 }
295322
296323 async updateMany ( object : string , query : any , data : Record < string , unknown > ) : Promise < number > {
297- this . ensureClient ( ) ;
324+ await this . ensureConnected ( ) ;
298325
299326 const columns = Object . keys ( data ) ;
300327 const setClauses = columns . map ( ( col ) => `"${ col } " = ?` ) . join ( ', ' ) ;
@@ -309,7 +336,7 @@ export class RemoteTransport {
309336 }
310337
311338 async deleteMany ( object : string , query : any ) : Promise < number > {
312- this . ensureClient ( ) ;
339+ await this . ensureConnected ( ) ;
313340
314341 const { whereClauses, args } = this . buildWhereSQL ( query ?. where ) ;
315342 let sql = `DELETE FROM "${ object } "` ;
@@ -324,7 +351,7 @@ export class RemoteTransport {
324351 // ===================================
325352
326353 async beginTransaction ( ) : Promise < any > {
327- this . ensureClient ( ) ;
354+ await this . ensureConnected ( ) ;
328355 return this . client ! . transaction ( ) ;
329356 }
330357
@@ -341,7 +368,7 @@ export class RemoteTransport {
341368 // ===================================
342369
343370 async syncSchema ( object : string , schema : any ) : Promise < void > {
344- this . ensureClient ( ) ;
371+ await this . ensureConnected ( ) ;
345372
346373 const objectDef = schema as { name : string ; fields ?: Record < string , any > } ;
347374 const tableName = object ;
@@ -391,7 +418,7 @@ export class RemoteTransport {
391418 * by the caller if a batch operation is not supported or fails.
392419 */
393420 async syncSchemasBatch ( schemas : Array < { object : string ; schema : any } > ) : Promise < void > {
394- this . ensureClient ( ) ;
421+ await this . ensureConnected ( ) ;
395422 if ( schemas . length === 0 ) return ;
396423
397424 // Validate all identifiers up-front
@@ -459,19 +486,45 @@ export class RemoteTransport {
459486 }
460487
461488 async dropTable ( object : string ) : Promise < void > {
462- this . ensureClient ( ) ;
489+ await this . ensureConnected ( ) ;
463490 await this . client ! . execute ( `DROP TABLE IF EXISTS "${ object } "` ) ;
464491 }
465492
466493 // ===================================
467494 // Internal Helpers
468495 // ===================================
469496
470- private ensureClient ( ) : Client {
471- if ( ! this . client ) {
472- throw new Error ( 'RemoteTransport: @libsql/client is not initialized. Call connect() first.' ) ;
497+ /**
498+ * Ensure the @libsql/client is initialized, attempting lazy connect if a
499+ * factory was registered and the client is not yet available.
500+ *
501+ * Uses a singleton promise to prevent concurrent reconnection storms:
502+ * multiple callers that race into this method while a connect is in flight
503+ * will all await the same promise.
504+ */
505+ private async ensureConnected ( ) : Promise < Client > {
506+ if ( this . client ) return this . client ;
507+
508+ if ( this . connectFactory ) {
509+ // De-duplicate concurrent connect attempts
510+ if ( ! this . connectPromise ) {
511+ this . connectPromise = this . connectFactory ( )
512+ . then ( ( client ) => {
513+ this . client = client ;
514+ this . connectPromise = null ;
515+ return client ;
516+ } )
517+ . catch ( ( err ) => {
518+ this . connectPromise = null ;
519+ throw new Error (
520+ `RemoteTransport: lazy connect failed: ${ err instanceof Error ? err . message : String ( err ) } `
521+ ) ;
522+ } ) ;
523+ }
524+ return this . connectPromise ;
473525 }
474- return this . client ;
526+
527+ throw new Error ( 'RemoteTransport: @libsql/client is not initialized. Call connect() first.' ) ;
475528 }
476529
477530 /**
0 commit comments