@@ -9,6 +9,7 @@ import { IDL } from "@dfinity/candid";
9
9
import { Principal } from "@dfinity/principal" ;
10
10
import {
11
11
CanisterAckMessageContent ,
12
+ CanisterCloseMessageContent ,
12
13
CanisterWsMessageArguments ,
13
14
ClientKeepAliveMessageContent ,
14
15
ClientKey ,
@@ -36,10 +37,15 @@ import {
36
37
import { WsAgent } from "./agent" ;
37
38
38
39
/**
39
- * The default expiration time for receiving an ack message from the canister after sending a message.
40
- * It's **3/2 times** the canister's default send ack period.
40
+ * The default interval (in milliseconds) at which the canister sends an ack message.
41
41
*/
42
- const DEFAULT_ACK_MESSAGE_TIMEOUT_MS = 450_000 ;
42
+ const DEFAULT_ACK_MESSAGE_INTERVAL_MS = 300_000 ;
43
+ /**
44
+ * The maximum communication latency allowed between the client and the canister (same as in the canister).
45
+ *
46
+ * Used to determine the ack message timeout.
47
+ */
48
+ export const COMMUNICATION_LATENCY_BOUND_MS = 30_000 ;
43
49
44
50
/**
45
51
* Interface to create a new IcWebSocketConfig. For a simple configuration, use {@link createWsConfig}.
@@ -62,12 +68,12 @@ export interface IcWebSocketConfig<S extends _WS_CANISTER_SERVICE> {
62
68
*/
63
69
networkUrl : string ;
64
70
/**
65
- * The expiration (in milliseconds) time for receiving an ack message from the canister after sending a message.
66
- * If the ack message is not received within this time, the connection will be closed .
67
- * This parameter should always me **3/2 times or more** the canister's send ack period.
68
- * @default 450_000 (7.5 minutes = 3/2 default send ack period on the canister)
71
+ * The interval (in milliseconds) at which the canister sends an ack message.
72
+ * This parameter must be **equal** to the canister's send ack interval .
73
+ *
74
+ * @default 300_000 ( default send ack period on the canister)
69
75
*/
70
- ackMessageTimeout ?: number ;
76
+ ackMessageIntervalMs ?: number ;
71
77
/**
72
78
* The maximum age of the certificate received from the canister, in minutes. You won't likely need to set this parameter. Used in tests.
73
79
*
@@ -104,6 +110,7 @@ export default class IcWebSocket<
104
110
private _clientKey : ClientKey ;
105
111
private _gatewayPrincipal : Principal | null = null ;
106
112
private _maxCertificateAgeInMinutes = 5 ;
113
+ private _openTimeout : NodeJS . Timeout | null = null ;
107
114
108
115
onclose : ( ( this : IcWebSocket < S , ApplicationMessageType > , ev : CloseEvent ) => any ) | null = null ;
109
116
onerror : ( ( this : IcWebSocket < S , ApplicationMessageType > , ev : ErrorEvent ) => any ) | null = null ;
@@ -174,7 +181,7 @@ export default class IcWebSocket<
174
181
} ) ;
175
182
176
183
this . _ackMessagesQueue = new AckMessagesQueue ( {
177
- expirationMs : config . ackMessageTimeout || DEFAULT_ACK_MESSAGE_TIMEOUT_MS ,
184
+ expirationMs : ( config . ackMessageIntervalMs || DEFAULT_ACK_MESSAGE_INTERVAL_MS ) + COMMUNICATION_LATENCY_BOUND_MS ,
178
185
timeoutExpiredCallback : this . _onAckMessageTimeout . bind ( this ) ,
179
186
} ) ;
180
187
@@ -226,6 +233,27 @@ export default class IcWebSocket<
226
233
this . _incomingMessagesQueue . addAndProcess ( event . data ) ;
227
234
}
228
235
236
+ private _startOpenTimeout ( ) {
237
+ // the timeout is double the maximum allowed network latency,
238
+ // because opening the connection involves a message sent by the client and one by the canister
239
+ this . _openTimeout = setTimeout ( ( ) => {
240
+ if ( ! this . _isConnectionEstablished ) {
241
+ logger . error ( "[onWsOpen] Error: Open timeout expired before receiving the open message" ) ;
242
+ this . _callOnErrorCallback ( new Error ( "Open timeout expired before receiving the open message" ) ) ;
243
+ this . _wsInstance . close ( 4000 , "Open connection timeout" ) ;
244
+ }
245
+
246
+ this . _openTimeout = null ;
247
+ } , 2 * COMMUNICATION_LATENCY_BOUND_MS ) ;
248
+ }
249
+
250
+ private _cancelOpenTimeout ( ) {
251
+ if ( this . _openTimeout ) {
252
+ clearTimeout ( this . _openTimeout ) ;
253
+ this . _openTimeout = null ;
254
+ }
255
+ }
256
+
229
257
private async _handleHandshakeMessage ( handshakeMessage : GatewayHandshakeMessage ) : Promise < boolean > {
230
258
// at this point, we're sure that the gateway_principal is valid
231
259
// because the isGatewayHandshakeMessage function checks it
@@ -234,6 +262,8 @@ export default class IcWebSocket<
234
262
235
263
try {
236
264
await this . _sendOpenMessage ( ) ;
265
+
266
+ this . _startOpenTimeout ( ) ;
237
267
} catch ( error ) {
238
268
logger . error ( "[onWsMessage] Handshake message error:" , error ) ;
239
269
// if a handshake message fails, we can't continue
@@ -335,12 +365,17 @@ export default class IcWebSocket<
335
365
}
336
366
337
367
this . _isConnectionEstablished = true ;
368
+ this . _cancelOpenTimeout ( ) ;
338
369
339
370
this . _callOnOpenCallback ( ) ;
340
371
341
372
this . _outgoingMessagesQueue . enableAndProcess ( ) ;
342
373
} else if ( "AckMessage" in serviceMessage ) {
343
374
await this . _handleAckMessageFromCanister ( serviceMessage . AckMessage ) ;
375
+ } else if ( "CloseMessage" in serviceMessage ) {
376
+ await this . _handleCloseMessageFromCanister ( serviceMessage . CloseMessage ) ;
377
+ // we don't have to process any further message (there shouldn't be any anyway)
378
+ return false ;
344
379
} else {
345
380
throw new Error ( "Invalid service message from canister" ) ;
346
381
}
@@ -369,6 +404,17 @@ export default class IcWebSocket<
369
404
await this . _sendKeepAliveMessage ( ) ;
370
405
}
371
406
407
+ private async _handleCloseMessageFromCanister ( content : CanisterCloseMessageContent ) : Promise < void > {
408
+ if ( "ClosedByApplication" in content . reason ) {
409
+ logger . debug ( "[onWsMessage] Received close message from canister. Reason: ClosedByApplication" ) ;
410
+ this . _wsInstance . close ( 4001 , "ClosedByApplication" ) ;
411
+ } else {
412
+ logger . error ( "[onWsMessage] Received close message from canister. Reason:" , content . reason ) ;
413
+ this . _callOnErrorCallback ( new Error ( `Received close message from canister. Reason: ${ content . reason } ` ) ) ;
414
+ this . _wsInstance . close ( 4000 , "Received close message from canister" ) ;
415
+ }
416
+ }
417
+
372
418
private async _sendKeepAliveMessage ( ) : Promise < void > {
373
419
const keepAliveMessageContent : ClientKeepAliveMessageContent = {
374
420
last_incoming_sequence_num : this . _incomingSequenceNum - BigInt ( 1 ) ,
0 commit comments