@dreamboard-games/cli 0.1.30-alpha.0 → 0.1.30-alpha.2

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 (114) hide show
  1. package/README.md +179 -22
  2. package/dist/{chunk-TSJVWTJO.js → chunk-N7XPNNUI.js} +14 -12
  3. package/dist/chunk-N7XPNNUI.js.map +1 -0
  4. package/dist/chunk-SEGVTWSK.js +44 -0
  5. package/dist/{chunk-3XNJT3RK.js → chunk-TAQKH67O.js} +21279 -35845
  6. package/dist/chunk-TAQKH67O.js.map +1 -0
  7. package/dist/{global-config-UKSWNDTX.js → global-config-S4ZIPECE.js} +3 -3
  8. package/dist/index.js +955 -230
  9. package/dist/index.js.map +1 -1
  10. package/dist/internal.js +3 -4
  11. package/dist/{agent-verifier/keychain-backend-TNOPQV3Z.mjs → keychain-backend-HDF4TZDL.js} +2 -1
  12. package/dist/{agent-verifier/prompt-3BAINGAQ.mjs → prompt-NDV3AE5L.js} +2 -1
  13. package/package.json +6 -6
  14. package/skills/dreamboard/references/building-your-first-game.md +510 -0
  15. package/skills/dreamboard/references/cli.md +104 -0
  16. package/skills/dreamboard/references/game-interface.md +548 -0
  17. package/skills/dreamboard/references/manifest-authoring.md +597 -0
  18. package/skills/dreamboard/references/quickstart.md +66 -0
  19. package/skills/dreamboard/references/reducer.md +864 -0
  20. package/skills/dreamboard/references/rule-authoring.md +147 -0
  21. package/skills/dreamboard/references/testing.md +249 -0
  22. package/skills/dreamboard/scripts/events-extract.mjs +218 -0
  23. package/dist/agent-verifier/agent-workspace-verifier.mjs +0 -227
  24. package/dist/agent-verifier/chunk-2E5P5NWG.mjs +0 -835
  25. package/dist/agent-verifier/chunk-2GBBP27W.mjs +0 -301
  26. package/dist/agent-verifier/chunk-2NZNKIND.mjs +0 -166
  27. package/dist/agent-verifier/chunk-2QMNAVV4.mjs +0 -14522
  28. package/dist/agent-verifier/chunk-2SZHMP6F.mjs +0 -264
  29. package/dist/agent-verifier/chunk-54TAYXUD.mjs +0 -12
  30. package/dist/agent-verifier/chunk-6A5HRJMQ.mjs +0 -3174
  31. package/dist/agent-verifier/chunk-6UUJEYDV.mjs +0 -213
  32. package/dist/agent-verifier/chunk-7653FPGJ.mjs +0 -381
  33. package/dist/agent-verifier/chunk-BVVNBJM4.mjs +0 -221
  34. package/dist/agent-verifier/chunk-CEDUHGNH.mjs +0 -74
  35. package/dist/agent-verifier/chunk-CEQ2VJWN.mjs +0 -149
  36. package/dist/agent-verifier/chunk-CFU5EWIC.mjs +0 -69
  37. package/dist/agent-verifier/chunk-DTMJCPS4.mjs +0 -730
  38. package/dist/agent-verifier/chunk-EIQWDQWJ.mjs +0 -186
  39. package/dist/agent-verifier/chunk-EOQIV6PS.mjs +0 -649
  40. package/dist/agent-verifier/chunk-HBNDKQT5.mjs +0 -8381
  41. package/dist/agent-verifier/chunk-HJFQDSTU.mjs +0 -225
  42. package/dist/agent-verifier/chunk-LI3ZR3BI.mjs +0 -41
  43. package/dist/agent-verifier/chunk-LM3OZLZG.mjs +0 -48
  44. package/dist/agent-verifier/chunk-MINCYHXN.mjs +0 -106
  45. package/dist/agent-verifier/chunk-MRCUP5SW.mjs +0 -128
  46. package/dist/agent-verifier/chunk-PM3SVG6R.mjs +0 -38
  47. package/dist/agent-verifier/chunk-RBDDIIPM.mjs +0 -19
  48. package/dist/agent-verifier/chunk-RJBLBYHX.mjs +0 -1681
  49. package/dist/agent-verifier/chunk-SHUMAVAP.mjs +0 -59
  50. package/dist/agent-verifier/chunk-SYPLYRGB.mjs +0 -2812
  51. package/dist/agent-verifier/chunk-U6OJN7XS.mjs +0 -8092
  52. package/dist/agent-verifier/chunk-VYJTHSYR.mjs +0 -44
  53. package/dist/agent-verifier/chunk-XYDL7GY6.mjs +0 -10
  54. package/dist/agent-verifier/compile-WNCQQVOF.mjs +0 -313
  55. package/dist/agent-verifier/global-config-WX3ZZIVU.mjs +0 -17
  56. package/dist/agent-verifier/local-files-MTPLP62S.mjs +0 -46
  57. package/dist/agent-verifier/local-typecheck-QFYYAZOK.mjs +0 -9
  58. package/dist/agent-verifier/materialize-workspace-EWGZIVOY.mjs +0 -90
  59. package/dist/agent-verifier/project-state-7GR6BQTQ.mjs +0 -32
  60. package/dist/agent-verifier/reducer-bundle-preflight-C73LEXI2.mjs +0 -23
  61. package/dist/agent-verifier/reducer-contract-preflight-22X7DSZW.mjs +0 -10
  62. package/dist/agent-verifier/reducer-native-test-harness-GMWBUISX.mjs +0 -53
  63. package/dist/agent-verifier/static-scaffold-4YEQME5N.mjs +0 -28
  64. package/dist/agent-verifier/sync-LOQAH4RC.mjs +0 -594
  65. package/dist/agent-verifier/test-YOJERVHN.mjs +0 -356
  66. package/dist/agent-verifier/testing-5K2BJYF2.mjs +0 -674
  67. package/dist/agent-verifier/workspace-codegen-JDZJRSDV.mjs +0 -11
  68. package/dist/agent-verifier/workspace-dependencies-HZ6VVS4G.mjs +0 -14
  69. package/dist/chunk-2H7UOFLK.js +0 -11
  70. package/dist/chunk-3XNJT3RK.js.map +0 -1
  71. package/dist/chunk-7FOO4AJI.js +0 -50
  72. package/dist/chunk-7FOO4AJI.js.map +0 -1
  73. package/dist/chunk-TSJVWTJO.js.map +0 -1
  74. package/dist/internal.d.ts +0 -311
  75. package/dist/keychain-backend-JHTXAKWC.js +0 -135
  76. package/dist/prompt-GMZABCJC.js +0 -756
  77. package/dist/runtime-packages/ui-host-runtime/src/actor-principal.ts +0 -71
  78. package/dist/runtime-packages/ui-host-runtime/src/browser-interaction.ts +0 -139
  79. package/dist/runtime-packages/ui-host-runtime/src/components/host-controls.tsx +0 -374
  80. package/dist/runtime-packages/ui-host-runtime/src/components/host-feedback-toaster.tsx +0 -266
  81. package/dist/runtime-packages/ui-host-runtime/src/components/host-feedback.tsx +0 -212
  82. package/dist/runtime-packages/ui-host-runtime/src/components/host-primitives.tsx +0 -271
  83. package/dist/runtime-packages/ui-host-runtime/src/components/host-session-metadata.tsx +0 -135
  84. package/dist/runtime-packages/ui-host-runtime/src/components/index.ts +0 -5
  85. package/dist/runtime-packages/ui-host-runtime/src/components/perf-overlay.tsx +0 -194
  86. package/dist/runtime-packages/ui-host-runtime/src/gameplay-authority-transport.ts +0 -626
  87. package/dist/runtime-packages/ui-host-runtime/src/host-controls.tsx +0 -1
  88. package/dist/runtime-packages/ui-host-runtime/src/host-feedback.tsx +0 -1
  89. package/dist/runtime-packages/ui-host-runtime/src/host-session-transport.ts +0 -294
  90. package/dist/runtime-packages/ui-host-runtime/src/index.ts +0 -3
  91. package/dist/runtime-packages/ui-host-runtime/src/logger.ts +0 -11
  92. package/dist/runtime-packages/ui-host-runtime/src/perf.ts +0 -324
  93. package/dist/runtime-packages/ui-host-runtime/src/plugin-bridge.ts +0 -195
  94. package/dist/runtime-packages/ui-host-runtime/src/plugin-health-check.ts +0 -138
  95. package/dist/runtime-packages/ui-host-runtime/src/plugin-messages.ts +0 -159
  96. package/dist/runtime-packages/ui-host-runtime/src/plugin-session-gateway.ts +0 -551
  97. package/dist/runtime-packages/ui-host-runtime/src/runtime/index.ts +0 -13
  98. package/dist/runtime-packages/ui-host-runtime/src/screenshot/projection-to-snapshot.ts +0 -122
  99. package/dist/runtime-packages/ui-host-runtime/src/screenshot/static-store-api.ts +0 -26
  100. package/dist/runtime-packages/ui-host-runtime/src/session-ingress-controller.ts +0 -583
  101. package/dist/runtime-packages/ui-host-runtime/src/session-ingress.ts +0 -219
  102. package/dist/runtime-packages/ui-host-runtime/src/session-live-runtime.ts +0 -117
  103. package/dist/runtime-packages/ui-host-runtime/src/session-model.ts +0 -431
  104. package/dist/runtime-packages/ui-host-runtime/src/session-projection.ts +0 -211
  105. package/dist/runtime-packages/ui-host-runtime/src/session-recovery.ts +0 -80
  106. package/dist/runtime-packages/ui-host-runtime/src/session-state-reducer.ts +0 -1034
  107. package/dist/runtime-packages/ui-host-runtime/src/sse-manager.ts +0 -416
  108. package/dist/runtime-packages/ui-host-runtime/src/unified-session-store.ts +0 -184
  109. package/dist/testing-KLSV6CPJ.js +0 -674
  110. package/dist/testing-KLSV6CPJ.js.map +0 -1
  111. /package/dist/{chunk-2H7UOFLK.js.map → chunk-SEGVTWSK.js.map} +0 -0
  112. /package/dist/{global-config-UKSWNDTX.js.map → global-config-S4ZIPECE.js.map} +0 -0
  113. /package/dist/{keychain-backend-JHTXAKWC.js.map → keychain-backend-HDF4TZDL.js.map} +0 -0
  114. /package/dist/{prompt-GMZABCJC.js.map → prompt-NDV3AE5L.js.map} +0 -0
@@ -1,294 +0,0 @@
1
- import {
2
- getDemoSessionByShortCode,
3
- getDemoSessionEventBatch,
4
- getDemoSessionSnapshot,
5
- getSessionByShortCode,
6
- getSessionEventBatch,
7
- getSessionSnapshot,
8
- startDemoGame,
9
- startGame,
10
- } from "@dreamboard-games/api-client";
11
- import type { ValidationResult } from "@dreamboard-games/sdk/runtime/runtime-api";
12
- import type {
13
- HostActionSubmitWireResponse,
14
- HostSessionWireEvent,
15
- HostSessionWireSnapshot,
16
- } from "./session-ingress.js";
17
-
18
- export interface HostSessionStream {
19
- stream: AsyncIterable<HostSessionWireEvent | null | undefined>;
20
- }
21
-
22
- export interface HostSessionTransport {
23
- loadSessionByShortCode(input: {
24
- shortCode: string;
25
- requestedPlayerId?: string | null;
26
- }): Promise<HostSessionWireSnapshot>;
27
- loadSessionSnapshot(input: {
28
- sessionId: string;
29
- requestedPlayerId?: string | null;
30
- }): Promise<HostSessionWireSnapshot>;
31
- startSession(input: { sessionId: string }): Promise<HostSessionWireSnapshot>;
32
- createDevSessionSnapshot?(input: {
33
- seed?: number | null;
34
- }): Promise<HostSessionWireSnapshot>;
35
- submitInteraction(input: {
36
- sessionId: string;
37
- playerId: string;
38
- interactionId: string;
39
- expectedVersion: number;
40
- actionSetVersion: string;
41
- params: unknown;
42
- clientActionId?: string | null;
43
- }): Promise<HostActionSubmitWireResponse>;
44
- validateInteraction(input: {
45
- sessionId: string;
46
- playerId: string;
47
- interactionId: string;
48
- expectedVersion: number;
49
- actionSetVersion: string;
50
- params: unknown;
51
- }): Promise<ValidationResult>;
52
- restoreHistory(input: {
53
- sessionId: string;
54
- entryId: string;
55
- }): Promise<HostSessionWireEvent | void>;
56
- subscribeToSessionEvents(input: {
57
- sessionId: string;
58
- clientId: string;
59
- connectionAttemptId: string;
60
- clientSource?: string;
61
- playerId?: string;
62
- signal?: AbortSignal;
63
- onSseError?: (error: unknown) => void;
64
- }): Promise<HostSessionStream>;
65
- disconnectSessionEvents(input: {
66
- sessionId: string;
67
- clientId: string;
68
- connectionAttemptId: string;
69
- playerId?: string;
70
- }): Promise<void>;
71
- }
72
-
73
- const SESSION_EVENT_LONG_POLL_WAIT_MS = 25_000;
74
- const HIDDEN_TAB_EMPTY_POLL_BACKOFF_MS = 5_000;
75
-
76
- async function requireData<T>(
77
- result: { data?: T; error?: unknown },
78
- message: string,
79
- ): Promise<T> {
80
- if (result.error || !result.data) {
81
- throw result.error ?? new Error(message);
82
- }
83
- return result.data;
84
- }
85
-
86
- function isAbortLikeError(error: unknown): boolean {
87
- return error instanceof Error && error.name === "AbortError";
88
- }
89
-
90
- function isDocumentHidden(): boolean {
91
- return typeof document !== "undefined" && document.hidden === true;
92
- }
93
-
94
- function shouldBackOffHiddenEmptyBatch(batch: {
95
- timedOut?: boolean;
96
- events?: unknown[];
97
- snapshot?: unknown;
98
- }): boolean {
99
- return (
100
- batch.timedOut === true &&
101
- !batch.snapshot &&
102
- (!Array.isArray(batch.events) || batch.events.length === 0) &&
103
- isDocumentHidden()
104
- );
105
- }
106
-
107
- async function waitForVisibleOrTimeout(
108
- delayMs: number,
109
- signal?: AbortSignal,
110
- ): Promise<void> {
111
- if (delayMs <= 0 || signal?.aborted || !isDocumentHidden()) return;
112
-
113
- await new Promise<void>((resolve) => {
114
- let timer: ReturnType<typeof setTimeout> | null = null;
115
-
116
- const cleanup = () => {
117
- if (timer) {
118
- clearTimeout(timer);
119
- timer = null;
120
- }
121
- document.removeEventListener("visibilitychange", onVisibilityChange);
122
- signal?.removeEventListener("abort", onAbort);
123
- };
124
-
125
- const finish = () => {
126
- cleanup();
127
- resolve();
128
- };
129
-
130
- const onVisibilityChange = () => {
131
- if (!isDocumentHidden()) {
132
- finish();
133
- }
134
- };
135
-
136
- const onAbort = () => {
137
- finish();
138
- };
139
-
140
- timer = setTimeout(finish, delayMs);
141
- document.addEventListener("visibilitychange", onVisibilityChange);
142
- signal?.addEventListener("abort", onAbort, { once: true });
143
- });
144
- }
145
-
146
- async function* pollSessionEvents(input: {
147
- sessionId: string;
148
- clientId: string;
149
- clientSource?: string;
150
- playerId?: string;
151
- signal?: AbortSignal;
152
- onSseError?: (error: unknown) => void;
153
- batchRequester?: typeof getSessionEventBatch;
154
- }): AsyncGenerator<HostSessionWireEvent | null | undefined> {
155
- let afterCursor: number | undefined;
156
- const batchRequester = input.batchRequester ?? getSessionEventBatch;
157
-
158
- while (!input.signal?.aborted) {
159
- try {
160
- const batch = await requireData(
161
- await batchRequester({
162
- path: { sessionId: input.sessionId },
163
- query: {
164
- clientId: input.clientId,
165
- clientSource: input.clientSource,
166
- playerId: input.playerId,
167
- afterCursor,
168
- waitMs: SESSION_EVENT_LONG_POLL_WAIT_MS,
169
- },
170
- signal: input.signal,
171
- }),
172
- "Failed to poll session events",
173
- );
174
-
175
- afterCursor = batch.cursor;
176
-
177
- if (batch.snapshot) {
178
- yield {
179
- type: "session.snapshot",
180
- reason: "load",
181
- snapshot: batch.snapshot,
182
- };
183
- }
184
-
185
- for (const event of batch.events) {
186
- yield event;
187
- }
188
-
189
- if (shouldBackOffHiddenEmptyBatch(batch)) {
190
- await waitForVisibleOrTimeout(
191
- HIDDEN_TAB_EMPTY_POLL_BACKOFF_MS,
192
- input.signal,
193
- );
194
- }
195
- } catch (error) {
196
- if (input.signal?.aborted || isAbortLikeError(error)) {
197
- return;
198
- }
199
- input.onSseError?.(error);
200
- throw error;
201
- }
202
- }
203
- }
204
-
205
- export const defaultHostSessionTransport: HostSessionTransport = {
206
- async loadSessionByShortCode(input) {
207
- return requireData(
208
- await getSessionByShortCode({
209
- path: { shortCode: input.shortCode },
210
- query: input.requestedPlayerId
211
- ? { playerId: input.requestedPlayerId }
212
- : undefined,
213
- }),
214
- "Failed to load session by short code",
215
- );
216
- },
217
- async loadSessionSnapshot(input) {
218
- return requireData(
219
- await getSessionSnapshot({
220
- path: { sessionId: input.sessionId },
221
- query: input.requestedPlayerId
222
- ? { playerId: input.requestedPlayerId }
223
- : undefined,
224
- }),
225
- "Failed to load session snapshot",
226
- );
227
- },
228
- async startSession(input) {
229
- return requireData(
230
- await startGame({ path: { sessionId: input.sessionId } }),
231
- "Failed to start session",
232
- );
233
- },
234
- async submitInteraction() {
235
- throw new Error(
236
- "Backend HTTP gameplay submit has been removed; configure gameplay authority transport.",
237
- );
238
- },
239
- async validateInteraction() {
240
- return { valid: true };
241
- },
242
- async restoreHistory() {
243
- throw new Error(
244
- "History restore requires the gameplay authority transport.",
245
- );
246
- },
247
- async subscribeToSessionEvents(input) {
248
- return { stream: pollSessionEvents(input) };
249
- },
250
- async disconnectSessionEvents() {
251
- // Long-poll requests are bounded and owned by their AbortSignal.
252
- },
253
- };
254
-
255
- export const demoHostSessionTransport: HostSessionTransport = {
256
- ...defaultHostSessionTransport,
257
- async loadSessionByShortCode(input) {
258
- return requireData(
259
- await getDemoSessionByShortCode({
260
- path: { shortCode: input.shortCode },
261
- query: input.requestedPlayerId
262
- ? { playerId: input.requestedPlayerId }
263
- : undefined,
264
- }),
265
- "Failed to load demo session by short code",
266
- );
267
- },
268
- async loadSessionSnapshot(input) {
269
- return requireData(
270
- await getDemoSessionSnapshot({
271
- path: { sessionId: input.sessionId },
272
- query: input.requestedPlayerId
273
- ? { playerId: input.requestedPlayerId }
274
- : undefined,
275
- }),
276
- "Failed to load demo session snapshot",
277
- );
278
- },
279
- async startSession(input) {
280
- return requireData(
281
- await startDemoGame({ path: { sessionId: input.sessionId } }),
282
- "Failed to start demo session",
283
- );
284
- },
285
- async subscribeToSessionEvents(input) {
286
- return {
287
- stream: pollSessionEvents({
288
- ...input,
289
- batchRequester:
290
- getDemoSessionEventBatch as unknown as typeof getSessionEventBatch,
291
- }),
292
- };
293
- },
294
- };
@@ -1,3 +0,0 @@
1
- export * from "./components/index.js";
2
- export * from "./runtime/index.js";
3
- export * from "./browser-interaction.js";
@@ -1,11 +0,0 @@
1
- export interface LoggerLike {
2
- log: (...args: unknown[]) => void;
3
- warn: (...args: unknown[]) => void;
4
- error: (...args: unknown[]) => void;
5
- }
6
-
7
- export const consoleLogger: LoggerLike = {
8
- log: (...args) => console.log(...args),
9
- warn: (...args) => console.warn(...args),
10
- error: (...args) => console.error(...args),
11
- };
@@ -1,324 +0,0 @@
1
- /**
2
- * Dev-only Tier-0 input-latency perf instrumentation.
3
- *
4
- * This module is used by the web host (and re-exported for the dev HUD)
5
- * to record `t0..t8` timing marks for each submitted interaction,
6
- * keyed by the client-minted `clientActionId`. It is deliberately
7
- * stateless across page reloads (in-memory ring buffer on `window`)
8
- * and gated behind `import.meta.env.DEV` or
9
- * `localStorage.getItem("dreamboard.perf") === "1"` so prod users
10
- * pay nothing.
11
- *
12
- * Design notes
13
- * - The plugin iframe lives in a separate window/performance context,
14
- * so plugin-side `performance.mark` entries are not reachable from
15
- * the host. We work around this by shipping plugin-minted `Date.now()`
16
- * timestamps across `postMessage` boundaries and recording them on
17
- * the host-side buffer keyed by `clientActionId`. Date.now() is used
18
- * (not `performance.now()`) because it shares a wall-clock base
19
- * across the iframe and the host.
20
- * - SSE `gameplay.updated` messages do not carry `clientActionId`; they
21
- * carry `version`. The host records a `version -> actionId` mapping
22
- * on `t3_http_response` (when the POST action submit response comes back
23
- * with `version`) so downstream marks (`t4_sse_received`,
24
- * `t5_store_applied`, `t6_state_sync_posted`) can be stitched by
25
- * version. Similarly `syncId -> actionId` is captured at `t5` so
26
- * plugin-side state-ack / state-rendered callbacks (which carry
27
- * `syncId`) can resolve back to the originating action.
28
- */
29
-
30
- export const PERF_MARK_NAMES = {
31
- T0_CLICK: "t0_click",
32
- T1_HOST_RECEIVED: "t1_host_received",
33
- T2_HTTP_SENT: "t2_http_sent",
34
- T3_HTTP_RESPONSE: "t3_http_response",
35
- /**
36
- * Fires when the store applies the submitter's gameplay snapshot
37
- * directly from the HTTP response (the Phase B2 eager-apply path).
38
- * Sits between `t3_http_response` and `t5_store_applied`; its delta
39
- * vs `t3_http_response` measures pure apply-in-store cost and its
40
- * delta vs `t4_sse_received` tells us how much SSE tail the eager
41
- * apply cuts off for the submitter.
42
- */
43
- T3B_RESPONSE_APPLIED: "t3b_response_applied",
44
- T4_SSE_RECEIVED: "t4_sse_received",
45
- T5_STORE_APPLIED: "t5_store_applied",
46
- T6_STATE_SYNC_POSTED: "t6_state_sync_posted",
47
- T7_STATE_SYNC_RECEIVED: "t7_state_sync_received",
48
- T8_RENDER_COMMIT: "t8_render_commit",
49
- } as const;
50
-
51
- export type PerfMarkName =
52
- (typeof PERF_MARK_NAMES)[keyof typeof PERF_MARK_NAMES];
53
-
54
- export interface PerfMarkRecord {
55
- name: string;
56
- timestampMs: number;
57
- extra?: Record<string, unknown>;
58
- }
59
-
60
- export interface PerfEntry {
61
- clientActionId: string;
62
- version?: number;
63
- syncId?: number;
64
- createdAtMs: number;
65
- marks: PerfMarkRecord[];
66
- }
67
-
68
- export interface PerfReceiptMetrics {
69
- action_submitter_projection_visible_ms?: number;
70
- action_pending_durability_ms?: number;
71
- action_durable_confirmation_ms?: number;
72
- action_other_player_visible_ms?: number;
73
- action_render_committed_ms?: number;
74
- action_server_response_ms?: number;
75
- }
76
-
77
- export interface PerfReceipt {
78
- clientActionId: string;
79
- version?: number;
80
- syncId?: number;
81
- metrics: PerfReceiptMetrics;
82
- }
83
-
84
- interface PerfBuffer {
85
- entries: PerfEntry[];
86
- versionIndex: Map<number, string>;
87
- syncIdIndex: Map<number, string>;
88
- }
89
-
90
- const GLOBAL_KEY = "__dreamboardPerf__";
91
- const DUMP_KEY = "__dreamboardPerfDump__";
92
- const MAX_ENTRIES = 50;
93
-
94
- type PerfGlobal = typeof globalThis & {
95
- [GLOBAL_KEY]?: PerfBuffer;
96
- [DUMP_KEY]?: () => PerfEntry[];
97
- };
98
-
99
- function getBuffer(): PerfBuffer | null {
100
- if (typeof globalThis === "undefined") {
101
- return null;
102
- }
103
- const scope = globalThis as PerfGlobal;
104
- const existing = scope[GLOBAL_KEY];
105
- if (existing) {
106
- return existing;
107
- }
108
- const buffer: PerfBuffer = {
109
- entries: [],
110
- versionIndex: new Map(),
111
- syncIdIndex: new Map(),
112
- };
113
- scope[GLOBAL_KEY] = buffer;
114
- scope[DUMP_KEY] = () => buffer.entries.slice();
115
- return buffer;
116
- }
117
-
118
- /**
119
- * Perf marks + HUD are off by default in prod. Enabled in dev builds
120
- * or when a maintainer opts in via `localStorage.dreamboard.perf = 1`.
121
- */
122
- export function isPerfEnabled(): boolean {
123
- if (typeof window === "undefined") {
124
- return false;
125
- }
126
- try {
127
- const env = (import.meta as unknown as { env?: { DEV?: boolean } }).env;
128
- if (env?.DEV) {
129
- return true;
130
- }
131
- } catch {
132
- // import.meta may be unavailable in non-Vite contexts; fall through
133
- }
134
- try {
135
- return window.localStorage.getItem("dreamboard.perf") === "1";
136
- } catch {
137
- return false;
138
- }
139
- }
140
-
141
- function nowMs(): number {
142
- return Date.now();
143
- }
144
-
145
- function findOrCreateEntry(
146
- buffer: PerfBuffer,
147
- clientActionId: string,
148
- ): PerfEntry {
149
- const existing = buffer.entries.find(
150
- (entry) => entry.clientActionId === clientActionId,
151
- );
152
- if (existing) {
153
- return existing;
154
- }
155
- const created: PerfEntry = {
156
- clientActionId,
157
- createdAtMs: nowMs(),
158
- marks: [],
159
- };
160
- buffer.entries.push(created);
161
- while (buffer.entries.length > MAX_ENTRIES) {
162
- const evicted = buffer.entries.shift();
163
- if (!evicted) break;
164
- if (evicted.version !== undefined) {
165
- buffer.versionIndex.delete(evicted.version);
166
- }
167
- if (evicted.syncId !== undefined) {
168
- buffer.syncIdIndex.delete(evicted.syncId);
169
- }
170
- }
171
- return created;
172
- }
173
-
174
- export interface RecordMarkOptions {
175
- timestampMs?: number;
176
- extra?: Record<string, unknown>;
177
- }
178
-
179
- /**
180
- * Record a perf mark against a `clientActionId`. No-ops silently when
181
- * perf is disabled or the runtime has no usable window/globalThis.
182
- */
183
- export function recordMark(
184
- clientActionId: string | undefined | null,
185
- name: string,
186
- options: RecordMarkOptions = {},
187
- ): void {
188
- if (!clientActionId) return;
189
- if (!isPerfEnabled()) return;
190
- const buffer = getBuffer();
191
- if (!buffer) return;
192
-
193
- const timestampMs = options.timestampMs ?? nowMs();
194
- const entry = findOrCreateEntry(buffer, clientActionId);
195
- entry.marks.push({
196
- name,
197
- timestampMs,
198
- extra: options.extra,
199
- });
200
-
201
- if (
202
- typeof performance !== "undefined" &&
203
- typeof performance.mark === "function"
204
- ) {
205
- try {
206
- performance.mark(`dreamboard.${name}.${clientActionId}`, {
207
- detail: { clientActionId, ...options.extra },
208
- });
209
- } catch {
210
- // performance.mark detail arg not supported in older browsers; ignore
211
- }
212
- }
213
- }
214
-
215
- /**
216
- * Associate the server-returned `version` (from SubmitInputResponse)
217
- * with a client-minted actionId so downstream SSE marks keyed by
218
- * `version` can resolve back to the original action.
219
- */
220
- export function correlateVersion(
221
- clientActionId: string,
222
- version: number,
223
- ): void {
224
- if (!isPerfEnabled()) return;
225
- const buffer = getBuffer();
226
- if (!buffer) return;
227
- const entry = findOrCreateEntry(buffer, clientActionId);
228
- entry.version = version;
229
- buffer.versionIndex.set(version, clientActionId);
230
- }
231
-
232
- /**
233
- * Associate the local `syncId` assigned when the store applies the
234
- * gameplay.updated with a clientActionId, so plugin-side state-ack /
235
- * state-rendered callbacks (which carry syncId rather than actionId)
236
- * can resolve back to the original action.
237
- */
238
- export function correlateSyncId(clientActionId: string, syncId: number): void {
239
- if (!isPerfEnabled()) return;
240
- const buffer = getBuffer();
241
- if (!buffer) return;
242
- const entry = findOrCreateEntry(buffer, clientActionId);
243
- entry.syncId = syncId;
244
- buffer.syncIdIndex.set(syncId, clientActionId);
245
- }
246
-
247
- export function findActionIdByVersion(version: number): string | undefined {
248
- const buffer = getBuffer();
249
- return buffer?.versionIndex.get(version);
250
- }
251
-
252
- export function findActionIdBySyncId(syncId: number): string | undefined {
253
- const buffer = getBuffer();
254
- return buffer?.syncIdIndex.get(syncId);
255
- }
256
-
257
- export function getPerfEntries(): PerfEntry[] {
258
- const buffer = getBuffer();
259
- return buffer ? buffer.entries.slice() : [];
260
- }
261
-
262
- export function getPerfReceipts(): PerfReceipt[] {
263
- return getPerfEntries().map((entry) => ({
264
- clientActionId: entry.clientActionId,
265
- version: entry.version,
266
- syncId: entry.syncId,
267
- metrics: {
268
- action_submitter_projection_visible_ms: deltaFor(
269
- entry,
270
- PERF_MARK_NAMES.T0_CLICK,
271
- PERF_MARK_NAMES.T3B_RESPONSE_APPLIED,
272
- ),
273
- action_pending_durability_ms: deltaFor(
274
- entry,
275
- PERF_MARK_NAMES.T3B_RESPONSE_APPLIED,
276
- PERF_MARK_NAMES.T4_SSE_RECEIVED,
277
- ),
278
- action_durable_confirmation_ms: deltaFor(
279
- entry,
280
- PERF_MARK_NAMES.T0_CLICK,
281
- PERF_MARK_NAMES.T4_SSE_RECEIVED,
282
- ),
283
- action_other_player_visible_ms: deltaFor(
284
- entry,
285
- PERF_MARK_NAMES.T0_CLICK,
286
- PERF_MARK_NAMES.T5_STORE_APPLIED,
287
- ),
288
- action_render_committed_ms: deltaFor(
289
- entry,
290
- PERF_MARK_NAMES.T0_CLICK,
291
- PERF_MARK_NAMES.T8_RENDER_COMMIT,
292
- ),
293
- action_server_response_ms: deltaFor(
294
- entry,
295
- PERF_MARK_NAMES.T2_HTTP_SENT,
296
- PERF_MARK_NAMES.T3_HTTP_RESPONSE,
297
- ),
298
- },
299
- }));
300
- }
301
-
302
- /** Drop every recorded entry; used by tests and the HUD "Clear" action. */
303
- export function clearPerfEntries(): void {
304
- const buffer = getBuffer();
305
- if (!buffer) return;
306
- buffer.entries = [];
307
- buffer.versionIndex.clear();
308
- buffer.syncIdIndex.clear();
309
- }
310
-
311
- function firstMarkMs(entry: PerfEntry, name: string): number | undefined {
312
- return entry.marks.find((mark) => mark.name === name)?.timestampMs;
313
- }
314
-
315
- function deltaFor(
316
- entry: PerfEntry,
317
- startName: string,
318
- endName: string,
319
- ): number | undefined {
320
- const start = firstMarkMs(entry, startName);
321
- const end = firstMarkMs(entry, endName);
322
- if (start === undefined || end === undefined) return undefined;
323
- return Math.round((end - start) * 10) / 10;
324
- }