@@ -12,6 +12,7 @@ import { loggers } from '@ydbjs/debug'
1212import { YDBError } from '@ydbjs/error'
1313import { type RetryConfig , defaultRetryConfig , retry } from '@ydbjs/retry'
1414import {
15+ type Channel ,
1516 type ChannelOptions ,
1617 type Client ,
1718 ClientError ,
@@ -21,13 +22,13 @@ import {
2122 Status ,
2223 composeClientMiddleware ,
2324 createClientFactory ,
24- waitForChannelReady ,
2525} from 'nice-grpc'
2626import pkg from '../package.json' with { type : 'json' }
2727import { type Connection , LazyConnection } from './conn.js'
2828import { debug } from './middleware.js'
2929import { ConnectionPool } from './pool.js'
3030import { detectRuntime } from './runtime.js'
31+ import { ConnectivityState } from '@grpc/grpc-js/build/src/connectivity-state.js'
3132
3233let dbg = loggers . driver
3334
@@ -170,24 +171,11 @@ export class Driver implements Disposable {
170171
171172 this . #pool = new ConnectionPool ( channelCredentials , this . options . channelOptions )
172173
173- this . #discoveryClient = createClientFactory ( )
174- . use ( this . #middleware)
175- . create ( DiscoveryServiceDefinition , this . #connection. channel )
176-
177174 if ( this . options [ 'ydb.sdk.enable_discovery' ] === false ) {
178175 dbg . log ( 'discovery disabled, using single endpoint' )
179- waitForChannelReady (
180- this . #connection. channel ,
181- new Date ( Date . now ( ) + ( this . options [ 'ydb.sdk.ready_timeout_ms' ] || 10000 ) )
182- )
183- . then ( ( ) => {
184- dbg . log ( 'single endpoint ready' )
185- return this . #ready. resolve ( )
186- } )
187- . catch ( ( error ) => {
188- dbg . log ( 'single endpoint failed to become ready: %O' , error )
189- this . #ready. reject ( error )
190- } )
176+ // Channel will be lazily created on first use
177+ // Readiness check is skipped to avoid memory leaks from Promise chains
178+ this . #ready. resolve ( )
191179 }
192180
193181 if ( this . options [ 'ydb.sdk.enable_discovery' ] === true ) {
@@ -242,6 +230,16 @@ export class Driver implements Disposable {
242230 return this . cs . protocol === 'https:' || this . cs . protocol === 'grpcs:'
243231 }
244232
233+ get #getDiscoveryClient( ) : Client < typeof DiscoveryServiceDefinition > {
234+ if ( this . #discoveryClient === null ) {
235+ dbg . log ( 'creating discovery client' )
236+ this . #discoveryClient = createClientFactory ( )
237+ . use ( this . #middleware)
238+ . create ( DiscoveryServiceDefinition , this . #connection. channel )
239+ }
240+ return this . #discoveryClient
241+ }
242+
245243 async #discovery( signal : AbortSignal ) : Promise < void > {
246244 dbg . log ( 'starting discovery for database: %s' , this . database )
247245
@@ -255,7 +253,7 @@ export class Driver implements Disposable {
255253
256254 let result = await retry ( retryConfig , async ( signal ) => {
257255 dbg . log ( 'attempting to list endpoints for database: %s' , this . database )
258- let response = await this . #discoveryClient . listEndpoints ( { database : this . database } , { signal } )
256+ let response = await this . #getDiscoveryClient . listEndpoints ( { database : this . database } , { signal } )
259257 if ( ! response . operation ) {
260258 throw new ClientError (
261259 DiscoveryServiceDefinition . listEndpoints . path ,
@@ -289,19 +287,63 @@ export class Driver implements Disposable {
289287
290288 async ready ( signal ?: AbortSignal ) : Promise < void > {
291289 dbg . log ( 'waiting for driver to become ready' )
292- signal = signal
293- ? AbortSignal . any ( [ signal , AbortSignal . timeout ( this . options [ 'ydb.sdk.ready_timeout_ms' ] ! ) ] )
294- : AbortSignal . timeout ( this . options [ 'ydb.sdk.ready_timeout_ms' ] ! )
290+
291+ let timeoutMs = this . options [ 'ydb.sdk.ready_timeout_ms' ] !
292+ let effectiveSignal = signal
293+ ? AbortSignal . any ( [ signal , AbortSignal . timeout ( timeoutMs ) ] )
294+ : AbortSignal . timeout ( timeoutMs )
295295
296296 try {
297- await abortable ( signal , this . #ready. promise )
297+ await abortable ( effectiveSignal , this . #ready. promise )
298+
299+ if ( this . options [ 'ydb.sdk.enable_discovery' ] === false ) {
300+ dbg . log ( 'checking channel connectivity for single endpoint mode' )
301+ await this . #checkChannelConnectivity( this . #connection. channel , timeoutMs , effectiveSignal )
302+ }
303+
298304 dbg . log ( 'driver is ready' )
299305 } catch ( error ) {
300306 dbg . log ( 'driver failed to become ready: %O' , error )
301307 throw error
302308 }
303309 }
304310
311+ async #checkChannelConnectivity( channel : Channel , timeoutMs : number , signal : AbortSignal ) : Promise < void > {
312+ let deadline = new Date ( Date . now ( ) + timeoutMs )
313+
314+ while ( true ) {
315+ if ( signal . aborted ) {
316+ throw signal . reason || new Error ( 'Aborted while waiting for channel connectivity' )
317+ }
318+
319+ let state = channel . getConnectivityState ( true ) // true = try to connect
320+ dbg . log ( 'channel connectivity state: %d' , state )
321+
322+ if ( state === ConnectivityState . READY ) {
323+ dbg . log ( 'channel is ready' )
324+ return
325+ }
326+
327+ if ( state === ConnectivityState . SHUTDOWN ) {
328+ throw new Error ( 'Channel is shutdown' )
329+ }
330+
331+ let { promise, resolve, reject } = Promise . withResolvers < void > ( )
332+ channel . watchConnectivityState ( state , deadline , ( err ?: Error ) => {
333+ if ( err ) {
334+ dbg . log ( 'channel connectivity state change timeout: %O' , err )
335+ reject ( err )
336+ } else {
337+ dbg . log ( 'channel connectivity state changed' )
338+ resolve ( )
339+ }
340+ } )
341+
342+ // oxlint-disable-next-line no-await-in-loop
343+ await abortable ( signal , promise )
344+ }
345+ }
346+
305347 close ( ) : void {
306348 dbg . log ( 'closing driver' )
307349 if ( this . #rediscoverTimer) {
0 commit comments