@4players/odin
Version:
A cross-platform SDK enabling developers to integrate real-time VoIP chat technology into their projects
290 lines (289 loc) • 10.3 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeHandler = exports.OdinStream = void 0;
const msgpackr_1 = require("msgpackr");
const schema_validation_1 = require("./schema-validation");
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();
}
}
}
exports.OdinStream = OdinStream;
/**
* 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 = (0, msgpackr_1.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 = (0, msgpackr_1.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((0, msgpackr_1.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((0, msgpackr_1.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)
*/
const makeHandler = (schemas, handlers, instance) => {
return (method, params) => {
if (isKnownMethod(schemas, method)) {
handleEvent(schemas, handlers, method, params, instance);
}
else {
throw Error('Unknown method!');
}
};
};
exports.makeHandler = makeHandler;
/**
* 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];
(0, schema_validation_1.validate)(params, 'parameters', schema);
handlers[method](params, instance);
};