@litert/televoke
Version:
A simple RPC service framework.
425 lines (300 loc) • 11.3 kB
text/typescript
/**
* 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.
*/
import * as CSv2 from './Constants.v2';
import * as CS from '../../Constants';
import { ProtocolError, TelevokeError, errors } from '../../Errors';
import type * as dEnc2 from './Encoding.v2.decl';
interface IDecodeContext {
chunks: Buffer[];
chIndex: number;
chOffset: number;
command: CSv2.ECommand;
type: CSv2.EPacketType;
seq: number;
}
const B8 = Buffer.allocUnsafe(8);
const B4 = Buffer.allocUnsafe(4);
const B2 = Buffer.allocUnsafe(2);
interface IPacketDecoder {
decode(ctx: IDecodeContext): dEnc2.ICommandPacket;
}
function readSmallBytes(bytes: number, buf: Buffer, ctx: IDecodeContext): Buffer {
let readBytes = 0;
while (readBytes < bytes) {
if (ctx.chIndex === ctx.chunks.length) {
throw new errors.incomplete_packet();
}
const c = ctx.chunks[ctx.chIndex];
const cLen = c.byteLength;
let cOff = ctx.chOffset;
do {
buf[readBytes++] = c[cOff++];
}
while (cOff < cLen && readBytes < bytes);
if (cOff === cLen) {
ctx.chIndex++;
ctx.chOffset = 0;
}
else {
ctx.chOffset = cOff;
}
if (readBytes === bytes) {
break;
}
}
return buf;
}
function readLargeBytes(bytes: number, ctx: IDecodeContext): Buffer[] {
let readBytes = 0;
const ret: Buffer[] = [];
while (readBytes < bytes) {
if (ctx.chIndex === ctx.chunks.length) {
throw new errors.incomplete_packet();
}
const c = ctx.chunks[ctx.chIndex];
const bytes2Read = Math.min(bytes - readBytes, c.byteLength - ctx.chOffset);
ret.push(c.subarray(ctx.chOffset, ctx.chOffset + bytes2Read));
readBytes += bytes2Read;
ctx.chOffset += bytes2Read;
if (ctx.chOffset === c.byteLength) {
ctx.chIndex++;
ctx.chOffset = 0;
}
if (readBytes === bytes) {
break;
}
}
return ret;
}
function readString(bytes: number, ctx: IDecodeContext): string {
const td = new TextDecoder('utf8');
let readBytes = 0;
let ret: string = '';
while (readBytes < bytes) {
if (ctx.chIndex === ctx.chunks.length) {
throw new errors.incomplete_packet();
}
const c = ctx.chunks[ctx.chIndex];
const bytes2Read = Math.min(bytes - readBytes, c.byteLength - ctx.chOffset);
ret += td.decode(c.subarray(ctx.chOffset, ctx.chOffset + bytes2Read), { stream: true });
readBytes += bytes2Read;
ctx.chOffset += bytes2Read;
if (ctx.chOffset === c.byteLength) {
ctx.chIndex++;
ctx.chOffset = 0;
}
if (readBytes === bytes) {
break;
}
}
return ret;
}
function readVarString(ctx: IDecodeContext): string {
const len = readSmallBytes(2, B2, ctx).readUInt16LE(0);
return readString(len, ctx);
}
function readVarBuffer(ctx: IDecodeContext): Buffer[] {
const len = readSmallBytes(4, B4, ctx).readUInt32LE(0);
return readLargeBytes(len, ctx);
}
function readVarBuffer16(ctx: IDecodeContext): Buffer[] {
const len = readSmallBytes(2, B4, ctx).readUInt16LE(0);
return readLargeBytes(len, ctx);
}
class TvErrorResponseDecoder implements IPacketDecoder {
public decode(ctx: IDecodeContext): dEnc2.ICommandPacket {
const len = readSmallBytes(2, B2, ctx).readUInt16LE(0);
const errorMsg = readString(len, ctx);
let err: TelevokeError;
if (errorMsg.startsWith(CS.PROTOCOL_ERROR_NAMESPACE)) {
const errMsg = errorMsg.slice(CS.PROTOCOL_ERROR_NAMESPACE.length + 1);
if (errMsg in errors) {
err = new (errors as any)[errMsg]();
}
else {
err = new ProtocolError(errMsg, null, null);
}
}
else {
err = new errors.app_error(errorMsg.slice(CS.APP_ERROR_NAMESPACE.length + 1), null);
}
return {
'cmd': ctx.command,
'typ': CSv2.EPacketType.ERROR_RESPONSE,
'seq': ctx.seq,
'ct': err,
} satisfies dEnc2.IErrorResponsePacket;
}
}
class TvApiRequestDecoder implements IPacketDecoder {
public decode(ctx: IDecodeContext): dEnc2.ICommandPacket {
const name = readVarString(ctx);
return {
'cmd': CSv2.ECommand.API_CALL,
'typ': CSv2.EPacketType.REQUEST,
'seq': ctx.seq,
'ct': {
'name': name,
'body': readVarBuffer(ctx),
}
} satisfies dEnc2.IApiRequestPacket;
}
}
class TvPingRequestDecoder implements IPacketDecoder {
public decode(ctx: IDecodeContext): dEnc2.ICommandPacket {
return {
'cmd': CSv2.ECommand.PING,
'typ': CSv2.EPacketType.REQUEST,
'seq': ctx.seq,
'ct': readVarBuffer16(ctx)
} satisfies dEnc2.IPingRequestPacket;
}
}
class TvPushMessageRequestDecoder implements IPacketDecoder {
public decode(ctx: IDecodeContext): dEnc2.ICommandPacket {
return {
'cmd': CSv2.ECommand.PUSH_MESSAGE,
'typ': CSv2.EPacketType.REQUEST,
'seq': ctx.seq,
'ct': readVarBuffer(ctx)
} satisfies dEnc2.IPushMessageRequestPacket;
}
}
class TvBinaryChunkRequestDecoder implements IPacketDecoder {
public decode(ctx: IDecodeContext): dEnc2.ICommandPacket {
const streamId = readSmallBytes(4, B4, ctx).readUInt32LE(0);
const chunkIndex = readSmallBytes(4, B4, ctx).readUInt32LE(0);
return {
'cmd': CSv2.ECommand.BINARY_CHUNK,
'typ': CSv2.EPacketType.REQUEST,
'seq': ctx.seq,
'ct': {
'streamId': streamId,
'index': chunkIndex,
'body': readVarBuffer(ctx),
}
} satisfies dEnc2.IBinaryChunkRequestPacket;
}
}
class TvCloseRequestDecoder implements IPacketDecoder {
public decode(ctx: IDecodeContext): dEnc2.ICommandPacket {
return {
'cmd': CSv2.ECommand.CLOSE,
'typ': CSv2.EPacketType.REQUEST,
'seq': ctx.seq,
'ct': null
} satisfies dEnc2.ICloseRequestPacket;
}
}
class TvVoidResponseDecoder implements IPacketDecoder {
public decode(ctx: IDecodeContext): dEnc2.ICommandPacket {
return {
'cmd': ctx.command,
'typ': CSv2.EPacketType.SUCCESS_RESPONSE,
'seq': ctx.seq,
'ct': null
} satisfies dEnc2.ISuccessResponsePacket;
}
}
class TvApiResponseDecoder implements IPacketDecoder {
public decode(ctx: IDecodeContext): dEnc2.ICommandPacket {
return {
'cmd': CSv2.ECommand.API_CALL,
'typ': CSv2.EPacketType.SUCCESS_RESPONSE,
'seq': ctx.seq,
'ct': readVarBuffer(ctx)
} satisfies dEnc2.IApiResponsePacket;
}
}
class TvPingResponseDecoder implements IPacketDecoder {
public decode(ctx: IDecodeContext): dEnc2.ICommandPacket {
return {
'cmd': CSv2.ECommand.PING,
'typ': CSv2.EPacketType.SUCCESS_RESPONSE,
'seq': ctx.seq,
'ct': readVarBuffer16(ctx)
} satisfies dEnc2.IPingResponsePacket;
}
}
const packetDecoders: Record<number, IPacketDecoder> = {
[CSv2.EPacketType.ERROR_RESPONSE + 0x100 * CSv2.ECommand.API_CALL]: new TvErrorResponseDecoder(),
[CSv2.EPacketType.ERROR_RESPONSE + 0x100 * CSv2.ECommand.PING]: new TvErrorResponseDecoder(),
[CSv2.EPacketType.ERROR_RESPONSE + 0x100 * CSv2.ECommand.BINARY_CHUNK]: new TvErrorResponseDecoder(),
[CSv2.EPacketType.ERROR_RESPONSE + 0x100 * CSv2.ECommand.CLOSE]: new TvErrorResponseDecoder(),
[CSv2.EPacketType.ERROR_RESPONSE + 0x100 * CSv2.ECommand.PUSH_MESSAGE]: new TvErrorResponseDecoder(),
[CSv2.EPacketType.REQUEST + 0x100 * CSv2.ECommand.API_CALL]: new TvApiRequestDecoder(),
[CSv2.EPacketType.REQUEST + 0x100 * CSv2.ECommand.PING]: new TvPingRequestDecoder(),
[CSv2.EPacketType.REQUEST + 0x100 * CSv2.ECommand.PUSH_MESSAGE]: new TvPushMessageRequestDecoder(),
[CSv2.EPacketType.REQUEST + 0x100 * CSv2.ECommand.CLOSE]: new TvCloseRequestDecoder(),
[CSv2.EPacketType.REQUEST + 0x100 * CSv2.ECommand.BINARY_CHUNK]: new TvBinaryChunkRequestDecoder(),
[CSv2.EPacketType.SUCCESS_RESPONSE + 0x100 * CSv2.ECommand.PUSH_MESSAGE]: new TvVoidResponseDecoder(),
[CSv2.EPacketType.SUCCESS_RESPONSE + 0x100 * CSv2.ECommand.CLOSE]: new TvVoidResponseDecoder(),
[CSv2.EPacketType.SUCCESS_RESPONSE + 0x100 * CSv2.ECommand.BINARY_CHUNK]: new TvVoidResponseDecoder(),
[CSv2.EPacketType.SUCCESS_RESPONSE + 0x100 * CSv2.ECommand.API_CALL]: new TvApiResponseDecoder(),
[CSv2.EPacketType.SUCCESS_RESPONSE + 0x100 * CSv2.ECommand.PING]: new TvPingResponseDecoder(),
};
export class TvDecoderV2 {
/**
* Pass all chunks of a whole packet to this method, and it will return an array of decoded results.
*
* @throws {TelevokeError}
*/
public decode(packetChunks: Buffer[]): dEnc2.ICommandPacket {
const ctx = this._decodeHeader(packetChunks);
try {
return packetDecoders[ctx.command * 0x100 + ctx.type].decode(ctx);
}
catch (e) {
if (e instanceof TelevokeError) {
throw e;
}
throw new errors.invalid_packet(null, e);
}
}
private _decodeHeader(chunks: Buffer[]): IDecodeContext {
try {
const ctx: IDecodeContext = {
chunks,
chIndex: 0,
chOffset: 0,
command: 0,
type: 0,
seq: 0,
};
readSmallBytes(8, B8, ctx);
ctx.command = B8[0];
ctx.type = B8[1];
if (undefined === CSv2.EPacketType[ctx.type]) {
throw new errors.invalid_packet({
unknownPacketType: ctx.type,
});
}
if (undefined === CSv2.ECommand[ctx.command]) {
throw new errors.invalid_packet({
unknownCommand: ctx.command,
});
}
ctx.seq = B8.readUInt16LE(2) * 0x1_0000_0000 + B8.readUInt32LE(4);
return ctx;
}
catch (e) {
if (e instanceof TelevokeError) {
throw e;
}
throw new errors.invalid_packet(null, e);
}
}
}