@litert/televoke
Version:
A simple RPC service framework.
450 lines • 15.5 kB
JavaScript
"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