fix(config): resolve remote hierarchy in memory for every profile
This commit is contained in:
@@ -89,9 +89,10 @@ export function assertLocalStartupContext(context: LocalStartupContext): void {
|
||||
const { config } = context;
|
||||
if (config.mode !== 'debug') fail('mode', 'must be debug');
|
||||
if (config.isDebugger !== true) fail('isDebugger', 'must be true');
|
||||
if (config.source !== 'direct') fail('source', 'must be direct');
|
||||
if (config.gameserver !== null) fail('gameserver', 'must be null');
|
||||
if (config.rawConfig !== null) fail('rawConfig', 'must be null');
|
||||
if (config.source !== 'remote') fail('source', 'must be remote');
|
||||
if (config.gameserver !== PROFILES.local!.gameserver) fail('gameserver', 'must equal local profile remote entry');
|
||||
if (!config.remoteConfig || typeof config.remoteConfig.getValue !== 'function') fail('remoteConfig', 'must provide memory view');
|
||||
if (!config.rawConfig || config.rawConfig !== config.remoteConfig.raw) fail('rawConfig', 'must share remoteConfig.raw');
|
||||
|
||||
try {
|
||||
resolveIdentity([() => config.identity]);
|
||||
|
||||
@@ -9,12 +9,12 @@ export interface DebugAccount {
|
||||
unionid?: string | number;
|
||||
}
|
||||
|
||||
export type ProfileSource = 'remote' | 'direct';
|
||||
export type ProfileSource = 'remote';
|
||||
|
||||
export interface DebugProfile {
|
||||
readonly name: string;
|
||||
readonly source: ProfileSource;
|
||||
readonly gameserver?: string;
|
||||
readonly gameserver: string;
|
||||
readonly servers?: readonly string[];
|
||||
readonly account?: DebugAccount;
|
||||
}
|
||||
@@ -27,7 +27,8 @@ export const PROFILES: Readonly<Record<string, DebugProfile>> = Object.freeze({
|
||||
staging: Object.freeze({ name: 'staging', source: 'remote', gameserver: DEFAULT_GAMESERVER }),
|
||||
local: Object.freeze({
|
||||
name: 'local',
|
||||
source: 'direct',
|
||||
source: 'remote',
|
||||
gameserver: DEFAULT_GAMESERVER,
|
||||
servers: Object.freeze(['ws://127.0.0.1:3088']),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
/** 原配置服务的唯一服务器字段,逐字对齐 ServerUrl_Succ 的 data.urlserver。 */
|
||||
import type { ChannelIdentity } from './identity.ts';
|
||||
|
||||
/** One in-memory view over the JSON root received by legacy getConfig_Succ. */
|
||||
export interface RemoteConfig {
|
||||
readonly data: {
|
||||
readonly urlserver: string | readonly string[];
|
||||
readonly [key: string]: unknown;
|
||||
};
|
||||
readonly raw: Readonly<Record<string, unknown>>;
|
||||
readonly getValue: (name: string) => unknown;
|
||||
}
|
||||
|
||||
/** 远程配置抓取抽象;cache bust 在 fetcher 内形成且只形成一次。 */
|
||||
export interface ConfigFetcher {
|
||||
fetch(gameserver: string, cacheBust?: () => string): Promise<unknown>;
|
||||
}
|
||||
@@ -14,33 +13,76 @@ export interface ConfigFetcher {
|
||||
export class ConfigFetchError extends Error {}
|
||||
export class ConfigParseError extends Error {}
|
||||
|
||||
function normalizeWebSocketUrl(value: string): string {
|
||||
if (value.startsWith('ws://') || value.startsWith('wss://')) return value;
|
||||
const scheme = /^([A-Za-z][A-Za-z0-9+.-]*):\/\//.exec(value);
|
||||
if (scheme) {
|
||||
throw new ConfigParseError(`远程配置 data.urlserver 不支持 scheme: ${scheme[1]}`);
|
||||
const SCOPES = [
|
||||
['agentlist', 'agentid'], ['gamelist', 'gameid'],
|
||||
['channellist', 'channelid'], ['marketlist', 'marketid'],
|
||||
] as const;
|
||||
|
||||
type ConfigObject = Record<string, unknown>;
|
||||
function requireObject(value: unknown, path: string): ConfigObject {
|
||||
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
||||
throw new ConfigParseError(`Invalid remote config ${path}: expected object`);
|
||||
}
|
||||
return `ws://${value}`;
|
||||
return value as ConfigObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* 只接受 data.urlserver;不读取旧的 *_server_tcp,也不去重或改变配置顺序。
|
||||
*/
|
||||
export function parseUrlServers(input: unknown): readonly string[] {
|
||||
if (typeof input !== 'object' || input === null || !('data' in input)) {
|
||||
throw new ConfigParseError('远程配置缺少 data.urlserver');
|
||||
}
|
||||
const data = (input as { data?: unknown }).data;
|
||||
if (typeof data !== 'object' || data === null || !('urlserver' in data)) {
|
||||
throw new ConfigParseError('远程配置缺少 data.urlserver');
|
||||
}
|
||||
function validateHierarchy(scope: ConfigObject, depth: number): void {
|
||||
if (depth === SCOPES.length) return;
|
||||
const [listName] = SCOPES[depth];
|
||||
if (!Object.prototype.hasOwnProperty.call(scope, listName)) return;
|
||||
const list = scope[listName];
|
||||
if (!Array.isArray(list)) throw new ConfigParseError(`Invalid remote config ${listName}: expected array`);
|
||||
for (const entry of list) validateHierarchy(requireObject(entry, listName), depth + 1);
|
||||
}
|
||||
|
||||
const raw = (data as { urlserver?: unknown }).urlserver;
|
||||
/** Preserve first loose identity match, truthy overrides and whole-object replacement. */
|
||||
export function createRemoteConfig(input: unknown, identity: Readonly<ChannelIdentity>): RemoteConfig {
|
||||
const raw = requireObject(input, 'root');
|
||||
validateHierarchy(raw, 0);
|
||||
const scopes = [raw];
|
||||
let scope = raw;
|
||||
for (const [listName, key] of SCOPES) {
|
||||
const list = scope[listName] as ConfigObject[] | undefined;
|
||||
// Legacy do_get_para skips any falsy identity, including numeric marketid=0.
|
||||
if (!list || !identity[key]) break;
|
||||
const match = list.find(entry => entry[key] == identity[key]);
|
||||
if (!match) break;
|
||||
scopes.push(match);
|
||||
scope = match;
|
||||
}
|
||||
return Object.freeze({
|
||||
raw,
|
||||
getValue(name: string): unknown {
|
||||
let value: unknown = null;
|
||||
for (const current of scopes) {
|
||||
if (Object.prototype.hasOwnProperty.call(current, name) && current[name]) value = current[name];
|
||||
}
|
||||
return value;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeWebSocketUrl(value: string): string {
|
||||
const url = value.startsWith('ws://') || value.startsWith('wss://') ? value : `ws://${value}`;
|
||||
if (/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(value) && !/^wss?:\/\//.test(value)) {
|
||||
throw new ConfigParseError('Invalid remote server: unsupported scheme');
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
if (!value || /\s/.test(value) || !parsed.hostname || parsed.username || parsed.password
|
||||
|| (parsed.protocol !== 'ws:' && parsed.protocol !== 'wss:')) throw new Error('invalid');
|
||||
} catch {
|
||||
throw new ConfigParseError('Invalid remote server: malformed WebSocket URL');
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
/** Legacy getConfig_Succ selects game_server_tcp when truthy, otherwise player_server_tcp. */
|
||||
export function parseUrlServers(config: RemoteConfig): readonly string[] {
|
||||
const raw = config.getValue('game_server_tcp') || config.getValue('player_server_tcp');
|
||||
const values = typeof raw === 'string' ? [raw] : raw;
|
||||
if (!Array.isArray(values) || values.length === 0
|
||||
|| values.some((value) => typeof value !== 'string' || value.length === 0)) {
|
||||
throw new ConfigParseError('远程配置 data.urlserver 必须是非空字符串或非空字符串数组');
|
||||
if (!Array.isArray(values) || values.length === 0 || values.some(value => typeof value !== 'string' || !value)) {
|
||||
throw new ConfigParseError('Remote game_server_tcp/player_server_tcp must be a nonempty string or string array');
|
||||
}
|
||||
|
||||
return Object.freeze(values.map(normalizeWebSocketUrl));
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
} from './profiles.ts';
|
||||
import {
|
||||
ConfigFetchError,
|
||||
ConfigParseError,
|
||||
createRemoteConfig,
|
||||
parseUrlServers,
|
||||
type ConfigFetcher,
|
||||
type RemoteConfig,
|
||||
@@ -29,9 +31,10 @@ export interface RuntimeConfig {
|
||||
readonly isDebugger: boolean;
|
||||
readonly identity: Readonly<ChannelIdentity>;
|
||||
readonly servers: readonly string[];
|
||||
readonly source: 'remote' | 'direct';
|
||||
readonly gameserver: string | null;
|
||||
readonly rawConfig: RemoteConfig | null;
|
||||
readonly source: 'remote';
|
||||
readonly gameserver: string;
|
||||
readonly rawConfig: RemoteConfig['raw'];
|
||||
readonly remoteConfig: RemoteConfig;
|
||||
}
|
||||
|
||||
export interface ResolveRuntimeConfigOptions {
|
||||
@@ -78,24 +81,6 @@ export async function resolveRuntimeConfig(
|
||||
const profile = selectedProfile(options.mode, options.search);
|
||||
const identity = resolveHostIdentity(options);
|
||||
|
||||
if (profile.source === 'direct') {
|
||||
if (options.mode !== 'debug' || profile.name !== 'local') {
|
||||
throw new ConfigFetchError(`profile '${profile.name}' 不是允许的 local debug direct 模式`);
|
||||
}
|
||||
if (!profile.servers || profile.servers.length === 0) {
|
||||
throw new ConfigFetchError(`profile '${profile.name}' 未配置完整 servers`);
|
||||
}
|
||||
return freezeResult({
|
||||
mode: options.mode,
|
||||
isDebugger: true,
|
||||
identity,
|
||||
servers: Object.freeze([...profile.servers]),
|
||||
source: 'direct',
|
||||
gameserver: null,
|
||||
rawConfig: null,
|
||||
});
|
||||
}
|
||||
|
||||
if (!profile.gameserver) {
|
||||
throw new ConfigFetchError(`profile '${profile.name}' 未配置 gameserver`);
|
||||
}
|
||||
@@ -108,10 +93,19 @@ export async function resolveRuntimeConfig(
|
||||
try {
|
||||
raw = await options.fetcher.fetch(gameserver, options.cacheBust);
|
||||
} catch (error) {
|
||||
if (error instanceof ConfigFetchError) throw error;
|
||||
if (error instanceof ConfigFetchError || error instanceof ConfigParseError) throw error;
|
||||
throw new ConfigFetchError(`远程配置抓取失败: ${gameserver}`);
|
||||
}
|
||||
const servers = parseUrlServers(raw);
|
||||
const remoteConfig = createRemoteConfig(raw, identity);
|
||||
let servers: readonly string[];
|
||||
if (profile.name === 'local') {
|
||||
if (!profile.servers || profile.servers.length === 0) {
|
||||
throw new ConfigFetchError('Local profile must define servers');
|
||||
}
|
||||
servers = Object.freeze([...profile.servers]);
|
||||
} else {
|
||||
servers = parseUrlServers(remoteConfig);
|
||||
}
|
||||
|
||||
return freezeResult({
|
||||
mode: options.mode,
|
||||
@@ -120,6 +114,7 @@ export async function resolveRuntimeConfig(
|
||||
servers,
|
||||
source: 'remote',
|
||||
gameserver,
|
||||
rawConfig: raw as RemoteConfig,
|
||||
rawConfig: remoteConfig.raw,
|
||||
remoteConfig,
|
||||
});
|
||||
}
|
||||
|
||||
+2
-1
@@ -114,7 +114,8 @@ export function createLocalLoginHost(options: LocalLoginHostOptions): LocalLogin
|
||||
requireActive();
|
||||
const context = await resolveLocalStartup(options.configOptions);
|
||||
requireActive();
|
||||
options.record({ kind: 'startup-config', data: context.config });
|
||||
const { mode, isDebugger, identity, servers, source, gameserver } = context.config;
|
||||
options.record({ kind: 'startup-config', data: { mode, isDebugger, identity, servers, source, gameserver } });
|
||||
requireActive();
|
||||
const entry = createDiagnosticGameEntry(context.config.identity.gameid, options.record);
|
||||
sources = createLocalLoginSources({ context, storage: options.storage, random: options.random,
|
||||
|
||||
Reference in New Issue
Block a user