fix(platform): preserve session failures and URL syntax

This commit is contained in:
2026-09-05 06:38:00 +08:00
parent 7e5a950cc7
commit 224df1c21c
6 changed files with 123 additions and 8 deletions
@@ -1,5 +1,8 @@
import type { GameHost } from '../sdk/contracts/index.ts';
import { GameSessionHost } from '../sdk/runtime/game-session-host.ts';
import {
GameSessionHost,
GameSessionOpenFault,
} from '../sdk/runtime/game-session-host.ts';
import {
parseLoginResponse,
parseSelfJoinRoomResponse,
@@ -167,7 +170,8 @@ export class RuntimeSession {
});
if (reconnect.present) this.options.gameSession.restore(reconnect.value);
} catch (error) {
this.throwFault(operation, [error, ...this.releaseGame()]);
const errors = error instanceof GameSessionOpenFault ? [...error.errors] : [error];
this.throwFault(operation, [...errors, ...this.releaseGame()]);
}
}
@@ -151,6 +151,9 @@ function parseSwitchTarget(address: string, path: string): string {
if (authority === undefined || authority.length === 0) {
throw new TypeError(`${path}: expected a WebSocket address with a non-empty host`);
}
if (target.includes('#') || authority.includes('@')) {
throw new TypeError(`${path}: expected a WebSocket address without fragment or userinfo`);
}
let parsed: URL;
try {
@@ -16,6 +16,16 @@ export type GameSessionState =
| 'disposing'
| 'disposed';
export class GameSessionOpenFault extends Error {
readonly errors: readonly unknown[];
constructor(primary: unknown, cleanup: unknown) {
super('Game session open failed and module cleanup also failed');
this.name = 'GameSessionOpenFault';
this.errors = Object.freeze([primary, cleanup]);
}
}
/** Validates the compile-time game identity before it reaches the runtime. */
export function assertGameEntry(entry: GameEntry): void {
assertNonEmptyText(entry.key, 'key');
@@ -38,7 +48,7 @@ export function assertGameEntry(entry: GameEntry): void {
try {
assertGameModule(module);
} finally {
disposeValidationModule(module);
disposeModuleIfPresent(module);
}
}
@@ -71,14 +81,20 @@ export class GameSessionHost {
this.module = module;
module.attach(host);
this.currentState = 'active';
} catch (error) {
} catch (primaryError) {
this.module = undefined;
let cleanupFailed = false;
let cleanupError: unknown;
try {
module?.dispose();
disposeModuleIfPresent(module);
} catch (error) {
cleanupFailed = true;
cleanupError = error;
} finally {
this.currentState = 'idle';
}
throw error;
if (cleanupFailed) throw new GameSessionOpenFault(primaryError, cleanupError);
throw primaryError;
}
}
@@ -174,7 +190,7 @@ function assertGameModule(module: unknown): asserts module is GameModule {
}
}
function disposeValidationModule(module: unknown): void {
function disposeModuleIfPresent(module: unknown): void {
if (!module || typeof module !== 'object') return;
const dispose = (module as Record<string, unknown>).dispose;
if (typeof dispose === 'function') dispose.call(module);