@dreamboard-games/cli 0.1.30-alpha.4 → 0.1.30-alpha.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -113
- package/dist/agent-verifier/agent-workspace-verifier.mjs +2084 -57
- package/dist/agent-verifier/agent-workspace-verifier.mjs.map +1 -1
- package/dist/agent-verifier/{chunk-XQXDOBYB.mjs → chunk-4I2WWAPK.mjs} +27 -10
- package/dist/agent-verifier/chunk-4I2WWAPK.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-C3VW3DTA.mjs → chunk-BWBN2TDJ.mjs} +535 -633
- package/dist/agent-verifier/chunk-BWBN2TDJ.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-TAEQKBJB.mjs → chunk-GWRZRWCF.mjs} +1 -1
- package/dist/agent-verifier/chunk-GWRZRWCF.mjs.map +1 -0
- package/dist/agent-verifier/chunk-HUBV22JQ.mjs +89 -0
- package/dist/agent-verifier/chunk-HUBV22JQ.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-MW2QIWWA.mjs → chunk-KAA3B4DI.mjs} +215 -223
- package/dist/agent-verifier/chunk-KAA3B4DI.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-27EEIZCI.mjs → chunk-KDAQ4CZY.mjs} +34 -27
- package/dist/agent-verifier/chunk-KDAQ4CZY.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-IAYRNVUC.mjs → chunk-LMW66VBH.mjs} +2 -13
- package/dist/agent-verifier/{chunk-IAYRNVUC.mjs.map → chunk-LMW66VBH.mjs.map} +1 -1
- package/dist/agent-verifier/{chunk-776W3UGV.mjs → chunk-LROY5SN2.mjs} +7 -45
- package/dist/agent-verifier/chunk-LROY5SN2.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-H76MT5UR.mjs → chunk-M7UVBANQ.mjs} +2 -1
- package/dist/agent-verifier/chunk-M7UVBANQ.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-5NYBTZB4.mjs → chunk-MIRGCMUC.mjs} +112 -26
- package/dist/agent-verifier/chunk-MIRGCMUC.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-NAK77WXW.mjs → chunk-MYMVXTZT.mjs} +4 -5
- package/dist/agent-verifier/chunk-MYMVXTZT.mjs.map +1 -0
- package/dist/agent-verifier/chunk-OJFZVGEL.mjs +492 -0
- package/dist/agent-verifier/chunk-OJFZVGEL.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-XKCJBIRY.mjs → chunk-QD4SQNUP.mjs} +2 -2
- package/dist/agent-verifier/{chunk-QBAF7EYR.mjs → chunk-TTB7AIHZ.mjs} +4 -4
- package/dist/agent-verifier/{chunk-QBAF7EYR.mjs.map → chunk-TTB7AIHZ.mjs.map} +1 -1
- package/dist/agent-verifier/{chunk-F2DIOJJZ.mjs → chunk-XCQQIPCO.mjs} +5 -46
- package/dist/agent-verifier/chunk-XCQQIPCO.mjs.map +1 -0
- package/dist/agent-verifier/{global-config-NYCSCAUI.mjs → global-config-2NUESNEQ.mjs} +5 -5
- package/dist/agent-verifier/{keychain-backend-A3MRWLPF.mjs → keychain-backend-FF4I6ODB.mjs} +11 -6
- package/dist/agent-verifier/keychain-backend-FF4I6ODB.mjs.map +1 -0
- package/dist/agent-verifier/{local-files-QVJ2H3MH.mjs → local-files-OF4QFISU.mjs} +8 -8
- package/dist/agent-verifier/{chunk-UIOLGH4A.mjs → local-typecheck-DHVLM37Z.mjs} +4 -4
- package/dist/agent-verifier/local-typecheck-DHVLM37Z.mjs.map +1 -0
- package/dist/agent-verifier/{materialize-workspace-OZKOQCSQ.mjs → materialize-workspace-JBDL6LF4.mjs} +22 -22
- package/dist/agent-verifier/materialize-workspace-JBDL6LF4.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-Z6OZWUIZ.mjs → reducer-bundle-preflight-GLUJKTWU.mjs} +75 -24
- package/dist/agent-verifier/reducer-bundle-preflight-GLUJKTWU.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-YDIOW2BO.mjs → reducer-contract-preflight-WVQQPW5F.mjs} +7 -6
- package/dist/agent-verifier/reducer-contract-preflight-WVQQPW5F.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-ON62IGWK.mjs → reducer-native-test-harness-XQUPIT5D.mjs} +480 -703
- package/dist/agent-verifier/reducer-native-test-harness-XQUPIT5D.mjs.map +1 -0
- package/dist/agent-verifier/static-scaffold-U5DXE23S.mjs +24 -0
- package/dist/agent-verifier/{workspace-codegen-WPZHMATU.mjs → workspace-codegen-SPPVHURX.mjs} +3 -3
- package/dist/agent-verifier/{workspace-dependencies-B6A2ZX55.mjs → workspace-dependencies-5HEEKZFP.mjs} +5 -3
- package/dist/authoring-compatibility-internal.js +12 -0
- package/dist/chunk-5IYJOVUA.js +3902 -0
- package/dist/chunk-5IYJOVUA.js.map +1 -0
- package/dist/chunk-6NYVJYN4.js +313 -0
- package/dist/chunk-6NYVJYN4.js.map +1 -0
- package/dist/chunk-EQNBQVIW.js +204 -0
- package/dist/chunk-EQNBQVIW.js.map +1 -0
- package/dist/{chunk-M4SCKH5M.js → chunk-USZAPMQ4.js} +2488 -4993
- package/dist/chunk-USZAPMQ4.js.map +1 -0
- package/dist/{global-config-YBFEGJQG.js → global-config-RBMW7IVA.js} +3 -2
- package/dist/index.js +3099 -6188
- package/dist/index.js.map +1 -1
- package/dist/internal.js +35 -9
- package/dist/internal.js.map +1 -1
- package/dist/{keychain-backend-JHTXAKWC.js → keychain-backend-FSNTNTZE.js} +11 -6
- package/dist/keychain-backend-FSNTNTZE.js.map +1 -0
- package/package.json +9 -19
- package/release/authoring-release-set.json +38 -0
- package/skills/dreamboard/SKILL.md +32 -30
- package/skills/dreamboard/references/building-your-first-game.md +16 -16
- package/skills/dreamboard/references/cli.md +54 -54
- package/skills/dreamboard/references/manifest-authoring.md +11 -3
- package/skills/dreamboard/references/quickstart.md +19 -16
- package/skills/dreamboard/references/testing.md +6 -13
- package/dist/agent-verifier/chunk-27EEIZCI.mjs.map +0 -1
- package/dist/agent-verifier/chunk-5NYBTZB4.mjs.map +0 -1
- package/dist/agent-verifier/chunk-776W3UGV.mjs.map +0 -1
- package/dist/agent-verifier/chunk-C3VW3DTA.mjs.map +0 -1
- package/dist/agent-verifier/chunk-F2DIOJJZ.mjs.map +0 -1
- package/dist/agent-verifier/chunk-G42BGGG2.mjs +0 -70
- package/dist/agent-verifier/chunk-G42BGGG2.mjs.map +0 -1
- package/dist/agent-verifier/chunk-H76MT5UR.mjs.map +0 -1
- package/dist/agent-verifier/chunk-IDVQXGAO.mjs +0 -222
- package/dist/agent-verifier/chunk-IDVQXGAO.mjs.map +0 -1
- package/dist/agent-verifier/chunk-JO5AMVZU.mjs +0 -1744
- package/dist/agent-verifier/chunk-JO5AMVZU.mjs.map +0 -1
- package/dist/agent-verifier/chunk-KDBSVLCF.mjs +0 -624
- package/dist/agent-verifier/chunk-KDBSVLCF.mjs.map +0 -1
- package/dist/agent-verifier/chunk-MW2QIWWA.mjs.map +0 -1
- package/dist/agent-verifier/chunk-NAK77WXW.mjs.map +0 -1
- package/dist/agent-verifier/chunk-ON62IGWK.mjs.map +0 -1
- package/dist/agent-verifier/chunk-QZH6IEZS.mjs +0 -39
- package/dist/agent-verifier/chunk-QZH6IEZS.mjs.map +0 -1
- package/dist/agent-verifier/chunk-TAEQKBJB.mjs.map +0 -1
- package/dist/agent-verifier/chunk-UIOLGH4A.mjs.map +0 -1
- package/dist/agent-verifier/chunk-XQXDOBYB.mjs.map +0 -1
- package/dist/agent-verifier/chunk-YDIOW2BO.mjs.map +0 -1
- package/dist/agent-verifier/chunk-Z6OZWUIZ.mjs.map +0 -1
- package/dist/agent-verifier/compile-576O7TYP.mjs +0 -312
- package/dist/agent-verifier/compile-576O7TYP.mjs.map +0 -1
- package/dist/agent-verifier/keychain-backend-A3MRWLPF.mjs.map +0 -1
- package/dist/agent-verifier/local-typecheck-2JWG5IGL.mjs +0 -10
- package/dist/agent-verifier/materialize-workspace-OZKOQCSQ.mjs.map +0 -1
- package/dist/agent-verifier/reducer-bundle-preflight-7NYZF5ZT.mjs +0 -20
- package/dist/agent-verifier/reducer-contract-preflight-COD2CO22.mjs +0 -11
- package/dist/agent-verifier/reducer-native-test-harness-QC7HZUK4.mjs +0 -50
- package/dist/agent-verifier/static-scaffold-JBUE3ROP.mjs +0 -27
- package/dist/agent-verifier/sync-C6S3OGCD.mjs +0 -588
- package/dist/agent-verifier/sync-C6S3OGCD.mjs.map +0 -1
- package/dist/agent-verifier/test-Y5UGQV7J.mjs +0 -353
- package/dist/agent-verifier/test-Y5UGQV7J.mjs.map +0 -1
- package/dist/agent-verifier/workspace-codegen-WPZHMATU.mjs.map +0 -1
- package/dist/agent-verifier/workspace-dependencies-B6A2ZX55.mjs.map +0 -1
- package/dist/chunk-3NRROR4P.js +0 -432
- package/dist/chunk-3NRROR4P.js.map +0 -1
- package/dist/chunk-M4SCKH5M.js.map +0 -1
- package/dist/dev-host/components/drawer.tsx +0 -132
- package/dist/dev-host/components/input.tsx +0 -21
- package/dist/dev-host/dev-api-proxy-plugin.ts +0 -328
- package/dist/dev-host/dev-author-dom-warnings.ts +0 -100
- package/dist/dev-host/dev-diagnostics.ts +0 -62
- package/dist/dev-host/dev-fallback-stylesheet.ts +0 -53
- package/dist/dev-host/dev-hmr-guard-plugin.ts +0 -47
- package/dist/dev-host/dev-host-controller.ts +0 -674
- package/dist/dev-host/dev-host-player-query.ts +0 -17
- package/dist/dev-host/dev-host-session-transport.ts +0 -52
- package/dist/dev-host/dev-host-storage.ts +0 -56
- package/dist/dev-host/dev-log-relay-plugin.ts +0 -510
- package/dist/dev-host/dev-runtime-config.ts +0 -14
- package/dist/dev-host/dev-runtime-platform.ts +0 -335
- package/dist/dev-host/dev-virtual-modules-plugin.ts +0 -64
- package/dist/dev-host/host-main.css +0 -224
- package/dist/dev-host/host-main.tsx +0 -948
- package/dist/dev-host/index.html +0 -56
- package/dist/dev-host/lib/utils.ts +0 -6
- package/dist/dev-host/plugin-main.ts +0 -61
- package/dist/dev-host/plugin.html +0 -24
- package/dist/dev-host/shared-styles.css +0 -144
- package/dist/dev-host/start-dev-server.ts +0 -140
- package/dist/dev-host/virtual-modules.d.ts +0 -27
- package/dist/global-config-YBFEGJQG.js.map +0 -1
- package/dist/keychain-backend-JHTXAKWC.js.map +0 -1
- package/skills/dreamboard/scripts/events-extract.mjs +0 -218
- /package/dist/agent-verifier/{chunk-XKCJBIRY.mjs.map → chunk-QD4SQNUP.mjs.map} +0 -0
- /package/dist/agent-verifier/{global-config-NYCSCAUI.mjs.map → global-config-2NUESNEQ.mjs.map} +0 -0
- /package/dist/agent-verifier/{local-files-QVJ2H3MH.mjs.map → local-files-OF4QFISU.mjs.map} +0 -0
- /package/dist/agent-verifier/{local-typecheck-2JWG5IGL.mjs.map → static-scaffold-U5DXE23S.mjs.map} +0 -0
- /package/dist/agent-verifier/{reducer-bundle-preflight-7NYZF5ZT.mjs.map → workspace-codegen-SPPVHURX.mjs.map} +0 -0
- /package/dist/agent-verifier/{reducer-contract-preflight-COD2CO22.mjs.map → workspace-dependencies-5HEEKZFP.mjs.map} +0 -0
- /package/dist/{agent-verifier/reducer-native-test-harness-QC7HZUK4.mjs.map → authoring-compatibility-internal.js.map} +0 -0
- /package/dist/{agent-verifier/static-scaffold-JBUE3ROP.mjs.map → global-config-RBMW7IVA.js.map} +0 -0
- /package/{dist/scaffold → scaffold}/assets/static/app/tsconfig.framework.json +0 -0
- /package/{dist/scaffold → scaffold}/assets/static/app/tsconfig.json +0 -0
- /package/{dist/scaffold → scaffold}/assets/static/ui/index.tsx +0 -0
- /package/{dist/scaffold → scaffold}/assets/static/ui/style.css +0 -0
- /package/{dist/scaffold → scaffold}/assets/static/ui/tsconfig.framework.json +0 -0
- /package/{dist/scaffold → scaffold}/assets/static/ui/tsconfig.json +0 -0
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import type { Plugin } from "vite";
|
|
3
|
-
|
|
4
|
-
export function createDevHmrGuardPlugin(options: {
|
|
5
|
-
projectRoot: string;
|
|
6
|
-
}): Plugin {
|
|
7
|
-
return {
|
|
8
|
-
name: "dreamboard-dev-hmr-guard",
|
|
9
|
-
handleHotUpdate(context) {
|
|
10
|
-
if (shouldHandleProjectHotUpdate(options.projectRoot, context.file)) {
|
|
11
|
-
return undefined;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
context.server.config.logger.info(
|
|
15
|
-
`[dreamboard] ignored HMR outside project: ${path.relative(
|
|
16
|
-
options.projectRoot,
|
|
17
|
-
context.file,
|
|
18
|
-
)}`,
|
|
19
|
-
);
|
|
20
|
-
return [];
|
|
21
|
-
},
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function shouldHandleProjectHotUpdate(
|
|
26
|
-
projectRoot: string,
|
|
27
|
-
file: string,
|
|
28
|
-
): boolean {
|
|
29
|
-
const normalizedProjectRoot = path.resolve(projectRoot);
|
|
30
|
-
const normalizedFile = path.resolve(file);
|
|
31
|
-
const relativePath = path.relative(normalizedProjectRoot, normalizedFile);
|
|
32
|
-
const isInsideProject =
|
|
33
|
-
relativePath === "" ||
|
|
34
|
-
(relativePath.length > 0 &&
|
|
35
|
-
!relativePath.startsWith("..") &&
|
|
36
|
-
!path.isAbsolute(relativePath));
|
|
37
|
-
|
|
38
|
-
if (!isInsideProject) {
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const pathSegments = relativePath.split(path.sep);
|
|
43
|
-
return (
|
|
44
|
-
!pathSegments.includes(".dreamboard") &&
|
|
45
|
-
!pathSegments.includes("node_modules")
|
|
46
|
-
);
|
|
47
|
-
}
|
|
@@ -1,674 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
PluginSessionGateway,
|
|
3
|
-
type GameSessionStoreApi,
|
|
4
|
-
type HostSessionTransport,
|
|
5
|
-
type LoggerLike,
|
|
6
|
-
type SessionContext,
|
|
7
|
-
type UnifiedSessionStore,
|
|
8
|
-
unifiedSessionSelectors,
|
|
9
|
-
} from "@dreamboard-games/ui-host-runtime/runtime";
|
|
10
|
-
import { formatConsoleArgs } from "./dev-diagnostics.js";
|
|
11
|
-
import type { ActiveSession, DevHostStorage } from "./dev-host-storage.js";
|
|
12
|
-
|
|
13
|
-
const AUTO_RECOVERY_SSE_FAILURE_THRESHOLD = 2;
|
|
14
|
-
const MAX_AUTO_RECOVERY_ATTEMPTS = 1;
|
|
15
|
-
type DevHostSessionSnapshot = Awaited<
|
|
16
|
-
ReturnType<NonNullable<HostSessionTransport["createDevSessionSnapshot"]>>
|
|
17
|
-
>;
|
|
18
|
-
|
|
19
|
-
type SessionStoreApi = {
|
|
20
|
-
getState: () => UnifiedSessionStore;
|
|
21
|
-
subscribe: (
|
|
22
|
-
listener: (
|
|
23
|
-
state: UnifiedSessionStore,
|
|
24
|
-
previousState: UnifiedSessionStore,
|
|
25
|
-
) => void,
|
|
26
|
-
) => () => void;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
function hasRenderableSnapshot(store: SessionStoreApi): boolean {
|
|
30
|
-
const state = store.getState();
|
|
31
|
-
return (
|
|
32
|
-
unifiedSessionSelectors.bootstrapStatus(state) === "renderable" &&
|
|
33
|
-
state.getPluginSnapshot().view !== null
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function createSubmissionError(
|
|
38
|
-
errorCode: string | undefined,
|
|
39
|
-
message: string | undefined,
|
|
40
|
-
fallbackMessage: string,
|
|
41
|
-
): Error & { errorCode?: string } {
|
|
42
|
-
const error = new Error(message ?? fallbackMessage) as Error & {
|
|
43
|
-
errorCode?: string;
|
|
44
|
-
};
|
|
45
|
-
error.name = "SubmissionError";
|
|
46
|
-
error.errorCode = errorCode;
|
|
47
|
-
return error;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface DevHostControllerConfig {
|
|
51
|
-
autoStartGame: boolean;
|
|
52
|
-
compiledResultId: string;
|
|
53
|
-
createDevSessionSnapshot?: NonNullable<
|
|
54
|
-
HostSessionTransport["createDevSessionSnapshot"]
|
|
55
|
-
>;
|
|
56
|
-
debug: boolean;
|
|
57
|
-
fallbackSession: ActiveSession;
|
|
58
|
-
gameId: string;
|
|
59
|
-
initialPlayerId?: string | null;
|
|
60
|
-
playerCount: number;
|
|
61
|
-
setupProfileId: string | null;
|
|
62
|
-
slug: string;
|
|
63
|
-
userId: string | null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Structured surface for reducer/runtime failures that bubble up from the
|
|
68
|
-
* backend. These are shown in the dev host overlay so authors see the same
|
|
69
|
-
* file/line/stack information they'd get from a `dreamboard dev` backend log.
|
|
70
|
-
*/
|
|
71
|
-
export interface DevHostRuntimeError {
|
|
72
|
-
title: string;
|
|
73
|
-
summary: string;
|
|
74
|
-
violations: Array<{
|
|
75
|
-
message: string;
|
|
76
|
-
field?: string;
|
|
77
|
-
code?: string;
|
|
78
|
-
}>;
|
|
79
|
-
correlationId?: string;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export interface DevHostControllerSnapshot {
|
|
83
|
-
session: ActiveSession;
|
|
84
|
-
seedValue: string;
|
|
85
|
-
isCreatingSession: boolean;
|
|
86
|
-
iframeSrc: string;
|
|
87
|
-
pluginReady: boolean;
|
|
88
|
-
runtimeError: DevHostRuntimeError | null;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export class DevHostController {
|
|
92
|
-
private readonly listeners = new Set<() => void>();
|
|
93
|
-
private readonly defaultSession: ActiveSession;
|
|
94
|
-
private readonly unsubscribeStore: () => void;
|
|
95
|
-
|
|
96
|
-
private currentSession: ActiveSession;
|
|
97
|
-
private seedValue: string;
|
|
98
|
-
private isCreatingSession = false;
|
|
99
|
-
private pluginReady = false;
|
|
100
|
-
private iframeLoaded = false;
|
|
101
|
-
private iframe: HTMLIFrameElement | null = null;
|
|
102
|
-
private gateway: PluginSessionGateway | null = null;
|
|
103
|
-
private gatewayStoreAttached = false;
|
|
104
|
-
private pluginFrameReloadCounter = 0;
|
|
105
|
-
private autoRecoveryAttempts = 0;
|
|
106
|
-
private sessionSnapshotSseFailureCount = 0;
|
|
107
|
-
private recoveryInFlight = false;
|
|
108
|
-
private runtimeError: DevHostRuntimeError | null = null;
|
|
109
|
-
private playerSwitchRequestId = 0;
|
|
110
|
-
|
|
111
|
-
constructor(
|
|
112
|
-
private readonly store: SessionStoreApi,
|
|
113
|
-
private readonly storage: DevHostStorage,
|
|
114
|
-
private readonly config: DevHostControllerConfig,
|
|
115
|
-
private readonly logger: LoggerLike,
|
|
116
|
-
) {
|
|
117
|
-
this.defaultSession = structuredClone(config.fallbackSession);
|
|
118
|
-
this.currentSession = structuredClone(this.defaultSession);
|
|
119
|
-
this.seedValue = String(this.currentSession.seed ?? 1337);
|
|
120
|
-
this.unsubscribeStore = this.store.subscribe((state) => {
|
|
121
|
-
if (unifiedSessionSelectors.bootstrapStatus(state) !== "loading") {
|
|
122
|
-
this.sessionSnapshotSseFailureCount = 0;
|
|
123
|
-
}
|
|
124
|
-
if (
|
|
125
|
-
this.pluginReady &&
|
|
126
|
-
!this.gatewayStoreAttached &&
|
|
127
|
-
hasRenderableSnapshot(this.store)
|
|
128
|
-
) {
|
|
129
|
-
this.attachStore();
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
subscribe(listener: () => void): () => void {
|
|
135
|
-
this.listeners.add(listener);
|
|
136
|
-
return () => {
|
|
137
|
-
this.listeners.delete(listener);
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
getSnapshot(): DevHostControllerSnapshot {
|
|
142
|
-
return {
|
|
143
|
-
session: this.currentSession,
|
|
144
|
-
seedValue: this.seedValue,
|
|
145
|
-
isCreatingSession: this.isCreatingSession,
|
|
146
|
-
iframeSrc: `/plugin.html?session=${encodeURIComponent(this.currentSession.sessionId)}&reload=${this.pluginFrameReloadCounter}`,
|
|
147
|
-
pluginReady: this.pluginReady,
|
|
148
|
-
runtimeError: this.runtimeError,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
dismissRuntimeError(): void {
|
|
153
|
-
if (this.runtimeError === null) {
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
this.runtimeError = null;
|
|
157
|
-
this.notify();
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Public entry point for surfacing runtime errors originating outside
|
|
162
|
-
* the controller (e.g. the api-client interceptor that detects a
|
|
163
|
-
* `session_invalid` envelope from the dev proxy and wants to route
|
|
164
|
-
* the failure through the existing overlay).
|
|
165
|
-
*/
|
|
166
|
-
reportRuntimeError(error: DevHostRuntimeError): void {
|
|
167
|
-
this.setRuntimeError(error);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
async initialize(): Promise<void> {
|
|
171
|
-
this.sessionSnapshotSseFailureCount = 0;
|
|
172
|
-
this.notify();
|
|
173
|
-
|
|
174
|
-
try {
|
|
175
|
-
const preferredPlayerId = this.resolvePreferredPlayerId();
|
|
176
|
-
await this.loadStoreSnapshot(preferredPlayerId, "dev-bootstrap");
|
|
177
|
-
this.syncCurrentSessionFromStore();
|
|
178
|
-
this.persistCurrentPlayerFromStore();
|
|
179
|
-
} catch (initialError) {
|
|
180
|
-
const preferredPlayerId = this.resolvePreferredPlayerId();
|
|
181
|
-
let error = initialError;
|
|
182
|
-
if (preferredPlayerId) {
|
|
183
|
-
this.logger.warn(
|
|
184
|
-
"[DevHost] Failed to bootstrap the requested player selection; retrying with the default player:",
|
|
185
|
-
formatErrorForLog(error),
|
|
186
|
-
);
|
|
187
|
-
this.storage.persistPreferredPlayerId(null);
|
|
188
|
-
try {
|
|
189
|
-
await this.loadStoreSnapshot(null, "dev-bootstrap");
|
|
190
|
-
this.syncCurrentSessionFromStore();
|
|
191
|
-
this.persistCurrentPlayerFromStore();
|
|
192
|
-
this.notify();
|
|
193
|
-
return;
|
|
194
|
-
} catch (retryError) {
|
|
195
|
-
error = retryError;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
this.logger.error(
|
|
199
|
-
"[DevHost] Failed to bootstrap the backend session:",
|
|
200
|
-
formatErrorForLog(error),
|
|
201
|
-
);
|
|
202
|
-
this.setRuntimeError(
|
|
203
|
-
convertProblemDetailsToRuntimeError(
|
|
204
|
-
error,
|
|
205
|
-
"Failed to bootstrap the backend session.",
|
|
206
|
-
),
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
this.notify();
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
setSeedValue(value: string): void {
|
|
213
|
-
this.seedValue = value;
|
|
214
|
-
this.notify();
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
async createNewSession(): Promise<void> {
|
|
218
|
-
const nextSeed = Number.parseInt(this.seedValue.trim(), 10);
|
|
219
|
-
if (!Number.isSafeInteger(nextSeed)) {
|
|
220
|
-
this.logger.error("[DevHost] Seed must be a safe integer.");
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
this.isCreatingSession = true;
|
|
225
|
-
this.notify();
|
|
226
|
-
|
|
227
|
-
try {
|
|
228
|
-
this.clearRuntimeError();
|
|
229
|
-
if (this.gateway) {
|
|
230
|
-
this.gateway.disconnect();
|
|
231
|
-
this.gateway = null;
|
|
232
|
-
}
|
|
233
|
-
this.gatewayStoreAttached = false;
|
|
234
|
-
this.pluginReady = false;
|
|
235
|
-
this.reloadPluginFrame();
|
|
236
|
-
this.store.getState().reset();
|
|
237
|
-
const snapshot = await this.createBackendDevSessionSnapshot(nextSeed);
|
|
238
|
-
this.adoptCreatedSession(snapshot, nextSeed);
|
|
239
|
-
await this.loadStoreSnapshot(null, "dev-new");
|
|
240
|
-
this.syncCurrentSessionFromStore(nextSeed);
|
|
241
|
-
this.persistCurrentPlayerFromStore();
|
|
242
|
-
} catch (error) {
|
|
243
|
-
this.logger.error("[DevHost] Failed to create a new session:", error);
|
|
244
|
-
} finally {
|
|
245
|
-
this.isCreatingSession = false;
|
|
246
|
-
this.notify();
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
async startGameFromSidebar(): Promise<void> {
|
|
251
|
-
try {
|
|
252
|
-
this.clearRuntimeError();
|
|
253
|
-
await this.store.getState().startSession({
|
|
254
|
-
sessionId: this.currentSession.sessionId,
|
|
255
|
-
userId: this.config.userId,
|
|
256
|
-
source: "dev-bootstrap",
|
|
257
|
-
});
|
|
258
|
-
this.syncCurrentSessionFromStore();
|
|
259
|
-
this.persistCurrentPlayerFromStore();
|
|
260
|
-
this.notify();
|
|
261
|
-
} catch (error) {
|
|
262
|
-
this.setRuntimeError(
|
|
263
|
-
convertProblemDetailsToRuntimeError(
|
|
264
|
-
error,
|
|
265
|
-
"Failed to start the backend session.",
|
|
266
|
-
),
|
|
267
|
-
);
|
|
268
|
-
this.logger.error(
|
|
269
|
-
"[DevHost] Failed to start the backend session:",
|
|
270
|
-
formatErrorForLog(error),
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
switchPlayer(playerId: string): void {
|
|
276
|
-
void this.switchPlayerFromBootstrap(playerId);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
async restoreHistoryEntry(entryId: string): Promise<void> {
|
|
280
|
-
await this.store.getState().restoreHistory({
|
|
281
|
-
sessionId: this.currentSession.sessionId,
|
|
282
|
-
entryId,
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
setIframe(element: HTMLIFrameElement | null): void {
|
|
287
|
-
this.iframe = element;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
onIframeLoad(): void {
|
|
291
|
-
this.iframeLoaded = true;
|
|
292
|
-
this.connectGateway();
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
matchesPluginWindow(source: MessageEvent["source"]): boolean {
|
|
296
|
-
return Boolean(this.iframe && source === this.iframe.contentWindow);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
handleSseTransportError(args: unknown[]): void {
|
|
300
|
-
if (
|
|
301
|
-
unifiedSessionSelectors.bootstrapStatus(this.store.getState()) !==
|
|
302
|
-
"loading" ||
|
|
303
|
-
this.recoveryInFlight
|
|
304
|
-
) {
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const errorMessage = args
|
|
309
|
-
.map((value) => (value instanceof Error ? value.message : String(value)))
|
|
310
|
-
.join(" ");
|
|
311
|
-
if (!errorMessage.includes("SSE failed: 400")) {
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
this.sessionSnapshotSseFailureCount += 1;
|
|
316
|
-
if (
|
|
317
|
-
this.sessionSnapshotSseFailureCount < AUTO_RECOVERY_SSE_FAILURE_THRESHOLD
|
|
318
|
-
) {
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
void this.recoverFromUnhealthySession(
|
|
323
|
-
"The current session stream is unhealthy, creating a fresh session...",
|
|
324
|
-
);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
dispose(): void {
|
|
328
|
-
this.unsubscribeStore();
|
|
329
|
-
if (this.gateway) {
|
|
330
|
-
this.gateway.disconnect();
|
|
331
|
-
this.gateway = null;
|
|
332
|
-
}
|
|
333
|
-
this.gatewayStoreAttached = false;
|
|
334
|
-
this.store.getState().closeStreams();
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
private notify(): void {
|
|
338
|
-
this.listeners.forEach((listener) => listener());
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
private setRuntimeError(error: DevHostRuntimeError): void {
|
|
342
|
-
this.runtimeError = error;
|
|
343
|
-
this.notify();
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
private clearRuntimeError(): void {
|
|
347
|
-
if (this.runtimeError === null) {
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
this.runtimeError = null;
|
|
351
|
-
this.notify();
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
private connectGateway(): void {
|
|
355
|
-
if (!this.iframeLoaded || !this.iframe) {
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
if (!unifiedSessionSelectors.sessionId(this.store.getState())) {
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const session = this.store.getState().getPluginSnapshot().session;
|
|
364
|
-
|
|
365
|
-
if (this.gateway) {
|
|
366
|
-
this.gateway.disconnect();
|
|
367
|
-
this.gateway = null;
|
|
368
|
-
}
|
|
369
|
-
this.gatewayStoreAttached = false;
|
|
370
|
-
|
|
371
|
-
this.pluginReady = false;
|
|
372
|
-
|
|
373
|
-
this.gateway = new PluginSessionGateway({
|
|
374
|
-
iframe: this.iframe,
|
|
375
|
-
sessionId: this.currentSession.sessionId,
|
|
376
|
-
controllablePlayerIds: session.controllablePlayerIds,
|
|
377
|
-
controllingPlayerId: session.controllingPlayerId ?? "",
|
|
378
|
-
userId: this.config.userId,
|
|
379
|
-
onReady: () => {
|
|
380
|
-
this.pluginReady = true;
|
|
381
|
-
if (hasRenderableSnapshot(this.store)) {
|
|
382
|
-
this.attachStore();
|
|
383
|
-
}
|
|
384
|
-
this.notify();
|
|
385
|
-
},
|
|
386
|
-
onError: (error) => {
|
|
387
|
-
this.pluginReady = false;
|
|
388
|
-
this.logger.error(
|
|
389
|
-
"[DevHost] Plugin iframe failed:",
|
|
390
|
-
error instanceof Error ? error.message : error,
|
|
391
|
-
);
|
|
392
|
-
this.notify();
|
|
393
|
-
},
|
|
394
|
-
onInteraction: async (
|
|
395
|
-
playerId: string,
|
|
396
|
-
interactionId: string,
|
|
397
|
-
params: unknown,
|
|
398
|
-
meta,
|
|
399
|
-
) => {
|
|
400
|
-
try {
|
|
401
|
-
await this.store.getState().submitInteraction({
|
|
402
|
-
sessionId: this.currentSession.sessionId,
|
|
403
|
-
playerId,
|
|
404
|
-
interactionId,
|
|
405
|
-
params,
|
|
406
|
-
clientActionId: meta?.clientActionId,
|
|
407
|
-
});
|
|
408
|
-
} catch (error) {
|
|
409
|
-
if (error instanceof Error && error.name === "SubmissionError") {
|
|
410
|
-
throw error;
|
|
411
|
-
}
|
|
412
|
-
throw createSubmissionError(
|
|
413
|
-
"api-error",
|
|
414
|
-
undefined,
|
|
415
|
-
"Failed to submit interaction",
|
|
416
|
-
);
|
|
417
|
-
}
|
|
418
|
-
},
|
|
419
|
-
onValidateInteraction: async (
|
|
420
|
-
playerId: string,
|
|
421
|
-
interactionId: string,
|
|
422
|
-
params: unknown,
|
|
423
|
-
) => {
|
|
424
|
-
const gameplay = this.store.getState().getRenderableGameplay();
|
|
425
|
-
if (!gameplay) {
|
|
426
|
-
return {
|
|
427
|
-
valid: false,
|
|
428
|
-
errorCode: "runtime-unavailable",
|
|
429
|
-
message: "No renderable gameplay snapshot is available.",
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
void playerId;
|
|
433
|
-
void interactionId;
|
|
434
|
-
void params;
|
|
435
|
-
return { valid: true };
|
|
436
|
-
},
|
|
437
|
-
onSwitchPlayer: (playerId: string) => {
|
|
438
|
-
this.switchPlayer(playerId);
|
|
439
|
-
},
|
|
440
|
-
onRestoreHistory: async (entryId: string) => {
|
|
441
|
-
await this.restoreHistoryEntry(entryId);
|
|
442
|
-
},
|
|
443
|
-
logger: this.logger,
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
this.gateway.connect();
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
private attachStore(): void {
|
|
450
|
-
if (!this.gateway || this.gatewayStoreAttached) {
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
const storeApi: GameSessionStoreApi = {
|
|
455
|
-
getStateSnapshot: this.store.getState().getPluginSnapshot,
|
|
456
|
-
subscribe: (listener: () => void) =>
|
|
457
|
-
this.store.subscribe((_state, _previousState) => {
|
|
458
|
-
listener();
|
|
459
|
-
}),
|
|
460
|
-
onStateAck: this.store.getState().onStateAck,
|
|
461
|
-
markNotificationRead: this.store.getState().markNotificationRead,
|
|
462
|
-
};
|
|
463
|
-
|
|
464
|
-
this.gateway.attachStore(storeApi);
|
|
465
|
-
this.gatewayStoreAttached = true;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
private reloadPluginFrame(): void {
|
|
469
|
-
this.pluginFrameReloadCounter += 1;
|
|
470
|
-
this.iframeLoaded = false;
|
|
471
|
-
this.pluginReady = false;
|
|
472
|
-
this.gatewayStoreAttached = false;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
private async recoverFromUnhealthySession(reason: string): Promise<void> {
|
|
476
|
-
if (
|
|
477
|
-
this.recoveryInFlight ||
|
|
478
|
-
this.autoRecoveryAttempts >= MAX_AUTO_RECOVERY_ATTEMPTS ||
|
|
479
|
-
unifiedSessionSelectors.bootstrapStatus(this.store.getState()) !==
|
|
480
|
-
"loading"
|
|
481
|
-
) {
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
this.recoveryInFlight = true;
|
|
486
|
-
this.autoRecoveryAttempts += 1;
|
|
487
|
-
|
|
488
|
-
try {
|
|
489
|
-
this.logger.warn("[DevHost] " + reason);
|
|
490
|
-
const seed = this.currentSession.seed ?? 1337;
|
|
491
|
-
const snapshot = await this.createBackendDevSessionSnapshot(seed);
|
|
492
|
-
this.adoptCreatedSession(snapshot, seed);
|
|
493
|
-
await this.loadStoreSnapshot(null, "dev-new");
|
|
494
|
-
this.syncCurrentSessionFromStore(seed);
|
|
495
|
-
this.persistCurrentPlayerFromStore();
|
|
496
|
-
} catch (error) {
|
|
497
|
-
this.logger.error(
|
|
498
|
-
"[DevHost] Automatic recovery failed:",
|
|
499
|
-
formatConsoleArgs([error]),
|
|
500
|
-
);
|
|
501
|
-
} finally {
|
|
502
|
-
this.recoveryInFlight = false;
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
private async loadStoreSnapshot(
|
|
507
|
-
playerId: string | null | undefined,
|
|
508
|
-
source: string,
|
|
509
|
-
expectedPerspectivePlayerId?: string | null,
|
|
510
|
-
): Promise<void> {
|
|
511
|
-
await this.store.getState().loadSessionSnapshot({
|
|
512
|
-
sessionId: this.currentSession.sessionId,
|
|
513
|
-
userId: this.config.userId,
|
|
514
|
-
requestedPlayerId: playerId,
|
|
515
|
-
expectedPerspectivePlayerId,
|
|
516
|
-
source,
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
private async createBackendDevSessionSnapshot(
|
|
521
|
-
seed: number,
|
|
522
|
-
): Promise<DevHostSessionSnapshot> {
|
|
523
|
-
if (!this.config.createDevSessionSnapshot) {
|
|
524
|
-
throw new Error("Dev host session transport is not configured.");
|
|
525
|
-
}
|
|
526
|
-
return this.config.createDevSessionSnapshot({ seed });
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
private adoptCreatedSession(
|
|
530
|
-
snapshot: DevHostSessionSnapshot,
|
|
531
|
-
seed: number | null,
|
|
532
|
-
): void {
|
|
533
|
-
this.currentSession = {
|
|
534
|
-
sessionId: snapshot.context.sessionId,
|
|
535
|
-
shortCode: snapshot.context.shortCode,
|
|
536
|
-
gameId: this.config.gameId,
|
|
537
|
-
seed,
|
|
538
|
-
};
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
private resolvePreferredPlayerId(): string | null {
|
|
542
|
-
return (
|
|
543
|
-
this.config.initialPlayerId?.trim() ||
|
|
544
|
-
this.storage.loadPreferredPlayerId()
|
|
545
|
-
);
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
private syncCurrentSessionFromStore(seedOverride?: number | null): void {
|
|
549
|
-
const context = unifiedSessionSelectors.sessionContext(
|
|
550
|
-
this.store.getState(),
|
|
551
|
-
) as SessionContext | null;
|
|
552
|
-
if (!context) {
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
this.currentSession = {
|
|
556
|
-
sessionId: context.identity.sessionId,
|
|
557
|
-
shortCode: context.identity.shortCode,
|
|
558
|
-
gameId: context.identity.gameId,
|
|
559
|
-
seed: seedOverride ?? this.currentSession.seed ?? null,
|
|
560
|
-
};
|
|
561
|
-
this.seedValue = String(this.currentSession.seed ?? 1337);
|
|
562
|
-
if (this.iframeLoaded) {
|
|
563
|
-
this.connectGateway();
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
private persistCurrentPlayerFromStore(): void {
|
|
568
|
-
const currentPlayerId = unifiedSessionSelectors.currentPlayerId(
|
|
569
|
-
this.store.getState(),
|
|
570
|
-
);
|
|
571
|
-
if (currentPlayerId) {
|
|
572
|
-
this.storage.persistPreferredPlayerId(currentPlayerId);
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
private async switchPlayerFromBootstrap(playerId: string): Promise<void> {
|
|
577
|
-
const requestId = ++this.playerSwitchRequestId;
|
|
578
|
-
try {
|
|
579
|
-
await this.loadStoreSnapshot(playerId, "player-switch", playerId);
|
|
580
|
-
if (requestId !== this.playerSwitchRequestId) {
|
|
581
|
-
return;
|
|
582
|
-
}
|
|
583
|
-
this.assertSwitchStoreMatchesPlayer(playerId);
|
|
584
|
-
this.clearRuntimeError();
|
|
585
|
-
this.syncCurrentSessionFromStore();
|
|
586
|
-
this.persistCurrentPlayerFromStore();
|
|
587
|
-
this.notify();
|
|
588
|
-
} catch (error) {
|
|
589
|
-
if (requestId !== this.playerSwitchRequestId) {
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
this.logger.error(
|
|
593
|
-
"[DevHost] Failed to switch player:",
|
|
594
|
-
formatErrorForLog(error),
|
|
595
|
-
);
|
|
596
|
-
this.setRuntimeError(
|
|
597
|
-
convertProblemDetailsToRuntimeError(
|
|
598
|
-
error,
|
|
599
|
-
`Failed to switch to ${playerId}.`,
|
|
600
|
-
),
|
|
601
|
-
);
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
private assertSwitchStoreMatchesPlayer(playerId: string): void {
|
|
606
|
-
const currentPlayerId = unifiedSessionSelectors.currentPlayerId(
|
|
607
|
-
this.store.getState(),
|
|
608
|
-
);
|
|
609
|
-
if (!currentPlayerId) {
|
|
610
|
-
return;
|
|
611
|
-
}
|
|
612
|
-
if (currentPlayerId !== playerId) {
|
|
613
|
-
throw new Error(
|
|
614
|
-
`Switch snapshot resolved ${currentPlayerId} instead of ${playerId}.`,
|
|
615
|
-
);
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
function formatErrorForLog(error: unknown): string {
|
|
621
|
-
return error instanceof Error ? error.message : formatConsoleArgs([error]);
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
type ApiErrorPayload = {
|
|
625
|
-
error?: string;
|
|
626
|
-
message?: string;
|
|
627
|
-
title?: string;
|
|
628
|
-
detail?: string;
|
|
629
|
-
status?: number;
|
|
630
|
-
requestId?: string;
|
|
631
|
-
violations?: Array<{
|
|
632
|
-
message?: string;
|
|
633
|
-
field?: string;
|
|
634
|
-
code?: string;
|
|
635
|
-
}>;
|
|
636
|
-
};
|
|
637
|
-
|
|
638
|
-
/**
|
|
639
|
-
* Convert a backend `ProblemDetails` (or an opaque error object) into the
|
|
640
|
-
* structured shape the dev host overlay renders. We surface every violation
|
|
641
|
-
* intentionally — when a reducer `initialize` throws, the JS stack lives in
|
|
642
|
-
* the second violation entry, so collapsing them into a single string would
|
|
643
|
-
* lose the file and line information we just went to the trouble of
|
|
644
|
-
* preserving in `JsExecutor.jsRejectionToException`.
|
|
645
|
-
*/
|
|
646
|
-
function convertProblemDetailsToRuntimeError(
|
|
647
|
-
error: unknown,
|
|
648
|
-
fallbackMessage: string,
|
|
649
|
-
): DevHostRuntimeError {
|
|
650
|
-
const payload = (error ?? {}) as ApiErrorPayload;
|
|
651
|
-
const violations = (payload.violations ?? [])
|
|
652
|
-
.filter((violation) => typeof violation?.message === "string")
|
|
653
|
-
.map((violation) => ({
|
|
654
|
-
message: violation.message as string,
|
|
655
|
-
field: typeof violation.field === "string" ? violation.field : undefined,
|
|
656
|
-
code: typeof violation.code === "string" ? violation.code : undefined,
|
|
657
|
-
}));
|
|
658
|
-
|
|
659
|
-
const title =
|
|
660
|
-
payload.title?.trim() ||
|
|
661
|
-
(payload.error === "session_invalid" ? "Session expired" : "") ||
|
|
662
|
-
"Game failed to start";
|
|
663
|
-
const summary =
|
|
664
|
-
payload.detail?.trim() ||
|
|
665
|
-
payload.message?.trim() ||
|
|
666
|
-
(error instanceof Error ? error.message : fallbackMessage);
|
|
667
|
-
|
|
668
|
-
return {
|
|
669
|
-
title,
|
|
670
|
-
summary,
|
|
671
|
-
violations,
|
|
672
|
-
correlationId: payload.requestId,
|
|
673
|
-
};
|
|
674
|
-
}
|