@babblevoice/babble-drachtio-callmanager 3.8.1 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/call.js CHANGED
@@ -293,6 +293,12 @@ class call {
293
293
  */
294
294
  this.parent = undefined
295
295
 
296
+ /**
297
+ * @summary Lifecycle callbacks supplied to newuac for this (outbound) call.
298
+ * @type { newuaccallbacks }
299
+ */
300
+ this.callbacks = {}
301
+
296
302
  /** @summary Other channels which we might need - for things like opening a channel on the same node. */
297
303
  this.relatives = new Set()
298
304
 
@@ -1472,6 +1478,15 @@ class call {
1472
1478
  this.epochs.mix = Math.floor( +new Date() / 1000 )
1473
1479
  if( this.parent ) this.parent.epochs.mix = Math.floor( +new Date() / 1000 )
1474
1480
 
1481
+ /* Now we are bridged/mixed with our parent, notify any onconnect handler. */
1482
+ if( this.callbacks.onconnect ) {
1483
+ try {
1484
+ await this.callbacks.onconnect( this )
1485
+ } catch( e ) {
1486
+ console.trace( e )
1487
+ }
1488
+ }
1489
+
1475
1490
  return this
1476
1491
  }
1477
1492
 
@@ -3808,6 +3823,8 @@ class call {
3808
3823
  return this
3809
3824
  }
3810
3825
 
3826
+ this.callbacks = callbacks
3827
+
3811
3828
  if( this.parent ) {
3812
3829
  const hangups = []
3813
3830
  for( const child of this.parent.children ) {
@@ -4062,11 +4079,18 @@ class call {
4062
4079
  * @param { call } call - our call object which is early
4063
4080
  */
4064
4081
 
4082
+ /**
4083
+ * @callback onconnectcallback
4084
+ * @async
4085
+ * @param { call } call - the (outbound) call which has just connected and been bridged with its parent
4086
+ */
4087
+
4065
4088
  /**
4066
4089
  * @typedef { object } newuaccallbacks
4067
4090
  * @property { earlycallback } [ early ] - callback to provide a call object with early call (pre dialog)
4068
4091
  * @property { confirmcallback } [ confirm ] - called when a dialog is confirmed but before it is bridged with a parent - this provides an opportunity for another call to adopt this call
4069
4092
  * @property { failcallback } [ fail ] - Called when child is terminated
4093
+ * @property { onconnectcallback } [ onconnect ] - called after this call has connected and been bridged/mixed with its parent
4070
4094
  */
4071
4095
 
4072
4096
  /**
@@ -4122,10 +4146,12 @@ class call {
4122
4146
  newcall.#confignetwork( options )
4123
4147
 
4124
4148
  let newdialog, earlypromise
4149
+ let settlewatchdog
4125
4150
  try {
4126
4151
  await newcall.#openchannelsfornewuac()
4127
4152
 
4128
- newdialog = await callmanager.options.srf.createUAC(
4153
+ const settlems = newcall.options.uactimeout || callmanager.options.uactimeout
4154
+ const createuacpromise = callmanager.options.srf.createUAC(
4129
4155
  options.contact + newcall.options.contactparams,
4130
4156
  {
4131
4157
  ...newcall.options,
@@ -4182,6 +4208,18 @@ class call {
4182
4208
  }
4183
4209
  }
4184
4210
  } )
4211
+
4212
+ const settlewatchdogpromise = new Promise( ( resolve, reject ) => {
4213
+ settlewatchdog = setTimeout( async () => {
4214
+ try {
4215
+ await newcall.hangup( hangupcodes.REQUEST_TIMEOUT )
4216
+ } catch ( e ) { /* silent */ }
4217
+
4218
+ reject( Object.assign( new Error( "newuac settle timeout" ), { status: 408 } ) )
4219
+ }, settlems )
4220
+ } )
4221
+
4222
+ newdialog = await Promise.race( [ createuacpromise, settlewatchdogpromise ] )
4185
4223
  } catch ( err ) {
4186
4224
  newcall.#onnewuaccatch( err )
4187
4225
  try{
@@ -4204,6 +4242,8 @@ class call {
4204
4242
  }
4205
4243
  }
4206
4244
  } catch( e ) { console.error( e ) }
4245
+ } finally {
4246
+ if( settlewatchdog ) clearTimeout( settlewatchdog )
4207
4247
  }
4208
4248
 
4209
4249
  if( earlypromise ) await earlypromise
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@babblevoice/babble-drachtio-callmanager",
3
- "version": "3.8.1",
3
+ "version": "3.9.0",
4
4
  "description": "Call processing to create a PBX",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -177,6 +177,54 @@ describe( "call object", function() {
177
177
 
178
178
  } )
179
179
 
180
+ it( "uas.newuac - onconnect callback is called after bridge with valid audio", async function() {
181
+
182
+ const srfscenario = new srf.srfscenario()
183
+
184
+ const call = await new Promise( ( resolve ) => {
185
+ srfscenario.oncall( async ( call ) => { resolve( call ) } )
186
+ srfscenario.inbound()
187
+ } )
188
+
189
+ let onconnectcount = 0
190
+ let connectedcall
191
+ let audioatconnect
192
+ let mixepochatconnect
193
+
194
+ const child = await call.newuac( { "contact": "1000@dummy" }, {
195
+ "onconnect": ( c ) => {
196
+ onconnectcount++
197
+ connectedcall = c
198
+ audioatconnect = c.channels.audio
199
+ mixepochatconnect = c.epochs.mix
200
+ }
201
+ } )
202
+
203
+ /* the handler ran exactly once, during newuac */
204
+ expect( onconnectcount ).to.equal( 1 )
205
+
206
+ /* it received the connected (child) call */
207
+ expect( connectedcall ).to.equal( child )
208
+
209
+ /* the audio channel was a valid object inside the handler - it must not
210
+ go undefined/false (the regression this callback originally exposed) */
211
+ expect( audioatconnect ).to.be.an( "object" )
212
+ expect( child.channels.audio ).to.equal( audioatconnect )
213
+
214
+ /* it fired AFTER the mix, i.e. once genuinely connected/bridged */
215
+ expect( mixepochatconnect ).to.be.a( "number" ).that.is.above( 0 )
216
+
217
+ await child.hangup()
218
+ await call.hangup()
219
+
220
+ expect( await callstore.stats() ).to.deep.include( {
221
+ "storebycallid": 0,
222
+ "storebyuuid": 0,
223
+ "storebyentity": 0
224
+ } )
225
+
226
+ } )
227
+
180
228
  it( "uas.newuac - create uac by entity no registrar", async function() {
181
229
  const srfscenario = new srf.srfscenario()
182
230
 
@@ -469,6 +517,47 @@ describe( "call object", function() {
469
517
  expect( child.state.cleaned ).to.be.true
470
518
  } )
471
519
 
520
+ it( "uas.newuac - createUAC never settles - watchdog forces return", async function() {
521
+
522
+ const srfscenario = new srf.srfscenario()
523
+ /* simulate a far end that never sends a final response and never honours
524
+ CANCEL: createUAC never resolves or rejects. Without the settle watchdog
525
+ newuac would hang here forever (and this test would hit the mocha timeout). */
526
+ srfscenario.oncreateUAC( () => new Promise( () => {} ) )
527
+
528
+ const call = await new Promise( ( resolve ) => {
529
+ srfscenario.oncall( async ( call ) => { resolve( call ) } )
530
+ srfscenario.inbound()
531
+ } )
532
+
533
+ const start = Date.now()
534
+ const child = await call.newuac( { "contact": "1000@dummy", "uactimeout": 50 } )
535
+ const elapsed = Date.now() - start
536
+
537
+ /* the watchdog must make newuac return promptly (~uactimeout) rather than
538
+ block on the never-settling createUAC */
539
+ expect( elapsed ).to.be.below( 1000 )
540
+
541
+ expect( child.destroyed ).to.be.true
542
+ expect( child.hangup_cause.sip ).to.equal( 408 )
543
+ expect( child.hangup_cause.reason ).to.equal( "REQUEST_TIMEOUT" )
544
+ expect( child.hangup_cause.src ).to.equal( "us" )
545
+
546
+ /* clean up runs on the event loop */
547
+ await new Promise( resolve => setTimeout( resolve, 100 ) )
548
+
549
+ await call.hangup()
550
+
551
+ expect( await callstore.stats() ).to.deep.include( {
552
+ "storebycallid": 0,
553
+ "storebyuuid": 0,
554
+ "storebyentity": 0
555
+ } )
556
+
557
+ expect( call.state.cleaned ).to.be.true
558
+ expect( child.state.cleaned ).to.be.true
559
+ } )
560
+
472
561
  it( "uas.newuac - child remote hangup", async function() {
473
562
 
474
563
  const srfscenario = new srf.srfscenario()