data-transport
Version:
A simple and responsive transport
150 lines (141 loc) • 4.57 kB
text/typescript
import type { Instance } from 'simple-peer';
import type {
BaseInteraction,
ListenerOptions,
SendOptions,
TransportOptions,
} from '../interface';
import { Transport } from '../transport';
const MAX_CHUNK_SIZE = 1024 * 60;
const BUFFER_FULL_THRESHOLD = 1024 * 64;
const EXPIRED_TIME = 1000 * 60;
export interface WebRTCTransportOptions extends Partial<TransportOptions> {
peer: Instance;
}
interface WebRTCTransportSendOptions extends SendOptions<{}> {
chunkId?: number;
length?: number;
}
abstract class WebRTCTransport<
T extends BaseInteraction = any
> extends Transport<T> {
private receiveBuffer = new Map<string, { data: any[]; timestamp: number }>();
constructor(_options: WebRTCTransportOptions) {
const {
peer,
listener = (callback) => {
const handler = (data: string) => {
const message: WebRTCTransportSendOptions = JSON.parse(data);
const key = Object.prototype.hasOwnProperty.call(message, 'request')
? 'request'
: 'response';
const buffer = this.receiveBuffer.get(
message.__DATA_TRANSPORT_UUID__
) ?? {
data: [],
timestamp: Date.now(),
};
this.receiveBuffer.set(message.__DATA_TRANSPORT_UUID__, buffer);
buffer.data[message.chunkId!] = message[key];
buffer.data.length = message.length!;
buffer.timestamp = Date.now();
const isComplete =
buffer.data.filter((item) => item).length === message.length;
if (isComplete) {
const data = JSON.parse(buffer.data.join(''));
message[key] = key === 'request' ? data : data[0];
delete message.length;
callback(message as ListenerOptions);
this.receiveBuffer.delete(message.__DATA_TRANSPORT_UUID__);
for (const [id, item] of this.receiveBuffer) {
if (Date.now() - item.timestamp > EXPIRED_TIME) {
this.receiveBuffer.delete(id);
}
}
}
};
peer.on('data', handler);
return () => {
peer.off('data', handler);
};
},
sender = (message: WebRTCTransportSendOptions) => {
const key = Object.prototype.hasOwnProperty.call(message, 'request')
? 'request'
: 'response';
message[key] = JSON.stringify(
key === 'request'
? message.request
: typeof message.response !== 'undefined'
? [message.response]
: []
);
let chunkId = 0;
const allChunksSize = Math.ceil(
(message[key] as string).length / MAX_CHUNK_SIZE
);
while ((message[key] as string).length > 0) {
const data = {
...message,
[key]: (message[key] as string).slice(0, MAX_CHUNK_SIZE),
chunkId,
length: allChunksSize,
};
peer.send(JSON.stringify(data));
message[key] = (message[key] as string).slice(MAX_CHUNK_SIZE);
chunkId += 1;
}
},
...options
} = _options;
super({
...options,
listener,
sender,
});
if (peer) {
let webRTCPaused = false;
const webRTCMessageQueue: any[] = [];
const peerSend = peer.send.bind(peer);
const sendMessageQueued = () => {
webRTCPaused = false;
let message = webRTCMessageQueue.shift();
while (message) {
if (
(peer as any)._channel.bufferedAmount &&
(peer as any)._channel.bufferedAmount > BUFFER_FULL_THRESHOLD
) {
webRTCPaused = true;
webRTCMessageQueue.unshift(message);
const listener = () => {
(peer as any)._channel.removeEventListener(
'bufferedamountlow',
listener
);
sendMessageQueued();
};
(peer as any)._channel.addEventListener(
'bufferedamountlow',
listener
);
return;
}
try {
peerSend(message);
message = webRTCMessageQueue.shift();
} catch (error: any) {
throw new Error(`Error send message to peer: ${error.message}`);
}
}
};
peer.send = function (chunk: any) {
webRTCMessageQueue.push(chunk);
if (webRTCPaused) {
return;
}
sendMessageQueued();
};
}
}
}
export { WebRTCTransport };