@jambonz/mrf 0.1.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.
@@ -0,0 +1,15 @@
1
+ name: CI
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v4
10
+ - uses: actions/setup-node@v4
11
+ with:
12
+ node-version: 22.x
13
+ - run: npm install
14
+ - run: npm run jslint
15
+ - run: npm test
@@ -0,0 +1,23 @@
1
+ name: npm-publish
2
+
3
+ # run when a tag is pushed or kick off manually
4
+ on:
5
+ push:
6
+ tags:
7
+ - '*'
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-node@v4
17
+ with:
18
+ node-version: lts/*
19
+ registry-url: 'https://registry.npmjs.org'
20
+ - run: npm install
21
+ - run: npm publish --access public
22
+ env:
23
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -0,0 +1,30 @@
1
+ const js = require('@eslint/js');
2
+
3
+ module.exports = [
4
+ js.configs.recommended,
5
+ {
6
+ files: ['**/*.js'],
7
+ languageOptions: {
8
+ ecmaVersion: 2022,
9
+ sourceType: 'commonjs',
10
+ globals: {
11
+ require: 'readonly',
12
+ module: 'writable',
13
+ process: 'readonly',
14
+ console: 'readonly',
15
+ setTimeout: 'readonly',
16
+ clearTimeout: 'readonly',
17
+ setInterval: 'readonly',
18
+ clearInterval: 'readonly',
19
+ Buffer: 'readonly',
20
+ __dirname: 'readonly'
21
+ }
22
+ },
23
+ rules: {
24
+ 'max-len': ['error', { code: 120 }],
25
+ quotes: ['error', 'single', { avoidEscape: true }],
26
+ semi: ['error', 'always'],
27
+ 'no-unused-vars': ['error', { argsIgnorePattern: '^_' }]
28
+ }
29
+ }
30
+ ];
package/index.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./lib/mrf');
@@ -0,0 +1,128 @@
1
+ const { EventEmitter } = require('events');
2
+ const net = require('net');
3
+
4
+ const REQUEST_TIMEOUT_MS = 10_000;
5
+ const PROTOCOL_VERSION = 1;
6
+
7
+ /**
8
+ * Connection speaks the mediajam control protocol: newline-delimited JSON
9
+ * over TCP with a hello handshake (see mediajam docs/control-protocol.md).
10
+ *
11
+ * Events: 'connect' (server hello data), 'close', 'error', 'stats' (data),
12
+ * 'evt' (full event frame).
13
+ */
14
+ class Connection extends EventEmitter {
15
+ constructor(logger) {
16
+ super();
17
+ this.logger = logger;
18
+ this.socket = null;
19
+ this.nextId = 0;
20
+ this.pending = new Map();
21
+ this.closed = false;
22
+ this._buf = '';
23
+ }
24
+
25
+ connect({ address, port }, clientInfo) {
26
+ return new Promise((resolve, reject) => {
27
+ const socket = net.connect({ host: address, port }, () => {
28
+ this._send({ t: 'hello', data: { version: PROTOCOL_VERSION, client: clientInfo } });
29
+ });
30
+ this.socket = socket;
31
+ socket.setNoDelay(true);
32
+
33
+ const onError = (err) => reject(err);
34
+ socket.once('error', onError);
35
+
36
+ this.once('connect', (helloData) => {
37
+ socket.removeListener('error', onError);
38
+ socket.on('error', (err) => this.emit('error', err));
39
+ resolve(helloData);
40
+ });
41
+
42
+ socket.on('data', (chunk) => this._onData(chunk));
43
+ socket.on('close', () => {
44
+ this.closed = true;
45
+ for (const [, p] of this.pending) p.reject(new Error('connection closed'));
46
+ this.pending.clear();
47
+ this.emit('close');
48
+ });
49
+ });
50
+ }
51
+
52
+ close() {
53
+ this.closed = true;
54
+ if (this.socket) this.socket.destroy();
55
+ }
56
+
57
+ _onData(chunk) {
58
+ this._buf += chunk.toString('utf8');
59
+ let idx;
60
+ while ((idx = this._buf.indexOf('\n')) >= 0) {
61
+ const line = this._buf.slice(0, idx);
62
+ this._buf = this._buf.slice(idx + 1);
63
+ if (!line.trim()) continue;
64
+ let frame;
65
+ try {
66
+ frame = JSON.parse(line);
67
+ } catch {
68
+ this.logger.error({ line }, 'mediajam: unparseable frame');
69
+ continue;
70
+ }
71
+ this._onFrame(frame);
72
+ }
73
+ }
74
+
75
+ _onFrame(frame) {
76
+ switch (frame.t) {
77
+ case 'hello':
78
+ this.emit('connect', frame.data || {});
79
+ break;
80
+ case 'res': {
81
+ const p = this.pending.get(frame.id);
82
+ if (!p) return;
83
+ this.pending.delete(frame.id);
84
+ clearTimeout(p.timer);
85
+ if (frame.ok) p.resolve(frame.data || {});
86
+ else {
87
+ const err = new Error(frame.err?.msg || 'command failed');
88
+ err.code = frame.err?.code;
89
+ p.reject(err);
90
+ }
91
+ break;
92
+ }
93
+ case 'evt':
94
+ this.emit('evt', frame);
95
+ break;
96
+ case 'stats':
97
+ this.emit('stats', frame.data || {});
98
+ break;
99
+ default:
100
+ this.logger.info({ frame }, 'mediajam: unexpected frame type');
101
+ }
102
+ }
103
+
104
+ _send(obj) {
105
+ this.socket.write(`${JSON.stringify(obj)}\n`);
106
+ }
107
+
108
+ /**
109
+ * Send a request, returning a promise for the response data.
110
+ */
111
+ request(cmd, ep, data) {
112
+ if (this.closed) return Promise.reject(new Error('connection closed'));
113
+ const id = String(++this.nextId);
114
+ const frame = { t: 'req', id, cmd };
115
+ if (ep) frame.ep = ep;
116
+ if (data) frame.data = data;
117
+ return new Promise((resolve, reject) => {
118
+ const timer = setTimeout(() => {
119
+ this.pending.delete(id);
120
+ reject(new Error(`timeout waiting for response to ${cmd}`));
121
+ }, REQUEST_TIMEOUT_MS);
122
+ this.pending.set(id, { resolve, reject, timer });
123
+ this._send(frame);
124
+ });
125
+ }
126
+ }
127
+
128
+ module.exports = Connection;