node-osc 11.1.0 → 11.2.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.
Files changed (69) hide show
  1. package/.gitattributes +11 -0
  2. package/.github/workflows/bump-version.yml +2 -0
  3. package/.github/workflows/nodejs.yml +3 -0
  4. package/LICENSE +201 -165
  5. package/README.md +135 -42
  6. package/dist/lib/Bundle.js +66 -0
  7. package/dist/lib/Client.js +137 -22
  8. package/dist/lib/Message.js +87 -1
  9. package/dist/lib/Server.js +117 -6
  10. package/dist/lib/index.js +3 -0
  11. package/dist/lib/internal/decode.js +4 -4
  12. package/{lib/internal/osc.mjs → dist/lib/osc.js} +94 -23
  13. package/dist/test/lib/osc.js +395 -0
  14. package/dist/test/test-client.js +152 -0
  15. package/dist/test/test-e2e.js +9 -3
  16. package/dist/test/test-encode-decode.js +849 -0
  17. package/dist/test/test-error-handling.js +116 -0
  18. package/dist/test/test-osc-internal.js +399 -41
  19. package/dist/test/test-promises.js +250 -0
  20. package/dist/test/test-types.js +42 -0
  21. package/dist/test/util.js +15 -8
  22. package/docs/API.md +477 -0
  23. package/docs/GUIDE.md +605 -0
  24. package/examples/README.md +119 -0
  25. package/examples/async-await.mjs +57 -0
  26. package/examples/bundle-example.mjs +92 -0
  27. package/examples/client.js +22 -5
  28. package/examples/error-handling.mjs +152 -0
  29. package/examples/esm.mjs +21 -0
  30. package/examples/server.js +16 -0
  31. package/jsdoc.json +16 -0
  32. package/lib/Bundle.mjs +66 -0
  33. package/lib/Client.mjs +137 -22
  34. package/lib/Message.mjs +87 -1
  35. package/lib/Server.mjs +117 -6
  36. package/lib/index.mjs +1 -0
  37. package/lib/internal/decode.mjs +4 -4
  38. package/{dist/lib/internal/osc.js → lib/osc.mjs} +71 -7
  39. package/package.json +12 -10
  40. package/rollup.config.mjs +48 -37
  41. package/scripts/generate-docs.mjs +229 -0
  42. package/test/fixtures/types/test-cjs-types.ts +19 -0
  43. package/test/fixtures/types/test-esm-types.ts +35 -0
  44. package/test/fixtures/types/tsconfig-cjs.test.json +17 -0
  45. package/test/fixtures/types/tsconfig-esm.test.json +17 -0
  46. package/test/test-bundle.mjs +0 -1
  47. package/test/test-client.mjs +152 -0
  48. package/test/test-e2e.mjs +9 -3
  49. package/test/test-encode-decode.mjs +847 -0
  50. package/test/test-error-handling.mjs +115 -0
  51. package/test/test-osc-internal.mjs +400 -42
  52. package/test/test-promises.mjs +249 -0
  53. package/test/test-types.mjs +39 -0
  54. package/test/util.mjs +15 -8
  55. package/tsconfig.json +45 -0
  56. package/types/Bundle.d.mts +70 -0
  57. package/types/Bundle.d.mts.map +1 -0
  58. package/types/Client.d.mts +101 -0
  59. package/types/Client.d.mts.map +1 -0
  60. package/types/Message.d.mts +84 -0
  61. package/types/Message.d.mts.map +1 -0
  62. package/types/Server.d.mts +98 -0
  63. package/types/Server.d.mts.map +1 -0
  64. package/types/index.d.mts +6 -0
  65. package/types/index.d.mts.map +1 -0
  66. package/types/internal/decode.d.mts +4 -0
  67. package/types/internal/decode.d.mts.map +1 -0
  68. package/types/osc.d.mts +66 -0
  69. package/types/osc.d.mts.map +1 -0
@@ -4,14 +4,84 @@ var node_dgram = require('node:dgram');
4
4
  var node_events = require('node:events');
5
5
  var decode = require('#decode');
6
6
 
7
+ /**
8
+ * OSC Server for receiving messages and bundles over UDP.
9
+ *
10
+ * Emits the following events:
11
+ * - 'listening': Emitted when the server starts listening
12
+ * - 'message': Emitted when an OSC message is received (receives msg array and rinfo object)
13
+ * - 'bundle': Emitted when an OSC bundle is received (receives bundle object and rinfo object)
14
+ * - 'error': Emitted when a socket error or decoding error occurs (receives error and rinfo)
15
+ * - Address-specific events: Emitted for each message address (e.g., '/test')
16
+ *
17
+ * @class
18
+ * @extends EventEmitter
19
+ *
20
+ * @fires Server#listening
21
+ * @fires Server#message
22
+ * @fires Server#bundle
23
+ * @fires Server#error
24
+ *
25
+ * @example
26
+ * // Create and listen for messages
27
+ * const server = new Server(3333, '0.0.0.0', () => {
28
+ * console.log('Server is listening');
29
+ * });
30
+ *
31
+ * server.on('message', (msg, rinfo) => {
32
+ * console.log('Message:', msg);
33
+ * console.log('From:', rinfo.address, rinfo.port);
34
+ * });
35
+ *
36
+ * @example
37
+ * // Using async/await with events.once
38
+ * import { once } from 'node:events';
39
+ *
40
+ * const server = new Server(3333, '0.0.0.0');
41
+ * await once(server, 'listening');
42
+ *
43
+ * server.on('message', (msg) => {
44
+ * console.log('Message:', msg);
45
+ * });
46
+ *
47
+ * @example
48
+ * // Listen for specific OSC addresses
49
+ * server.on('/note', (msg) => {
50
+ * const [address, pitch, velocity] = msg;
51
+ * console.log(`Note: ${pitch}, Velocity: ${velocity}`);
52
+ * });
53
+ */
7
54
  class Server extends node_events.EventEmitter {
55
+ /**
56
+ * Create an OSC Server.
57
+ *
58
+ * @param {number} port - The port to listen on.
59
+ * @param {string} [host='127.0.0.1'] - The host address to bind to. Use '0.0.0.0' to listen on all interfaces.
60
+ * @param {Function} [cb] - Optional callback function called when server starts listening.
61
+ *
62
+ * @example
63
+ * // Basic server
64
+ * const server = new Server(3333);
65
+ *
66
+ * @example
67
+ * // Server on all interfaces with callback
68
+ * const server = new Server(3333, '0.0.0.0', () => {
69
+ * console.log('Server started');
70
+ * });
71
+ *
72
+ * @example
73
+ * // Host parameter can be omitted, callback as second parameter
74
+ * const server = new Server(3333, () => {
75
+ * console.log('Server started on 127.0.0.1');
76
+ * });
77
+ */
8
78
  constructor(port, host='127.0.0.1', cb) {
9
79
  super();
10
80
  if (typeof host === 'function') {
11
81
  cb = host;
12
82
  host = '127.0.0.1';
13
83
  }
14
- if (!cb) cb = () => {};
84
+
15
85
  let decoded;
16
86
  this.port = port;
17
87
  this.host = host;
@@ -20,10 +90,20 @@ class Server extends node_events.EventEmitter {
20
90
  reuseAddr: true
21
91
  });
22
92
  this._sock.bind(port, host);
23
- this._sock.on('listening', () => {
24
- this.emit('listening');
25
- cb();
26
- });
93
+
94
+ // Support both callback and promise-based listening
95
+ if (cb) {
96
+ this._sock.on('listening', () => {
97
+ this.emit('listening');
98
+ cb();
99
+ });
100
+ } else {
101
+ // For promise support, still emit the event but don't require a callback
102
+ this._sock.on('listening', () => {
103
+ this.emit('listening');
104
+ });
105
+ }
106
+
27
107
  this._sock.on('message', (msg, rinfo) => {
28
108
  try {
29
109
  decoded = decode(msg);
@@ -41,9 +121,40 @@ class Server extends node_events.EventEmitter {
41
121
  this.emit(decoded[0], decoded, rinfo);
42
122
  }
43
123
  });
124
+
125
+ this._sock.on('error', (err) => {
126
+ this.emit('error', err);
127
+ });
44
128
  }
129
+ /**
130
+ * Close the server socket.
131
+ *
132
+ * This method can be used with either a callback or as a Promise.
133
+ *
134
+ * @param {Function} [cb] - Optional callback function called when socket is closed.
135
+ * @returns {Promise<void>|undefined} Returns a Promise if no callback is provided.
136
+ *
137
+ * @example
138
+ * // With callback
139
+ * server.close((err) => {
140
+ * if (err) console.error(err);
141
+ * });
142
+ *
143
+ * @example
144
+ * // With async/await
145
+ * await server.close();
146
+ */
45
147
  close(cb) {
46
- this._sock.close(cb);
148
+ if (cb) {
149
+ this._sock.close(cb);
150
+ } else {
151
+ return new Promise((resolve, reject) => {
152
+ this._sock.close((err) => {
153
+ if (err) reject(err);
154
+ else resolve();
155
+ });
156
+ });
157
+ }
47
158
  }
48
159
  }
49
160
 
package/dist/lib/index.js CHANGED
@@ -4,6 +4,7 @@ var Message = require('./Message.js');
4
4
  var Bundle = require('./Bundle.js');
5
5
  var Server = require('./Server.js');
6
6
  var Client = require('./Client.js');
7
+ var osc = require('./osc.js');
7
8
 
8
9
 
9
10
 
@@ -11,3 +12,5 @@ exports.Message = Message;
11
12
  exports.Bundle = Bundle;
12
13
  exports.Server = Server;
13
14
  exports.Client = Client;
15
+ exports.decode = osc.decode;
16
+ exports.encode = osc.encode;
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var _osc = require('#osc');
3
+ var osc = require('../osc.js');
4
4
 
5
5
  function sanitizeMessage(decoded) {
6
6
  const message = [];
@@ -19,8 +19,8 @@ function sanitizeBundle(decoded) {
19
19
  return decoded;
20
20
  }
21
21
 
22
- function decode(data, customFromBuffer = _osc.fromBuffer) {
23
- const decoded = customFromBuffer(data);
22
+ function decodeAndSanitize(data, customDecode = osc.decode) {
23
+ const decoded = customDecode(data);
24
24
  if (decoded.oscType === 'bundle') {
25
25
  return sanitizeBundle(decoded);
26
26
  }
@@ -32,4 +32,4 @@ function decode(data, customFromBuffer = _osc.fromBuffer) {
32
32
  }
33
33
  }
34
34
 
35
- module.exports = decode;
35
+ module.exports = decodeAndSanitize;
@@ -1,7 +1,10 @@
1
+ 'use strict';
2
+
3
+ var node_buffer = require('node:buffer');
4
+
1
5
  // OSC 1.0 Protocol Implementation
2
6
  // Based on http://opensoundcontrol.org/spec-1_0
3
7
 
4
- // Helper functions for OSC encoding/decoding
5
8
 
6
9
  function padString(str) {
7
10
  const nullTerminated = str + '\0';
@@ -21,7 +24,7 @@ function readString(buffer, offset) {
21
24
  }
22
25
 
23
26
  function writeInt32(value) {
24
- const buffer = Buffer.alloc(4);
27
+ const buffer = node_buffer.Buffer.alloc(4);
25
28
  buffer.writeInt32BE(value, 0);
26
29
  return buffer;
27
30
  }
@@ -32,7 +35,7 @@ function readInt32(buffer, offset) {
32
35
  }
33
36
 
34
37
  function writeFloat32(value) {
35
- const buffer = Buffer.alloc(4);
38
+ const buffer = node_buffer.Buffer.alloc(4);
36
39
  buffer.writeFloatBE(value, 0);
37
40
  return buffer;
38
41
  }
@@ -46,8 +49,8 @@ function writeBlob(value) {
46
49
  const length = value.length;
47
50
  const lengthBuffer = writeInt32(length);
48
51
  const padding = 4 - (length % 4);
49
- const paddingBuffer = Buffer.alloc(padding === 4 ? 0 : padding);
50
- return Buffer.concat([lengthBuffer, value, paddingBuffer]);
52
+ const paddingBuffer = node_buffer.Buffer.alloc(padding === 4 ? 0 : padding);
53
+ return node_buffer.Buffer.concat([lengthBuffer, value, paddingBuffer]);
51
54
  }
52
55
 
53
56
  function readBlob(buffer, offset) {
@@ -62,7 +65,7 @@ function readBlob(buffer, offset) {
62
65
  function writeTimeTag(value) {
63
66
  // For now, treat timetag as a double (8 bytes)
64
67
  // OSC timetag is 64-bit: 32-bit seconds since 1900, 32-bit fractional
65
- const buffer = Buffer.alloc(8);
68
+ const buffer = node_buffer.Buffer.alloc(8);
66
69
  if (typeof value === 'number') {
67
70
  // Convert to OSC timetag format
68
71
  const seconds = Math.floor(value);
@@ -97,9 +100,9 @@ function readTimeTag(buffer, offset) {
97
100
 
98
101
  function writeMidi(value) {
99
102
  // MIDI message is 4 bytes: port id, status byte, data1, data2
100
- const buffer = Buffer.alloc(4);
103
+ const buffer = node_buffer.Buffer.alloc(4);
101
104
 
102
- if (Buffer.isBuffer(value)) {
105
+ if (node_buffer.Buffer.isBuffer(value)) {
103
106
  if (value.length !== 4) {
104
107
  throw new Error('MIDI message must be exactly 4 bytes');
105
108
  }
@@ -138,7 +141,7 @@ function encodeArgument(arg) {
138
141
  return { tag: 'f', data: writeFloat32(arg.value) };
139
142
  case 's':
140
143
  case 'string':
141
- return { tag: 's', data: Buffer.from(padString(arg.value)) };
144
+ return { tag: 's', data: node_buffer.Buffer.from(padString(arg.value)) };
142
145
  case 'b':
143
146
  case 'blob':
144
147
  return { tag: 'b', data: writeBlob(arg.value) };
@@ -147,8 +150,11 @@ function encodeArgument(arg) {
147
150
  // For doubles, use float for now (OSC 1.0 doesn't have double)
148
151
  return { tag: 'f', data: writeFloat32(arg.value) };
149
152
  case 'T':
153
+ return { tag: 'T', data: node_buffer.Buffer.alloc(0) };
154
+ case 'F':
155
+ return { tag: 'F', data: node_buffer.Buffer.alloc(0) };
150
156
  case 'boolean':
151
- return arg.value ? { tag: 'T', data: Buffer.alloc(0) } : { tag: 'F', data: Buffer.alloc(0) };
157
+ return arg.value ? { tag: 'T', data: node_buffer.Buffer.alloc(0) } : { tag: 'F', data: node_buffer.Buffer.alloc(0) };
152
158
  case 'm':
153
159
  case 'midi':
154
160
  return { tag: 'm', data: writeMidi(arg.value) };
@@ -166,11 +172,11 @@ function encodeArgument(arg) {
166
172
  return { tag: 'f', data: writeFloat32(arg) };
167
173
  }
168
174
  case 'string':
169
- return { tag: 's', data: Buffer.from(padString(arg)) };
175
+ return { tag: 's', data: node_buffer.Buffer.from(padString(arg)) };
170
176
  case 'boolean':
171
- return arg ? { tag: 'T', data: Buffer.alloc(0) } : { tag: 'F', data: Buffer.alloc(0) };
177
+ return arg ? { tag: 'T', data: node_buffer.Buffer.alloc(0) } : { tag: 'F', data: node_buffer.Buffer.alloc(0) };
172
178
  default:
173
- if (Buffer.isBuffer(arg)) {
179
+ if (node_buffer.Buffer.isBuffer(arg)) {
174
180
  return { tag: 'b', data: writeBlob(arg) };
175
181
  }
176
182
  throw new Error(`Don't know how to encode argument: ${arg}`);
@@ -200,7 +206,37 @@ function decodeArgument(tag, buffer, offset) {
200
206
  }
201
207
  }
202
208
 
203
- export function toBuffer(message) {
209
+ /**
210
+ * Encode an OSC message or bundle to a Buffer.
211
+ *
212
+ * This low-level function converts OSC messages and bundles into binary format
213
+ * for transmission or storage. Useful for sending OSC over custom transports
214
+ * (WebSocket, TCP, HTTP), storing to files, or implementing custom OSC routers.
215
+ *
216
+ * @param {Object} message - OSC message or bundle object with oscType property
217
+ * @returns {Buffer} The encoded OSC data ready for transmission
218
+ *
219
+ * @example
220
+ * // Encode a message
221
+ * import { Message, encode } from 'node-osc';
222
+ *
223
+ * const message = new Message('/oscillator/frequency', 440);
224
+ * const buffer = encode(message);
225
+ * console.log('Encoded bytes:', buffer.length);
226
+ *
227
+ * @example
228
+ * // Encode a bundle
229
+ * import { Bundle, encode } from 'node-osc';
230
+ *
231
+ * const bundle = new Bundle(['/one', 1], ['/two', 2]);
232
+ * const buffer = encode(bundle);
233
+ *
234
+ * @example
235
+ * // Send over WebSocket
236
+ * const buffer = encode(message);
237
+ * websocket.send(buffer);
238
+ */
239
+ function encode(message) {
204
240
  if (message.oscType === 'bundle') {
205
241
  return encodeBundleToBuffer(message);
206
242
  } else {
@@ -215,15 +251,15 @@ function encodeMessageToBuffer(message) {
215
251
  // Arguments (encoded according to type tags)
216
252
 
217
253
  const address = padString(message.address);
218
- const addressBuffer = Buffer.from(address);
254
+ const addressBuffer = node_buffer.Buffer.from(address);
219
255
 
220
256
  const encodedArgs = message.args.map(encodeArgument);
221
257
  const typeTags = ',' + encodedArgs.map(arg => arg.tag).join('');
222
- const typeTagsBuffer = Buffer.from(padString(typeTags));
258
+ const typeTagsBuffer = node_buffer.Buffer.from(padString(typeTags));
223
259
 
224
260
  const argumentBuffers = encodedArgs.map(arg => arg.data);
225
261
 
226
- return Buffer.concat([addressBuffer, typeTagsBuffer, ...argumentBuffers]);
262
+ return node_buffer.Buffer.concat([addressBuffer, typeTagsBuffer, ...argumentBuffers]);
227
263
  }
228
264
 
229
265
  function encodeBundleToBuffer(bundle) {
@@ -233,7 +269,7 @@ function encodeBundleToBuffer(bundle) {
233
269
  // Elements (each prefixed with size)
234
270
 
235
271
  const bundleString = padString('#bundle');
236
- const bundleStringBuffer = Buffer.from(bundleString);
272
+ const bundleStringBuffer = node_buffer.Buffer.from(bundleString);
237
273
 
238
274
  const timetagBuffer = writeTimeTag(bundle.timetag);
239
275
 
@@ -245,13 +281,45 @@ function encodeBundleToBuffer(bundle) {
245
281
  elementBuffer = encodeMessageToBuffer(element);
246
282
  }
247
283
  const sizeBuffer = writeInt32(elementBuffer.length);
248
- return Buffer.concat([sizeBuffer, elementBuffer]);
284
+ return node_buffer.Buffer.concat([sizeBuffer, elementBuffer]);
249
285
  });
250
286
 
251
- return Buffer.concat([bundleStringBuffer, timetagBuffer, ...elementBuffers]);
287
+ return node_buffer.Buffer.concat([bundleStringBuffer, timetagBuffer, ...elementBuffers]);
252
288
  }
253
289
 
254
- export function fromBuffer(buffer) {
290
+ /**
291
+ * Decode a Buffer containing OSC data into a message or bundle object.
292
+ *
293
+ * This low-level function parses binary OSC data back into JavaScript objects.
294
+ * Useful for receiving OSC over custom transports, reading from files,
295
+ * or implementing custom OSC routers.
296
+ *
297
+ * @param {Buffer} buffer - The Buffer containing OSC data
298
+ * @returns {Object} The decoded OSC message or bundle. Messages have
299
+ * {oscType: 'message', address: string, args: Array}, bundles have
300
+ * {oscType: 'bundle', timetag: number, elements: Array}
301
+ * @throws {Error} If the buffer contains malformed OSC data
302
+ *
303
+ * @example
304
+ * // Decode received data
305
+ * import { decode } from 'node-osc';
306
+ *
307
+ * const decoded = decode(buffer);
308
+ * if (decoded.oscType === 'message') {
309
+ * console.log('Address:', decoded.address);
310
+ * console.log('Arguments:', decoded.args);
311
+ * }
312
+ *
313
+ * @example
314
+ * // Round-trip encode/decode
315
+ * import { Message, encode, decode } from 'node-osc';
316
+ *
317
+ * const original = new Message('/test', 42, 'hello');
318
+ * const buffer = encode(original);
319
+ * const decoded = decode(buffer);
320
+ * console.log(decoded.address); // '/test'
321
+ */
322
+ function decode(buffer) {
255
323
  // Check if it's a bundle or message
256
324
  if (buffer.length >= 8 && buffer.subarray(0, 8).toString() === '#bundle\0') {
257
325
  return decodeBundleFromBuffer(buffer);
@@ -311,7 +379,7 @@ function decodeBundleFromBuffer(buffer) {
311
379
 
312
380
  // Read element data
313
381
  const elementBuffer = buffer.subarray(offset, offset + size);
314
- const element = fromBuffer(elementBuffer);
382
+ const element = decode(elementBuffer);
315
383
  elements.push(element);
316
384
  offset += size;
317
385
  }
@@ -321,4 +389,7 @@ function decodeBundleFromBuffer(buffer) {
321
389
  timetag,
322
390
  elements
323
391
  };
324
- }
392
+ }
393
+
394
+ exports.decode = decode;
395
+ exports.encode = encode;