@babblevoice/babble-drachtio-callmanager 3.9.0 → 3.10.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
@@ -1351,7 +1351,16 @@ class call {
1351
1351
  if ( this.options.privacy )
1352
1352
  other.options.privacy = this.options.privacy
1353
1353
 
1354
- if( mix ) this.mix( other )
1354
+ /* When we adopt-and-mix (enterprise queues and ring groups connect this
1355
+ way rather than via #answerparent) we are the parent leg. preconnect
1356
+ runs before the mix (projectrtp cannot play into a mixing channel),
1357
+ then we mix, then postconnect. */
1358
+ if( mix ) {
1359
+ this.#notifyconnecthook( this, other, "preconnect" )
1360
+ .then( () => this.mix( other ) )
1361
+ .then( () => this.#notifyconnecthook( this, other, "postconnect" ) )
1362
+ .catch( ( e ) => console.trace( e ) )
1363
+ }
1355
1364
  return this
1356
1365
  }
1357
1366
 
@@ -1368,6 +1377,34 @@ class call {
1368
1377
  return this
1369
1378
  }
1370
1379
 
1380
+ /**
1381
+ * Register a handler to run once, on this (caller) leg, immediately BEFORE we
1382
+ * are mixed with a party connecting to us - whether via a direct bridge
1383
+ * (#answerparent) or an adopt-and-mix (enterprise queues, ring groups). Use
1384
+ * this to play a prompt to each leg on connect: projectrtp cannot play into a
1385
+ * channel that is being mixed, so it must happen before the mix. The handler
1386
+ * is passed the connecting (callee/agent) leg and fires at most once.
1387
+ * @param { connecthookcallback } handler
1388
+ * @return { call }
1389
+ */
1390
+ preconnect( handler ) {
1391
+ this.callbacks.preconnect = handler
1392
+ return this
1393
+ }
1394
+
1395
+ /**
1396
+ * Register a handler to run once, on this (caller) leg, immediately AFTER we
1397
+ * are mixed with a party connecting to us. Counterpart to preconnect() for
1398
+ * work that should happen once the two legs are bridged. The handler is passed
1399
+ * the connecting (callee/agent) leg and fires at most once.
1400
+ * @param { connecthookcallback } handler
1401
+ * @return { call }
1402
+ */
1403
+ postconnect( handler ) {
1404
+ this.callbacks.postconnect = handler
1405
+ return this
1406
+ }
1407
+
1371
1408
  /**
1372
1409
  * Disown distant relatives.
1373
1410
  * @return { call }
@@ -1451,6 +1488,32 @@ class call {
1451
1488
  return false
1452
1489
  }
1453
1490
 
1491
+ /**
1492
+ * Fire a parent leg's connect hook (preconnect/postconnect) exactly once, as
1493
+ * a child leg connects to it. Handlers are registered on the caller leg via
1494
+ * call.preconnect()/call.postconnect() (stored on callbacks - vars is reserved
1495
+ * for client apps) and cleared here so each runs once per registration (i.e.
1496
+ * once per call) even if the caller subsequently connects to further legs
1497
+ * (re-queue, transfer, ...).
1498
+ * @param { call } parentleg - the leg the handler was registered on (the caller)
1499
+ * @param { call } childleg - the leg that is connecting (callee/agent)
1500
+ * @param { string } hook - "preconnect" (before the mix) or "postconnect" (after)
1501
+ * @returns { Promise< void > }
1502
+ * @private
1503
+ */
1504
+ async #notifyconnecthook( parentleg, childleg, hook ) {
1505
+ if( !parentleg || !parentleg.callbacks || !parentleg.callbacks[ hook ] ) return
1506
+
1507
+ const handler = parentleg.callbacks[ hook ]
1508
+ parentleg.callbacks[ hook ] = undefined /* fire once */
1509
+
1510
+ try {
1511
+ await handler( childleg )
1512
+ } catch( e ) {
1513
+ console.trace( e )
1514
+ }
1515
+ }
1516
+
1454
1517
  /**
1455
1518
  *
1456
1519
  * @returns { Promise< object > } - return ourself
@@ -1468,6 +1531,12 @@ class call {
1468
1531
 
1469
1532
  if( this.#answerparenterrors() ) return this
1470
1533
 
1534
+ /* preconnect: our parent leg's handler runs BEFORE the mix. projectrtp
1535
+ cannot play audio into a channel that is being mixed, so a prompt that
1536
+ must be heard on connect is played to each leg separately here (both
1537
+ channels are open by this point), then we mix. */
1538
+ await this.#notifyconnecthook( this.parent, this, "preconnect" )
1539
+
1471
1540
  this.channels.audio.mix( this.parent.channels.audio )
1472
1541
 
1473
1542
  this._em.emit( "call.mix", this )
@@ -1478,7 +1547,9 @@ class call {
1478
1547
  this.epochs.mix = Math.floor( +new Date() / 1000 )
1479
1548
  if( this.parent ) this.parent.epochs.mix = Math.floor( +new Date() / 1000 )
1480
1549
 
1481
- /* Now we are bridged/mixed with our parent, notify any onconnect handler. */
1550
+ /* Now we are bridged/mixed with our parent, notify any post-connect handlers.
1551
+ The child-level handler is the one supplied to this leg's own newuac; the
1552
+ parent-leg postconnect is registered on the caller via call.postconnect(). */
1482
1553
  if( this.callbacks.onconnect ) {
1483
1554
  try {
1484
1555
  await this.callbacks.onconnect( this )
@@ -1487,6 +1558,8 @@ class call {
1487
1558
  }
1488
1559
  }
1489
1560
 
1561
+ await this.#notifyconnecthook( this.parent, this, "postconnect" )
1562
+
1490
1563
  return this
1491
1564
  }
1492
1565
 
@@ -4085,6 +4158,12 @@ class call {
4085
4158
  * @param { call } call - the (outbound) call which has just connected and been bridged with its parent
4086
4159
  */
4087
4160
 
4161
+ /**
4162
+ * @callback connecthookcallback
4163
+ * @async
4164
+ * @param { call } call - the connecting (callee/agent) leg being bridged with the caller leg the hook was registered on
4165
+ */
4166
+
4088
4167
  /**
4089
4168
  * @typedef { object } newuaccallbacks
4090
4169
  * @property { earlycallback } [ early ] - callback to provide a call object with early call (pre dialog)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@babblevoice/babble-drachtio-callmanager",
3
- "version": "3.9.0",
3
+ "version": "3.10.0",
4
4
  "description": "Call processing to create a PBX",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -225,6 +225,134 @@ describe( "call object", function() {
225
225
 
226
226
  } )
227
227
 
228
+ it( "call.preconnect/postconnect - parent-leg hooks fire around the mix when a child bridges (ringall/calldevice path)", async function() {
229
+
230
+ const srfscenario = new srf.srfscenario()
231
+
232
+ const call = await new Promise( ( resolve ) => {
233
+ srfscenario.oncall( async ( call ) => { resolve( call ) } )
234
+ srfscenario.inbound()
235
+ } )
236
+
237
+ let precount = 0, postcount = 0
238
+ let preleg, postleg
239
+ let premixepoch, postmixepoch
240
+ let preaudio
241
+
242
+ /* hooks are registered on the parent (caller) leg, before the child exists */
243
+ call.preconnect( ( c ) => {
244
+ precount++
245
+ preleg = c
246
+ premixepoch = c.epochs.mix /* not mixed yet -> 0 */
247
+ preaudio = c.channels.audio /* channel must be open before the mix */
248
+ } )
249
+ call.postconnect( ( c ) => {
250
+ postcount++
251
+ postleg = c
252
+ postmixepoch = c.epochs.mix /* mixed now -> > 0 */
253
+ } )
254
+
255
+ /* a normal (non-orphan) newuac - the child bridges via #answerparent */
256
+ const child = await call.newuac( { "contact": "1000@dummy" } )
257
+
258
+ /* each fired exactly once, received the connecting child */
259
+ expect( precount ).to.equal( 1 )
260
+ expect( postcount ).to.equal( 1 )
261
+ expect( preleg ).to.equal( child )
262
+ expect( postleg ).to.equal( child )
263
+
264
+ /* preconnect ran BEFORE the mix, with a valid (open) audio channel;
265
+ postconnect ran AFTER the mix */
266
+ expect( premixepoch ).to.equal( 0 )
267
+ expect( preaudio ).to.be.an( "object" )
268
+ expect( postmixepoch ).to.be.a( "number" ).that.is.above( 0 )
269
+
270
+ /* fire-once: both hooks have been cleared off the parent leg */
271
+ expect( call.callbacks.preconnect ).to.be.undefined
272
+ expect( call.callbacks.postconnect ).to.be.undefined
273
+
274
+ await child.hangup()
275
+ await call.hangup()
276
+
277
+ expect( await callstore.stats() ).to.deep.include( {
278
+ "storebycallid": 0,
279
+ "storebyuuid": 0,
280
+ "storebyentity": 0
281
+ } )
282
+
283
+ } )
284
+
285
+ it( "call.preconnect/postconnect - parent-leg hooks fire around adopt-and-mix (enterprise/ring-group path)", async function() {
286
+
287
+ const srfscenario = new srf.srfscenario()
288
+
289
+ const call = await new Promise( ( resolve ) => {
290
+ srfscenario.oncall( async ( call ) => { resolve( call ) } )
291
+ srfscenario.inbound()
292
+ } )
293
+
294
+ await call.answer()
295
+
296
+ /* enterprise queues create the agent leg orphaned (no parent), so it does
297
+ NOT bridge via #answerparent - it is later adopted by the winning caller */
298
+ const agent = await call.newuac( { "contact": "1000@dummy", "orphan": true } )
299
+ expect( agent.parent ).to.be.undefined
300
+
301
+ let precount = 0, postcount = 0
302
+ let preleg, postleg
303
+ let premixepoch, postmixepoch
304
+ let resolveconnected
305
+ const connected = new Promise( ( resolve ) => { resolveconnected = resolve } )
306
+
307
+ call.preconnect( ( c ) => {
308
+ precount++
309
+ preleg = c
310
+ premixepoch = c.epochs.mix
311
+ } )
312
+ call.postconnect( ( c ) => {
313
+ postcount++
314
+ postleg = c
315
+ postmixepoch = c.epochs.mix
316
+ resolveconnected()
317
+ } )
318
+
319
+ /* the orphaned agent existing did not fire either hook */
320
+ expect( precount ).to.equal( 0 )
321
+ expect( postcount ).to.equal( 0 )
322
+
323
+ /* the caller adopts-and-mixes the agent - this is the connect moment */
324
+ call.adopt( agent, true )
325
+ await connected
326
+
327
+ expect( precount ).to.equal( 1 )
328
+ expect( postcount ).to.equal( 1 )
329
+ expect( preleg ).to.equal( agent )
330
+ expect( postleg ).to.equal( agent )
331
+
332
+ /* preconnect ran before the mix, postconnect after */
333
+ expect( premixepoch ).to.equal( 0 )
334
+ expect( postmixepoch ).to.be.a( "number" ).that.is.above( 0 )
335
+
336
+ /* fire-once: a second adopt-and-mix does not refire */
337
+ expect( call.callbacks.preconnect ).to.be.undefined
338
+ expect( call.callbacks.postconnect ).to.be.undefined
339
+ const agent2 = await call.newuac( { "contact": "1000@dummy", "orphan": true } )
340
+ call.adopt( agent2, true )
341
+ expect( precount ).to.equal( 1 )
342
+ expect( postcount ).to.equal( 1 )
343
+
344
+ await agent2.hangup()
345
+ await agent.hangup()
346
+ await call.hangup()
347
+
348
+ expect( await callstore.stats() ).to.deep.include( {
349
+ "storebycallid": 0,
350
+ "storebyuuid": 0,
351
+ "storebyentity": 0
352
+ } )
353
+
354
+ } )
355
+
228
356
  it( "uas.newuac - create uac by entity no registrar", async function() {
229
357
  const srfscenario = new srf.srfscenario()
230
358