UNPKG

ts-event-bus

Version:
226 lines (176 loc) 6.23 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ChunkedChannel = void 0; var _GenericChannel = require("./GenericChannel"); var _Message = require("../Message"); /** * A chunk is a array of bytes. * It is stored as a array of numbers and is manipulated using a Uint16Array. */ const utils = { getRandomId: () => [...Array(30)].map(() => Math.random().toString(36)[3]).join(''), str2byteArray: str => { const bufView = new Uint16Array(str.length); for (let i = 0, strLen = str.length; i < strLen; i++) { bufView[i] = str.charCodeAt(i); } return bufView; }, convertUintArrayToString: (a, maxStringAlloc) => { if (maxStringAlloc === -1) { return String.fromCharCode.apply(null, a); } else { let result = ''; for (let i = 0; i < a.length; i += maxStringAlloc) { if (i + maxStringAlloc > a.length) { result += String.fromCharCode.apply(null, a.subarray(i)); } else { result += String.fromCharCode.apply(null, a.subarray(i, i + maxStringAlloc)); } } return result; } }, checkForChunkId: message => { if (!message.chunkId) { throw new Error(`ChunkedMessage did not have a chunkId: ${JSON.stringify(message)}`); } } }; /** * Overrides the `send` and `_messageReceived` methods of the GenericChannel class * to offer transparent message chunking over a fragile underlying channel. */ class ChunkedChannel extends _GenericChannel.GenericChannel { constructor(opts) { super(opts.timeout); this._chunkSize = void 0; this._maxStringAlloc = void 0; this._sender = void 0; this._buffer = {}; this._chunkSize = opts.chunkSize; this._sender = opts.sender; this._maxStringAlloc = opts.maxStringAlloc || -1; } /** * The size of the data array in each chunk. * Note that the total "size" of the message will be larger * because of the chunking metadata. */ /** * Defines the maximum string length that will be allocated at once when * merging the buffered chunks into the original string. * This is only needed if the environment where this instance is running applies restriction * on memory for string allocation. * Omitting to set this will just create the string from the chunks in one go. */ /** The actual sending via the underlying channel (eg. websocket) */ /** Stores chunks pending flush */ /** * This method override will chunk messages so that an array of no more than * `chunkSize` bytes (excluding internal metadata) will be sent for each call * to a given slot. */ send(message) { const stringified = JSON.stringify(message); if (stringified.length <= this._chunkSize) { this._sender(message); return; } const messageAsByteArray = utils.str2byteArray(stringified); const chunkId = utils.getRandomId(); this._sender({ type: 'chunk_start', chunkId, size: stringified.length }); const sendChunks = (start = 0) => { let chunk = messageAsByteArray.slice(start, start + this._chunkSize); if (chunk.length) { this._sender({ type: 'chunk_data', chunkId, // To avoid having the underlying channel implemetation interpret/cast // the UintArray into something else, we explicitely send an array data: Array.from(chunk) }); sendChunks(start + this._chunkSize); } }; sendChunks(); this._sender({ type: 'chunk_end', chunkId }); } /** * When a message is received on this channel, either it has been chunked because its original size * was greater than the chunkSize in which case it will be a `ChunkedTransportMessage`, * or it was small enough so that it could be sent un chunked in which * case it will be a plain `TransportMessage`. */ _messageReceived(message) { switch (message.type) { case 'chunk_start': this._receiveNewChunk(message); break; case 'chunk_data': this._receiveChunkData(message); break; case 'chunk_end': const decodedMessage = this._mergeChunks(message); super._messageReceived(decodedMessage); break; default: // If the message is small enough, it won't be chunked before sending // so it won't need merging/buffering here super._messageReceived(message); } } _receiveNewChunk(message) { utils.checkForChunkId(message); if (this._buffer[message.chunkId]) { throw new Error(`There was already an entry in the buffer for chunkId ${message.chunkId}`); } this._buffer[message.chunkId] = { id: message.chunkId, chunks: [], size: message.size }; } _receiveChunkData(message) { utils.checkForChunkId(message); if (!this._buffer[message.chunkId]) { throw new Error(`ChunkId ${message.chunkId} was not found in the buffer`); } this._buffer[message.chunkId].chunks.push(message.data); } _mergeChunks(message) { utils.checkForChunkId(message); if (!this._buffer[message.chunkId]) { throw new Error(`ChunkId ${message.chunkId} was not found in the buffer`); } // Store all the chunks into one Uint16Array const mergedChunks = this._buffer[message.chunkId].chunks.reduce((d, chunk, ix) => { chunk.forEach((byte, i) => d.uintArray[d.currentIx + i] = byte); d.currentIx += chunk.length; return d; }, { uintArray: new Uint16Array(this._buffer[message.chunkId].size), currentIx: 0 }); let transportMessage; // Then rebuild the object from the merged chunk, now stored as one string const dataAsString = utils.convertUintArrayToString(mergedChunks.uintArray, this._maxStringAlloc); try { transportMessage = JSON.parse(dataAsString); } catch (e) { throw new Error(`Not a valid JSON string: ${dataAsString}`); } if (!(0, _Message.isTransportMessage)(transportMessage)) { throw new Error(`Not a transport message: ${JSON.stringify(transportMessage)}`); } return transportMessage; } } exports.ChunkedChannel = ChunkedChannel;