@@ -22,6 +22,8 @@ import { UserAgent } from "../../../api/user-agent.js";
2222import { UserAgentOptions } from "../../../api/user-agent-options.js" ;
2323import { UserAgentState } from "../../../api/user-agent-state.js" ;
2424import { Logger } from "../../../core/log/logger.js" ;
25+ import { OutgoingRequest } from "../../../core/messages/outgoing-request.js" ;
26+ import { URI } from "../../../grammar/uri.js" ;
2527import { SessionDescriptionHandler } from "../session-description-handler/session-description-handler.js" ;
2628import { SessionDescriptionHandlerOptions } from "../session-description-handler/session-description-handler-options.js" ;
2729import { Transport } from "../transport/transport.js" ;
@@ -47,9 +49,13 @@ export class SessionManager {
4749 private attemptingReconnection = false ;
4850 private logger : Logger ;
4951 private options : Required < SessionManagerOptions > ;
52+ private optionsPingFailure = false ;
53+ private optionsPingRequest ?: OutgoingRequest ;
54+ private optionsPingRunning = false ;
55+ private optionsPingTimeout ?: ReturnType < typeof setTimeout > ;
5056 private registrationAttemptTimeout ?: ReturnType < typeof setTimeout > ;
51- private registerer : Registerer | undefined ;
52- private registererOptions : RegistererOptions | undefined ;
57+ private registerer ? : Registerer ;
58+ private registererOptions ? : RegistererOptions ;
5359 private registererRegisterOptions : RegistererRegisterOptions ;
5460 private shouldBeConnected = false ;
5561 private shouldBeRegistered = false ;
@@ -74,6 +80,8 @@ export class SessionManager {
7480 managedSessionFactory : defaultManagedSessionFactory ( ) ,
7581 maxSimultaneousSessions : 2 ,
7682 media : { } ,
83+ optionsPingInterval : - 1 ,
84+ optionsPingRequestURI : "" ,
7785 reconnectionAttempts : 3 ,
7886 reconnectionDelay : 4 ,
7987 registrationRetry : false ,
@@ -127,21 +135,36 @@ export class SessionManager {
127135 if ( this . delegate && this . delegate . onServerConnect ) {
128136 this . delegate . onServerConnect ( ) ;
129137 }
130- // Attempt to register if we are supposed to be registered.
138+ // Attempt to register if we are supposed to be registered
131139 if ( this . shouldBeRegistered ) {
132140 this . register ( ) ;
133141 }
142+ // Start OPTIONS pings if we are to be pinging
143+ if ( this . options . optionsPingInterval > 0 ) {
144+ this . optionsPingStart ( ) ;
145+ }
134146 } ,
135147 // Handle connection with server lost
136148 onDisconnect : async ( error ?: Error ) : Promise < void > => {
137149 this . logger . log ( `Disconnected` ) ;
150+
151+ // Stop OPTIONS ping if need be.
152+ let optionsPingFailure = false ;
153+ if ( this . options . optionsPingInterval > 0 ) {
154+ optionsPingFailure = this . optionsPingFailure ;
155+ this . optionsPingFailure = false ;
156+ this . optionsPingStop ( ) ;
157+ }
158+
159+ // Let delgate know we have disconnected
138160 if ( this . delegate && this . delegate . onServerDisconnect ) {
139161 this . delegate . onServerDisconnect ( error ) ;
140162 }
163+
141164 // If the user called `disconnect` a graceful cleanup will be done therein.
142165 // Only cleanup if network/server dropped the connection.
143166 // Only reconnect if network/server dropped the connection
144- if ( error ) {
167+ if ( error || optionsPingFailure ) {
145168 // There is no transport at this point, so we are not expecting to be able to
146169 // send messages much less get responses. So just dispose of everything without
147170 // waiting for anything to succeed.
@@ -266,12 +289,25 @@ export class SessionManager {
266289 }
267290 } ) ;
268291
269- // Before unload, clean up and disconnect.
292+ // NOTE: The autoStop option does not currently work as one likley expects.
293+ // This code is here because the "autoStop behavior" and this assoicated
294+ // implemenation has been a recurring request. So instead of removing
295+ // the implementation again (because it doesn't work) and then having
296+ // to explain agian the issue over and over again to those who want it,
297+ // we have included it here to break that cycle. The implementation is
298+ // harmless and serves to provide an explaination for those interested.
270299 if ( this . options . autoStop ) {
300+ // Standard operation workflow will resume after this callback exits, meaning
301+ // that any asynchronous operations are likely not going to be finished, especially
302+ // if they are guaranteed to not be executed in the current tick (promises fall
303+ // under this category, they will never be resolved synchronously by design).
271304 window . addEventListener ( "beforeunload" , async ( ) => {
272305 this . shouldBeConnected = false ;
273306 this . shouldBeRegistered = false ;
274- await this . userAgent . stop ( ) ;
307+ if ( this . userAgent . state !== UserAgentState . Stopped ) {
308+ // The stop() method returns a promise which will not resolve before the page unloads.
309+ await this . userAgent . stop ( ) ;
310+ }
275311 } ) ;
276312 }
277313 }
@@ -1167,6 +1203,130 @@ export class SessionManager {
11671203 } ;
11681204 }
11691205
1206+ /**
1207+ * Periodically send OPTIONS pings and disconnect when a ping fails.
1208+ * @param requestURI - Request URI to target
1209+ * @param fromURI - From URI
1210+ * @param toURI - To URI
1211+ */
1212+ private optionsPingRun ( requestURI : URI , fromURI : URI , toURI : URI ) : void {
1213+ // Guard against nvalid interval
1214+ if ( this . options . optionsPingInterval < 1 ) {
1215+ throw new Error ( "Invalid options ping interval." ) ;
1216+ }
1217+ // Guard against sending a ping when there is one outstanading
1218+ if ( this . optionsPingRunning ) {
1219+ return ;
1220+ }
1221+ this . optionsPingRunning = true ;
1222+
1223+ // Setup next ping to run in future
1224+ this . optionsPingTimeout = setTimeout ( ( ) => {
1225+ this . optionsPingTimeout = undefined ;
1226+
1227+ // If ping succeeds...
1228+ const onPingSuccess = ( ) => {
1229+ // record success or failure
1230+ this . optionsPingFailure = false ;
1231+ // if we are still running, queue up the next ping
1232+ if ( this . optionsPingRunning ) {
1233+ this . optionsPingRunning = false ;
1234+ this . optionsPingRun ( requestURI , fromURI , toURI ) ;
1235+ }
1236+ } ;
1237+
1238+ // If ping fails...
1239+ const onPingFailure = ( ) => {
1240+ this . logger . error ( "OPTIONS ping failed" ) ;
1241+ // record success or failure
1242+ this . optionsPingFailure = true ;
1243+ // stop running
1244+ this . optionsPingRunning = false ;
1245+ // disconnect the transport
1246+ this . userAgent . transport . disconnect ( ) . catch ( ( error ) => this . logger . error ( error ) ) ;
1247+ } ;
1248+
1249+ // Create an OPTIONS request message
1250+ const core = this . userAgent . userAgentCore ;
1251+ const message = core . makeOutgoingRequestMessage ( "OPTIONS" , requestURI , fromURI , toURI , { } ) ;
1252+
1253+ // Send the request message
1254+ this . optionsPingRequest = core . request ( message , {
1255+ onAccept : ( ) => {
1256+ this . optionsPingRequest = undefined ;
1257+ onPingSuccess ( ) ;
1258+ } ,
1259+ onReject : ( response ) => {
1260+ this . optionsPingRequest = undefined ;
1261+ // Ping fails on following responses...
1262+ // - 408 Request Timeout (no response was received)
1263+ // - 503 Service Unavailable (a transport layer error occured)
1264+ if ( response . message . statusCode === 408 || response . message . statusCode === 503 ) {
1265+ onPingFailure ( ) ;
1266+ } else {
1267+ onPingSuccess ( ) ;
1268+ }
1269+ }
1270+ } ) ;
1271+ } , this . options . optionsPingInterval * 1000 ) ;
1272+ }
1273+
1274+ /**
1275+ * Start sending OPTIONS pings.
1276+ */
1277+ private optionsPingStart ( ) : void {
1278+ this . logger . log ( `OPTIONS pings started` ) ;
1279+
1280+ // Create the URIs needed to send OPTIONS pings
1281+ let requestURI , fromURI , toURI ;
1282+ if ( this . options . optionsPingRequestURI ) {
1283+ // Use whatever specific RURI is provided.
1284+ requestURI = UserAgent . makeURI ( this . options . optionsPingRequestURI ) ;
1285+ if ( ! requestURI ) {
1286+ throw new Error ( "Failed to create Request URI." ) ;
1287+ }
1288+ // Use the user agent's contact URI for From and To URIs
1289+ fromURI = this . userAgent . contact . uri . clone ( ) ;
1290+ toURI = this . userAgent . contact . uri . clone ( ) ;
1291+ } else if ( this . options . aor ) {
1292+ // Otherwise use the AOR provided to target the assocated registrar server.
1293+ const uri = UserAgent . makeURI ( this . options . aor ) ;
1294+ if ( ! uri ) {
1295+ throw new Error ( "Failed to create URI." ) ;
1296+ }
1297+ requestURI = uri . clone ( ) ;
1298+ requestURI . user = undefined ; // target the registrar server
1299+ fromURI = uri . clone ( ) ;
1300+ toURI = uri . clone ( ) ;
1301+ } else {
1302+ this . logger . error (
1303+ "You have enabled sending OPTIONS pings and as such you must provide either " +
1304+ "a) an AOR to register, or b) an RURI to use for the target of the OPTIONS ping requests. "
1305+ ) ;
1306+ return ;
1307+ }
1308+
1309+ // Send the OPTIONS pings
1310+ this . optionsPingRun ( requestURI , fromURI , toURI ) ;
1311+ }
1312+
1313+ /**
1314+ * Stop sending OPTIONS pings.
1315+ */
1316+ private optionsPingStop ( ) : void {
1317+ this . logger . log ( `OPTIONS pings stopped` ) ;
1318+ this . optionsPingRunning = false ;
1319+ this . optionsPingFailure = false ;
1320+ if ( this . optionsPingRequest ) {
1321+ this . optionsPingRequest . dispose ( ) ;
1322+ this . optionsPingRequest = undefined ;
1323+ }
1324+ if ( this . optionsPingTimeout ) {
1325+ clearTimeout ( this . optionsPingTimeout ) ;
1326+ this . optionsPingTimeout = undefined ;
1327+ }
1328+ }
1329+
11701330 /** Helper function to init send then send invite. */
11711331 private async sendInvite (
11721332 inviter : Inviter ,
0 commit comments