fix(platform): snapshot session fault errors safely

This commit is contained in:
2026-09-05 07:05:06 +08:00
parent 77b2bff40a
commit a383c0330e
4 changed files with 209 additions and 26 deletions
@@ -1,7 +1,7 @@
import type { GameHost } from '../sdk/contracts/index.ts';
import {
extractGameSessionOpenFaultErrors,
GameSessionHost,
isGameSessionOpenFault,
} from '../sdk/runtime/game-session-host.ts';
import {
parseLoginResponse,
@@ -170,8 +170,12 @@ export class RuntimeSession {
});
if (reconnect.present) this.options.gameSession.restore(reconnect.value);
} catch (error) {
const errors = isGameSessionOpenFault(error) ? [...error.errors] : [error];
this.throwFault(operation, [...errors, ...this.releaseGame()]);
const openErrors = extractGameSessionOpenFaultErrors(error);
const errors = openErrors === null
? [error]
: [openErrors[0], openErrors[1]];
errors.push(...this.releaseGame());
this.throwFault(operation, errors);
}
}
@@ -19,39 +19,64 @@ export type GameSessionState =
| 'disposing'
| 'disposed';
export type GameSessionOpenFaultErrors = readonly [unknown, unknown];
export class GameSessionOpenFault extends Error {
readonly errors: readonly unknown[];
declare readonly errors: GameSessionOpenFaultErrors;
constructor(primary: unknown, cleanup: unknown) {
super('Game session open failed and module cleanup also failed');
this.name = 'GameSessionOpenFault';
Object.defineProperty(this, GAME_SESSION_OPEN_FAULT_BRAND, { value: true });
this.errors = Object.freeze([primary, cleanup]);
const errors = Object.freeze([primary, cleanup] as [unknown, unknown]);
Object.defineProperties(this, {
[GAME_SESSION_OPEN_FAULT_BRAND]: {
value: true,
writable: false,
enumerable: false,
configurable: false,
},
errors: {
value: errors,
writable: false,
enumerable: true,
configurable: false,
},
});
}
}
export interface GameSessionOpenFaultShape {
readonly errors: readonly [unknown, unknown];
readonly errors: GameSessionOpenFaultErrors;
}
/** Recognizes open faults across realms and duplicate module instances. */
export function isGameSessionOpenFault(value: unknown): value is GameSessionOpenFaultShape {
if (value === null || typeof value !== 'object') return false;
/** Snapshots open-fault errors across realms and duplicate module instances. */
export function extractGameSessionOpenFaultErrors(
value: unknown,
): GameSessionOpenFaultErrors | null {
if (value === null || typeof value !== 'object') return null;
try {
const brand = ownDataDescriptor(value, GAME_SESSION_OPEN_FAULT_BRAND);
const errors = ownDataDescriptor(value, 'errors');
if (brand?.value !== true || !errors || !Array.isArray(errors.value)) return false;
if (brand?.value !== true || !errors || !Array.isArray(errors.value)) return null;
const errorList = errors.value;
return ownDataDescriptor(errorList, 'length')?.value === 2
&& ownDataDescriptor(errorList, '0') !== undefined
&& ownDataDescriptor(errorList, '1') !== undefined;
const length = ownDataDescriptor(errorList, 'length');
const primary = ownDataDescriptor(errorList, '0');
const cleanup = ownDataDescriptor(errorList, '1');
if (length?.value !== 2 || !primary || !cleanup) return null;
return Object.freeze([primary.value, cleanup.value] as [unknown, unknown]);
} catch {
return false;
return null;
}
}
/** Recognizes open faults across realms and duplicate module instances. */
export function isGameSessionOpenFault(value: unknown): value is GameSessionOpenFaultShape {
return extractGameSessionOpenFaultErrors(value) !== null;
}
/** Validates the compile-time game identity before it reaches the runtime. */
export function assertGameEntry(entry: GameEntry): void {
assertNonEmptyText(entry.key, 'key');