node-rtc-connection 1.0.18 → 2.0.4

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 (65) hide show
  1. package/README.md +94 -85
  2. package/dist/index.cjs +20 -5421
  3. package/dist/index.mjs +25 -5413
  4. package/dist/types/crypto/der.d.ts +107 -0
  5. package/dist/types/crypto/x509.d.ts +56 -0
  6. package/dist/types/datachannel/RTCDataChannel.d.ts +179 -0
  7. package/dist/types/dtls/RTCCertificate.d.ts +163 -0
  8. package/dist/types/dtls/cipher.d.ts +81 -0
  9. package/dist/types/dtls/connection.d.ts +81 -0
  10. package/dist/types/dtls/prf.d.ts +29 -0
  11. package/dist/types/dtls/protocol.d.ts +127 -0
  12. package/dist/types/foundation/ByteBufferQueue.d.ts +71 -0
  13. package/dist/types/foundation/RTCError.d.ts +152 -0
  14. package/dist/types/ice/RTCIceCandidate.d.ts +161 -0
  15. package/dist/types/ice/ice-agent.d.ts +154 -0
  16. package/dist/types/ice/stun-message.d.ts +92 -0
  17. package/dist/types/index.d.ts +29 -0
  18. package/dist/types/peerconnection/RTCPeerConnection.d.ts +74 -0
  19. package/dist/types/sctp/association.d.ts +77 -0
  20. package/dist/types/sctp/chunks.d.ts +200 -0
  21. package/dist/types/sctp/crc32c.d.ts +24 -0
  22. package/dist/types/sctp/datachannel-manager.d.ts +51 -0
  23. package/dist/types/sctp/dcep.d.ts +56 -0
  24. package/dist/types/sdp/RTCSessionDescription.d.ts +73 -0
  25. package/dist/types/sdp/sdp-utils.d.ts +103 -0
  26. package/dist/types/stun/stun-client.d.ts +119 -0
  27. package/dist/types/transport-stack.d.ts +68 -0
  28. package/package.json +26 -21
  29. package/src/crypto/der.ts +205 -0
  30. package/src/crypto/x509.ts +146 -0
  31. package/src/datachannel/RTCDataChannel.ts +388 -0
  32. package/src/dtls/RTCCertificate.ts +396 -0
  33. package/src/dtls/cipher.ts +198 -0
  34. package/src/dtls/connection.ts +974 -0
  35. package/src/dtls/prf.ts +62 -0
  36. package/src/dtls/protocol.ts +204 -0
  37. package/src/foundation/{ByteBufferQueue.js → ByteBufferQueue.ts} +74 -72
  38. package/src/foundation/{RTCError.js → RTCError.ts} +110 -60
  39. package/src/ice/{RTCIceCandidate.js → RTCIceCandidate.ts} +140 -92
  40. package/src/ice/ice-agent.ts +609 -0
  41. package/src/ice/stun-message.ts +260 -0
  42. package/src/index.ts +72 -0
  43. package/src/peerconnection/RTCPeerConnection.ts +430 -0
  44. package/src/sctp/association.ts +523 -0
  45. package/src/sctp/chunks.ts +350 -0
  46. package/src/sctp/crc32c.ts +57 -0
  47. package/src/sctp/datachannel-manager.ts +187 -0
  48. package/src/sctp/dcep.ts +94 -0
  49. package/src/sdp/{RTCSessionDescription.js → RTCSessionDescription.ts} +42 -29
  50. package/src/sdp/sdp-utils.ts +229 -0
  51. package/src/stun/stun-client.ts +936 -0
  52. package/src/transport-stack.ts +165 -0
  53. package/dist/index.cjs.map +0 -1
  54. package/dist/index.mjs.map +0 -1
  55. package/src/datachannel/RTCDataChannel.js +0 -354
  56. package/src/dtls/RTCCertificate.js +0 -310
  57. package/src/dtls/RTCDtlsTransport.js +0 -247
  58. package/src/ice/RTCIceTransport.js +0 -998
  59. package/src/index.d.ts +0 -400
  60. package/src/index.js +0 -92
  61. package/src/network/network-transport.js +0 -478
  62. package/src/peerconnection/RTCPeerConnection.js +0 -851
  63. package/src/sctp/RTCSctpTransport.js +0 -253
  64. package/src/sdp/sdp-utils.js +0 -224
  65. package/src/stun/stun-client.js +0 -643
@@ -0,0 +1,146 @@
1
+ /**
2
+ * @file x509.ts
3
+ * @description Self-signed X.509 v3 certificate generation for WebRTC DTLS.
4
+ * @module crypto/x509
5
+ *
6
+ * WebRTC peers authenticate by self-signed certificate. The SDP carries
7
+ * a=fingerprint as the hash of the DER-encoded certificate (RFC 8122), which
8
+ * the peer verifies against the certificate presented during the DTLS
9
+ * handshake. Node has no certificate builder, so we assemble a minimal but
10
+ * spec-valid ECDSA P-256 / ecdsa-with-SHA256 certificate by hand.
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ import * as crypto from 'crypto';
16
+ import * as der from './der';
17
+
18
+ /**
19
+ * Options for {@link generateSelfSigned}.
20
+ */
21
+ export interface GenerateSelfSignedOptions {
22
+ /** CN; WebRTC uses a random value. */
23
+ commonName?: string;
24
+ /** Validity period in days. */
25
+ days?: number;
26
+ /** Override start time (default: now - 1 day). */
27
+ notBefore?: Date;
28
+ }
29
+
30
+ /**
31
+ * Result of {@link generateSelfSigned}.
32
+ */
33
+ export interface SelfSignedCertificate {
34
+ /** DER-encoded certificate. */
35
+ certDer: Buffer;
36
+ privateKey: crypto.KeyObject;
37
+ publicKey: crypto.KeyObject;
38
+ notBefore: Date;
39
+ notAfter: Date;
40
+ }
41
+
42
+ // OIDs used in the certificate.
43
+ export const OID = Object.freeze({
44
+ ecPublicKey: '1.2.840.10045.2.1',
45
+ prime256v1: '1.2.840.10045.3.1.7',
46
+ ecdsaWithSHA256: '1.2.840.10045.4.3.2',
47
+ commonName: '2.5.4.3',
48
+ });
49
+
50
+ /**
51
+ * Build a Name with a single CN RDN.
52
+ * @param {string} cn
53
+ * @returns {Buffer}
54
+ */
55
+ function buildName(cn: string): Buffer {
56
+ const attr = der.encodeSequence([
57
+ der.encodeOID(OID.commonName),
58
+ der.encodeUTF8String(cn),
59
+ ]);
60
+ const rdn = der.encodeSet([attr]);
61
+ return der.encodeSequence([rdn]);
62
+ }
63
+
64
+ /**
65
+ * The AlgorithmIdentifier for ecdsa-with-SHA256 (no parameters).
66
+ * @returns {Buffer}
67
+ */
68
+ function ecdsaWithSHA256AlgId(): Buffer {
69
+ return der.encodeSequence([der.encodeOID(OID.ecdsaWithSHA256)]);
70
+ }
71
+
72
+ /**
73
+ * Generate a self-signed ECDSA P-256 certificate.
74
+ *
75
+ * @param {GenerateSelfSignedOptions} [options]
76
+ * @returns {SelfSignedCertificate}
77
+ */
78
+ export function generateSelfSigned(
79
+ options: GenerateSelfSignedOptions = {}
80
+ ): SelfSignedCertificate {
81
+ const commonName =
82
+ options.commonName || `WebRTC-${crypto.randomBytes(8).toString('hex')}`;
83
+ const days = options.days || 30;
84
+
85
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('ec', {
86
+ namedCurve: 'prime256v1',
87
+ });
88
+
89
+ // Node exports a complete SubjectPublicKeyInfo in DER — reuse verbatim.
90
+ const spki = publicKey.export({ type: 'spki', format: 'der' }) as Buffer;
91
+
92
+ // Validity. Start one day in the past to tolerate clock skew between peers.
93
+ const notBefore =
94
+ options.notBefore || new Date(Date.now() - 24 * 60 * 60 * 1000);
95
+ const notAfter = new Date(notBefore.getTime() + days * 24 * 60 * 60 * 1000);
96
+
97
+ // Serial number: positive 20-byte random (high bit cleared via encoder).
98
+ const serial = crypto.randomBytes(20);
99
+ serial[0]! &= 0x7f;
100
+ if (serial[0] === 0) serial[0] = 0x01;
101
+
102
+ const name = buildName(commonName);
103
+
104
+ // TBSCertificate (X.509 v3).
105
+ const tbs = der.encodeSequence([
106
+ der.encodeExplicit(0, der.encodeInteger(2)), // version v3 (value 2)
107
+ der.encodeIntegerFromBuffer(serial), // serialNumber
108
+ ecdsaWithSHA256AlgId(), // signature algorithm
109
+ name, // issuer (== subject, self-signed)
110
+ der.encodeSequence([der.encodeTime(notBefore), der.encodeTime(notAfter)]),
111
+ name, // subject
112
+ spki, // subjectPublicKeyInfo
113
+ ]);
114
+
115
+ // Sign the TBS. Node returns a DER ECDSA-Sig-Value (SEQUENCE { r, s }).
116
+ const signature = crypto.sign('sha256', tbs, privateKey);
117
+
118
+ const certDer = der.encodeSequence([
119
+ tbs,
120
+ ecdsaWithSHA256AlgId(),
121
+ der.encodeBitString(signature),
122
+ ]);
123
+
124
+ return { certDer, privateKey, publicKey, notBefore, notAfter };
125
+ }
126
+
127
+ /**
128
+ * Compute the certificate fingerprint as used in SDP a=fingerprint (RFC 8122):
129
+ * hash over the DER-encoded certificate, uppercase hex, colon-separated.
130
+ *
131
+ * @param {Buffer} certDer
132
+ * @param {string} [algorithm='sha-256'] - 'sha-256' | 'sha-384' | 'sha-512'
133
+ * @returns {string}
134
+ */
135
+ export function fingerprint(
136
+ certDer: Buffer,
137
+ algorithm: string = 'sha-256'
138
+ ): string {
139
+ const nodeAlgo = algorithm.replace('-', '').toLowerCase(); // sha-256 -> sha256
140
+ const digest = crypto
141
+ .createHash(nodeAlgo)
142
+ .update(certDer)
143
+ .digest('hex')
144
+ .toUpperCase();
145
+ return digest.match(/.{2}/g)!.join(':');
146
+ }
@@ -0,0 +1,388 @@
1
+ /**
2
+ * @file RTCDataChannel.ts
3
+ * @description WebRTC DataChannel implementation for peer-to-peer data transfer.
4
+ * @module datachannel/RTCDataChannel
5
+ *
6
+ * Implements the W3C RTCDataChannel interface
7
+ * (https://www.w3.org/TR/webrtc/#rtcdatachannel).
8
+ */
9
+
10
+ import { EventEmitter } from 'events';
11
+
12
+ /**
13
+ * RTCDataChannelState - Current state of the data channel
14
+ * @readonly
15
+ * @enum {string}
16
+ */
17
+ export const RTCDataChannelState = Object.freeze({
18
+ CONNECTING: 'connecting',
19
+ OPEN: 'open',
20
+ CLOSING: 'closing',
21
+ CLOSED: 'closed'
22
+ });
23
+
24
+ type RTCDataChannelReadyState = 'connecting' | 'open' | 'closing' | 'closed';
25
+ type RTCDataChannelBinaryType = 'arraybuffer' | 'blob';
26
+
27
+ /**
28
+ * Package-internal events that wire an RTCDataChannel to the SCTP transport.
29
+ * They are keyed by Symbol so they never collide with — or leak into — the
30
+ * public event surface ('open'/'message'/'close'/'error'/'bufferedamountlow').
31
+ * The SCTP data-channel manager and the channel communicate purely by emitting
32
+ * these on the channel's own EventEmitter:
33
+ *
34
+ * - SEND channel → transport: outbound frame `(data: Buffer, isBinary: boolean)`
35
+ * - RECEIVE transport → channel: inbound frame `(data: Buffer, isBinary: boolean)`
36
+ * - OPEN transport → channel: transition the channel to 'open'
37
+ * - SET_ID transport → channel: assign the SCTP stream id `(id: number)`
38
+ */
39
+ export const RTCDataChannelEvents = Object.freeze({
40
+ SEND: Symbol('rtcdatachannel:send'),
41
+ RECEIVE: Symbol('rtcdatachannel:receive'),
42
+ OPEN: Symbol('rtcdatachannel:open'),
43
+ SET_ID: Symbol('rtcdatachannel:setId'),
44
+ });
45
+
46
+ /**
47
+ * RTCDataChannelInit - Configuration for creating a data channel
48
+ * @typedef {Object} RTCDataChannelInit
49
+ * @property {boolean} [ordered=true] - Whether messages must arrive in order
50
+ * @property {number} [maxPacketLifeTime] - Maximum packet lifetime in milliseconds
51
+ * @property {number} [maxRetransmits] - Maximum number of retransmissions
52
+ * @property {string} [protocol=''] - Subprotocol name
53
+ * @property {boolean} [negotiated=false] - Whether channel was negotiated out-of-band
54
+ * @property {number} [id] - Channel ID (required if negotiated is true)
55
+ */
56
+ export interface RTCDataChannelInit {
57
+ ordered?: boolean;
58
+ maxPacketLifeTime?: number;
59
+ maxRetransmits?: number;
60
+ protocol?: string;
61
+ negotiated?: boolean;
62
+ id?: number;
63
+ }
64
+
65
+ /**
66
+ * @class RTCDataChannel
67
+ * @extends EventEmitter
68
+ * @description Represents a bidirectional data channel between peers.
69
+ * Provides reliable or unreliable data transfer with configurable ordering.
70
+ *
71
+ * Events:
72
+ * - 'open': Fired when the channel opens
73
+ * - 'message': Fired when a message is received
74
+ * - 'bufferedamountlow': Fired when bufferedAmount drops below threshold
75
+ * - 'error': Fired when an error occurs
76
+ * - 'closing': Fired when the channel is closing
77
+ * - 'close': Fired when the channel closes
78
+ *
79
+ * @example
80
+ * const dataChannel = peerConnection.createDataChannel('myChannel', {
81
+ * ordered: true,
82
+ * maxRetransmits: 3
83
+ * });
84
+ *
85
+ * dataChannel.on('open', () => {
86
+ * console.log('Channel opened');
87
+ * dataChannel.send('Hello!');
88
+ * });
89
+ *
90
+ * dataChannel.on('message', (event) => {
91
+ * console.log('Received:', event.data);
92
+ * });
93
+ */
94
+ export class RTCDataChannel extends EventEmitter {
95
+ #label: string;
96
+ #ordered: boolean;
97
+ #maxPacketLifeTime: number | null;
98
+ #maxRetransmits: number | null;
99
+ #protocol: string;
100
+ #negotiated: boolean;
101
+ #id: number | null;
102
+ #readyState: RTCDataChannelReadyState;
103
+ #bufferedAmount: number;
104
+ #bufferedAmountLowThreshold: number;
105
+ #binaryType: RTCDataChannelBinaryType;
106
+ /** Whether a transport is listening for outbound SEND events. */
107
+ #connected: boolean;
108
+
109
+ /**
110
+ * Create an RTCDataChannel instance.
111
+ * @param {string} label - Channel label
112
+ * @param {RTCDataChannelInit} [init] - Channel configuration
113
+ */
114
+ constructor(label: string, init: RTCDataChannelInit = {}) {
115
+ super();
116
+
117
+ if (typeof label !== 'string') {
118
+ throw new TypeError('label must be a string');
119
+ }
120
+
121
+ // Channel configuration
122
+ this.#label = label;
123
+ this.#ordered = init.ordered !== undefined ? init.ordered : true;
124
+ this.#maxPacketLifeTime = init.maxPacketLifeTime || null;
125
+ this.#maxRetransmits = init.maxRetransmits || null;
126
+ this.#protocol = init.protocol || '';
127
+ this.#negotiated = init.negotiated || false;
128
+ this.#id = init.id !== undefined ? init.id : null;
129
+
130
+ // State
131
+ this.#readyState = RTCDataChannelState.CONNECTING as RTCDataChannelReadyState;
132
+ this.#bufferedAmount = 0;
133
+ this.#bufferedAmountLowThreshold = 0;
134
+ this.#binaryType = 'arraybuffer'; // or 'blob'
135
+ this.#connected = false;
136
+
137
+ // Transport drives the channel via internal (Symbol-keyed) events.
138
+ this.on(RTCDataChannelEvents.SET_ID, (id: number) => { this.#id = id; });
139
+ this.on(RTCDataChannelEvents.OPEN, () => {
140
+ this.#connected = true;
141
+ this.#setState(RTCDataChannelState.OPEN as RTCDataChannelReadyState);
142
+ });
143
+ this.on(RTCDataChannelEvents.RECEIVE, (data: Buffer, isBinary: boolean) => {
144
+ this.#receiveMessage(data, isBinary);
145
+ });
146
+ }
147
+
148
+ /**
149
+ * Get the channel label.
150
+ * @returns {string} Channel label
151
+ */
152
+ get label(): string {
153
+ return this.#label;
154
+ }
155
+
156
+ /**
157
+ * Check if messages are delivered in order.
158
+ * @returns {boolean} True if ordered
159
+ */
160
+ get ordered(): boolean {
161
+ return this.#ordered;
162
+ }
163
+
164
+ /**
165
+ * Get the maximum packet lifetime in milliseconds.
166
+ * @returns {number|null} Maximum lifetime or null if not set
167
+ */
168
+ get maxPacketLifeTime(): number | null {
169
+ return this.#maxPacketLifeTime;
170
+ }
171
+
172
+ /**
173
+ * Get the maximum number of retransmissions.
174
+ * @returns {number|null} Maximum retransmits or null if not set
175
+ */
176
+ get maxRetransmits(): number | null {
177
+ return this.#maxRetransmits;
178
+ }
179
+
180
+ /**
181
+ * Get the subprotocol name.
182
+ * @returns {string} Protocol name
183
+ */
184
+ get protocol(): string {
185
+ return this.#protocol;
186
+ }
187
+
188
+ /**
189
+ * Check if the channel was negotiated out-of-band.
190
+ * @returns {boolean} True if negotiated
191
+ */
192
+ get negotiated(): boolean {
193
+ return this.#negotiated;
194
+ }
195
+
196
+ /**
197
+ * Get the channel ID.
198
+ * @returns {number|null} Channel ID or null if not assigned
199
+ */
200
+ get id(): number | null {
201
+ return this.#id;
202
+ }
203
+
204
+ /**
205
+ * Get the current state of the channel.
206
+ * @returns {string} Channel state
207
+ */
208
+ get readyState(): RTCDataChannelReadyState {
209
+ return this.#readyState;
210
+ }
211
+
212
+ /**
213
+ * Get the number of bytes queued to send.
214
+ * @returns {number} Buffered amount in bytes
215
+ */
216
+ get bufferedAmount(): number {
217
+ return this.#bufferedAmount;
218
+ }
219
+
220
+ /**
221
+ * Get the threshold for bufferedamountlow event.
222
+ * @returns {number} Threshold in bytes
223
+ */
224
+ get bufferedAmountLowThreshold(): number {
225
+ return this.#bufferedAmountLowThreshold;
226
+ }
227
+
228
+ /**
229
+ * Set the threshold for bufferedamountlow event.
230
+ * @param {number} value - Threshold in bytes
231
+ */
232
+ set bufferedAmountLowThreshold(value: number) {
233
+ this.#bufferedAmountLowThreshold = value;
234
+ }
235
+
236
+ /**
237
+ * Get the binary data type.
238
+ * @returns {string} 'arraybuffer' or 'blob'
239
+ */
240
+ get binaryType(): RTCDataChannelBinaryType {
241
+ return this.#binaryType;
242
+ }
243
+
244
+ /**
245
+ * Set the binary data type.
246
+ * @param {string} value - 'arraybuffer' or 'blob'
247
+ * @throws {TypeError} If value is invalid
248
+ */
249
+ set binaryType(value: RTCDataChannelBinaryType) {
250
+ if (value !== 'arraybuffer' && value !== 'blob') {
251
+ throw new TypeError('binaryType must be "arraybuffer" or "blob"');
252
+ }
253
+ this.#binaryType = value;
254
+ }
255
+
256
+ /**
257
+ * Check if the channel is reliable (deprecated).
258
+ * @returns {boolean} True if ordered and no packet lifetime/retransmit limits
259
+ * @deprecated Use ordered, maxPacketLifeTime, and maxRetransmits instead
260
+ */
261
+ get reliable(): boolean {
262
+ return this.#ordered &&
263
+ this.#maxPacketLifeTime === null &&
264
+ this.#maxRetransmits === null;
265
+ }
266
+
267
+ /**
268
+ * Send a message through the channel.
269
+ * @param {string|ArrayBuffer|ArrayBufferView|Blob} data - Data to send
270
+ * @throws {Error} If channel is not open or data is invalid
271
+ */
272
+ send(data: string | ArrayBuffer | ArrayBufferView | Buffer): void {
273
+ if (this.#readyState !== RTCDataChannelState.OPEN) {
274
+ throw new Error('RTCDataChannel.readyState is not "open"');
275
+ }
276
+
277
+ let dataToSend: Buffer;
278
+ let byteLength = 0;
279
+ let isBinary: boolean;
280
+
281
+ if (typeof data === 'string') {
282
+ dataToSend = Buffer.from(data, 'utf8');
283
+ byteLength = dataToSend.length;
284
+ isBinary = false;
285
+ } else if (data instanceof ArrayBuffer) {
286
+ dataToSend = Buffer.from(data);
287
+ byteLength = data.byteLength;
288
+ isBinary = true;
289
+ } else if (ArrayBuffer.isView(data)) {
290
+ dataToSend = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
291
+ byteLength = data.byteLength;
292
+ isBinary = true;
293
+ } else if (Buffer.isBuffer(data)) {
294
+ dataToSend = data as Buffer;
295
+ byteLength = (data as Buffer).length;
296
+ isBinary = true;
297
+ } else if (data && typeof (data as { arrayBuffer?: unknown }).arrayBuffer === 'function') {
298
+ // Blob-like object
299
+ throw new Error('Blob sending not yet implemented');
300
+ } else {
301
+ throw new TypeError('Invalid data type');
302
+ }
303
+
304
+ // Emit the outbound frame for the transport to carry. The isBinary flag
305
+ // lets the peer reconstruct the right JS type; binary is transmitted as raw
306
+ // bytes (no JSON), avoiding corruption of Buffer/ArrayBuffer payloads.
307
+ if (!this.#connected) {
308
+ throw new Error('Data channel not connected to a transport');
309
+ }
310
+
311
+ // Update buffered amount, then decrement once the transport has taken it.
312
+ this.#bufferedAmount += byteLength;
313
+ try {
314
+ this.emit(RTCDataChannelEvents.SEND, dataToSend, isBinary);
315
+ this.#bufferedAmount = Math.max(0, this.#bufferedAmount - byteLength);
316
+ this.#emitBufferedAmountLow();
317
+ } catch (err) {
318
+ this.#bufferedAmount = Math.max(0, this.#bufferedAmount - byteLength);
319
+ this.emit('error', err);
320
+ throw err;
321
+ }
322
+ }
323
+
324
+ /** Emit bufferedamountlow if appropriate. */
325
+ #emitBufferedAmountLow(): void {
326
+ if (this.#bufferedAmount <= this.#bufferedAmountLowThreshold) {
327
+ this.emit('bufferedamountlow');
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Close the data channel.
333
+ */
334
+ close(): void {
335
+ if (this.#readyState === RTCDataChannelState.CLOSING ||
336
+ this.#readyState === RTCDataChannelState.CLOSED) {
337
+ return;
338
+ }
339
+
340
+ this.#setState(RTCDataChannelState.CLOSING as RTCDataChannelReadyState);
341
+
342
+ // Transition to closed asynchronously
343
+ setImmediate(() => {
344
+ if (this.#readyState === RTCDataChannelState.CLOSING) {
345
+ this.#setState(RTCDataChannelState.CLOSED as RTCDataChannelReadyState);
346
+ }
347
+ });
348
+ }
349
+
350
+ /** Set the channel state and emit the matching lifecycle event. */
351
+ #setState(newState: RTCDataChannelReadyState): void {
352
+ const oldState = this.#readyState;
353
+ if (oldState === newState) {
354
+ return;
355
+ }
356
+
357
+ this.#readyState = newState;
358
+
359
+ // Emit state-specific events
360
+ if (newState === RTCDataChannelState.OPEN) {
361
+ this.emit('open');
362
+ } else if (newState === RTCDataChannelState.CLOSING) {
363
+ this.emit('closing');
364
+ } else if (newState === RTCDataChannelState.CLOSED) {
365
+ this.emit('close');
366
+ }
367
+ }
368
+
369
+ /**
370
+ * Deliver a received message to listeners.
371
+ *
372
+ * Mirrors the browser RTCDataChannel: text frames surface as a string;
373
+ * binary frames surface as an ArrayBuffer (binaryType 'arraybuffer') or a
374
+ * Node Buffer (binaryType 'blob', which we approximate with Buffer since
375
+ * Node has no Blob in older runtimes).
376
+ */
377
+ #receiveMessage(data: Buffer, isBinary: boolean): void {
378
+ let payload: string | ArrayBuffer | Buffer;
379
+ if (!isBinary) {
380
+ payload = data.toString('utf8');
381
+ } else if (this.#binaryType === 'arraybuffer') {
382
+ payload = (data.buffer as ArrayBuffer).slice(data.byteOffset, data.byteOffset + data.byteLength);
383
+ } else {
384
+ payload = data;
385
+ }
386
+ this.emit('message', { data: payload });
387
+ }
388
+ }