UNPKG

@litert/televoke

Version:
450 lines 15.5 kB
"use strict"; /** * Copyright 2025 Angus.Fenying <fenying@litert.org> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.AbstractTvChannelV2 = exports.encoder = exports.decoder = void 0; const E = require("./Errors"); const node_events_1 = require("node:events"); const v2 = require("./Encodings/v2"); const Utils_1 = require("./Utils"); const DEFAULT_PING_MESSAGE = Buffer.from('PING'); exports.decoder = new v2.TvDecoderV2(); exports.encoder = new v2.TvEncoderV2(); var EState; (function (EState) { EState[EState["ACTIVE"] = 0] = "ACTIVE"; EState[EState["ENDING"] = 1] = "ENDING"; EState[EState["ENDED"] = 2] = "ENDED"; })(EState || (EState = {})); class AbstractTvChannelV2 extends node_events_1.EventEmitter { get isMessageSupported() { return true; } get isBinaryStreamSupported() { return this.streams.maxStreams !== 0; } get finished() { return this._state === EState.ENDED; } get writable() { return this._state === EState.ACTIVE && this.transporter.writable; } constructor(id, transporter, timeout, streamManagerFactory) { super(); this.id = id; this.transporter = transporter; this.timeout = timeout; this._seqCounter = 0; this.context = {}; /** * The context of requests sent out, waiting for replies. */ this._sentRequests = {}; /** * The qty of received and not responded requests, waiting for replies. */ this._recvRequests = 0; this._state = EState.ACTIVE; this.ended = false; this._onData = (frameChunks) => { try { const packet = exports.decoder.decode(frameChunks); switch (packet.typ) { case v2.EPacketType.REQUEST: this._onRequest(packet); break; case v2.EPacketType.ERROR_RESPONSE: case v2.EPacketType.SUCCESS_RESPONSE: this._onResponse(packet); break; default: this.emit('error', new E.errors.invalid_packet({ reason: 'invalid_packet_type', packet, })); } } catch (e) { this._end(); this.emit('error', e); } }; this._onConnError = (e) => { this.emit('error', e); }; this._onConnClose = () => { for (const k of Object.keys(this._sentRequests)) { const req = this._sentRequests[k]; delete this._sentRequests[k]; if (req.timer) { clearTimeout(req.timer); } try { req.callback({ 'cmd': req.cmd, 'typ': v2.EPacketType.ERROR_RESPONSE, 'seq': req.seq, 'ct': new E.errors.channel_closed(), }); } catch (e) { this.emit('warning', e); } } this.ended = true; this._state = EState.ENDED; this.emit('close'); }; this._onRemoteEnded = () => { if (this.ended) { return; } this.ended = true; this._state = EState.ENDING; this.emit('end'); }; this._onLocalEnded = () => { if (this._state === EState.ENDED) { return; } this._state = EState.ENDED; this.emit('finish'); }; this.streams = streamManagerFactory(this); this._setup(); } _setup() { this.transporter .on('frame', this._onData) .on('error', this._onConnError) .on('close', this._onConnClose) .on('end', this._onRemoteEnded) .on('finish', this._onLocalEnded); } _end() { // for (const id of Object.keys(this._streams)) { // this._streams[id].abort(); // delete this._streams[id]; // } if (this._isIdle()) { this._state = EState.ENDED; this.transporter.end(); } else { this._state = EState.ENDING; } } _tryClean() { if (this._state !== EState.ENDING) { return; } if (this._isIdle()) { this._state = EState.ENDED; this.transporter.end(); } } _isIdle() { return Object.keys(this._sentRequests).length === 0 && this._recvRequests === 0; } _onResponse(packet) { const request = this._sentRequests[packet.seq]; if (!request) { // Timeout response, drop it. return; } delete this._sentRequests[request.seq]; if (request.timer !== undefined) { clearTimeout(request.timer); } if (request.cmd !== packet.cmd) { request.callback({ typ: v2.EPacketType.ERROR_RESPONSE, seq: request.seq, cmd: request.cmd, ct: new E.errors.invalid_response({ reason: 'mismatched_command', packet, }) }); this._end(); return; } request.callback(packet); } _reply(packet) { try { this.transporter.write(exports.encoder.encode(packet)); } catch (e) { // ignore errors here. this.emit('warning', e); } } _onRequest(packet) { if (this._state !== EState.ACTIVE) { this._reply({ 'cmd': packet.cmd, 'typ': v2.EPacketType.ERROR_RESPONSE, 'seq': packet.seq, 'ct': new E.errors.channel_inactive() }); return; } switch (packet.cmd) { default: this._reply({ 'cmd': packet.cmd, 'typ': v2.EPacketType.ERROR_RESPONSE, 'seq': packet.seq, 'ct': new E.errors.invalid_packet({ reason: 'unknown_command', packet, }) }); return; case v2.ECommand.CLOSE: this.on('finish', () => { this._reply({ 'cmd': v2.ECommand.CLOSE, 'typ': v2.EPacketType.SUCCESS_RESPONSE, 'seq': packet.seq, 'ct': new E.errors.invalid_packet({ reason: 'unknown_command', packet, }) }); }); this._end(); return; case v2.ECommand.PING: this._reply({ 'cmd': v2.ECommand.PING, 'typ': v2.EPacketType.SUCCESS_RESPONSE, 'seq': packet.seq, 'ct': packet.ct, }); this.emit('ping'); return; case v2.ECommand.PUSH_MESSAGE: if (!this.listenerCount('push_message')) { this._reply({ 'cmd': v2.ECommand.PUSH_MESSAGE, 'typ': v2.EPacketType.ERROR_RESPONSE, 'seq': packet.seq, 'ct': new E.errors.cmd_not_impl() }); break; } this._reply({ 'cmd': v2.ECommand.PUSH_MESSAGE, 'typ': v2.EPacketType.SUCCESS_RESPONSE, 'seq': packet.seq, 'ct': null, }); this.emit('push_message', packet.ct, packet.seq); break; case v2.ECommand.API_CALL: if (!this.listenerCount('api_call')) { this._reply({ 'cmd': v2.ECommand.API_CALL, 'typ': v2.EPacketType.ERROR_RESPONSE, 'seq': packet.seq, 'ct': new E.errors.cmd_not_impl() }); break; } this._recvRequests++; this.emit('api_call', (0, Utils_1.once)((response) => { this._recvRequests--; this._reply({ 'cmd': v2.ECommand.API_CALL, 'typ': response instanceof E.TelevokeError ? v2.EPacketType.ERROR_RESPONSE : v2.EPacketType.SUCCESS_RESPONSE, 'seq': packet.seq, 'ct': response, }); this._tryClean(); }), packet.ct.name, packet.ct.body, packet.seq); break; case v2.ECommand.BINARY_CHUNK: { const stream = this.streams.get(packet.ct.streamId); if (!stream) { this._reply({ 'cmd': v2.ECommand.BINARY_CHUNK, 'typ': v2.EPacketType.ERROR_RESPONSE, 'seq': packet.seq, 'ct': new E.errors.stream_not_found({ 'sid': packet.ct.streamId, 'chId': this.id, }), }); break; } const chunkSegments = packet.ct.body; const chunkIndex = packet.ct.index; if (chunkIndex === 0xFFFFFFFF) { stream.abort(); } else if (chunkIndex !== stream.nextIndex) { this._reply({ 'cmd': v2.ECommand.BINARY_CHUNK, 'typ': v2.EPacketType.ERROR_RESPONSE, 'seq': packet.seq, 'ct': new E.errors.stream_index_mismatch({ 'sid': packet.ct.streamId, 'chId': this.id, }), }); break; } else if (!chunkSegments[0]?.byteLength) { stream.close(); } else { stream.append(chunkSegments); } this._reply({ 'cmd': v2.ECommand.BINARY_CHUNK, 'typ': v2.EPacketType.SUCCESS_RESPONSE, 'seq': packet.seq, 'ct': null, }); break; } } } _setTimeout(cmd, seq, callback) { const req = this._sentRequests[seq] = { 'cmd': cmd, 'seq': seq, 'callback': callback, }; if (this.timeout < 1 || !req) { return; } req.timer = setTimeout(() => { if (!req.timer) { return; } delete this._sentRequests[req.seq]; req.callback({ 'cmd': req.cmd, 'typ': v2.EPacketType.ERROR_RESPONSE, 'seq': req.seq, 'ct': new E.errors.timeout(), }); }, this.timeout); } openBinaryStream() { if (!this.writable || !this.streams) { throw new E.errors.channel_inactive(); } return this.streams.create(); } ping(message) { if (!this.writable) { return Promise.reject(new E.errors.channel_inactive()); } message ?? (message = DEFAULT_PING_MESSAGE); const seq = this._seqCounter++; this.transporter.write(exports.encoder.encode({ 'cmd': v2.ECommand.PING, 'typ': v2.EPacketType.REQUEST, 'seq': seq, 'ct': message, })); return new Promise((resolve, reject) => { this._setTimeout(v2.ECommand.PING, seq, (p) => { if (p.typ === v2.EPacketType.SUCCESS_RESPONSE) { if (Array.isArray(p.ct)) { resolve(Buffer.concat(p.ct)); } else { resolve(p.ct); } } else { reject(p.ct); } }); }); } sendBinaryChunk(streamId, index, chunk) { if (!this.writable) { return Promise.reject(new E.errors.channel_inactive()); } const seq = this._seqCounter++; this.transporter.write(exports.encoder.encode({ 'cmd': v2.ECommand.BINARY_CHUNK, 'typ': v2.EPacketType.REQUEST, 'seq': seq, 'ct': { streamId, 'index': index === false ? 0xFFFFFFFF : index, 'body': chunk ?? [], } })); return new Promise((resolve, reject) => { this._setTimeout(v2.ECommand.BINARY_CHUNK, seq, (p) => { if (p.typ === v2.EPacketType.SUCCESS_RESPONSE) { resolve(); } else { reject(p.ct); } }); }); } sendMessage(message) { if (!this.writable) { return Promise.reject(new E.errors.channel_inactive()); } const seq = this._seqCounter++; this.transporter.write(exports.encoder.encode({ 'cmd': v2.ECommand.PUSH_MESSAGE, 'typ': v2.EPacketType.REQUEST, 'seq': seq, 'ct': message, })); return new Promise((resolve, reject) => { this._setTimeout(v2.ECommand.PUSH_MESSAGE, seq, (p) => { if (p.typ === v2.EPacketType.SUCCESS_RESPONSE) { resolve(); } else { reject(p.ct); } }); }); } close() { if (!this.writable) { return; } const seq = this._seqCounter++; try { this.transporter.write(exports.encoder.encode({ 'cmd': v2.ECommand.CLOSE, 'typ': v2.EPacketType.REQUEST, 'seq': seq, 'ct': null, })); } catch { // ignore errors here. } this._end(); } } exports.AbstractTvChannelV2 = AbstractTvChannelV2; //# sourceMappingURL=Channel.impl.js.map