node-osc 11.2.2 → 11.3.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/.github/dependabot.yml +19 -0
- package/.node-version +1 -0
- package/{agent.md → AGENTS.md} +11 -8
- package/README.md +4 -2
- package/dist/lib/Client.js +4 -41
- package/dist/lib/Server.js +65 -0
- package/dist/lib/internal/send.js +48 -0
- package/dist/test/test-promises.js +16 -0
- package/dist/test/test-server.js +61 -0
- package/docs/API.md +5 -5
- package/docs/GUIDE.md +5 -1
- package/docs/README.md +1 -1
- package/lib/Client.mjs +4 -41
- package/lib/Server.mjs +65 -0
- package/lib/internal/send.mjs +46 -0
- package/package.json +4 -4
- package/scripts/generate-docs.mjs +173 -167
- package/test/fixtures/types/test-cjs-types.ts +1 -0
- package/test/fixtures/types/test-esm-types.ts +1 -0
- package/test/test-promises.mjs +16 -0
- package/test/test-server.mjs +61 -0
- package/types/Client.d.mts +2 -3
- package/types/Client.d.mts.map +1 -1
- package/types/Server.d.mts +30 -2
- package/types/Server.d.mts.map +1 -1
- package/types/internal/send.d.mts +3 -0
- package/types/internal/send.d.mts.map +1 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: "npm"
|
|
4
|
+
directory: "/"
|
|
5
|
+
schedule:
|
|
6
|
+
interval: "weekly"
|
|
7
|
+
day: "monday"
|
|
8
|
+
time: "08:00"
|
|
9
|
+
timezone: "UTC"
|
|
10
|
+
open-pull-requests-limit: 10
|
|
11
|
+
|
|
12
|
+
- package-ecosystem: "github-actions"
|
|
13
|
+
directory: "/"
|
|
14
|
+
schedule:
|
|
15
|
+
interval: "weekly"
|
|
16
|
+
day: "monday"
|
|
17
|
+
time: "08:30"
|
|
18
|
+
timezone: "UTC"
|
|
19
|
+
open-pull-requests-limit: 10
|
package/.node-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
24
|
package/{agent.md → AGENTS.md}
RENAMED
|
@@ -12,7 +12,7 @@ This document provides context and instructions for AI agents (GitHub Copilot, C
|
|
|
12
12
|
- Both callback and async/await APIs
|
|
13
13
|
- TypeScript type definitions generated from JSDoc
|
|
14
14
|
- Well-tested with comprehensive test coverage
|
|
15
|
-
- Supports Node.js 20, 22, and 24
|
|
15
|
+
- Supports Node.js 20, 22, and 24+
|
|
16
16
|
|
|
17
17
|
## Architecture
|
|
18
18
|
|
|
@@ -43,9 +43,9 @@ This document provides context and instructions for AI agents (GitHub Copilot, C
|
|
|
43
43
|
The project uses **ESM as the source format** but provides **dual ESM/CommonJS support**:
|
|
44
44
|
- Source files: `lib/**/*.mjs` (ESM)
|
|
45
45
|
- Built CommonJS files: `dist/lib/**/*.js` (transpiled via Rollup)
|
|
46
|
-
- TypeScript definitions: `types/index.d.mts`
|
|
46
|
+
- Generated TypeScript definitions: `types/*.d.mts` (with `types/index.d.mts` as the package entry point)
|
|
47
47
|
|
|
48
|
-
**Important:**
|
|
48
|
+
**Important:** `types/index.d.mts` is the exported type entry point for both ESM and CommonJS consumers, and the supporting `.d.mts` files in `types/` are generated artifacts.
|
|
49
49
|
|
|
50
50
|
### Package Exports
|
|
51
51
|
|
|
@@ -95,7 +95,7 @@ npm run clean
|
|
|
95
95
|
- Tests are written in ESM format in `test/test-*.mjs`
|
|
96
96
|
- Tests are run against both ESM source (`lib/`) and transpiled CJS (`dist/`)
|
|
97
97
|
- Uses `tap` test framework
|
|
98
|
-
-
|
|
98
|
+
- Tests typically use ephemeral ports via `new Server(0, ...)` and wait for readiness with `once(server, 'listening')`
|
|
99
99
|
- Always run `npm run build` before running CJS tests
|
|
100
100
|
- **100% test coverage is required** - All lines, branches, functions, and statements must be covered
|
|
101
101
|
|
|
@@ -122,7 +122,7 @@ The build is automatically run before publishing (`prepublishOnly` script).
|
|
|
122
122
|
- **JSDoc comments**: All public APIs must have JSDoc comments
|
|
123
123
|
- **Type annotations**: Use JSDoc types for TypeScript generation
|
|
124
124
|
- **Examples**: Include code examples in JSDoc comments
|
|
125
|
-
- **Auto-generated docs**: Run `npm run docs` after changing JSDoc comments
|
|
125
|
+
- **Auto-generated docs**: Run `npm run docs` after changing JSDoc comments; update `scripts/generate-docs.mjs` if the API doc layout or anchor generation needs to change
|
|
126
126
|
|
|
127
127
|
Example JSDoc pattern:
|
|
128
128
|
```javascript
|
|
@@ -181,21 +181,24 @@ When writing code that needs to work in both ESM and CJS:
|
|
|
181
181
|
|
|
182
182
|
### Tests
|
|
183
183
|
- `test/test-*.mjs` - Test files using tap framework
|
|
184
|
-
- `test/util.mjs` - Test utilities and helpers
|
|
185
184
|
- `test/fixtures/` - Test data and fixtures
|
|
186
185
|
|
|
187
186
|
### Documentation
|
|
188
187
|
- `README.md` - Main documentation with quick start guide
|
|
189
188
|
- `docs/API.md` - Auto-generated API reference (do not edit manually)
|
|
190
189
|
- `docs/GUIDE.md` - Best practices, error handling, troubleshooting
|
|
190
|
+
- `docs/README.md` - Documentation hub and maintenance notes
|
|
191
191
|
- `examples/` - Working example code for various use cases
|
|
192
192
|
|
|
193
193
|
### Configuration
|
|
194
|
+
- `.github/workflows/` - CI, release, and automation workflows
|
|
195
|
+
- `.github/dependabot.yml` - Dependabot configuration for npm and GitHub Actions updates
|
|
194
196
|
- `package.json` - Package configuration, scripts, exports
|
|
195
197
|
- `eslint.config.mjs` - ESLint configuration
|
|
196
198
|
- `rollup.config.mjs` - Rollup build configuration (ESM to CJS)
|
|
197
199
|
- `tsconfig.json` - TypeScript compiler options for type generation
|
|
198
200
|
- `jsdoc.json` - JSDoc configuration for documentation generation
|
|
201
|
+
- `scripts/generate-docs.mjs` - Regenerates API documentation and handles API doc anchor/link formatting logic
|
|
199
202
|
|
|
200
203
|
## Making Changes
|
|
201
204
|
|
|
@@ -206,7 +209,7 @@ When writing code that needs to work in both ESM and CJS:
|
|
|
206
209
|
3. **Export** from `lib/index.mjs` if it's a public API
|
|
207
210
|
4. **Write tests** in `test/test-*.mjs` - **must achieve 100% coverage** (lines, branches, functions, statements)
|
|
208
211
|
5. **Run tests**: `npm test` (tests both ESM and CJS)
|
|
209
|
-
6. **Update docs**: `npm run docs` to regenerate API.md
|
|
212
|
+
6. **Update docs**: `npm run docs` to regenerate `docs/API.md`
|
|
210
213
|
7. **Update README.md** if adding user-facing functionality
|
|
211
214
|
|
|
212
215
|
### Fixing a Bug
|
|
@@ -273,7 +276,7 @@ const decoded = decode(buffer);
|
|
|
273
276
|
|
|
274
277
|
### Test Issues
|
|
275
278
|
|
|
276
|
-
- **Port conflicts**: Tests
|
|
279
|
+
- **Port conflicts**: Tests usually avoid conflicts by binding servers to port `0` and reading back the assigned port after the `'listening'` event
|
|
277
280
|
- **Timing issues**: Use async/await and proper event handling
|
|
278
281
|
- **ESM/CJS differences**: Ensure code works in both environments
|
|
279
282
|
|
package/README.md
CHANGED
|
@@ -52,11 +52,13 @@ server.on('message', (msg) => {
|
|
|
52
52
|
- 📘 **[Usage Guide](./docs/GUIDE.md)** - Best practices, error handling, and troubleshooting
|
|
53
53
|
- 📖 **[Examples](./examples/)** - Working examples for various use cases
|
|
54
54
|
|
|
55
|
+
The API reference is generated from the source JSDoc comments. If you change a public API or its JSDoc, run `npm run docs` and review `docs/API.md`.
|
|
56
|
+
|
|
55
57
|
## Compatibility
|
|
56
58
|
|
|
57
|
-
Written
|
|
59
|
+
Written as ESM and published with both ESM and CommonJS entry points.
|
|
58
60
|
|
|
59
|
-
Supports
|
|
61
|
+
Supports Node.js 20, 22, and 24+ in both ESM and CJS environments.
|
|
60
62
|
|
|
61
63
|
## TypeScript
|
|
62
64
|
|
package/dist/lib/Client.js
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
var node_dgram = require('node:dgram');
|
|
4
4
|
var node_events = require('node:events');
|
|
5
|
-
var
|
|
6
|
-
var Message = require('./Message.js');
|
|
5
|
+
var send = require('./internal/send.js');
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* OSC Client for sending messages and bundles over UDP.
|
|
@@ -82,34 +81,6 @@ class Client extends node_events.EventEmitter {
|
|
|
82
81
|
});
|
|
83
82
|
}
|
|
84
83
|
}
|
|
85
|
-
_performSend(message, args, callback) {
|
|
86
|
-
let mes;
|
|
87
|
-
let buf;
|
|
88
|
-
try {
|
|
89
|
-
switch (typeof message) {
|
|
90
|
-
case 'object':
|
|
91
|
-
buf = osc.encode(message);
|
|
92
|
-
this._sock.send(buf, 0, buf.length, this.port, this.host, callback);
|
|
93
|
-
break;
|
|
94
|
-
case 'string':
|
|
95
|
-
mes = new Message(args[0]);
|
|
96
|
-
for (let i = 1; i < args.length; i++) {
|
|
97
|
-
mes.append(args[i]);
|
|
98
|
-
}
|
|
99
|
-
buf = osc.encode(mes);
|
|
100
|
-
this._sock.send(buf, 0, buf.length, this.port, this.host, callback);
|
|
101
|
-
break;
|
|
102
|
-
default:
|
|
103
|
-
throw new TypeError('That Message Just Doesn\'t Seem Right');
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
catch (e) {
|
|
107
|
-
if (e.code !== 'ERR_SOCKET_DGRAM_NOT_RUNNING') throw e;
|
|
108
|
-
const error = new ReferenceError('Cannot send message on closed socket.');
|
|
109
|
-
error.code = e.code;
|
|
110
|
-
callback(error);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
84
|
/**
|
|
114
85
|
* Send an OSC message or bundle to the server.
|
|
115
86
|
*
|
|
@@ -149,20 +120,12 @@ class Client extends node_events.EventEmitter {
|
|
|
149
120
|
* await client.send(bundle);
|
|
150
121
|
*/
|
|
151
122
|
send(...args) {
|
|
152
|
-
let message = args[0];
|
|
153
123
|
let callback;
|
|
154
|
-
|
|
155
|
-
// Convert array syntax to message object
|
|
156
|
-
if (message instanceof Array) {
|
|
157
|
-
message = {
|
|
158
|
-
address: message[0],
|
|
159
|
-
args: message.slice(1)
|
|
160
|
-
};
|
|
161
|
-
}
|
|
124
|
+
const message = args.shift();
|
|
162
125
|
|
|
163
126
|
if (typeof args[args.length - 1] === 'function') {
|
|
164
127
|
callback = args.pop();
|
|
165
|
-
this.
|
|
128
|
+
send(this._sock, message, args, this.port, this.host, callback);
|
|
166
129
|
}
|
|
167
130
|
else {
|
|
168
131
|
// No callback provided, return a Promise
|
|
@@ -171,7 +134,7 @@ class Client extends node_events.EventEmitter {
|
|
|
171
134
|
if (err) reject(err);
|
|
172
135
|
else resolve();
|
|
173
136
|
};
|
|
174
|
-
this.
|
|
137
|
+
send(this._sock, message, args, this.port, this.host, callback);
|
|
175
138
|
});
|
|
176
139
|
}
|
|
177
140
|
}
|
package/dist/lib/Server.js
CHANGED
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
var node_dgram = require('node:dgram');
|
|
4
4
|
var node_events = require('node:events');
|
|
5
|
+
var send = require('./internal/send.js');
|
|
5
6
|
var decode = require('#decode');
|
|
6
7
|
|
|
8
|
+
function createSocketNotReadyError() {
|
|
9
|
+
return new Error('Cannot send message before server is listening. Wait for the "listening" event.');
|
|
10
|
+
}
|
|
11
|
+
|
|
7
12
|
/**
|
|
8
13
|
* OSC Server for receiving messages and bundles over UDP.
|
|
9
14
|
*
|
|
@@ -85,6 +90,8 @@ class Server extends node_events.EventEmitter {
|
|
|
85
90
|
let decoded;
|
|
86
91
|
this.port = port;
|
|
87
92
|
this.host = host;
|
|
93
|
+
this._isListening = false;
|
|
94
|
+
this._isClosed = false;
|
|
88
95
|
this._sock = node_dgram.createSocket({
|
|
89
96
|
type: 'udp4',
|
|
90
97
|
reuseAddr: true
|
|
@@ -94,6 +101,8 @@ class Server extends node_events.EventEmitter {
|
|
|
94
101
|
// Update port and emit listening event when socket is ready
|
|
95
102
|
this._sock.on('listening', () => {
|
|
96
103
|
// Update port with actual bound port (important when using port 0)
|
|
104
|
+
this._isListening = true;
|
|
105
|
+
this._isClosed = false;
|
|
97
106
|
this.port = this._sock.address().port;
|
|
98
107
|
this.emit('listening');
|
|
99
108
|
if (cb) cb();
|
|
@@ -120,6 +129,62 @@ class Server extends node_events.EventEmitter {
|
|
|
120
129
|
this._sock.on('error', (err) => {
|
|
121
130
|
this.emit('error', err);
|
|
122
131
|
});
|
|
132
|
+
|
|
133
|
+
this._sock.on('close', () => {
|
|
134
|
+
this._isListening = false;
|
|
135
|
+
this._isClosed = true;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Send an OSC message or bundle from the server's bound socket.
|
|
140
|
+
*
|
|
141
|
+
* This method can be used with either a callback or as a Promise.
|
|
142
|
+
*
|
|
143
|
+
* @param {import('./Message.mjs').default|import('./Bundle.mjs').default|Array|string} message - The message, bundle, address, or array to send.
|
|
144
|
+
* @param {number} port - The remote port to send to.
|
|
145
|
+
* @param {string} host - The remote host to send to.
|
|
146
|
+
* @param {Function} [cb] - Optional callback function called when send completes.
|
|
147
|
+
* @returns {Promise<void>|undefined} Returns a Promise if no callback is provided.
|
|
148
|
+
*
|
|
149
|
+
* @throws {Error} If the server socket is not yet listening.
|
|
150
|
+
* @throws {TypeError} If the message format is invalid.
|
|
151
|
+
* @throws {ReferenceError} If attempting to send on a closed socket.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* // Send an address-only message
|
|
155
|
+
* await server.send('/ping', 9000, '127.0.0.1');
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* // Send an array message
|
|
159
|
+
* server.send(['/ack', 1], 9000, '192.168.1.42', (err) => {
|
|
160
|
+
* if (err) console.error(err);
|
|
161
|
+
* });
|
|
162
|
+
*/
|
|
163
|
+
send(message, port, host, cb) {
|
|
164
|
+
if (!this._isListening && !this._isClosed) {
|
|
165
|
+
const error = createSocketNotReadyError();
|
|
166
|
+
|
|
167
|
+
if (cb) {
|
|
168
|
+
cb(error);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (cb) {
|
|
176
|
+
send(this._sock, message, [], port, host, cb);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
return new Promise((resolve, reject) => {
|
|
180
|
+
const callback = (err) => {
|
|
181
|
+
if (err) reject(err);
|
|
182
|
+
else resolve();
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
send(this._sock, message, [], port, host, callback);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
123
188
|
}
|
|
124
189
|
/**
|
|
125
190
|
* Close the server socket.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var osc = require('../osc.js');
|
|
4
|
+
var Message = require('../Message.js');
|
|
5
|
+
|
|
6
|
+
function normalizeMessage(message) {
|
|
7
|
+
if (message instanceof Array) {
|
|
8
|
+
return {
|
|
9
|
+
address: message[0],
|
|
10
|
+
args: message.slice(1)
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return message;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function performSend(sock, message, args, port, host, callback) {
|
|
18
|
+
let mes;
|
|
19
|
+
let buf;
|
|
20
|
+
const normalizedMessage = normalizeMessage(message);
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
switch (typeof normalizedMessage) {
|
|
24
|
+
case 'object':
|
|
25
|
+
buf = osc.encode(normalizedMessage);
|
|
26
|
+
sock.send(buf, 0, buf.length, port, host, callback);
|
|
27
|
+
break;
|
|
28
|
+
case 'string':
|
|
29
|
+
mes = new Message(normalizedMessage);
|
|
30
|
+
for (const arg of args) {
|
|
31
|
+
mes.append(arg);
|
|
32
|
+
}
|
|
33
|
+
buf = osc.encode(mes);
|
|
34
|
+
sock.send(buf, 0, buf.length, port, host, callback);
|
|
35
|
+
break;
|
|
36
|
+
default:
|
|
37
|
+
throw new TypeError('That Message Just Doesn\'t Seem Right');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
if (e.code !== 'ERR_SOCKET_DGRAM_NOT_RUNNING') throw e;
|
|
42
|
+
const error = new ReferenceError('Cannot send message on closed socket.');
|
|
43
|
+
error.code = e.code;
|
|
44
|
+
callback(error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = performSend;
|
|
@@ -141,6 +141,22 @@ tap.test('server: close with promise', async (t) => {
|
|
|
141
141
|
t.pass('Server closed successfully with promise');
|
|
142
142
|
});
|
|
143
143
|
|
|
144
|
+
tap.test('server: send promise rejection on closed socket', async (t) => {
|
|
145
|
+
const oscServer = new nodeOsc.Server(0, '127.0.0.1');
|
|
146
|
+
|
|
147
|
+
t.plan(1);
|
|
148
|
+
|
|
149
|
+
await node_events.once(oscServer, 'listening');
|
|
150
|
+
await oscServer.close();
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
await oscServer.send('/boom', 3333, '127.0.0.1');
|
|
154
|
+
t.fail('Should have thrown an error');
|
|
155
|
+
} catch (err) {
|
|
156
|
+
t.equal(err.code, 'ERR_SOCKET_DGRAM_NOT_RUNNING', 'Should reject with correct error code');
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
144
160
|
tap.test('server: no callback still emits listening event', async (t) => {
|
|
145
161
|
const oscServer = new nodeOsc.Server(0, '127.0.0.1');
|
|
146
162
|
|
package/dist/test/test-server.js
CHANGED
|
@@ -89,6 +89,67 @@ tap.test('server: callback as second arg', async (t) => {
|
|
|
89
89
|
});
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
+
tap.test('server: send to remote port', async (t) => {
|
|
93
|
+
const sender = new nodeOsc.Server(0, '127.0.0.1');
|
|
94
|
+
const receiver = new nodeOsc.Server(0, '127.0.0.1');
|
|
95
|
+
await Promise.all([node_events.once(sender, 'listening'), node_events.once(receiver, 'listening')]);
|
|
96
|
+
|
|
97
|
+
t.plan(1);
|
|
98
|
+
|
|
99
|
+
t.teardown(async () => {
|
|
100
|
+
await Promise.all([sender.close(), receiver.close()]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const receivedMessage = node_events.once(receiver, 'message');
|
|
104
|
+
await sender.send(['/test', 1], receiver.port, '127.0.0.1');
|
|
105
|
+
const [msg] = await receivedMessage;
|
|
106
|
+
|
|
107
|
+
t.same(msg, ['/test', 1], 'server should send a message from its bound socket');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
tap.test('server: can receive and reply from within message handler', async (t) => {
|
|
111
|
+
const requester = new nodeOsc.Server(0, '127.0.0.1');
|
|
112
|
+
const responder = new nodeOsc.Server(0, '127.0.0.1');
|
|
113
|
+
await Promise.all([node_events.once(requester, 'listening'), node_events.once(responder, 'listening')]);
|
|
114
|
+
|
|
115
|
+
t.plan(3);
|
|
116
|
+
|
|
117
|
+
t.teardown(async () => {
|
|
118
|
+
await Promise.all([requester.close(), responder.close()]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
responder.on('message', (msg, rinfo) => {
|
|
122
|
+
t.same(msg, ['/ping', 1], 'responder should receive the incoming message');
|
|
123
|
+
responder.send(['/ack', 1], rinfo.port, rinfo.address, (err) => {
|
|
124
|
+
t.error(err, 'responder should reply without error');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const receivedReply = node_events.once(requester, 'message');
|
|
129
|
+
await requester.send(['/ping', 1], responder.port, '127.0.0.1');
|
|
130
|
+
const [reply] = await receivedReply;
|
|
131
|
+
|
|
132
|
+
t.same(reply, ['/ack', 1], 'requester should receive the reply on the same socket');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
tap.test('server: send before listening returns a clear error', async (t) => {
|
|
136
|
+
t.plan(3);
|
|
137
|
+
|
|
138
|
+
const server = new nodeOsc.Server(0, '127.0.0.1');
|
|
139
|
+
|
|
140
|
+
t.throws(() => {
|
|
141
|
+
server.send('/test', 3333, '127.0.0.1');
|
|
142
|
+
}, /before server is listening/i, 'send without callback should throw before listening');
|
|
143
|
+
|
|
144
|
+
server.send('/test', 3333, '127.0.0.1', (err) => {
|
|
145
|
+
t.ok(err instanceof Error, 'send should fail with an Error before listening');
|
|
146
|
+
t.match(err.message, /before server is listening/i, 'error message should explain the socket is not ready');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await node_events.once(server, 'listening');
|
|
150
|
+
await server.close();
|
|
151
|
+
});
|
|
152
|
+
|
|
92
153
|
tap.test('server: bad message', async (t) => {
|
|
93
154
|
t.plan(2);
|
|
94
155
|
const oscServer = new nodeOsc.Server(0, '127.0.0.1');
|
package/docs/API.md
CHANGED
|
@@ -13,17 +13,17 @@ For usage guides, best practices, and troubleshooting, see the **[Guide](./GUIDE
|
|
|
13
13
|
|
|
14
14
|
- [Server](#server)
|
|
15
15
|
- [Constructor](#server-constructor)
|
|
16
|
-
- [close()](#
|
|
16
|
+
- [close()](#serverclose)
|
|
17
17
|
- [Client](#client)
|
|
18
18
|
- [Constructor](#client-constructor)
|
|
19
|
-
- [close()](#
|
|
20
|
-
- [send()](#
|
|
19
|
+
- [close()](#clientclose)
|
|
20
|
+
- [send()](#clientsend)
|
|
21
21
|
- [Message](#message)
|
|
22
22
|
- [Constructor](#message-constructor)
|
|
23
|
-
- [append()](#
|
|
23
|
+
- [append()](#messageappend)
|
|
24
24
|
- [Bundle](#bundle)
|
|
25
25
|
- [Constructor](#bundle-constructor)
|
|
26
|
-
- [append()](#
|
|
26
|
+
- [append()](#bundleappend)
|
|
27
27
|
- [Low Level Functions](#low-level-functions)
|
|
28
28
|
- [encode()](#encode)
|
|
29
29
|
- [decode()](#decode)
|
package/docs/GUIDE.md
CHANGED
|
@@ -9,6 +9,8 @@ This guide provides best practices, patterns, and detailed information for using
|
|
|
9
9
|
- [Type System](#type-system)
|
|
10
10
|
- [Best Practices](#best-practices)
|
|
11
11
|
- [Troubleshooting](#troubleshooting)
|
|
12
|
+
- [Advanced Topics](#advanced-topics)
|
|
13
|
+
- [Further Reading](#further-reading)
|
|
12
14
|
|
|
13
15
|
## Events
|
|
14
16
|
|
|
@@ -483,10 +485,12 @@ await client.close();
|
|
|
483
485
|
Ensure you wait for the server to start before sending messages:
|
|
484
486
|
|
|
485
487
|
```javascript
|
|
488
|
+
import { once } from 'node:events';
|
|
489
|
+
|
|
486
490
|
const server = new Server(3333, '0.0.0.0');
|
|
487
491
|
|
|
488
492
|
// Wait for server to be ready
|
|
489
|
-
await
|
|
493
|
+
await once(server, 'listening');
|
|
490
494
|
|
|
491
495
|
// Now safe to send messages
|
|
492
496
|
console.log('Server ready!');
|
package/docs/README.md
CHANGED
|
@@ -56,7 +56,7 @@ A comprehensive guide covering:
|
|
|
56
56
|
|
|
57
57
|
The API documentation is automatically generated from JSDoc comments:
|
|
58
58
|
|
|
59
|
-
1. Edit JSDoc comments in the source files (`lib/**/*.mjs`)
|
|
59
|
+
1. Edit JSDoc comments in the source files (`lib/**/*.mjs`). If the docs layout or anchor generation needs to change, update `scripts/generate-docs.mjs`.
|
|
60
60
|
2. Run `npm run docs` to regenerate `API.md`
|
|
61
61
|
3. Review the changes and commit
|
|
62
62
|
|
package/lib/Client.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { createSocket } from 'node:dgram';
|
|
2
2
|
import { EventEmitter } from 'node:events';
|
|
3
|
-
import
|
|
4
|
-
import Message from './Message.mjs';
|
|
3
|
+
import performSend from './internal/send.mjs';
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* OSC Client for sending messages and bundles over UDP.
|
|
@@ -80,34 +79,6 @@ class Client extends EventEmitter {
|
|
|
80
79
|
});
|
|
81
80
|
}
|
|
82
81
|
}
|
|
83
|
-
_performSend(message, args, callback) {
|
|
84
|
-
let mes;
|
|
85
|
-
let buf;
|
|
86
|
-
try {
|
|
87
|
-
switch (typeof message) {
|
|
88
|
-
case 'object':
|
|
89
|
-
buf = encode(message);
|
|
90
|
-
this._sock.send(buf, 0, buf.length, this.port, this.host, callback);
|
|
91
|
-
break;
|
|
92
|
-
case 'string':
|
|
93
|
-
mes = new Message(args[0]);
|
|
94
|
-
for (let i = 1; i < args.length; i++) {
|
|
95
|
-
mes.append(args[i]);
|
|
96
|
-
}
|
|
97
|
-
buf = encode(mes);
|
|
98
|
-
this._sock.send(buf, 0, buf.length, this.port, this.host, callback);
|
|
99
|
-
break;
|
|
100
|
-
default:
|
|
101
|
-
throw new TypeError('That Message Just Doesn\'t Seem Right');
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
catch (e) {
|
|
105
|
-
if (e.code !== 'ERR_SOCKET_DGRAM_NOT_RUNNING') throw e;
|
|
106
|
-
const error = new ReferenceError('Cannot send message on closed socket.');
|
|
107
|
-
error.code = e.code;
|
|
108
|
-
callback(error);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
82
|
/**
|
|
112
83
|
* Send an OSC message or bundle to the server.
|
|
113
84
|
*
|
|
@@ -147,20 +118,12 @@ class Client extends EventEmitter {
|
|
|
147
118
|
* await client.send(bundle);
|
|
148
119
|
*/
|
|
149
120
|
send(...args) {
|
|
150
|
-
let message = args[0];
|
|
151
121
|
let callback;
|
|
152
|
-
|
|
153
|
-
// Convert array syntax to message object
|
|
154
|
-
if (message instanceof Array) {
|
|
155
|
-
message = {
|
|
156
|
-
address: message[0],
|
|
157
|
-
args: message.slice(1)
|
|
158
|
-
};
|
|
159
|
-
}
|
|
122
|
+
const message = args.shift();
|
|
160
123
|
|
|
161
124
|
if (typeof args[args.length - 1] === 'function') {
|
|
162
125
|
callback = args.pop();
|
|
163
|
-
this.
|
|
126
|
+
performSend(this._sock, message, args, this.port, this.host, callback);
|
|
164
127
|
}
|
|
165
128
|
else {
|
|
166
129
|
// No callback provided, return a Promise
|
|
@@ -169,7 +132,7 @@ class Client extends EventEmitter {
|
|
|
169
132
|
if (err) reject(err);
|
|
170
133
|
else resolve();
|
|
171
134
|
};
|
|
172
|
-
this.
|
|
135
|
+
performSend(this._sock, message, args, this.port, this.host, callback);
|
|
173
136
|
});
|
|
174
137
|
}
|
|
175
138
|
}
|
package/lib/Server.mjs
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { createSocket } from 'node:dgram';
|
|
2
2
|
import { EventEmitter } from 'node:events';
|
|
3
|
+
import performSend from './internal/send.mjs';
|
|
3
4
|
|
|
4
5
|
import decode from '#decode';
|
|
5
6
|
|
|
7
|
+
function createSocketNotReadyError() {
|
|
8
|
+
return new Error('Cannot send message before server is listening. Wait for the "listening" event.');
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
/**
|
|
7
12
|
* OSC Server for receiving messages and bundles over UDP.
|
|
8
13
|
*
|
|
@@ -84,6 +89,8 @@ class Server extends EventEmitter {
|
|
|
84
89
|
let decoded;
|
|
85
90
|
this.port = port;
|
|
86
91
|
this.host = host;
|
|
92
|
+
this._isListening = false;
|
|
93
|
+
this._isClosed = false;
|
|
87
94
|
this._sock = createSocket({
|
|
88
95
|
type: 'udp4',
|
|
89
96
|
reuseAddr: true
|
|
@@ -93,6 +100,8 @@ class Server extends EventEmitter {
|
|
|
93
100
|
// Update port and emit listening event when socket is ready
|
|
94
101
|
this._sock.on('listening', () => {
|
|
95
102
|
// Update port with actual bound port (important when using port 0)
|
|
103
|
+
this._isListening = true;
|
|
104
|
+
this._isClosed = false;
|
|
96
105
|
this.port = this._sock.address().port;
|
|
97
106
|
this.emit('listening');
|
|
98
107
|
if (cb) cb();
|
|
@@ -119,6 +128,62 @@ class Server extends EventEmitter {
|
|
|
119
128
|
this._sock.on('error', (err) => {
|
|
120
129
|
this.emit('error', err);
|
|
121
130
|
});
|
|
131
|
+
|
|
132
|
+
this._sock.on('close', () => {
|
|
133
|
+
this._isListening = false;
|
|
134
|
+
this._isClosed = true;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Send an OSC message or bundle from the server's bound socket.
|
|
139
|
+
*
|
|
140
|
+
* This method can be used with either a callback or as a Promise.
|
|
141
|
+
*
|
|
142
|
+
* @param {import('./Message.mjs').default|import('./Bundle.mjs').default|Array|string} message - The message, bundle, address, or array to send.
|
|
143
|
+
* @param {number} port - The remote port to send to.
|
|
144
|
+
* @param {string} host - The remote host to send to.
|
|
145
|
+
* @param {Function} [cb] - Optional callback function called when send completes.
|
|
146
|
+
* @returns {Promise<void>|undefined} Returns a Promise if no callback is provided.
|
|
147
|
+
*
|
|
148
|
+
* @throws {Error} If the server socket is not yet listening.
|
|
149
|
+
* @throws {TypeError} If the message format is invalid.
|
|
150
|
+
* @throws {ReferenceError} If attempting to send on a closed socket.
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* // Send an address-only message
|
|
154
|
+
* await server.send('/ping', 9000, '127.0.0.1');
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* // Send an array message
|
|
158
|
+
* server.send(['/ack', 1], 9000, '192.168.1.42', (err) => {
|
|
159
|
+
* if (err) console.error(err);
|
|
160
|
+
* });
|
|
161
|
+
*/
|
|
162
|
+
send(message, port, host, cb) {
|
|
163
|
+
if (!this._isListening && !this._isClosed) {
|
|
164
|
+
const error = createSocketNotReadyError();
|
|
165
|
+
|
|
166
|
+
if (cb) {
|
|
167
|
+
cb(error);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (cb) {
|
|
175
|
+
performSend(this._sock, message, [], port, host, cb);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
return new Promise((resolve, reject) => {
|
|
179
|
+
const callback = (err) => {
|
|
180
|
+
if (err) reject(err);
|
|
181
|
+
else resolve();
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
performSend(this._sock, message, [], port, host, callback);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
122
187
|
}
|
|
123
188
|
/**
|
|
124
189
|
* Close the server socket.
|