feat(framework): net 信封编解码 + 握手/心跳/错误包识别
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,43 @@
|
|||||||
|
import { APP } from '../core/constants.ts';
|
||||||
|
import type { InboundMessage } from '../core/types/envelope.ts';
|
||||||
|
|
||||||
|
/** 组装单层出站信封字符串。docs/protocol/01 §3.1。 */
|
||||||
|
export function encodeOutbound(route: string, rpc: string, data: unknown): string {
|
||||||
|
return JSON.stringify({ app: APP, route, rpc, data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DecodeResult =
|
||||||
|
| { kind: 'handshake' } // @toconcon,忽略
|
||||||
|
| { kind: 'heartbeat' } // @serverheartbeat,忽略不回包
|
||||||
|
| { kind: 'serverDown' } // webserve-服务器未工作
|
||||||
|
| { kind: 'message'; message: InboundMessage } // 正常业务内层
|
||||||
|
| { kind: 'ignore' }; // 无法解析,安全忽略
|
||||||
|
|
||||||
|
function parseMaybe(v: unknown): unknown {
|
||||||
|
return typeof v === 'string' ? JSON.parse(v) : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解一帧原始字符串为分类结果。docs/protocol/01 §3.2 步骤 3–8。
|
||||||
|
* 不做 TcpID/isSendLoginState/submit_error 过滤(那是连接级,见 net-client)。
|
||||||
|
*/
|
||||||
|
export function decodeFrame(frame: string): DecodeResult {
|
||||||
|
let outer: any;
|
||||||
|
try { outer = JSON.parse(frame); } catch { return { kind: 'ignore' }; }
|
||||||
|
let data = outer?.data;
|
||||||
|
|
||||||
|
if (data === 'webserve-服务器未工作') return { kind: 'serverDown' };
|
||||||
|
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
if (data.substr(0, 9) === '@toconcon') return { kind: 'handshake' };
|
||||||
|
try { data = JSON.parse(data); } catch { return { kind: 'ignore' }; }
|
||||||
|
}
|
||||||
|
if (data && typeof data === 'object' && (data as any).com === '@serverheartbeat') {
|
||||||
|
return { kind: 'heartbeat' };
|
||||||
|
}
|
||||||
|
const inner = parseMaybe(data) as InboundMessage | undefined;
|
||||||
|
if (inner && typeof inner === 'object' && 'route' in inner && 'rpc' in inner) {
|
||||||
|
return { kind: 'message', message: inner };
|
||||||
|
}
|
||||||
|
return { kind: 'ignore' };
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { encodeOutbound, decodeFrame } from '../../YouleNexus/assets/framework/net/envelope-codec.ts';
|
||||||
|
|
||||||
|
test('encodeOutbound 产出单层信封字符串', () => {
|
||||||
|
const s = encodeOutbound('agent', 'player_login', { openid: 'o' });
|
||||||
|
assert.deepEqual(JSON.parse(s), { app: 'youle', route: 'agent', rpc: 'player_login', data: { openid: 'o' } });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('decodeFrame:握手包 @toconcon 识别为 handshake', () => {
|
||||||
|
const frame = JSON.stringify({ data: '@toconconXYZ...' });
|
||||||
|
assert.deepEqual(decodeFrame(frame), { kind: 'handshake' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('decodeFrame:心跳包 @serverheartbeat 识别为 heartbeat', () => {
|
||||||
|
const frame = JSON.stringify({ data: JSON.stringify({ com: '@serverheartbeat' }) });
|
||||||
|
assert.deepEqual(decodeFrame(frame), { kind: 'heartbeat' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('decodeFrame:特殊错误包 webserve-服务器未工作', () => {
|
||||||
|
const frame = JSON.stringify({ data: 'webserve-服务器未工作' });
|
||||||
|
assert.deepEqual(decodeFrame(frame), { kind: 'serverDown' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('decodeFrame:正常业务包解出内层 {route,rpc,data}', () => {
|
||||||
|
const inner = { route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } };
|
||||||
|
const frame = JSON.stringify({ data: JSON.stringify(inner) });
|
||||||
|
assert.deepEqual(decodeFrame(frame), { kind: 'message', message: inner });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('decodeFrame:内层为对象(非字符串)也能解', () => {
|
||||||
|
const inner = { route: 'room', rpc: 'other_join_room', data: { seat: 1 } };
|
||||||
|
const frame = JSON.stringify({ data: inner });
|
||||||
|
assert.deepEqual(decodeFrame(frame), { kind: 'message', message: inner });
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user