@jambonz/mrf 0.1.1 → 0.1.3

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/endpoint.js CHANGED
@@ -46,12 +46,16 @@ class Endpoint extends EventEmitter {
46
46
  seekOffset = file.seekOffset;
47
47
  file = file.file;
48
48
  }
49
- const urls = (Array.isArray(file) ? file : [file]).map(translatePlayUrl);
49
+ const files = Array.isArray(file) ? file : [file];
50
+ const urls = files.map(translatePlayUrl);
50
51
  const data = { urls };
51
52
  if (seekOffset > 0) data.seekOffset = parseInt(seekOffset, 10);
52
53
  const { playId } = await this._request('play.start', data);
53
54
  return new Promise((resolve, reject) => {
54
- this._pendingPlays.set(playId, { resolve, reject });
55
+ // file (the caller's original, untranslated path) rides on the
56
+ // playback-start/stop events: FS parity — the say/play tasks match
57
+ // events to plays by evt.file when there is no tts playback id
58
+ this._pendingPlays.set(playId, { resolve, reject, file: files[0] });
55
59
  });
56
60
  }
57
61
 
@@ -457,9 +461,12 @@ class Endpoint extends EventEmitter {
457
461
  case 'dtmf':
458
462
  this.emit('dtmf', { dtmf: data.digit, duration: data.durationMs, source: data.source });
459
463
  break;
460
- case 'play.start':
461
- this.emit('playback-start', { playId: data.playId, ...ttsVars(data.tts) });
464
+ case 'play.start': {
465
+ const file = this._pendingPlays.get(data.playId)?.file;
466
+ this.emit('playback-start',
467
+ { playId: data.playId, ...(file && {file}), ...ttsVars(data.tts) });
462
468
  break;
469
+ }
463
470
  case 'play.done': {
464
471
  const p = this._pendingPlays.get(data.playId);
465
472
  if (p) {
@@ -475,7 +482,9 @@ class Endpoint extends EventEmitter {
475
482
  playbackLastOffsetPos: data.lastOffsetPos ?? 0
476
483
  });
477
484
  }
478
- this.emit('playback-stop', { playId: data.playId, reason: data.reason, ...ttsVars(data.tts) });
485
+ this.emit('playback-stop',
486
+ { playId: data.playId, reason: data.reason, ...(p?.file && {file: p.file}),
487
+ ...ttsVars(data.tts) });
479
488
  break;
480
489
  }
481
490
  default: {
@@ -1,5 +1,14 @@
1
1
  const { EventEmitter } = require('events');
2
2
  const Endpoint = require('./endpoint');
3
+ const Connection = require('./connection');
4
+
5
+ const RECONNECT_DELAY_MS = 500;
6
+ const RECONNECT_DELAY_MAX_MS = 5000;
7
+
8
+ function clientInfo() {
9
+ const { name, version } = require('../package.json');
10
+ return `${name}/${version}`;
11
+ }
3
12
 
4
13
  /**
5
14
  * MediaServer represents one connection to a mediajam server, exposing the
@@ -28,25 +37,69 @@ class MediaServer extends EventEmitter {
28
37
  // fsmrf compatibility: feature-server listens on ms.conn for esl events
29
38
  this.conn = new EventEmitter();
30
39
 
40
+ this._wireConnection(connection);
41
+ }
42
+
43
+ _wireConnection(connection) {
44
+ this._connection = connection;
31
45
  connection.on('evt', (frame) => this._onEvent(frame));
32
46
  connection.on('stats', (data) => this._onStats(data));
33
- connection.on('close', () => {
34
- this.connected = false;
35
- // fsmrf parity: a self-initiated destroy()/disconnect() tears down
36
- // quietly; esl::end and endpoint destroy events are reserved for an
37
- // unexpected loss of the media server
38
- if (this._selfDestroyed) {
39
- this._endpoints.clear();
47
+ connection.on('close', () => this._onConnectionClose());
48
+ connection.on('error', (err) => this.emit('error', err));
49
+ }
50
+
51
+ /* An unexpected loss of the media server ends its endpoints (the
52
+ * server died with them) but NOT this MediaServer: mediajam restarts
53
+ * (deploys) must not take the feature-server down with them, so we
54
+ * quietly redial with backoff and re-emit esl::ready on recovery —
55
+ * mirroring the ESL auto-reconnect of the freeswitch world. esl::end
56
+ * is never emitted for a transient loss because the feature-server
57
+ * treats it as fatal (it tears down drachtio when its last media
58
+ * server is gone). A self-initiated destroy()/disconnect() tears down
59
+ * quietly with no reconnect. */
60
+ _onConnectionClose() {
61
+ this.connected = false;
62
+ if (this._selfDestroyed) {
63
+ this._endpoints.clear();
64
+ return;
65
+ }
66
+ for (const [, ep] of this._endpoints) {
67
+ ep._onEvent('endpoint.destroyed', { reason: 'connectionLost' });
68
+ }
69
+ this._endpoints.clear();
70
+ this.emit('disconnect');
71
+ this._reconnect();
72
+ }
73
+
74
+ async _reconnect() {
75
+ if (this._reconnecting) return;
76
+ this._reconnecting = true;
77
+ this.logger?.info(`mediajam at ${this.address}:${this.port} lost; reconnecting`);
78
+ let delay = RECONNECT_DELAY_MS;
79
+ let attempts = 0;
80
+ while (!this._selfDestroyed) {
81
+ // unref: a pending reconnect must not hold the process open
82
+ await new Promise((r) => setTimeout(r, delay).unref());
83
+ try {
84
+ const connection = new Connection(this.logger);
85
+ const helloData = await connection.connect(
86
+ { address: this.address, port: this.port }, clientInfo());
87
+ this._wireConnection(connection);
88
+ this._reconnecting = false;
89
+ this._onHello(helloData);
90
+ this.logger?.info(`reconnected to mediajam at ${this.address}:${this.port}` +
91
+ ` after ${attempts + 1} attempt(s)`);
40
92
  return;
93
+ } catch (err) {
94
+ attempts++;
95
+ if (attempts % 10 === 0) {
96
+ this.logger?.info(`still unable to reach mediajam at ${this.address}:${this.port}` +
97
+ ` (${attempts} attempts): ${err.message}`);
98
+ }
99
+ delay = Math.min(delay * 2, RECONNECT_DELAY_MAX_MS);
41
100
  }
42
- for (const [, ep] of this._endpoints) {
43
- ep._onEvent('endpoint.destroyed', { reason: 'connectionLost' });
44
- }
45
- this._endpoints.clear();
46
- this.conn.emit('esl::end');
47
- this.emit('disconnect');
48
- });
49
- connection.on('error', (err) => this.emit('error', err));
101
+ }
102
+ this._reconnecting = false;
50
103
  }
51
104
 
52
105
  _onHello(helloData) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jambonz/mrf",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "test": "node --test",
@@ -174,19 +174,28 @@ test('stats update mediaserver gauges', async(t) => {
174
174
  assert.equal(ms.cpuIdle, 88);
175
175
  });
176
176
 
177
- test('connection loss destroys endpoints with connectionLost', async() => {
177
+ test('connection loss destroys endpoints and reconnects', async() => {
178
178
  const mock = new MockMediajam();
179
179
  const port = await mock.listen();
180
180
  const mrf = new Mrf();
181
181
  const ms = await mrf.connect({ address: '127.0.0.1', port });
182
182
  const ep = await ms.createEndpoint({});
183
183
  const destroyed = new Promise((resolve) => ep.once('destroy', resolve));
184
- const ended = new Promise((resolve) => ms.conn.once('esl::end', resolve));
184
+ const disconnected = new Promise((resolve) => ms.once('disconnect', resolve));
185
+ const ready = new Promise((resolve) => ms.conn.once('esl::ready', resolve));
185
186
  mock.close();
186
187
  for (const socket of mock.sockets) socket.destroy();
187
188
  const evt = await destroyed;
188
- await ended;
189
+ await disconnected;
189
190
  assert.equal(evt.reason, 'connectionLost');
191
+ // esl::end must NOT fire on transient loss (it is fatal to the
192
+ // feature-server); the wrapper redials and re-emits esl::ready
193
+ ms.conn.once('esl::end', () => assert.fail('esl::end fired on transient loss'));
194
+ await mock.listen(port);
195
+ await ready;
196
+ assert.equal(ms.connected, true);
197
+ ms.destroy();
198
+ mock.close();
190
199
  });
191
200
 
192
201
  test('setLogLevel changes and queries server log level', async(t) => {
@@ -14,10 +14,10 @@ class MockMediajam {
14
14
  this.requests = []; // every req frame received, for assertions
15
15
  }
16
16
 
17
- listen() {
17
+ listen(port = 0) {
18
18
  return new Promise((resolve) => {
19
19
  this.server = net.createServer((socket) => this._onConnection(socket));
20
- this.server.listen(0, '127.0.0.1', () => {
20
+ this.server.listen(port, '127.0.0.1', () => {
21
21
  this.port = this.server.address().port;
22
22
  resolve(this.port);
23
23
  });