@jambonz/mrf 0.1.0 → 0.1.2

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.
@@ -6,8 +6,8 @@ jobs:
6
6
  build:
7
7
  runs-on: ubuntu-latest
8
8
  steps:
9
- - uses: actions/checkout@v4
10
- - uses: actions/setup-node@v4
9
+ - uses: actions/checkout@v5
10
+ - uses: actions/setup-node@v5
11
11
  with:
12
12
  node-version: 22.x
13
13
  - run: npm install
@@ -12,8 +12,8 @@ jobs:
12
12
  runs-on: ubuntu-latest
13
13
 
14
14
  steps:
15
- - uses: actions/checkout@v4
16
- - uses: actions/setup-node@v4
15
+ - uses: actions/checkout@v5
16
+ - uses: actions/setup-node@v5
17
17
  with:
18
18
  node-version: lts/*
19
19
  registry-url: 'https://registry.npmjs.org'
package/lib/endpoint.js CHANGED
@@ -487,7 +487,7 @@ class Endpoint extends EventEmitter {
487
487
  if (data && typeof data.json === 'string') {
488
488
  try {
489
489
  payload = JSON.parse(data.json);
490
- } catch (err) {
490
+ } catch {
491
491
  payload = data.json;
492
492
  }
493
493
  } else if (data && data.body !== undefined) {
@@ -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.0",
3
+ "version": "0.1.2",
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
  });