@mertushka/node-datachannel
Version:
WebRTC For Node.js and Electron. libdatachannel node bindings.
238 lines (204 loc) • 7.14 kB
text/typescript
/* 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';
}
}