@@ -78,6 +78,9 @@ export class SQLiteCloudWebsocketConnection extends SQLiteCloudConnection {
7878 /** Socket.io used to communicated with SQLiteCloud server */
7979 private socket ?: Socket
8080
81+ /** Commands sent to the gateway that are still waiting for their ack, failed if the socket goes away */
82+ private pendingCommands ?: Set < ( error : Error ) => void >
83+
8184 /** True if connection is open */
8285 get connected ( ) : boolean {
8386 return ! ! ( this . socket && this . socket ?. connected )
@@ -116,24 +119,34 @@ export class SQLiteCloudWebsocketConnection extends SQLiteCloudConnection {
116119 }
117120 this . socket = io ( gatewayUrl , ioOpts )
118121
122+ // the connect callback reports the outcome of the connection attempt and is called only once,
123+ // errors that happen later are reported to the commands that are waiting for a response
124+ let connectReported = false
125+ const reportConnect = ( error : Error | null ) => {
126+ if ( ! connectReported ) {
127+ connectReported = true
128+ callback ?. call ( this , error )
129+ }
130+ }
131+ const fail = ( error : Error ) => {
132+ this . closeWithError ( error )
133+ reportConnect ( error )
134+ }
135+
119136 this . socket . on ( 'connect' , ( ) => {
120- callback ?. call ( this , null )
137+ reportConnect ( null )
121138 } )
122139
123140 this . socket . on ( 'disconnect' , ( reason , description ) => {
124- this . close ( )
125- callback ?. call (
126- this ,
141+ fail (
127142 ( reason === 'parse error' && getAttachmentLimitError ( description , websocketMaxAttachments ) ) ||
128143 new SQLiteCloudError ( 'Disconnected' , { errorCode : 'ERR_CONNECTION_ENDED' , cause : reason } )
129144 )
130145 } )
131146
132147 this . socket . on ( 'connect_error' , ( error : any ) => {
133- this . close ( )
134148 if ( error ?. message === 'parse error' || error ?. cause === 'parse error' ) {
135- callback ?. call (
136- this ,
149+ fail (
137150 getAttachmentLimitError ( error ?. description || error ?. data || error ?. cause , websocketMaxAttachments ) ||
138151 new SQLiteCloudError ( 'Connection error' , { errorCode : 'ERR_CONNECTION_ERROR' , cause : error } )
139152 )
@@ -148,12 +161,11 @@ export class SQLiteCloudWebsocketConnection extends SQLiteCloudConnection {
148161 message = error . context . responseText
149162 }
150163 }
151- callback ?. call ( this , new SQLiteCloudError ( message , { errorCode : 'ERR_CONNECTION_ERROR' } ) )
164+ fail ( new SQLiteCloudError ( message , { errorCode : 'ERR_CONNECTION_ERROR' } ) )
152165 } )
153166
154167 this . socket . on ( 'error' , ( error : Error ) => {
155- this . close ( )
156- callback ?. call ( this , new SQLiteCloudError ( 'Connection error' , { errorCode : 'ERR_CONNECTION_ERROR' , cause : error } ) )
168+ fail ( new SQLiteCloudError ( 'Connection error' , { errorCode : 'ERR_CONNECTION_ERROR' , cause : error } ) )
157169 } )
158170 }
159171 } catch ( error ) {
@@ -175,6 +187,28 @@ export class SQLiteCloudWebsocketConnection extends SQLiteCloudConnection {
175187 commands = { query : commands }
176188 }
177189
190+ // the callback is called exactly once: with the gateway response, on timeout or when the connection drops
191+ let settled = false
192+ let timeout : ReturnType < typeof setTimeout > | undefined
193+ const finish = ( error : Error | null , results ?: any ) => {
194+ if ( settled ) return
195+ settled = true
196+ clearTimeout ( timeout )
197+ this . pendingCommands ?. delete ( fail )
198+ callback ?. call ( this , error , results )
199+ }
200+ const fail = ( error : Error ) => finish ( error )
201+
202+ this . pendingCommands ||= new Set ( )
203+ this . pendingCommands . add ( fail )
204+
205+ const timeoutMs = this . config ?. timeout ?? 0
206+ if ( timeoutMs > 0 ) {
207+ timeout = setTimeout ( ( ) => {
208+ finish ( new SQLiteCloudError ( `Request timed out after ${ timeoutMs } ms` , { errorCode : 'ERR_CONNECTION_TIMEOUT' } ) )
209+ } , timeoutMs )
210+ }
211+
178212 this . socket . emit (
179213 'GET /v2/weblite/sql' ,
180214 {
@@ -194,7 +228,7 @@ export class SQLiteCloudWebsocketConnection extends SQLiteCloudConnection {
194228 // for the `errors[]` shape; fall back to top-level for the legacy `error` shape.
195229 const errorFields = ( gatewayError . meta && typeof gatewayError . meta === 'object' ) ? gatewayError . meta : gatewayError
196230 const error = new SQLiteCloudError ( gatewayError . detail || gatewayError . message || 'Gateway error' , { ...errorFields } )
197- callback ?. call ( this , error )
231+ finish ( error )
198232 } else {
199233 const { metadata } = response
200234 const blobTransferFormat = getResponseBlobTransferFormat ( response )
@@ -208,11 +242,11 @@ export class SQLiteCloudWebsocketConnection extends SQLiteCloudConnection {
208242 console . assert ( Array . isArray ( data ) , 'SQLiteCloudWebsocketConnection.transportCommands - data is not an array' )
209243 // we can recreate a SQLiteCloudRowset from the response which we know to be an array of arrays
210244 const rowset = new SQLiteCloudRowset ( metadata , data . flat ( ) )
211- callback ?. call ( this , null , rowset )
245+ finish ( null , rowset )
212246 return
213247 }
214248 }
215- callback ?. call ( this , null , data )
249+ finish ( null , data )
216250 }
217251 }
218252 )
@@ -222,6 +256,11 @@ export class SQLiteCloudWebsocketConnection extends SQLiteCloudConnection {
222256
223257 /** Disconnect socket.io from server */
224258 public close ( ) : this {
259+ return this . closeWithError ( new SQLiteCloudError ( 'Connection closed' , { errorCode : 'ERR_CONNECTION_CLOSED' } ) )
260+ }
261+
262+ /** Disconnect socket.io from server and fail the commands still waiting for a response with the given error */
263+ private closeWithError ( error : Error ) : this {
225264 console . assert ( this . socket !== null , 'SQLiteCloudWebsocketConnection.close - connection already closed' )
226265 if ( this . socket ) {
227266 this . socket ?. removeAllListeners ( )
@@ -230,6 +269,10 @@ export class SQLiteCloudWebsocketConnection extends SQLiteCloudConnection {
230269 }
231270
232271 this . operations . clear ( )
272+
273+ const pendingCommands = this . pendingCommands
274+ this . pendingCommands = undefined
275+ pendingCommands ?. forEach ( fail => fail ( error ) )
233276 return this
234277 }
235278}
0 commit comments