node-osc 11.2.3 → 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 +0 -1
- package/types/Client.d.mts.map +1 -1
- package/types/Server.d.mts +28 -0
- 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.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { encode } from '../osc.mjs';
|
|
2
|
+
import Message from '../Message.mjs';
|
|
3
|
+
|
|
4
|
+
function normalizeMessage(message) {
|
|
5
|
+
if (message instanceof Array) {
|
|
6
|
+
return {
|
|
7
|
+
address: message[0],
|
|
8
|
+
args: message.slice(1)
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return message;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function performSend(sock, message, args, port, host, callback) {
|
|
16
|
+
let mes;
|
|
17
|
+
let buf;
|
|
18
|
+
const normalizedMessage = normalizeMessage(message);
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
switch (typeof normalizedMessage) {
|
|
22
|
+
case 'object':
|
|
23
|
+
buf = encode(normalizedMessage);
|
|
24
|
+
sock.send(buf, 0, buf.length, port, host, callback);
|
|
25
|
+
break;
|
|
26
|
+
case 'string':
|
|
27
|
+
mes = new Message(normalizedMessage);
|
|
28
|
+
for (const arg of args) {
|
|
29
|
+
mes.append(arg);
|
|
30
|
+
}
|
|
31
|
+
buf = encode(mes);
|
|
32
|
+
sock.send(buf, 0, buf.length, port, host, callback);
|
|
33
|
+
break;
|
|
34
|
+
default:
|
|
35
|
+
throw new TypeError('That Message Just Doesn\'t Seem Right');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
if (e.code !== 'ERR_SOCKET_DGRAM_NOT_RUNNING') throw e;
|
|
40
|
+
const error = new ReferenceError('Cannot send message on closed socket.');
|
|
41
|
+
error.code = e.code;
|
|
42
|
+
callback(error);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default performSend;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-osc",
|
|
3
3
|
"description": "pyOSC inspired library for sending and receiving OSC messages",
|
|
4
|
-
"version": "11.
|
|
4
|
+
"version": "11.3.0",
|
|
5
5
|
"exports": {
|
|
6
6
|
"types": "./types/index.d.mts",
|
|
7
7
|
"require": "./dist/lib/index.js",
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
"url": "git+https://github.com/MylesBorins/node-osc.git"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@eslint/js": "^
|
|
51
|
-
"eslint": "^
|
|
52
|
-
"globals": "^
|
|
50
|
+
"@eslint/js": "^10.0.1",
|
|
51
|
+
"eslint": "^10.0.3",
|
|
52
|
+
"globals": "^17.4.0",
|
|
53
53
|
"jsdoc": "^4.0.5",
|
|
54
54
|
"rollup": "^4.46.2",
|
|
55
55
|
"tap": "^21.1.0",
|
|
@@ -5,120 +5,89 @@
|
|
|
5
5
|
* This script reads JSDoc JSON data and generates a formatted Markdown file.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { writeFileSync } from 'node:fs';
|
|
9
8
|
import { execSync } from 'node:child_process';
|
|
9
|
+
import { writeFileSync } from 'node:fs';
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
jsdocJson = execSync('npx jsdoc -X -c jsdoc.json', {
|
|
15
|
-
encoding: 'utf8',
|
|
16
|
-
maxBuffer: 10 * 1024 * 1024
|
|
17
|
-
});
|
|
18
|
-
} catch (error) {
|
|
19
|
-
console.error('❌ Failed to run JSDoc:');
|
|
20
|
-
console.error(error.message);
|
|
21
|
-
process.exit(1);
|
|
22
|
-
}
|
|
11
|
+
const classOrder = ['Server', 'Client', 'Message', 'Bundle'];
|
|
12
|
+
const functionOrder = ['encode', 'decode'];
|
|
23
13
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
process.exit(1);
|
|
14
|
+
function githubAnchor(text) {
|
|
15
|
+
return text
|
|
16
|
+
.trim()
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
.replace(/[^\w\- ]+/g, '')
|
|
19
|
+
.replace(/\s+/g, '-');
|
|
31
20
|
}
|
|
32
21
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (!classes[item.name]) {
|
|
42
|
-
classes[item.name] = {
|
|
43
|
-
desc: item.classdesc,
|
|
44
|
-
constructor: null,
|
|
45
|
-
methods: [],
|
|
46
|
-
examples: item.examples || [],
|
|
47
|
-
augments: item.augments || []
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
// Look for constructor params
|
|
51
|
-
if (item.params) {
|
|
52
|
-
classes[item.name].constructor = {
|
|
53
|
-
params: item.params,
|
|
54
|
-
examples: item.examples || []
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
} else if (item.kind === 'function' && item.memberof) {
|
|
58
|
-
// Method of a class
|
|
59
|
-
const className = item.memberof;
|
|
60
|
-
if (!classes[className]) {
|
|
61
|
-
classes[className] = {
|
|
62
|
-
desc: '',
|
|
63
|
-
constructor: null,
|
|
64
|
-
methods: [],
|
|
65
|
-
examples: []
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
classes[className].methods.push(item);
|
|
69
|
-
} else if (item.kind === 'function' && !item.memberof && item.scope === 'global') {
|
|
70
|
-
// Top-level function
|
|
71
|
-
functions[item.name] = item;
|
|
22
|
+
function runJsdoc() {
|
|
23
|
+
try {
|
|
24
|
+
return execSync('npx jsdoc -X -c jsdoc.json', {
|
|
25
|
+
encoding: 'utf8',
|
|
26
|
+
maxBuffer: 10 * 1024 * 1024
|
|
27
|
+
});
|
|
28
|
+
} catch (error) {
|
|
29
|
+
throw new Error(`Failed to run JSDoc: ${error.message}`, { cause: error });
|
|
72
30
|
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Generate Markdown
|
|
76
|
-
let markdown = `<!-- Generated by JSDoc. Update this documentation by updating the source code. -->
|
|
77
|
-
|
|
78
|
-
# API Reference
|
|
79
|
-
|
|
80
|
-
> **⚠️ This file is auto-generated from JSDoc comments in the source code.**
|
|
81
|
-
> To update this documentation, edit the JSDoc comments in the source files and run \`npm run docs\`.
|
|
82
|
-
|
|
83
|
-
This document provides detailed API reference for all classes, methods, and functions in node-osc.
|
|
31
|
+
}
|
|
84
32
|
|
|
85
|
-
|
|
33
|
+
function parseJsdocJson(jsdocJson) {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(jsdocJson);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
throw new Error(`Failed to parse JSDoc JSON output: ${error.message}`, { cause: error });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
86
40
|
|
|
87
|
-
|
|
41
|
+
function collectDocs(docs) {
|
|
42
|
+
const classes = {};
|
|
43
|
+
const functions = {};
|
|
88
44
|
|
|
89
|
-
|
|
45
|
+
docs.forEach(item => {
|
|
46
|
+
if (item.undocumented || item.ignore) return;
|
|
90
47
|
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
48
|
+
if (item.kind === 'class' && item.classdesc) {
|
|
49
|
+
const classInfo = classes[item.name] || {
|
|
50
|
+
desc: '',
|
|
51
|
+
constructor: null,
|
|
52
|
+
methods: [],
|
|
53
|
+
examples: [],
|
|
54
|
+
augments: []
|
|
55
|
+
};
|
|
94
56
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
57
|
+
classInfo.desc = item.classdesc;
|
|
58
|
+
classInfo.examples = item.examples || [];
|
|
59
|
+
classInfo.augments = item.augments || [];
|
|
60
|
+
classes[item.name] = classInfo;
|
|
61
|
+
|
|
62
|
+
if (item.params) {
|
|
63
|
+
classes[item.name].constructor = {
|
|
64
|
+
params: item.params,
|
|
65
|
+
examples: item.examples || []
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
} else if (item.kind === 'function' && item.memberof) {
|
|
69
|
+
const className = item.memberof;
|
|
70
|
+
if (!classes[className]) {
|
|
71
|
+
classes[className] = {
|
|
72
|
+
desc: '',
|
|
73
|
+
constructor: null,
|
|
74
|
+
methods: [],
|
|
75
|
+
examples: [],
|
|
76
|
+
augments: []
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
classes[className].methods.push(item);
|
|
80
|
+
} else if (item.kind === 'function' && !item.memberof && item.scope === 'global') {
|
|
81
|
+
functions[item.name] = item;
|
|
101
82
|
}
|
|
102
|
-
|
|
103
|
-
markdown += ` - [${method.name}()](#${name.toLowerCase()}-${method.name.toLowerCase()})\n`;
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
// Add functions to TOC
|
|
109
|
-
markdown += `- [Low Level Functions](#low-level-functions)\n`;
|
|
110
|
-
functionOrder.forEach(name => {
|
|
111
|
-
if (functions[name]) {
|
|
112
|
-
markdown += ` - [${name}()](#${name.toLowerCase()})\n`;
|
|
113
|
-
}
|
|
114
|
-
});
|
|
83
|
+
});
|
|
115
84
|
|
|
116
|
-
|
|
85
|
+
return { classes, functions };
|
|
86
|
+
}
|
|
117
87
|
|
|
118
|
-
// Helper function to format parameters
|
|
119
88
|
function formatParams(params) {
|
|
120
89
|
if (!params || params.length === 0) return '';
|
|
121
|
-
|
|
90
|
+
|
|
122
91
|
let result = '\n**Parameters:**\n\n';
|
|
123
92
|
params.forEach(param => {
|
|
124
93
|
const optional = param.optional ? ' (optional)' : '';
|
|
@@ -129,10 +98,9 @@ function formatParams(params) {
|
|
|
129
98
|
return result;
|
|
130
99
|
}
|
|
131
100
|
|
|
132
|
-
// Helper function to format examples
|
|
133
101
|
function formatExamples(examples) {
|
|
134
102
|
if (!examples || examples.length === 0) return '';
|
|
135
|
-
|
|
103
|
+
|
|
136
104
|
let result = '\n**Examples:**\n\n';
|
|
137
105
|
examples.forEach(example => {
|
|
138
106
|
result += '```javascript\n' + example + '\n```\n\n';
|
|
@@ -140,19 +108,17 @@ function formatExamples(examples) {
|
|
|
140
108
|
return result;
|
|
141
109
|
}
|
|
142
110
|
|
|
143
|
-
// Helper function to format returns
|
|
144
111
|
function formatReturns(returns) {
|
|
145
112
|
if (!returns || returns.length === 0) return '';
|
|
146
|
-
|
|
113
|
+
|
|
147
114
|
const ret = returns[0];
|
|
148
115
|
const types = ret.type ? ret.type.names.join(' | ') : 'any';
|
|
149
116
|
return `\n**Returns:** *{${types}}* - ${ret.description || ''}\n`;
|
|
150
117
|
}
|
|
151
118
|
|
|
152
|
-
// Helper function to format throws
|
|
153
119
|
function formatThrows(exceptions) {
|
|
154
120
|
if (!exceptions || exceptions.length === 0) return '';
|
|
155
|
-
|
|
121
|
+
|
|
156
122
|
let result = '\n**Throws:**\n\n';
|
|
157
123
|
exceptions.forEach(ex => {
|
|
158
124
|
const types = ex.type ? ex.type.names.join(' | ') : 'Error';
|
|
@@ -161,69 +127,109 @@ function formatThrows(exceptions) {
|
|
|
161
127
|
return result;
|
|
162
128
|
}
|
|
163
129
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
markdown += formatThrows(method.exceptions);
|
|
198
|
-
markdown += formatExamples(method.examples);
|
|
130
|
+
function generateMarkdown(classes, functions) {
|
|
131
|
+
let markdown = `<!-- Generated by JSDoc. Update this documentation by updating the source code. -->
|
|
132
|
+
|
|
133
|
+
# API Reference
|
|
134
|
+
|
|
135
|
+
> **⚠️ This file is auto-generated from JSDoc comments in the source code.**
|
|
136
|
+
> To update this documentation, edit the JSDoc comments in the source files and run \`npm run docs\`.
|
|
137
|
+
|
|
138
|
+
This document provides detailed API reference for all classes, methods, and functions in node-osc.
|
|
139
|
+
|
|
140
|
+
For usage guides, best practices, and troubleshooting, see the **[Guide](./GUIDE.md)**.
|
|
141
|
+
|
|
142
|
+
## Table of Contents
|
|
143
|
+
|
|
144
|
+
`;
|
|
145
|
+
|
|
146
|
+
classOrder.forEach(name => {
|
|
147
|
+
if (classes[name]) {
|
|
148
|
+
markdown += `- [${name}](#${githubAnchor(name)})\n`;
|
|
149
|
+
if (classes[name].constructor) {
|
|
150
|
+
markdown += ` - [Constructor](#${githubAnchor(`${name} Constructor`)})\n`;
|
|
151
|
+
}
|
|
152
|
+
classes[name].methods.forEach(method => {
|
|
153
|
+
markdown += ` - [${method.name}()](#${githubAnchor(`${name}.${method.name}()`)})\n`;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
markdown += `- [Low Level Functions](#${githubAnchor('Low Level Functions')})\n`;
|
|
159
|
+
functionOrder.forEach(name => {
|
|
160
|
+
if (functions[name]) {
|
|
161
|
+
markdown += ` - [${name}()](#${githubAnchor(`${name}()`)})\n`;
|
|
162
|
+
}
|
|
199
163
|
});
|
|
200
|
-
|
|
164
|
+
|
|
201
165
|
markdown += `\n---\n\n`;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
166
|
+
|
|
167
|
+
classOrder.forEach(className => {
|
|
168
|
+
const classInfo = classes[className];
|
|
169
|
+
if (!classInfo) return;
|
|
170
|
+
|
|
171
|
+
markdown += `## ${className}\n\n`;
|
|
172
|
+
|
|
173
|
+
if (classInfo.augments && classInfo.augments.length > 0) {
|
|
174
|
+
markdown += `**Extends:** ${classInfo.augments.join(', ')}\n\n`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
markdown += `${classInfo.desc}\n`;
|
|
178
|
+
|
|
179
|
+
if (classInfo.examples.length > 0 && !classInfo.constructor) {
|
|
180
|
+
markdown += formatExamples(classInfo.examples);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (classInfo.constructor) {
|
|
184
|
+
markdown += `\n### ${className} Constructor\n\n`;
|
|
185
|
+
markdown += `Creates a new ${className} instance.\n`;
|
|
186
|
+
markdown += formatParams(classInfo.constructor.params);
|
|
187
|
+
markdown += formatExamples(classInfo.constructor.examples);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
classInfo.methods.forEach(method => {
|
|
191
|
+
markdown += `\n### ${className}.${method.name}()\n\n`;
|
|
192
|
+
markdown += `${method.description || ''}\n`;
|
|
193
|
+
markdown += formatParams(method.params);
|
|
194
|
+
markdown += formatReturns(method.returns);
|
|
195
|
+
markdown += formatThrows(method.exceptions);
|
|
196
|
+
markdown += formatExamples(method.examples);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
markdown += `\n---\n\n`;
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
markdown += `## Low Level Functions\n\n`;
|
|
203
|
+
markdown += 'These functions provide low-level access to OSC encoding and decoding for advanced use cases.\n\n';
|
|
204
|
+
|
|
205
|
+
functionOrder.forEach(funcName => {
|
|
206
|
+
const func = functions[funcName];
|
|
207
|
+
if (!func) return;
|
|
208
|
+
|
|
209
|
+
markdown += `### ${funcName}()\n\n`;
|
|
210
|
+
markdown += `${func.description || ''}\n`;
|
|
211
|
+
markdown += formatParams(func.params);
|
|
212
|
+
markdown += formatReturns(func.returns);
|
|
213
|
+
markdown += formatThrows(func.exceptions);
|
|
214
|
+
markdown += formatExamples(func.examples);
|
|
215
|
+
markdown += '\n';
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
return markdown;
|
|
229
219
|
}
|
|
220
|
+
|
|
221
|
+
function main() {
|
|
222
|
+
try {
|
|
223
|
+
const jsdocJson = runJsdoc();
|
|
224
|
+
const docs = parseJsdocJson(jsdocJson);
|
|
225
|
+
const { classes, functions } = collectDocs(docs);
|
|
226
|
+
const markdown = generateMarkdown(classes, functions);
|
|
227
|
+
writeFileSync('docs/API.md', markdown, 'utf8');
|
|
228
|
+
console.log('✅ API documentation generated: docs/API.md');
|
|
229
|
+
} catch (error) {
|
|
230
|
+
console.error(`❌ ${error.message}`);
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
main();
|
|
@@ -4,6 +4,7 @@ const osc = require('node-osc');
|
|
|
4
4
|
|
|
5
5
|
// Create server first (typical usage pattern)
|
|
6
6
|
const server: Server = new osc.Server(3333, '0.0.0.0');
|
|
7
|
+
server.send(['/server-test', 1], 3334, '127.0.0.1', () => {});
|
|
7
8
|
|
|
8
9
|
// Create client after server
|
|
9
10
|
const client: Client = new osc.Client('127.0.0.1', 3333);
|
|
@@ -7,6 +7,7 @@ const server: Server = new Server(3333, '0.0.0.0');
|
|
|
7
7
|
|
|
8
8
|
// Wait for server to be ready (pattern from examples)
|
|
9
9
|
await once(server, 'listening');
|
|
10
|
+
await server.send(['/server-test', 1], 3334, '127.0.0.1');
|
|
10
11
|
|
|
11
12
|
server.on('message', (msg) => {
|
|
12
13
|
console.log('Received message:', msg);
|
package/test/test-promises.mjs
CHANGED
|
@@ -140,6 +140,22 @@ test('server: close with promise', async (t) => {
|
|
|
140
140
|
t.pass('Server closed successfully with promise');
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
+
test('server: send promise rejection on closed socket', async (t) => {
|
|
144
|
+
const oscServer = new Server(0, '127.0.0.1');
|
|
145
|
+
|
|
146
|
+
t.plan(1);
|
|
147
|
+
|
|
148
|
+
await once(oscServer, 'listening');
|
|
149
|
+
await oscServer.close();
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
await oscServer.send('/boom', 3333, '127.0.0.1');
|
|
153
|
+
t.fail('Should have thrown an error');
|
|
154
|
+
} catch (err) {
|
|
155
|
+
t.equal(err.code, 'ERR_SOCKET_DGRAM_NOT_RUNNING', 'Should reject with correct error code');
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
143
159
|
test('server: no callback still emits listening event', async (t) => {
|
|
144
160
|
const oscServer = new Server(0, '127.0.0.1');
|
|
145
161
|
|
package/test/test-server.mjs
CHANGED
|
@@ -88,6 +88,67 @@ test('server: callback as second arg', async (t) => {
|
|
|
88
88
|
});
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
+
test('server: send to remote port', async (t) => {
|
|
92
|
+
const sender = new Server(0, '127.0.0.1');
|
|
93
|
+
const receiver = new Server(0, '127.0.0.1');
|
|
94
|
+
await Promise.all([once(sender, 'listening'), once(receiver, 'listening')]);
|
|
95
|
+
|
|
96
|
+
t.plan(1);
|
|
97
|
+
|
|
98
|
+
t.teardown(async () => {
|
|
99
|
+
await Promise.all([sender.close(), receiver.close()]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const receivedMessage = once(receiver, 'message');
|
|
103
|
+
await sender.send(['/test', 1], receiver.port, '127.0.0.1');
|
|
104
|
+
const [msg] = await receivedMessage;
|
|
105
|
+
|
|
106
|
+
t.same(msg, ['/test', 1], 'server should send a message from its bound socket');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('server: can receive and reply from within message handler', async (t) => {
|
|
110
|
+
const requester = new Server(0, '127.0.0.1');
|
|
111
|
+
const responder = new Server(0, '127.0.0.1');
|
|
112
|
+
await Promise.all([once(requester, 'listening'), once(responder, 'listening')]);
|
|
113
|
+
|
|
114
|
+
t.plan(3);
|
|
115
|
+
|
|
116
|
+
t.teardown(async () => {
|
|
117
|
+
await Promise.all([requester.close(), responder.close()]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
responder.on('message', (msg, rinfo) => {
|
|
121
|
+
t.same(msg, ['/ping', 1], 'responder should receive the incoming message');
|
|
122
|
+
responder.send(['/ack', 1], rinfo.port, rinfo.address, (err) => {
|
|
123
|
+
t.error(err, 'responder should reply without error');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const receivedReply = once(requester, 'message');
|
|
128
|
+
await requester.send(['/ping', 1], responder.port, '127.0.0.1');
|
|
129
|
+
const [reply] = await receivedReply;
|
|
130
|
+
|
|
131
|
+
t.same(reply, ['/ack', 1], 'requester should receive the reply on the same socket');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('server: send before listening returns a clear error', async (t) => {
|
|
135
|
+
t.plan(3);
|
|
136
|
+
|
|
137
|
+
const server = new Server(0, '127.0.0.1');
|
|
138
|
+
|
|
139
|
+
t.throws(() => {
|
|
140
|
+
server.send('/test', 3333, '127.0.0.1');
|
|
141
|
+
}, /before server is listening/i, 'send without callback should throw before listening');
|
|
142
|
+
|
|
143
|
+
server.send('/test', 3333, '127.0.0.1', (err) => {
|
|
144
|
+
t.ok(err instanceof Error, 'send should fail with an Error before listening');
|
|
145
|
+
t.match(err.message, /before server is listening/i, 'error message should explain the socket is not ready');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
await once(server, 'listening');
|
|
149
|
+
await server.close();
|
|
150
|
+
});
|
|
151
|
+
|
|
91
152
|
test('server: bad message', async (t) => {
|
|
92
153
|
t.plan(2);
|
|
93
154
|
const oscServer = new Server(0, '127.0.0.1');
|
package/types/Client.d.mts
CHANGED
package/types/Client.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Client.d.mts","sourceRoot":"","sources":["../lib/Client.mjs"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"Client.d.mts","sourceRoot":"","sources":["../lib/Client.mjs"],"names":[],"mappings":";AAIA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH;IACE;;;;;;;;OAQG;IACH,kBANW,MAAM,QACN,MAAM,EAiBhB;IAVC,aAAgB;IAChB,aAAgB;IAChB,mCAGE;IAMJ;;;;;;;;;;;;;;;;;OAiBG;IACH,sBAZa,OAAO,CAAC,IAAI,CAAC,GAAC,SAAS,CAuBnC;IACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAqCG;IACH,cA7Bc,GAAC,EAAA,GAIF,OAAO,CAAC,IAAI,CAAC,GAAC,SAAS,CA2CnC;CACF;6BAzI4B,aAAa"}
|
package/types/Server.d.mts
CHANGED
|
@@ -73,7 +73,35 @@ declare class Server extends EventEmitter<any> {
|
|
|
73
73
|
constructor(port: number, host?: string, cb?: Function);
|
|
74
74
|
port: number;
|
|
75
75
|
host: string;
|
|
76
|
+
_isListening: boolean;
|
|
77
|
+
_isClosed: boolean;
|
|
76
78
|
_sock: import("node:dgram").Socket;
|
|
79
|
+
/**
|
|
80
|
+
* Send an OSC message or bundle from the server's bound socket.
|
|
81
|
+
*
|
|
82
|
+
* This method can be used with either a callback or as a Promise.
|
|
83
|
+
*
|
|
84
|
+
* @param {import('./Message.mjs').default|import('./Bundle.mjs').default|Array|string} message - The message, bundle, address, or array to send.
|
|
85
|
+
* @param {number} port - The remote port to send to.
|
|
86
|
+
* @param {string} host - The remote host to send to.
|
|
87
|
+
* @param {Function} [cb] - Optional callback function called when send completes.
|
|
88
|
+
* @returns {Promise<void>|undefined} Returns a Promise if no callback is provided.
|
|
89
|
+
*
|
|
90
|
+
* @throws {Error} If the server socket is not yet listening.
|
|
91
|
+
* @throws {TypeError} If the message format is invalid.
|
|
92
|
+
* @throws {ReferenceError} If attempting to send on a closed socket.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* // Send an address-only message
|
|
96
|
+
* await server.send('/ping', 9000, '127.0.0.1');
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* // Send an array message
|
|
100
|
+
* server.send(['/ack', 1], 9000, '192.168.1.42', (err) => {
|
|
101
|
+
* if (err) console.error(err);
|
|
102
|
+
* });
|
|
103
|
+
*/
|
|
104
|
+
send(message: import("./Message.mjs").default | import("./Bundle.mjs").default | any[] | string, port: number, host: string, cb?: Function): Promise<void> | undefined;
|
|
77
105
|
/**
|
|
78
106
|
* Close the server socket.
|
|
79
107
|
*
|
package/types/Server.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Server.d.mts","sourceRoot":"","sources":["../lib/Server.mjs"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"Server.d.mts","sourceRoot":"","sources":["../lib/Server.mjs"],"names":[],"mappings":";AAUA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH;IACE;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,kBApBW,MAAM,SACN,MAAM,iBAyEhB;IA9CC,aAAgB;IAChB,aAAgB;IAChB,sBAAyB;IACzB,mBAAsB;IACtB,mCAGE;IAwCJ;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,cApBW,OAAO,eAAe,EAAE,OAAO,GAAC,OAAO,cAAc,EAAE,OAAO,WAAO,MAAM,QAC3E,MAAM,QACN,MAAM,kBAEJ,OAAO,CAAC,IAAI,CAAC,GAAC,SAAS,CAyCnC;IACD;;;;;;;;;;;;;;;;;OAiBG;IACH,sBAZa,OAAO,CAAC,IAAI,CAAC,GAAC,SAAS,CAuBnC;CACF;6BAxN4B,aAAa"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"send.d.mts","sourceRoot":"","sources":["../../lib/internal/send.mjs"],"names":[],"mappings":";AAcA,4GA6BC"}
|