UNPKG

@mertushka/node-datachannel

Version:

WebRTC For Node.js and Electron. libdatachannel node bindings.

238 lines (204 loc) 7.14 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ import * as exceptions from './Exception'; import { DataChannel } from '../lib/index'; import { RTCErrorEvent } from './Events'; export default class RTCDataChannel extends EventTarget implements globalThis.RTCDataChannel { #dataChannel: DataChannel; #readyState: globalThis.RTCDataChannelState; #bufferedAmountLowThreshold: number; #binaryType: BinaryType; #maxPacketLifeTime: number | null; #maxRetransmits: number | null; #negotiated: boolean; #ordered: boolean; #id: number; #label: string; #protocol: string; #closeRequested = false; // events onbufferedamountlow: globalThis.RTCDataChannel['onbufferedamountlow'] = null; onclose: globalThis.RTCDataChannel['onclose'] = null; onclosing: globalThis.RTCDataChannel['onclosing'] = null; onerror: globalThis.RTCDataChannel['onerror'] = null; onmessage: globalThis.RTCDataChannel['onmessage'] = null; onopen: globalThis.RTCDataChannel['onopen'] = null; constructor(dataChannel: DataChannel, opts: globalThis.RTCDataChannelInit = {}) { super(); this.#dataChannel = dataChannel; this.#binaryType = 'blob'; this.#readyState = this.#dataChannel.isOpen() ? 'open' : 'connecting'; this.#bufferedAmountLowThreshold = 0; this.#maxPacketLifeTime = opts.maxPacketLifeTime ?? null; this.#maxRetransmits = opts.maxRetransmits ?? null; this.#negotiated = opts.negotiated ?? false; this.#ordered = opts.ordered ?? true; this.#id = this.#dataChannel.getId(); this.#label = this.#dataChannel.getLabel(); this.#protocol = this.#dataChannel.getProtocol(); // forward dataChannel events this.#dataChannel.onOpen(() => { this.#readyState = 'open'; this.dispatchEvent(new Event('open', {})); }); this.#dataChannel.onClosed(() => { if (this.#readyState === 'closed') return; if (!this.#closeRequested) { // if close was not requested, we emit 'closing' before 'close' to match the spec behavior this.#readyState = 'closing'; this.dispatchEvent(new Event('closing')); } setImmediate(() => { if (this.#readyState !== 'closed') { this.#readyState = 'closed'; this.dispatchEvent(new Event('close')); } }); }); this.#dataChannel.onError((msg) => { this.dispatchEvent( new RTCErrorEvent('error', { error: new RTCError( { errorDetail: 'data-channel-failure', }, msg, ), }), ); }); this.#dataChannel.onBufferedAmountLow(() => { this.dispatchEvent(new Event('bufferedamountlow')); }); this.#dataChannel.onMessage((data) => { if (ArrayBuffer.isView(data)) { data = this.binaryType === 'arraybuffer' ? (data.buffer as ArrayBuffer) : Buffer.from(data.buffer); } this.dispatchEvent(new MessageEvent('message', { data })); }); // forward events to properties this.addEventListener('open', (e) => this.onopen?.(e)); this.addEventListener('message', (e) => this.onmessage?.(e as MessageEvent)); this.addEventListener('error', (e) => this.onerror?.(e as RTCErrorEvent)); this.addEventListener('close', (e) => this.onclose?.(e)); this.addEventListener('closing', (e) => this.onclosing?.(e)); this.addEventListener('bufferedamountlow', (e) => this.onbufferedamountlow?.(e)); } set binaryType(type) { if (type !== 'blob' && type !== 'arraybuffer') { throw new DOMException( "Failed to set the 'binaryType' property on 'RTCDataChannel': Unknown binary type : " + type, 'TypeMismatchError', ); } this.#binaryType = type; } get binaryType(): BinaryType { return this.#binaryType; } get bufferedAmount(): number { return this.#dataChannel.bufferedAmount(); } get bufferedAmountLowThreshold(): number { return this.#bufferedAmountLowThreshold; } set bufferedAmountLowThreshold(value) { const number = Number(value) || 0; this.#bufferedAmountLowThreshold = number; this.#dataChannel.setBufferedAmountLowThreshold(number); } get id(): number | null { return this.#id; } get label(): string { return this.#label; } get maxPacketLifeTime(): number | null { return this.#maxPacketLifeTime; } get maxRetransmits(): number | null { return this.#maxRetransmits; } get negotiated(): boolean { return this.#negotiated; } get ordered(): boolean { return this.#ordered; } get protocol(): string { return this.#protocol; } get readyState(): globalThis.RTCDataChannelState { return this.#readyState; } send(data: string | Blob | ArrayBuffer | ArrayBufferView): void { if (this.#readyState !== 'open') { if (process.env.DATACHANNEL_SEND_DEBUG === 'true') { console.error( new exceptions.InvalidStateError( "Failed to execute 'send' on 'RTCDataChannel': RTCDataChannel.readyState is not 'open'", ), ); } return; } // Needs network error, type error implemented try { if (typeof data === 'string') { this.#dataChannel.sendMessage(data); } else if (data instanceof Blob) { data.arrayBuffer().then((ab) => { if (process?.versions?.bun) { this.#dataChannel.sendMessageBinary(Buffer.from(ab)); } else { this.#dataChannel.sendMessageBinary(new Uint8Array(ab)); } }); } else if (data instanceof Uint8Array) { this.#dataChannel.sendMessageBinary(data); } else { if (process?.versions?.bun) { this.#dataChannel.sendMessageBinary(Buffer.from(data as ArrayBuffer)); } else { this.#dataChannel.sendMessageBinary(new Uint8Array(data as ArrayBuffer)); } } } catch (error) { //If error contains "DataChannel is closed" throw InvalidStateError if (error instanceof Error && error?.message?.includes('DataChannel is closed')) { // @ts-expect-error i-hate-ts if (this.#readyState !== 'closed') { this.#readyState = 'closed'; this.dispatchEvent(new Event('close')); } if (process.env.DATACHANNEL_SEND_DEBUG === 'true') { console.error( new exceptions.InvalidStateError( "Failed to execute 'send' on 'RTCDataChannel': RTCDataChannel is not 'open'", ), ); } } else { // Otherwise re-throw the error if (process.env.DATACHANNEL_SEND_DEBUG === 'true') { console.error(error); } } } } close(): void { if (this.#readyState === 'closing' || this.#readyState === 'closed') return; this.#closeRequested = true; this.#readyState = 'closing'; setImmediate(() => { this.#dataChannel.close(); }); } _forceCloseAbruptly(): void { if (this.#readyState === 'closing' || this.#readyState === 'closed') return; this.#readyState = 'closed'; } }