feat(framework): config 远程配置分层取参 getParam + 服务器解析 resolveServers
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
|||||||
|
import type { ChannelIdentity } from './identity.ts';
|
||||||
|
|
||||||
|
/** 远程配置内层数据(键名逐字一致原 get_paravalue / getConfig_Succ)。 */
|
||||||
|
export interface MarketEntry { marketid: string | number; [k: string]: unknown; }
|
||||||
|
export interface ChannelEntry { channelid: string | number; marketlist?: MarketEntry[]; [k: string]: unknown; }
|
||||||
|
export interface GameEntry { gameid: string | number; channellist?: ChannelEntry[]; [k: string]: unknown; }
|
||||||
|
export interface AgentEntry { agentid: string | number; gamelist?: GameEntry[]; [k: string]: unknown; }
|
||||||
|
export interface RemoteConfigData { agentlist?: AgentEntry[]; [k: string]: unknown; }
|
||||||
|
export interface RemoteConfig { data?: RemoteConfigData; }
|
||||||
|
|
||||||
|
/** 远程配置抓取抽象(生产注入 HttpConfigFetcher,测试注入 fake)。 */
|
||||||
|
export interface ConfigFetcher { fetch(url: string): Promise<RemoteConfig>; }
|
||||||
|
|
||||||
|
export class ConfigFetchError extends Error {}
|
||||||
|
export class ConfigParseError extends Error {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分层取参,忠实原 get_paravalue:顶层 data[key] 为默认,逐层进入匹配项;
|
||||||
|
* 某层有该 key(truthy)则更新返回值;匹配链中断则返回当前最具体值。
|
||||||
|
*/
|
||||||
|
export function getParam(config: RemoteConfig, key: string, id: ChannelIdentity): unknown {
|
||||||
|
const data = config.data;
|
||||||
|
if (!data) return null;
|
||||||
|
let value: unknown = data[key] ? data[key] : null;
|
||||||
|
|
||||||
|
const descend = <T extends Record<string, unknown>>(
|
||||||
|
list: T[] | undefined, idKey: string, idVal: unknown,
|
||||||
|
): T | null => {
|
||||||
|
if (!list) return null;
|
||||||
|
for (const item of list) {
|
||||||
|
if (item[idKey] == idVal) { // 宽松比较,id 可能 string/number
|
||||||
|
if (item[key]) value = item[key];
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const agent = descend(data.agentlist, 'agentid', id.agentid);
|
||||||
|
if (!agent) return value;
|
||||||
|
const game = descend(agent.gamelist, 'gameid', id.gameid);
|
||||||
|
if (!game) return value;
|
||||||
|
const channel = descend(game.channellist, 'channelid', id.channelid);
|
||||||
|
if (!channel) return value;
|
||||||
|
descend(channel.marketlist, 'marketid', id.marketid);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 连接相关参数键(逐字一致)。优先级即数组顺序。 */
|
||||||
|
const SERVER_KEYS = ['player_server_tcp', 'visitor_server_tcp', 'game_server_tcp'] as const;
|
||||||
|
|
||||||
|
function toWs(addr: string): string {
|
||||||
|
return (addr.startsWith('ws://') || addr.startsWith('wss://')) ? addr : 'ws://' + addr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析 ws 候选服务器列表(按 SERVER_KEYS 优先级,去重)。 */
|
||||||
|
export function resolveServers(config: RemoteConfig, id: ChannelIdentity): string[] {
|
||||||
|
const out: string[] = [];
|
||||||
|
for (const key of SERVER_KEYS) {
|
||||||
|
const v = getParam(config, key, id);
|
||||||
|
if (typeof v === 'string' && v) {
|
||||||
|
const ws = toWs(v);
|
||||||
|
if (!out.includes(ws)) out.push(ws);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { getParam, resolveServers, type RemoteConfig } from '../../YouleNexus/assets/framework/config/remote-config.ts';
|
||||||
|
import type { ChannelIdentity } from '../../YouleNexus/assets/framework/config/identity.ts';
|
||||||
|
|
||||||
|
const ID: ChannelIdentity = { agentid: 'A', channelid: 'C', gameid: 'G', marketid: 2, version: '1', versionCode: 1 };
|
||||||
|
|
||||||
|
const CONFIG: RemoteConfig = {
|
||||||
|
data: {
|
||||||
|
player_server_tcp: '1.1.1.1:1000', // 顶层默认
|
||||||
|
agentlist: [{
|
||||||
|
agentid: 'A',
|
||||||
|
player_server_tcp: '2.2.2.2:2000', // agent 级覆盖
|
||||||
|
gamelist: [{
|
||||||
|
gameid: 'G',
|
||||||
|
channellist: [{
|
||||||
|
channelid: 'C',
|
||||||
|
player_server_tcp: '3.3.3.3:3000', // channel 级覆盖(最具体)
|
||||||
|
visitor_server_tcp: '4.4.4.4:4000',
|
||||||
|
marketlist: [{ marketid: 2 }],
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
test('getParam:最具体层级胜出', () => {
|
||||||
|
assert.equal(getParam(CONFIG, 'player_server_tcp', ID), '3.3.3.3:3000');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getParam:channel 未匹配时回退到 agent 级值', () => {
|
||||||
|
const id2 = { ...ID, channelid: 'OTHER' };
|
||||||
|
assert.equal(getParam(CONFIG, 'player_server_tcp', id2), '2.2.2.2:2000');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getParam:agent 未匹配时回退到顶层默认', () => {
|
||||||
|
const id3 = { ...ID, agentid: 'NONE' };
|
||||||
|
assert.equal(getParam(CONFIG, 'player_server_tcp', id3), '1.1.1.1:1000');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getParam:键不存在 → null', () => {
|
||||||
|
assert.equal(getParam(CONFIG, 'not_a_key', ID), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getParam:无 data → null', () => {
|
||||||
|
assert.equal(getParam({}, 'player_server_tcp', ID), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolveServers:player 优先、visitor 回退,前缀 ws://', () => {
|
||||||
|
assert.deepEqual(resolveServers(CONFIG, ID), ['ws://3.3.3.3:3000', 'ws://4.4.4.4:4000']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolveServers:已带 ws:// 不重复前缀;无地址 → 空数组', () => {
|
||||||
|
const cfg: RemoteConfig = { data: { player_server_tcp: 'ws://9.9.9.9:9' } };
|
||||||
|
assert.deepEqual(resolveServers(cfg, ID), ['ws://9.9.9.9:9']);
|
||||||
|
assert.deepEqual(resolveServers({ data: {} }, ID), []);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user