界面迁移更新

This commit is contained in:
2026-09-05 00:12:11 +08:00
parent e2762d631d
commit 0aa66e1ec7
3187 changed files with 667915 additions and 211 deletions
@@ -0,0 +1,40 @@
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' } });
});
// 服务器协议为单层:frame 即服务器实发帧(传输层已取 MessageEvent.data)。
test('decodeFrame:握手包 @toconcon 识别为 handshake', () => {
assert.deepEqual(decodeFrame('@toconconXYZ...'), { kind: 'handshake' });
});
test('decodeFrame:心跳包 @serverheartbeat 识别为 heartbeat', () => {
const frame = JSON.stringify({ com: '@serverheartbeat' });
assert.deepEqual(decodeFrame(frame), { kind: 'heartbeat' });
});
test('decodeFrame:特殊错误包 webserve-服务器未工作', () => {
assert.deepEqual(decodeFrame('webserve-服务器未工作'), { kind: 'serverDown' });
});
test('decodeFrame:正常业务包(单层)解出 {route,rpc,data}', () => {
const inner = { route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } };
assert.deepEqual(decodeFrame(JSON.stringify(inner)), { kind: 'message', message: inner });
});
test('decodeFrame:带 app 字段的单层业务包(服务器实测格式)', () => {
const frame = JSON.stringify({ app: 'youle', route: 'agent', rpc: 'kick_server', data: { msg: '检查到新版本' } });
const r = decodeFrame(frame);
assert.equal(r.kind, 'message');
assert.equal(r.kind === 'message' && r.message.rpc, 'kick_server');
});
test('decodeFrame:无法解析的帧安全忽略', () => {
assert.deepEqual(decodeFrame('not json'), { kind: 'ignore' });
assert.deepEqual(decodeFrame(JSON.stringify({ foo: 1 })), { kind: 'ignore' });
});
@@ -0,0 +1,48 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { HeartbeatWatchdog, type Clock } from '../../YouleNexus/assets/framework/net/heartbeat.ts';
function fakeClock() {
let seq = 0;
const timers = new Map<number, { fn: () => void; at: number }>();
let now = 0;
const clock: Clock = {
setTimeout: (fn, ms) => { const id = ++seq; timers.set(id, { fn, at: now + ms }); return id; },
clearTimeout: (id: any) => { timers.delete(id); },
};
const advance = (ms: number) => {
now += ms;
for (const [id, t] of [...timers]) if (t.at <= now) { timers.delete(id); t.fn(); }
};
return { clock, advance };
}
test('超时未 feed 触发回调', () => {
const { clock, advance } = fakeClock();
let fired = 0;
const wd = new HeartbeatWatchdog(30000, () => { fired++; }, clock);
wd.feed();
advance(30000);
assert.equal(fired, 1);
});
test('feed 重置计时,未到阈值不触发', () => {
const { clock, advance } = fakeClock();
let fired = 0;
const wd = new HeartbeatWatchdog(30000, () => { fired++; }, clock);
wd.feed();
advance(20000);
wd.feed(); // 重置
advance(20000); // 距上次 feed 仅 20s
assert.equal(fired, 0);
});
test('stop 后不再触发', () => {
const { clock, advance } = fakeClock();
let fired = 0;
const wd = new HeartbeatWatchdog(30000, () => { fired++; }, clock);
wd.feed();
wd.stop();
advance(30000);
assert.equal(fired, 0);
});
@@ -0,0 +1,181 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { NetClient } from '../../YouleNexus/assets/framework/net/net-client.ts';
import { FakeTransport } from '../helpers/fake-transport.ts';
import type { Clock } from '../../YouleNexus/assets/framework/net/heartbeat.ts';
function fakeClock() {
let seq = 0, now = 0;
const timers = new Map<number, { fn: () => void; at: number }>();
const clock: Clock = {
setTimeout: (fn, ms) => { const id = ++seq; timers.set(id, { fn, at: now + ms }); return id; },
clearTimeout: (id: any) => { timers.delete(id); },
};
const advance = (ms: number) => { now += ms; for (const [id, t] of [...timers]) if (t.at <= now) { timers.delete(id); t.fn(); } };
return { clock, advance };
}
const IDENTITY = { agentid: 1, gameid: 2, openid: 'o', nickname: 'n', avatar: 'a', sex: 0, province: '', city: '', unionid: 'u', version: 1, channelid: 0, marketid: 0 };
function makeClient(transports: FakeTransport[], clock: Clock) {
let i = 0;
const client = new NetClient({
servers: 'ws://a',
transportFactory: () => transports[i++] ?? transports[transports.length - 1],
clock,
});
client.setIdentity(IDENTITY);
return client;
}
test('onOpen 后自动发 player_login(单层信封)', async () => {
const t = new FakeTransport();
const { clock } = fakeClock();
const client = makeClient([t], clock);
client.start();
await t.flush();
assert.equal(t.sent.length, 1);
const env = JSON.parse(t.sent[0]);
assert.equal(env.app, 'youle');
assert.equal(env.route, 'agent');
assert.equal(env.rpc, 'player_login');
assert.equal(env.data.openid, 'o');
});
test('isSendLoginState 门控:login 响应前的其它包被丢弃,login 响应放行并 emit login', async () => {
const t = new FakeTransport();
const { clock } = fakeClock();
const client = makeClient([t], clock);
const messages: any[] = [];
let loginResp: any = null;
client.on('message', (m: any) => messages.push(m));
client.on('login', (d: any) => { loginResp = d; });
client.start();
await t.flush();
// 登录响应前推一个业务包 → 应被门控丢弃
t.serverPush(JSON.stringify({ route: 'agent', rpc: 'update_bean', data: { bean: 5 } }));
await t.flush();
assert.equal(messages.length, 0);
// 推 login 响应 → 放行,emit login,清门控
t.serverPush(JSON.stringify({ route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } }));
await t.flush();
assert.equal(loginResp.playerid, 9);
// 门控已清,后续业务包放行
t.serverPush(JSON.stringify({ route: 'agent', rpc: 'update_bean', data: { bean: 5 } }));
await t.flush();
assert.equal(messages.length, 1);
assert.equal(messages[0].rpc, 'update_bean');
});
test('握手包与心跳包被忽略且不进 message', async () => {
const t = new FakeTransport();
const { clock } = fakeClock();
const client = makeClient([t], clock);
const messages: any[] = [];
client.on('message', (m: any) => messages.push(m));
client.start();
await t.flush();
// 先完成登录以清门控
t.serverPush(JSON.stringify({ route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } }));
await t.flush();
t.serverPush('@toconconABC'); // 握手(原始串)
t.serverPush(JSON.stringify({ com: '@serverheartbeat' })); // 心跳(单层)
await t.flush();
assert.equal(messages.length, 0);
});
test('收包超时 emit slow 并触发重连(onClose→新连接重发 login)', async () => {
const t1 = new FakeTransport(), t2 = new FakeTransport();
const { clock, advance } = fakeClock();
const client = makeClient([t1, t2], clock);
let slow = 0;
client.on('slow', () => { slow++; });
client.start();
await t1.flush();
// 先完成登录:清 4s 守护、feed 收包看门狗(否则 advance 会先撞上 loginGuard 而非 watchdog)
t1.serverPush(JSON.stringify({ route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } }));
await t1.flush();
// 30s 无收包 → watchdog → slow + 关闭当前连接
advance(30000);
await t1.flush();
assert.equal(slow, 1);
// 重连定时器到点 → 新连接 t2 建立并重发 login
advance(10000);
await t2.flush();
assert.equal(t2.sent.length, 1);
assert.equal(JSON.parse(t2.sent[0]).rpc, 'player_login');
});
test('TcpID 去重:旧连接的残留包被丢弃', async () => {
const t1 = new FakeTransport(), t2 = new FakeTransport();
const { clock, advance } = fakeClock();
const client = makeClient([t1, t2], clock);
const messages: any[] = [];
client.on('message', (m: any) => messages.push(m));
client.start();
await t1.flush();
t1.serverPush(JSON.stringify({ route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } }));
await t1.flush();
// 触发重连切到 t2
advance(30000); await t1.flush(); advance(10000); await t2.flush();
// 旧连接 t1 仍推包 → 应被 TcpID 去重丢弃
t1.serverPush(JSON.stringify({ route: 'agent', rpc: 'update_bean', data: { bean: 1 } }));
await t1.flush();
assert.equal(messages.length, 0);
});
test('kick_server:emit kicked 且不再自动重连', async () => {
const t1 = new FakeTransport(), t2 = new FakeTransport();
const { clock, advance } = fakeClock();
const client = makeClient([t1, t2], clock);
let kicked = 0;
client.on('kicked', () => { kicked++; });
client.start();
await t1.flush();
// 服务器踢人(登录态期间)
t1.serverPush(JSON.stringify({ route: 'agent', rpc: 'kick_server', data: { msg: 'x' } }));
await t1.flush();
assert.equal(kicked, 1);
// 推进重连/超时定时器 → 不应建立新连接或重发 login
advance(30000); advance(10000);
await t2.flush();
assert.equal(t2.sent.length, 0);
});
test('debug=true 记录 send 与 recv(注入 logger)', async () => {
const t = new FakeTransport();
const { clock } = fakeClock();
const logs: unknown[][] = [];
const client = new NetClient({
servers: 'ws://a',
transportFactory: () => t,
clock,
debug: true,
logger: (...a: unknown[]) => logs.push(a),
});
client.setIdentity(IDENTITY);
client.start();
await t.flush();
assert.ok(logs.some((l) => l[0] === '[net] →')); // 发出 player_login 被记录
t.serverPush(JSON.stringify({ route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } }));
await t.flush();
assert.ok(logs.some((l) => l[0] === '[net] ←')); // 收包被记录
});
test('debug 默认关闭:logger 不被调用', async () => {
const t = new FakeTransport();
const { clock } = fakeClock();
const logs: unknown[][] = [];
const client = new NetClient({
servers: 'ws://a',
transportFactory: () => t,
clock,
logger: (...a: unknown[]) => logs.push(a),
});
client.setIdentity(IDENTITY);
client.start();
await t.flush();
t.serverPush(JSON.stringify({ route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } }));
await t.flush();
assert.equal(logs.length, 0);
});
@@ -0,0 +1,32 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { ReconnectPolicy } from '../../YouleNexus/assets/framework/net/reconnect.ts';
test('单服务器:失败永不轮换', () => {
const p = new ReconnectPolicy('ws://a', 3);
assert.equal(p.current(), 'ws://a');
for (let i = 0; i < 10; i++) assert.equal(p.onFailure(), 'ws://a');
});
test('多服务器:每 3 次失败轮换下一个', () => {
const p = new ReconnectPolicy(['ws://a', 'ws://b', 'ws://c'], 3);
assert.equal(p.onFailure(), 'ws://a'); // 1
assert.equal(p.onFailure(), 'ws://a'); // 2
assert.equal(p.onFailure(), 'ws://b'); // 3 → 轮换
assert.equal(p.onFailure(), 'ws://b'); // 4
assert.equal(p.onFailure(), 'ws://b'); // 5
assert.equal(p.onFailure(), 'ws://c'); // 6 → 轮换
});
test('reset 清零失败计数', () => {
const p = new ReconnectPolicy(['ws://a', 'ws://b'], 3);
p.onFailure(); p.onFailure();
p.reset();
assert.equal(p.onFailure(), 'ws://a'); // 计数从 1 起,未到 3,不轮换
});
test('setCurrent 切换到指定服务器(服务器切换指令)', () => {
const p = new ReconnectPolicy(['ws://a', 'ws://b'], 3);
p.setCurrent('ws://room1');
assert.equal(p.current(), 'ws://room1');
});
@@ -0,0 +1,36 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { FakeTransport } from '../helpers/fake-transport.ts';
test('FakeTransport:connect 后异步触发 onOpen', async () => {
const t = new FakeTransport();
let opened = false;
t.onOpen(() => { opened = true; });
t.connect('ws://x');
await t.flush();
assert.equal(opened, true);
});
test('FakeTransport:客户端 send 进入 sent 队列;serverPush 触发 onMessage', async () => {
const t = new FakeTransport();
const got: string[] = [];
t.onMessage((f) => got.push(f));
t.connect('ws://x');
await t.flush();
t.send('hello');
assert.deepEqual(t.sent, ['hello']);
t.serverPush('world');
await t.flush();
assert.deepEqual(got, ['world']);
});
test('FakeTransport:close 触发 onClose', async () => {
const t = new FakeTransport();
let closed = false;
t.onClose(() => { closed = true; });
t.connect('ws://x');
await t.flush();
t.close();
await t.flush();
assert.equal(closed, true);
});