@dreamboard-games/cli 0.1.30-alpha.0

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.
Files changed (134) hide show
  1. package/README.md +50 -0
  2. package/dist/agent-verifier/agent-workspace-verifier.mjs +227 -0
  3. package/dist/agent-verifier/chunk-2E5P5NWG.mjs +835 -0
  4. package/dist/agent-verifier/chunk-2GBBP27W.mjs +301 -0
  5. package/dist/agent-verifier/chunk-2NZNKIND.mjs +166 -0
  6. package/dist/agent-verifier/chunk-2QMNAVV4.mjs +14522 -0
  7. package/dist/agent-verifier/chunk-2SZHMP6F.mjs +264 -0
  8. package/dist/agent-verifier/chunk-54TAYXUD.mjs +12 -0
  9. package/dist/agent-verifier/chunk-6A5HRJMQ.mjs +3174 -0
  10. package/dist/agent-verifier/chunk-6UUJEYDV.mjs +213 -0
  11. package/dist/agent-verifier/chunk-7653FPGJ.mjs +381 -0
  12. package/dist/agent-verifier/chunk-BVVNBJM4.mjs +221 -0
  13. package/dist/agent-verifier/chunk-CEDUHGNH.mjs +74 -0
  14. package/dist/agent-verifier/chunk-CEQ2VJWN.mjs +149 -0
  15. package/dist/agent-verifier/chunk-CFU5EWIC.mjs +69 -0
  16. package/dist/agent-verifier/chunk-DTMJCPS4.mjs +730 -0
  17. package/dist/agent-verifier/chunk-EIQWDQWJ.mjs +186 -0
  18. package/dist/agent-verifier/chunk-EOQIV6PS.mjs +649 -0
  19. package/dist/agent-verifier/chunk-HBNDKQT5.mjs +8381 -0
  20. package/dist/agent-verifier/chunk-HJFQDSTU.mjs +225 -0
  21. package/dist/agent-verifier/chunk-LI3ZR3BI.mjs +41 -0
  22. package/dist/agent-verifier/chunk-LM3OZLZG.mjs +48 -0
  23. package/dist/agent-verifier/chunk-MINCYHXN.mjs +106 -0
  24. package/dist/agent-verifier/chunk-MRCUP5SW.mjs +128 -0
  25. package/dist/agent-verifier/chunk-PM3SVG6R.mjs +38 -0
  26. package/dist/agent-verifier/chunk-RBDDIIPM.mjs +19 -0
  27. package/dist/agent-verifier/chunk-RJBLBYHX.mjs +1681 -0
  28. package/dist/agent-verifier/chunk-SHUMAVAP.mjs +59 -0
  29. package/dist/agent-verifier/chunk-SYPLYRGB.mjs +2812 -0
  30. package/dist/agent-verifier/chunk-U6OJN7XS.mjs +8092 -0
  31. package/dist/agent-verifier/chunk-VYJTHSYR.mjs +44 -0
  32. package/dist/agent-verifier/chunk-XYDL7GY6.mjs +10 -0
  33. package/dist/agent-verifier/compile-WNCQQVOF.mjs +313 -0
  34. package/dist/agent-verifier/global-config-WX3ZZIVU.mjs +17 -0
  35. package/dist/agent-verifier/keychain-backend-TNOPQV3Z.mjs +134 -0
  36. package/dist/agent-verifier/local-files-MTPLP62S.mjs +46 -0
  37. package/dist/agent-verifier/local-typecheck-QFYYAZOK.mjs +9 -0
  38. package/dist/agent-verifier/materialize-workspace-EWGZIVOY.mjs +90 -0
  39. package/dist/agent-verifier/project-state-7GR6BQTQ.mjs +32 -0
  40. package/dist/agent-verifier/prompt-3BAINGAQ.mjs +755 -0
  41. package/dist/agent-verifier/reducer-bundle-preflight-C73LEXI2.mjs +23 -0
  42. package/dist/agent-verifier/reducer-contract-preflight-22X7DSZW.mjs +10 -0
  43. package/dist/agent-verifier/reducer-native-test-harness-GMWBUISX.mjs +53 -0
  44. package/dist/agent-verifier/static-scaffold-4YEQME5N.mjs +28 -0
  45. package/dist/agent-verifier/sync-LOQAH4RC.mjs +594 -0
  46. package/dist/agent-verifier/test-YOJERVHN.mjs +356 -0
  47. package/dist/agent-verifier/testing-5K2BJYF2.mjs +674 -0
  48. package/dist/agent-verifier/workspace-codegen-JDZJRSDV.mjs +11 -0
  49. package/dist/agent-verifier/workspace-dependencies-HZ6VVS4G.mjs +14 -0
  50. package/dist/chunk-2H7UOFLK.js +11 -0
  51. package/dist/chunk-2H7UOFLK.js.map +1 -0
  52. package/dist/chunk-3XNJT3RK.js +39809 -0
  53. package/dist/chunk-3XNJT3RK.js.map +1 -0
  54. package/dist/chunk-7FOO4AJI.js +50 -0
  55. package/dist/chunk-7FOO4AJI.js.map +1 -0
  56. package/dist/chunk-TSJVWTJO.js +430 -0
  57. package/dist/chunk-TSJVWTJO.js.map +1 -0
  58. package/dist/dev-host/components/drawer.tsx +132 -0
  59. package/dist/dev-host/components/input.tsx +21 -0
  60. package/dist/dev-host/dev-api-proxy-plugin.ts +328 -0
  61. package/dist/dev-host/dev-author-dom-warnings.ts +100 -0
  62. package/dist/dev-host/dev-diagnostics.ts +62 -0
  63. package/dist/dev-host/dev-fallback-stylesheet.ts +53 -0
  64. package/dist/dev-host/dev-hmr-guard-plugin.ts +47 -0
  65. package/dist/dev-host/dev-host-controller.ts +674 -0
  66. package/dist/dev-host/dev-host-player-query.ts +17 -0
  67. package/dist/dev-host/dev-host-session-transport.ts +52 -0
  68. package/dist/dev-host/dev-host-storage.ts +56 -0
  69. package/dist/dev-host/dev-log-relay-plugin.ts +510 -0
  70. package/dist/dev-host/dev-runtime-config.ts +14 -0
  71. package/dist/dev-host/dev-runtime-platform.ts +335 -0
  72. package/dist/dev-host/dev-virtual-modules-plugin.ts +64 -0
  73. package/dist/dev-host/host-main.css +224 -0
  74. package/dist/dev-host/host-main.tsx +948 -0
  75. package/dist/dev-host/index.html +56 -0
  76. package/dist/dev-host/lib/utils.ts +6 -0
  77. package/dist/dev-host/plugin-main.ts +61 -0
  78. package/dist/dev-host/plugin.html +24 -0
  79. package/dist/dev-host/shared-styles.css +144 -0
  80. package/dist/dev-host/start-dev-server.ts +140 -0
  81. package/dist/dev-host/virtual-modules.d.ts +27 -0
  82. package/dist/global-config-UKSWNDTX.js +15 -0
  83. package/dist/global-config-UKSWNDTX.js.map +1 -0
  84. package/dist/index.js +8473 -0
  85. package/dist/index.js.map +1 -0
  86. package/dist/internal.d.ts +311 -0
  87. package/dist/internal.js +52 -0
  88. package/dist/internal.js.map +1 -0
  89. package/dist/keychain-backend-JHTXAKWC.js +135 -0
  90. package/dist/keychain-backend-JHTXAKWC.js.map +1 -0
  91. package/dist/prompt-GMZABCJC.js +756 -0
  92. package/dist/prompt-GMZABCJC.js.map +1 -0
  93. package/dist/runtime-packages/ui-host-runtime/src/actor-principal.ts +71 -0
  94. package/dist/runtime-packages/ui-host-runtime/src/browser-interaction.ts +139 -0
  95. package/dist/runtime-packages/ui-host-runtime/src/components/host-controls.tsx +374 -0
  96. package/dist/runtime-packages/ui-host-runtime/src/components/host-feedback-toaster.tsx +266 -0
  97. package/dist/runtime-packages/ui-host-runtime/src/components/host-feedback.tsx +212 -0
  98. package/dist/runtime-packages/ui-host-runtime/src/components/host-primitives.tsx +271 -0
  99. package/dist/runtime-packages/ui-host-runtime/src/components/host-session-metadata.tsx +135 -0
  100. package/dist/runtime-packages/ui-host-runtime/src/components/index.ts +5 -0
  101. package/dist/runtime-packages/ui-host-runtime/src/components/perf-overlay.tsx +194 -0
  102. package/dist/runtime-packages/ui-host-runtime/src/gameplay-authority-transport.ts +626 -0
  103. package/dist/runtime-packages/ui-host-runtime/src/host-controls.tsx +1 -0
  104. package/dist/runtime-packages/ui-host-runtime/src/host-feedback.tsx +1 -0
  105. package/dist/runtime-packages/ui-host-runtime/src/host-session-transport.ts +294 -0
  106. package/dist/runtime-packages/ui-host-runtime/src/index.ts +3 -0
  107. package/dist/runtime-packages/ui-host-runtime/src/logger.ts +11 -0
  108. package/dist/runtime-packages/ui-host-runtime/src/perf.ts +324 -0
  109. package/dist/runtime-packages/ui-host-runtime/src/plugin-bridge.ts +195 -0
  110. package/dist/runtime-packages/ui-host-runtime/src/plugin-health-check.ts +138 -0
  111. package/dist/runtime-packages/ui-host-runtime/src/plugin-messages.ts +159 -0
  112. package/dist/runtime-packages/ui-host-runtime/src/plugin-session-gateway.ts +551 -0
  113. package/dist/runtime-packages/ui-host-runtime/src/runtime/index.ts +13 -0
  114. package/dist/runtime-packages/ui-host-runtime/src/screenshot/projection-to-snapshot.ts +122 -0
  115. package/dist/runtime-packages/ui-host-runtime/src/screenshot/static-store-api.ts +26 -0
  116. package/dist/runtime-packages/ui-host-runtime/src/session-ingress-controller.ts +583 -0
  117. package/dist/runtime-packages/ui-host-runtime/src/session-ingress.ts +219 -0
  118. package/dist/runtime-packages/ui-host-runtime/src/session-live-runtime.ts +117 -0
  119. package/dist/runtime-packages/ui-host-runtime/src/session-model.ts +431 -0
  120. package/dist/runtime-packages/ui-host-runtime/src/session-projection.ts +211 -0
  121. package/dist/runtime-packages/ui-host-runtime/src/session-recovery.ts +80 -0
  122. package/dist/runtime-packages/ui-host-runtime/src/session-state-reducer.ts +1034 -0
  123. package/dist/runtime-packages/ui-host-runtime/src/sse-manager.ts +416 -0
  124. package/dist/runtime-packages/ui-host-runtime/src/unified-session-store.ts +184 -0
  125. package/dist/scaffold/assets/static/app/tsconfig.framework.json +26 -0
  126. package/dist/scaffold/assets/static/app/tsconfig.json +3 -0
  127. package/dist/scaffold/assets/static/ui/index.tsx +13 -0
  128. package/dist/scaffold/assets/static/ui/style.css +4 -0
  129. package/dist/scaffold/assets/static/ui/tsconfig.framework.json +19 -0
  130. package/dist/scaffold/assets/static/ui/tsconfig.json +5 -0
  131. package/dist/testing-KLSV6CPJ.js +674 -0
  132. package/dist/testing-KLSV6CPJ.js.map +1 -0
  133. package/package.json +72 -0
  134. package/skills/dreamboard/SKILL.md +130 -0
@@ -0,0 +1,583 @@
1
+ import type { StoreApi } from "zustand/vanilla";
2
+ import type { ValidationResult } from "@dreamboard-games/sdk/runtime/runtime-api";
3
+ import type { LoggerLike } from "./logger.js";
4
+ import {
5
+ PERF_MARK_NAMES,
6
+ correlateVersion,
7
+ correlateSyncId,
8
+ findActionIdByVersion,
9
+ recordMark,
10
+ } from "./perf.js";
11
+ import type { HostSessionTransport } from "./host-session-transport.js";
12
+ import type {
13
+ HostActionSubmitWireResponse,
14
+ NormalizedSubmitResponse,
15
+ } from "./session-ingress.js";
16
+ import {
17
+ normalizeEvent,
18
+ normalizeSnapshot,
19
+ normalizeSubmitResponse,
20
+ } from "./session-ingress.js";
21
+ import { getSessionRecoveryDetails } from "./session-recovery.js";
22
+ import {
23
+ getGameplayViewport,
24
+ getSessionContext,
25
+ resolveControllablePlayerIds,
26
+ } from "./session-model.js";
27
+ import type { SSEManagerLike } from "./session-live-runtime.js";
28
+ import { createSessionLiveRuntime } from "./session-live-runtime.js";
29
+ import {
30
+ createInitialUnifiedSessionState,
31
+ reduceSessionState,
32
+ type SessionStateEffect,
33
+ type SessionStateIngress,
34
+ type SessionStateReducerEnvironment,
35
+ type UnifiedSessionState,
36
+ } from "./session-state-reducer.js";
37
+
38
+ export interface SessionIngressControllerActions {
39
+ loadSessionByShortCode: (input: {
40
+ shortCode: string;
41
+ userId?: string | null;
42
+ requestedPlayerId?: string | null;
43
+ source?: string;
44
+ }) => Promise<void>;
45
+ loadSessionSnapshot: (input: {
46
+ sessionId: string;
47
+ userId?: string | null;
48
+ requestedPlayerId?: string | null;
49
+ expectedPerspectivePlayerId?: string | null;
50
+ source?: string;
51
+ }) => Promise<void>;
52
+ startSession: (input: {
53
+ sessionId: string;
54
+ userId?: string | null;
55
+ source?: string;
56
+ }) => Promise<void>;
57
+ submitInteraction: (input: {
58
+ sessionId: string;
59
+ playerId: string;
60
+ interactionId: string;
61
+ params: unknown;
62
+ clientActionId?: string | null;
63
+ }) => Promise<void>;
64
+ validateInteraction: (input: {
65
+ sessionId: string;
66
+ playerId: string;
67
+ interactionId: string;
68
+ params: unknown;
69
+ }) => Promise<ValidationResult>;
70
+ restoreHistory: (input: {
71
+ sessionId: string;
72
+ entryId: string;
73
+ }) => Promise<void>;
74
+ selectPlayer: (playerId: string) => void;
75
+ clearConnectionError: () => void;
76
+ enqueueActionRejected: (reason: string, targetPlayer?: string) => void;
77
+ closeStreams: () => void;
78
+ onStateAck: (syncId: number) => void;
79
+ markNotificationRead: (id: string) => void;
80
+ clearNotifications: () => void;
81
+ dismissHostFeedback: (id: string) => void;
82
+ clearHostFeedback: () => void;
83
+ clearSSEEvents: () => void;
84
+ reset: () => void;
85
+ }
86
+
87
+ export interface CreateSessionIngressControllerOptions<
88
+ TStore extends UnifiedSessionState,
89
+ > {
90
+ store: StoreApi<TStore>;
91
+ createSseManager: () => SSEManagerLike;
92
+ transport: HostSessionTransport;
93
+ logger: LoggerLike;
94
+ fallbackToAllSeatsWhenUserIdMissing: boolean;
95
+ reducerEnvironment: SessionStateReducerEnvironment;
96
+ }
97
+
98
+ function describeSseError(error: unknown): string {
99
+ if (error instanceof Error) {
100
+ if (
101
+ error.message.includes("429") ||
102
+ error.message.includes("400 Bad Request")
103
+ ) {
104
+ return "Too many live views are open for this session. Close unused tabs or devices, then refresh.";
105
+ }
106
+ return "Live session updates disconnected. Check your network and refresh to reconnect.";
107
+ }
108
+ return "Live session updates disconnected. Refresh to reconnect.";
109
+ }
110
+
111
+ function describeCommandFailure(error: unknown, fallback: string): string {
112
+ if (error instanceof Error && error.message.trim()) {
113
+ return error.message;
114
+ }
115
+ const payload = error as { detail?: unknown; message?: unknown } | null;
116
+ if (typeof payload?.detail === "string" && payload.detail.trim()) {
117
+ return payload.detail;
118
+ }
119
+ if (typeof payload?.message === "string" && payload.message.trim()) {
120
+ return payload.message;
121
+ }
122
+ return fallback;
123
+ }
124
+
125
+ function createSubmissionError(
126
+ errorCode: string | undefined,
127
+ message: string | undefined,
128
+ ): Error & { errorCode?: string } {
129
+ const error = new Error(message ?? "Interaction rejected") as Error & {
130
+ errorCode?: string;
131
+ };
132
+ error.name = "SubmissionError";
133
+ error.errorCode = errorCode;
134
+ return error;
135
+ }
136
+
137
+ function sleep(ms: number): Promise<void> {
138
+ return new Promise((resolve) => setTimeout(resolve, ms));
139
+ }
140
+
141
+ export function createSessionIngressController<
142
+ TStore extends UnifiedSessionState,
143
+ >(
144
+ options: CreateSessionIngressControllerOptions<TStore>,
145
+ ): SessionIngressControllerActions {
146
+ const {
147
+ store,
148
+ transport,
149
+ logger,
150
+ fallbackToAllSeatsWhenUserIdMissing,
151
+ reducerEnvironment,
152
+ } = options;
153
+
154
+ const dispatchIngress = (ingress: SessionStateIngress) => {
155
+ const { state, effects } = reduceSessionState(
156
+ store.getState(),
157
+ ingress,
158
+ reducerEnvironment,
159
+ );
160
+ store.setState(state as Partial<TStore>);
161
+ effects.forEach(executeEffect);
162
+ };
163
+
164
+ const runtime = createSessionLiveRuntime({
165
+ createSseManager: options.createSseManager,
166
+ onConnectionChange: (channel, connected) => {
167
+ dispatchIngress({ type: "connection.changed", channel, connected });
168
+ },
169
+ onConnectionError: (error) => {
170
+ logger.error("[UnifiedSession] SSE connection error:", error);
171
+ dispatchIngress({
172
+ type: "connection.failed",
173
+ message: describeSseError(error),
174
+ });
175
+ },
176
+ onConnectionRecovering: (details) => {
177
+ dispatchIngress({
178
+ type: "connection.recovering",
179
+ message: details.message,
180
+ retryAfterMs: details.retryAfterMs,
181
+ attempt: details.attempt,
182
+ });
183
+ },
184
+ onLiveMessage: (message) => {
185
+ const state = store.getState();
186
+ const context = getSessionContext(state.session);
187
+ const event = normalizeEvent(message, {
188
+ currentContext: context,
189
+ currentGameplay: getGameplayViewport(state.session),
190
+ userId: context?.userId ?? null,
191
+ });
192
+ dispatchIngress({
193
+ type: "event.received",
194
+ source: "sse",
195
+ event,
196
+ debugEvent: { eventType: message.type, data: message },
197
+ clientActionId:
198
+ message.type === "session.gameplayUpdated"
199
+ ? message.causation?.clientActionId
200
+ : undefined,
201
+ });
202
+ },
203
+ });
204
+
205
+ function executeEffect(effect: SessionStateEffect): void {
206
+ switch (effect.type) {
207
+ case "requestGameplayResync":
208
+ logger.warn(
209
+ `[UnifiedSession] ${effect.reason}; reconnecting to refresh gameplay.bootstrap`,
210
+ );
211
+ runtime.requestGameplayResync(
212
+ effect.sessionId,
213
+ effect.playerId,
214
+ "board-static-resync",
215
+ );
216
+ break;
217
+ case "reconnectGameplay":
218
+ runtime.reconnectGameplay(
219
+ effect.sessionId,
220
+ effect.playerId,
221
+ effect.source,
222
+ );
223
+ break;
224
+ case "perf.storeApplied": {
225
+ const actionId = findActionIdByVersion(effect.gameplayVersion);
226
+ if (!actionId) break;
227
+ recordMark(actionId, PERF_MARK_NAMES.T5_STORE_APPLIED, {
228
+ extra: {
229
+ version: effect.gameplayVersion,
230
+ syncId: effect.syncId,
231
+ },
232
+ });
233
+ correlateSyncId(actionId, effect.syncId);
234
+ break;
235
+ }
236
+ case "log.warn":
237
+ logger.warn(effect.message);
238
+ break;
239
+ }
240
+ }
241
+
242
+ function connectLiveSession(
243
+ sessionId: string,
244
+ userId: string | null,
245
+ connectOptions: { source?: string; playerId?: string } = {},
246
+ ) {
247
+ const context = getSessionContext(store.getState().session);
248
+ const controllablePlayerIds = context
249
+ ? resolveControllablePlayerIds(
250
+ context.switchablePlayerIds,
251
+ context.seats,
252
+ userId,
253
+ fallbackToAllSeatsWhenUserIdMissing,
254
+ )
255
+ : [];
256
+ const session = store.getState().session;
257
+ const playerId =
258
+ connectOptions.playerId ??
259
+ (session.type === "gameplay"
260
+ ? session.perspective.playerId
261
+ : controllablePlayerIds[0]);
262
+ dispatchIngress({
263
+ type: "connection.prepared",
264
+ userId,
265
+ switchablePlayerIds: controllablePlayerIds,
266
+ });
267
+ logger.log(`[UnifiedSession] Connecting to session: ${sessionId}`);
268
+ runtime.connectSession(sessionId, {
269
+ source: connectOptions.source,
270
+ playerId,
271
+ });
272
+ }
273
+
274
+ async function runSnapshotCommand(
275
+ input: {
276
+ target?: { sessionId?: string; shortCode?: string };
277
+ sourceLabel: "short-code" | "snapshot" | "start" | "dev-new";
278
+ connectSource?: string;
279
+ userId?: string | null;
280
+ expectedPerspectivePlayerId?: string | null;
281
+ preserveCurrentOnFailure?: boolean;
282
+ load: () => Promise<
283
+ Awaited<ReturnType<HostSessionTransport["loadSessionSnapshot"]>>
284
+ >;
285
+ },
286
+ failureMessage: string,
287
+ ): Promise<void> {
288
+ if (!input.preserveCurrentOnFailure) {
289
+ dispatchIngress({ type: "command.loading", target: input.target });
290
+ }
291
+ let recoveryAttempt = 0;
292
+ while (true) {
293
+ try {
294
+ const snapshot = await input.load();
295
+ const normalized = normalizeSnapshot(snapshot, {
296
+ userId: input.userId ?? null,
297
+ previousGameplay: getGameplayViewport(store.getState().session),
298
+ });
299
+ dispatchIngress({
300
+ type: "snapshot.loaded",
301
+ source: input.sourceLabel,
302
+ snapshot: normalized,
303
+ expectedPerspectivePlayerId: input.expectedPerspectivePlayerId,
304
+ });
305
+ connectLiveSession(
306
+ normalized.context.identity.sessionId,
307
+ input.userId ?? null,
308
+ {
309
+ source: input.connectSource,
310
+ playerId:
311
+ normalized.type === "gameplay"
312
+ ? normalized.perspective.playerId
313
+ : undefined,
314
+ },
315
+ );
316
+ return;
317
+ } catch (error) {
318
+ const recovery = getSessionRecoveryDetails(error);
319
+ if (recovery && recoveryAttempt < 60) {
320
+ recoveryAttempt += 1;
321
+ dispatchIngress({
322
+ type: "connection.recovering",
323
+ message: recovery.message,
324
+ retryAfterMs: recovery.retryAfterMs,
325
+ attempt: recoveryAttempt,
326
+ });
327
+ await sleep(recovery.retryAfterMs);
328
+ continue;
329
+ }
330
+ if (!input.preserveCurrentOnFailure) {
331
+ dispatchIngress({
332
+ type: "command.failed",
333
+ message: describeCommandFailure(error, failureMessage),
334
+ });
335
+ }
336
+ throw error;
337
+ }
338
+ }
339
+ }
340
+
341
+ function normalizeSubmitResponseForCurrentState(
342
+ response: HostActionSubmitWireResponse,
343
+ ): NormalizedSubmitResponse {
344
+ const state = store.getState();
345
+ const context = getSessionContext(state.session);
346
+ return normalizeSubmitResponse(response, {
347
+ currentContext: context,
348
+ currentGameplay: getGameplayViewport(state.session),
349
+ userId: context?.userId ?? null,
350
+ });
351
+ }
352
+
353
+ function dispatchSubmitResponse(
354
+ event: NormalizedSubmitResponse,
355
+ clientActionId?: string | null,
356
+ ) {
357
+ if (!event) return;
358
+ if (clientActionId && event.type === "session.gameplayUpdated") {
359
+ recordMark(clientActionId, PERF_MARK_NAMES.T3B_RESPONSE_APPLIED, {
360
+ extra: { version: event.gameplay.version },
361
+ });
362
+ }
363
+ dispatchIngress({
364
+ type: "event.received",
365
+ source: "submit-response",
366
+ event,
367
+ clientActionId,
368
+ });
369
+ }
370
+
371
+ return {
372
+ loadSessionByShortCode: async (input) => {
373
+ await runSnapshotCommand(
374
+ {
375
+ target: { shortCode: input.shortCode },
376
+ sourceLabel: "short-code",
377
+ connectSource: input.source,
378
+ userId: input.userId,
379
+ load: () =>
380
+ transport.loadSessionByShortCode({
381
+ shortCode: input.shortCode,
382
+ requestedPlayerId: input.requestedPlayerId,
383
+ }),
384
+ },
385
+ "Failed to load session by short code.",
386
+ );
387
+ },
388
+ loadSessionSnapshot: async (input) => {
389
+ await runSnapshotCommand(
390
+ {
391
+ target: { sessionId: input.sessionId },
392
+ sourceLabel: input.source === "dev-new" ? "dev-new" : "snapshot",
393
+ connectSource: input.source,
394
+ userId: input.userId,
395
+ expectedPerspectivePlayerId: input.expectedPerspectivePlayerId,
396
+ preserveCurrentOnFailure: input.source === "player-switch",
397
+ load: () =>
398
+ transport.loadSessionSnapshot({
399
+ sessionId: input.sessionId,
400
+ requestedPlayerId: input.requestedPlayerId,
401
+ }),
402
+ },
403
+ "Failed to load session snapshot.",
404
+ );
405
+ },
406
+ startSession: async (input) => {
407
+ await runSnapshotCommand(
408
+ {
409
+ target: { sessionId: input.sessionId },
410
+ sourceLabel: "start",
411
+ connectSource: input.source,
412
+ userId: input.userId,
413
+ load: () => transport.startSession({ sessionId: input.sessionId }),
414
+ },
415
+ "Failed to start session.",
416
+ );
417
+ },
418
+ submitInteraction: async (input) => {
419
+ const gameplay = getGameplayViewport(store.getState().session);
420
+ if (!gameplay) {
421
+ throw new Error("No renderable gameplay snapshot is available.");
422
+ }
423
+ if (input.clientActionId) {
424
+ recordMark(input.clientActionId, PERF_MARK_NAMES.T2_HTTP_SENT, {
425
+ extra: {
426
+ playerId: input.playerId,
427
+ interactionId: input.interactionId,
428
+ expectedVersion: gameplay.version,
429
+ },
430
+ });
431
+ }
432
+ let response: HostActionSubmitWireResponse | null = null;
433
+ let recoveryAttempt = 0;
434
+ while (!response) {
435
+ try {
436
+ response = await transport.submitInteraction({
437
+ sessionId: input.sessionId,
438
+ playerId: input.playerId,
439
+ interactionId: input.interactionId,
440
+ expectedVersion: gameplay.version,
441
+ actionSetVersion: gameplay.actionSetVersion,
442
+ params: input.params,
443
+ clientActionId: input.clientActionId,
444
+ });
445
+ } catch (error) {
446
+ const recovery = getSessionRecoveryDetails(error);
447
+ if (recovery && recoveryAttempt < 60) {
448
+ recoveryAttempt += 1;
449
+ dispatchIngress({
450
+ type: "connection.recovering",
451
+ message: recovery.message,
452
+ retryAfterMs: recovery.retryAfterMs,
453
+ attempt: recoveryAttempt,
454
+ });
455
+ await sleep(recovery.retryAfterMs);
456
+ continue;
457
+ }
458
+ const message = describeCommandFailure(error, "Interaction rejected");
459
+ dispatchIngress({
460
+ type: "feedback.actionRejected",
461
+ reason: message,
462
+ targetPlayer: input.playerId,
463
+ });
464
+ throw createSubmissionError("api-error", message);
465
+ }
466
+ }
467
+ const normalizedResponse =
468
+ normalizeSubmitResponseForCurrentState(response);
469
+ if (input.clientActionId) {
470
+ recordMark(input.clientActionId, PERF_MARK_NAMES.T3_HTTP_RESPONSE, {
471
+ extra: {
472
+ accepted: response.accepted,
473
+ errorCode: response.errorCode,
474
+ version: response.version,
475
+ transport: "ok",
476
+ },
477
+ });
478
+ const acceptedGameplayVersion =
479
+ normalizedResponse?.type === "session.gameplayUpdated"
480
+ ? normalizedResponse.gameplay.version
481
+ : response.version;
482
+ if (
483
+ acceptedGameplayVersion !== undefined &&
484
+ response.accepted !== false
485
+ ) {
486
+ correlateVersion(input.clientActionId, acceptedGameplayVersion);
487
+ }
488
+ }
489
+ if (response.accepted !== false) {
490
+ dispatchSubmitResponse(normalizedResponse, input.clientActionId);
491
+ return;
492
+ }
493
+ dispatchIngress({
494
+ type: "feedback.actionRejected",
495
+ reason: response.message ?? "Interaction rejected",
496
+ targetPlayer: input.playerId,
497
+ });
498
+ throw createSubmissionError(
499
+ response.errorCode ?? undefined,
500
+ response.message ?? "Interaction rejected",
501
+ );
502
+ },
503
+ validateInteraction: async (input) => {
504
+ const gameplay = getGameplayViewport(store.getState().session);
505
+ if (!gameplay) {
506
+ return {
507
+ valid: false,
508
+ errorCode: "runtime-unavailable",
509
+ message: "No renderable gameplay snapshot is available.",
510
+ };
511
+ }
512
+ return transport.validateInteraction({
513
+ sessionId: input.sessionId,
514
+ playerId: input.playerId,
515
+ interactionId: input.interactionId,
516
+ expectedVersion: gameplay.version,
517
+ actionSetVersion: gameplay.actionSetVersion,
518
+ params: input.params,
519
+ });
520
+ },
521
+ restoreHistory: async (input) => {
522
+ const event = await transport.restoreHistory(input);
523
+ if (event) {
524
+ dispatchIngress({
525
+ type: "event.received",
526
+ source: "submit-response",
527
+ event: normalizeEvent(event, {
528
+ currentContext: getSessionContext(store.getState().session),
529
+ currentGameplay: getGameplayViewport(store.getState().session),
530
+ userId: getSessionContext(store.getState().session)?.userId ?? null,
531
+ }),
532
+ debugEvent: { eventType: event.type, data: event },
533
+ });
534
+ }
535
+ },
536
+ selectPlayer: (playerId) => {
537
+ const session = store.getState().session;
538
+ const currentPlayerId =
539
+ session.type === "gameplay"
540
+ ? session.perspective.playerId
541
+ : session.type === "gameplayLoading"
542
+ ? session.requestedPlayerId
543
+ : null;
544
+ if (playerId === currentPlayerId) {
545
+ return;
546
+ }
547
+ const context = getSessionContext(session);
548
+ if (!context) return;
549
+ dispatchIngress({
550
+ type: "local.playerSelected",
551
+ playerId,
552
+ sessionId:
553
+ runtime.getConnectedSessionId() ?? context.identity.sessionId,
554
+ });
555
+ },
556
+ clearConnectionError: () =>
557
+ dispatchIngress({ type: "connection.errorCleared" }),
558
+ enqueueActionRejected: (reason, targetPlayer) =>
559
+ dispatchIngress({
560
+ type: "feedback.actionRejected",
561
+ reason,
562
+ targetPlayer,
563
+ }),
564
+ closeStreams: () => {
565
+ runtime.closeStreams();
566
+ dispatchIngress({ type: "streams.closed" });
567
+ },
568
+ onStateAck: () => dispatchIngress({ type: "activity.stateAcked" }),
569
+ markNotificationRead: (id) =>
570
+ dispatchIngress({ type: "activity.notificationRead", id }),
571
+ clearNotifications: () =>
572
+ dispatchIngress({ type: "activity.notificationsCleared" }),
573
+ dismissHostFeedback: (id) =>
574
+ dispatchIngress({ type: "activity.hostFeedbackDismissed", id }),
575
+ clearHostFeedback: () =>
576
+ dispatchIngress({ type: "activity.hostFeedbackCleared" }),
577
+ clearSSEEvents: () => dispatchIngress({ type: "debug.sseEventsCleared" }),
578
+ reset: () => {
579
+ runtime.closeStreams();
580
+ store.setState(createInitialUnifiedSessionState() as Partial<TStore>);
581
+ },
582
+ };
583
+ }