@4players/odin
Version:
A cross-platform SDK enabling developers to integrate real-time VoIP chat technology into their projects
277 lines (276 loc) • 9.32 kB
JavaScript
import { __awaiter } from "tslib";
import { pack, unpack } from 'msgpackr';
import { validate } from './schema-validation';
export class OdinStream {
constructor(_url, _handler, _timeout = 5000) {
this._url = _url;
this._handler = _handler;
this._timeout = _timeout;
this._requests = new Map();
this.nextId = 0;
this._websocket = new WebSocket(_url);
this._websocket.binaryType = 'arraybuffer';
this._websocket.addEventListener('close', () => {
this._websocket = null;
this._requests.forEach(({ reject }) => reject(new Error('closed')));
this._requests.clear();
});
this._websocket.addEventListener('message', (e) => __awaiter(this, void 0, void 0, function* () {
yield receive(this, e.data);
}));
}
get url() {
return this._url;
}
get websocket() {
var _a;
return (_a = this._websocket) !== null && _a !== void 0 ? _a : null;
}
get timeout() {
return this._timeout;
}
get requests() {
return this._requests;
}
get handler() {
return this._handler;
}
get onopen() {
if (this.websocket) {
return this.websocket.onopen;
}
else {
return null;
}
}
set onopen(value) {
if (this.websocket) {
this.websocket.onopen = value;
}
}
get onclose() {
if (this.websocket) {
return this.websocket.onclose;
}
else {
return null;
}
}
set onclose(value) {
if (this.websocket) {
this.websocket.onclose = value;
}
}
set onerror(value) {
if (this.websocket) {
this.websocket.onerror = value;
}
}
get onerror() {
if (this.websocket) {
return this.websocket.onerror;
}
return null;
}
addEventListener(type, listener, options) {
return __awaiter(this, void 0, void 0, function* () {
if (this.websocket) {
return this.websocket.addEventListener(type, listener, options);
}
});
}
removeEventListener(type, listener, options) {
if (this.websocket) {
return this.websocket.removeEventListener(type, listener, options);
}
}
request(method, params) {
const id = this.nextId++;
return send(this, id, method, params);
}
close() {
if (this.websocket) {
this.websocket.close();
}
}
}
/**
* Packs and sends the request via the websocket. Requests getting cached until they are resolved.
*/
function send(stream, id, method, params) {
return __awaiter(this, void 0, void 0, function* () {
if (stream.websocket === null) {
throw new Error('Stream is closed');
}
const request = id !== null ? [0, id, method, params] : [2, method, params];
const packedRequest = pack(request);
stream.websocket.send(packedRequest);
if (id === null) {
return Promise.resolve();
}
else {
// If a timeout was set, define a new timeout handle which deletes the request and closes the stream
return new Promise((resolve, reject) => {
let timeoutHandle = null;
if (stream.timeout > 0) {
timeoutHandle = setTimeout(() => {
if (stream.requests.delete(id)) {
reject(new Error(`Stream timeout at method ${method}`));
stream.close();
}
}, stream.timeout);
}
stream.requests.set(id, { method, resolve, reject, timeoutHandle });
});
}
});
}
/**
* Event listener to process the event.data from the given stream.
*
* Implementation of the MessagePack-RPC Specification:
* @see https://github.com/msgpack-rpc/msgpack-rpc/blob/master/spec.md
*
* @param {Stream} stream
* @param {ArrayBuffer} bytes (event.data)
**/
function receive(stream, bytes) {
return __awaiter(this, void 0, void 0, function* () {
const message = unpack(new Uint8Array(bytes));
const valid = Array.isArray(message) && message.length > 0 && typeof message[0] === 'number';
if (!valid) {
console.error('received invalid formatted message', message);
return;
}
/**
* Request Message
* [type, msgid, method, params]
*/
switch (message[0]) {
case 0: {
const valid = message.length === 4 &&
typeof message[1] === 'number' &&
typeof message[2] === 'string' &&
typeof message[3] === 'object';
if (valid) {
yield receiveRequest(stream, message[1], message[2], message[3]);
}
else {
console.error('received invalid formatted request', message);
}
break;
}
/**
* Response Message
* [type, msgid, error, result]
*/
case 1: {
const valid = message.length === 4 &&
typeof message[1] === 'number' &&
(message[2] === null || typeof message[2] === 'string') &&
(message[3] === null || typeof message[3] === 'object') &&
(message[2] === null || message[3] === null);
const result = message[3];
if (valid) {
receiveResponse(stream, message[1], message[2], result);
}
else {
console.error('received invalid formatted response', message);
}
break;
}
/**
* Notification Message
* [type, method, params]
*/
case 2: {
const valid = message.length === 3 && typeof message[1] === 'string' && typeof message[2] === 'object';
if (valid) {
yield receiveRequest(stream, null, message[1], message[2]);
}
else {
console.error('received invalid formatted request', message);
}
break;
}
}
});
}
/**
* MessagePack RPC request handling. Calls the given handler method.
*/
function receiveRequest(stream, id, method, params) {
return __awaiter(this, void 0, void 0, function* () {
try {
const response = stream.handler(method, params);
if (id !== null && stream.websocket !== null) {
stream.websocket.send(pack([1, id, null, response]));
}
}
catch (error) {
if (id !== null && stream.websocket !== null) {
const message = error instanceof Error ? error.message : 'internal error';
stream.websocket.send(pack([1, id, message, null]));
}
}
});
}
/**
* MessagePack response handling.
*/
function receiveResponse(stream, id, error, result) {
const request = stream.requests.get(id);
if (request === undefined) {
return;
}
stream.requests.delete(id);
if (request.timeoutHandle !== null) {
clearTimeout(request.timeoutHandle);
}
if (error === null) {
request.resolve(result);
}
else {
request.reject(new Error(error));
}
}
/**
* Creates a handler which correctly handles the event depending on the method and validates its params.
*
* @param schemas The object which provides the schemas that are used to check recursively the type of the value at runtime
* @param handlers All event handler for the given schema<T>
* @returns Returns a handler function that takes the method (type) and the params as argument (The msgpack params)
*/
export const makeHandler = (schemas, handlers, instance) => {
return (method, params) => {
if (isKnownMethod(schemas, method)) {
handleEvent(schemas, handlers, method, params, instance);
}
else {
throw Error('Unknown method!');
}
};
};
/**
* Check if the method (from msgpack) is a property of the schema. If not, the event can not be handled.
*
* @param schemas Schema object
* @param method Name of the method
* @returns true if the property is a keyof EventSchemas (boolean)
*/
const isKnownMethod = (schemas, method) => {
return Object.getOwnPropertyDescriptor(schemas, method) != null;
};
/**
* Validates the params and calls the given eventHandler, determined by the given method.
*
* @param schemas Schema object
* @param handlers All provided EventHandlers<T> while T is a EventSchema
* @param method A string which is one of the EventHandler names
* @param params The params that getting passes through and getting called
*/
const handleEvent = (schemas, handlers, method, params, instance) => {
const schema = schemas[method];
validate(params, 'parameters', schema);
handlers[method](params, instance);
};