UNPKG

@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
"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); };