feat: bound local platform login diagnostics and wire scope
This commit is contained in:
+33
@@ -0,0 +1,33 @@
|
||||
import type { GameEntry } from '../../framework/sdk/contracts/game-entry.ts';
|
||||
import type { EvidenceSink } from './local-login-transport.ts';
|
||||
|
||||
/** Satisfies entry validation while making every actual game operation a scope error. */
|
||||
export function createDiagnosticGameEntry(gameId: string | number, record: EvidenceSink): GameEntry {
|
||||
let nextInstanceId = 0;
|
||||
function violation(operation: string, instanceId?: number): never {
|
||||
record({ kind: 'scope-violation', data: { operation, instanceId } });
|
||||
throw new Error(`Local platform login scope forbids ${operation}`);
|
||||
}
|
||||
return {
|
||||
key: 'local-platform-login',
|
||||
route: 'local-platform-login',
|
||||
gameId,
|
||||
resolveSeatCount: () => violation('resolveSeatCount'),
|
||||
createModule() {
|
||||
const instanceId = ++nextInstanceId;
|
||||
let disposed = false;
|
||||
record({ kind: 'diagnostic-module', data: { instanceId, state: 'created' } });
|
||||
return {
|
||||
attach: () => violation('attach', instanceId),
|
||||
restore: () => violation('restore', instanceId),
|
||||
handlePlatformEvent: () => violation('handlePlatformEvent', instanceId),
|
||||
handleGameMessage: () => violation('handleGameMessage', instanceId),
|
||||
dispose() {
|
||||
if (disposed) return;
|
||||
disposed = true;
|
||||
record({ kind: 'diagnostic-module', data: { instanceId, state: 'disposed' } });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
import type { LocalStartupContext } from '../../framework/config/local-startup.ts';
|
||||
import type { Transport } from '../../framework/net/transport.ts';
|
||||
import { WireClient, type WireEvent } from '../../framework/net/wire-client.ts';
|
||||
import type { PlatformWireClient } from '../../framework/platform/runtime.ts';
|
||||
import { parseLoginResponse } from '../../framework/protocol/contracts/index.ts';
|
||||
import type { EvidenceSink } from './local-login-transport.ts';
|
||||
|
||||
interface LocalLoginWireOptions {
|
||||
context: LocalStartupContext;
|
||||
transportFactory: () => Transport;
|
||||
onFault: (error: unknown) => void;
|
||||
record: EvidenceSink;
|
||||
}
|
||||
|
||||
/** ES2020-compatible aggregate retaining original errors, including non-Error values. */
|
||||
class LocalLoginWireFault extends Error {
|
||||
readonly errors: readonly unknown[];
|
||||
constructor(errors: readonly unknown[]) {
|
||||
super('Local login Wire failed with additional cleanup or evidence errors');
|
||||
this.name = 'LocalLoginWireFault';
|
||||
this.errors = Object.freeze([...errors]);
|
||||
}
|
||||
}
|
||||
|
||||
export function createLocalLoginWire(options: LocalLoginWireOptions): PlatformWireClient {
|
||||
const wire = new WireClient({ servers: options.context.config.servers, transportFactory: options.transportFactory });
|
||||
const listeners = new Set<(event: WireEvent) => void>();
|
||||
let stopped = false;
|
||||
let faulted = false;
|
||||
let terminalDelivery = false;
|
||||
|
||||
function stop(): void {
|
||||
if (stopped) return;
|
||||
stopped = true;
|
||||
unsubscribe();
|
||||
listeners.clear();
|
||||
wire.stop();
|
||||
}
|
||||
|
||||
function fail(error: unknown, operation?: string, event?: WireEvent): unknown {
|
||||
if (faulted) return error;
|
||||
faulted = true;
|
||||
const errors = [error];
|
||||
try { stop(); } catch (cleanupError) { errors.push(cleanupError); }
|
||||
if (operation !== undefined) {
|
||||
try { options.record({ kind: 'scope-violation', data: { operation, event } }); }
|
||||
catch (recordError) { errors.push(recordError); }
|
||||
}
|
||||
const fault = errors.length === 1 ? error : new LocalLoginWireFault(errors);
|
||||
options.onFault(fault);
|
||||
return fault;
|
||||
}
|
||||
|
||||
function scopeError(operation: string): Error {
|
||||
return new Error(`Local platform login scope forbids ${operation}`);
|
||||
}
|
||||
|
||||
function assertActive(): void {
|
||||
if (stopped || faulted || terminalDelivery) throw new Error('Local login Wire is stopped or terminal');
|
||||
}
|
||||
|
||||
function deliver(event: WireEvent): void {
|
||||
for (const listener of [...listeners]) {
|
||||
if (stopped || faulted) break;
|
||||
listener(event);
|
||||
}
|
||||
}
|
||||
|
||||
const unsubscribe = wire.subscribe(event => {
|
||||
if (stopped || faulted || terminalDelivery) return;
|
||||
try {
|
||||
options.record({ kind: 'wire-event', data: event });
|
||||
if (stopped || faulted) return;
|
||||
if (event.type !== 'message') { deliver(event); return; }
|
||||
const { route, rpc, data } = event.message;
|
||||
if (rpc === 'connect_agentserver' || rpc === 'connect_roomserver' || (route !== 'agent' && route !== 'platform')) {
|
||||
fail(scopeError(`${route}/${rpc}`), `${route}/${rpc}`, event);
|
||||
return;
|
||||
}
|
||||
let terminalOperation: string | undefined;
|
||||
if (route === 'agent' && rpc === 'player_login') {
|
||||
const parsed = parseLoginResponse(data);
|
||||
if (parsed.state === 0 && parsed.room !== null) {
|
||||
fail(scopeError('login room recovery'), 'login room recovery', event);
|
||||
return;
|
||||
}
|
||||
if (parsed.state !== 0) terminalOperation = 'login rejected';
|
||||
}
|
||||
if (route === 'agent' && rpc === 'kick_server') terminalOperation = 'kick_server';
|
||||
if (terminalOperation !== undefined) {
|
||||
// Existing runtime failure display runs first, but cannot initiate more traffic.
|
||||
terminalDelivery = true;
|
||||
try { deliver(event); }
|
||||
catch (error) { fail(error); return; }
|
||||
fail(scopeError(terminalOperation), terminalOperation, event);
|
||||
return;
|
||||
}
|
||||
deliver(event);
|
||||
} catch (error) { fail(error); }
|
||||
});
|
||||
|
||||
return {
|
||||
subscribe(listener) {
|
||||
if (stopped || faulted) return () => {};
|
||||
listeners.add(listener);
|
||||
return () => { listeners.delete(listener); };
|
||||
},
|
||||
start() { assertActive(); wire.start(); },
|
||||
send(envelope) {
|
||||
assertActive();
|
||||
if (envelope.route !== 'agent' || envelope.rpc !== 'player_login') {
|
||||
throw fail(scopeError(`outbound ${envelope.route}/${envelope.rpc}`), `outbound ${envelope.route}/${envelope.rpc}`);
|
||||
}
|
||||
wire.send(envelope);
|
||||
},
|
||||
switchServer() { assertActive(); throw fail(scopeError('switchServer'), 'switchServer'); },
|
||||
reconnectCurrent() { assertActive(); wire.reconnectCurrent(); },
|
||||
stop,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user