dreamboard 0.1.21 → 0.1.22

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 (186) hide show
  1. package/dist/{chunk-EYYWGWTO.js → chunk-2RCUHMGL.js} +343 -1911
  2. package/dist/chunk-2RCUHMGL.js.map +1 -0
  3. package/dist/{chunk-MOVHYB6E.js → chunk-AQ5UUNJS.js} +1018 -692
  4. package/dist/chunk-AQ5UUNJS.js.map +1 -0
  5. package/dist/{chunk-BMYC6772.js → chunk-H3O43F5P.js} +9765 -3068
  6. package/dist/chunk-H3O43F5P.js.map +1 -0
  7. package/dist/dev-host/dev-api-proxy-plugin.ts +330 -0
  8. package/dist/dev-host/dev-diagnostics.ts +62 -0
  9. package/dist/dev-host/dev-fallback-stylesheet.ts +50 -0
  10. package/dist/dev-host/dev-host-controller.ts +686 -0
  11. package/dist/dev-host/dev-host-player-query.ts +17 -0
  12. package/dist/dev-host/dev-host-session-transport.ts +52 -0
  13. package/dist/dev-host/dev-host-storage.ts +56 -0
  14. package/dist/dev-host/dev-log-relay-plugin.ts +469 -0
  15. package/dist/dev-host/dev-runtime-config.ts +14 -0
  16. package/dist/dev-host/dev-runtime-platform.ts +419 -0
  17. package/dist/dev-host/dev-virtual-modules-plugin.ts +63 -0
  18. package/dist/dev-host/host-main.css +182 -0
  19. package/dist/dev-host/host-main.tsx +754 -0
  20. package/dist/dev-host/index.html +56 -0
  21. package/dist/dev-host/plugin-main.ts +55 -0
  22. package/dist/dev-host/plugin.html +24 -0
  23. package/dist/dev-host/start-dev-server.ts +138 -0
  24. package/dist/dev-host/virtual-modules.d.ts +27 -0
  25. package/dist/dist-FEPN3BDN.js +16543 -0
  26. package/dist/dist-FEPN3BDN.js.map +1 -0
  27. package/dist/{dist-WJRJNFLI.js → dist-FRURQI7Q.js} +4 -2
  28. package/dist/index.js +3 -3
  29. package/dist/internal.js +159 -124
  30. package/dist/internal.js.map +1 -1
  31. package/dist/runtime-packages/tailwind-config/shared-styles.css +146 -0
  32. package/dist/runtime-packages/ui/src/components/.gitkeep +0 -0
  33. package/dist/runtime-packages/ui/src/components/accordion.tsx +66 -0
  34. package/dist/runtime-packages/ui/src/components/ai-elements/actions.tsx +65 -0
  35. package/dist/runtime-packages/ui/src/components/ai-elements/artifact.tsx +147 -0
  36. package/dist/runtime-packages/ui/src/components/ai-elements/branch.tsx +215 -0
  37. package/dist/runtime-packages/ui/src/components/ai-elements/canvas.tsx +22 -0
  38. package/dist/runtime-packages/ui/src/components/ai-elements/chain-of-thought.tsx +228 -0
  39. package/dist/runtime-packages/ui/src/components/ai-elements/code-block.tsx +179 -0
  40. package/dist/runtime-packages/ui/src/components/ai-elements/confirmation.tsx +158 -0
  41. package/dist/runtime-packages/ui/src/components/ai-elements/connection.tsx +28 -0
  42. package/dist/runtime-packages/ui/src/components/ai-elements/context.tsx +408 -0
  43. package/dist/runtime-packages/ui/src/components/ai-elements/controls.tsx +18 -0
  44. package/dist/runtime-packages/ui/src/components/ai-elements/conversation.tsx +97 -0
  45. package/dist/runtime-packages/ui/src/components/ai-elements/edge.tsx +140 -0
  46. package/dist/runtime-packages/ui/src/components/ai-elements/image.tsx +24 -0
  47. package/dist/runtime-packages/ui/src/components/ai-elements/inline-citation.tsx +287 -0
  48. package/dist/runtime-packages/ui/src/components/ai-elements/loader.tsx +96 -0
  49. package/dist/runtime-packages/ui/src/components/ai-elements/message.tsx +463 -0
  50. package/dist/runtime-packages/ui/src/components/ai-elements/node.tsx +71 -0
  51. package/dist/runtime-packages/ui/src/components/ai-elements/open-in-chat.tsx +365 -0
  52. package/dist/runtime-packages/ui/src/components/ai-elements/panel.tsx +15 -0
  53. package/dist/runtime-packages/ui/src/components/ai-elements/plan.tsx +142 -0
  54. package/dist/runtime-packages/ui/src/components/ai-elements/prompt-input.tsx +1380 -0
  55. package/dist/runtime-packages/ui/src/components/ai-elements/queue.tsx +274 -0
  56. package/dist/runtime-packages/ui/src/components/ai-elements/reasoning.tsx +182 -0
  57. package/dist/runtime-packages/ui/src/components/ai-elements/response.tsx +22 -0
  58. package/dist/runtime-packages/ui/src/components/ai-elements/shimmer.tsx +64 -0
  59. package/dist/runtime-packages/ui/src/components/ai-elements/sources.tsx +77 -0
  60. package/dist/runtime-packages/ui/src/components/ai-elements/suggestion.tsx +53 -0
  61. package/dist/runtime-packages/ui/src/components/ai-elements/task.tsx +87 -0
  62. package/dist/runtime-packages/ui/src/components/ai-elements/tool.tsx +165 -0
  63. package/dist/runtime-packages/ui/src/components/ai-elements/toolbar.tsx +16 -0
  64. package/dist/runtime-packages/ui/src/components/ai-elements/web-preview.tsx +263 -0
  65. package/dist/runtime-packages/ui/src/components/alert-dialog.tsx +157 -0
  66. package/dist/runtime-packages/ui/src/components/alert.tsx +66 -0
  67. package/dist/runtime-packages/ui/src/components/avatar.tsx +53 -0
  68. package/dist/runtime-packages/ui/src/components/badge.tsx +46 -0
  69. package/dist/runtime-packages/ui/src/components/button-group.tsx +83 -0
  70. package/dist/runtime-packages/ui/src/components/button.tsx +65 -0
  71. package/dist/runtime-packages/ui/src/components/card.tsx +92 -0
  72. package/dist/runtime-packages/ui/src/components/carousel.tsx +241 -0
  73. package/dist/runtime-packages/ui/src/components/collapsible.tsx +33 -0
  74. package/dist/runtime-packages/ui/src/components/command.tsx +184 -0
  75. package/dist/runtime-packages/ui/src/components/context-menu.tsx +252 -0
  76. package/dist/runtime-packages/ui/src/components/counter.tsx +11 -0
  77. package/dist/runtime-packages/ui/src/components/dialog.tsx +146 -0
  78. package/dist/runtime-packages/ui/src/components/drawer.tsx +132 -0
  79. package/dist/runtime-packages/ui/src/components/dropdown-menu.tsx +257 -0
  80. package/dist/runtime-packages/ui/src/components/form.tsx +168 -0
  81. package/dist/runtime-packages/ui/src/components/header.tsx +13 -0
  82. package/dist/runtime-packages/ui/src/components/hover-card.tsx +44 -0
  83. package/dist/runtime-packages/ui/src/components/input-group.tsx +178 -0
  84. package/dist/runtime-packages/ui/src/components/input.tsx +21 -0
  85. package/dist/runtime-packages/ui/src/components/item.tsx +193 -0
  86. package/dist/runtime-packages/ui/src/components/label.tsx +24 -0
  87. package/dist/runtime-packages/ui/src/components/navigation-menu.tsx +168 -0
  88. package/dist/runtime-packages/ui/src/components/popover.tsx +55 -0
  89. package/dist/runtime-packages/ui/src/components/progress.tsx +31 -0
  90. package/dist/runtime-packages/ui/src/components/radio-group.tsx +45 -0
  91. package/dist/runtime-packages/ui/src/components/resizable.tsx +56 -0
  92. package/dist/runtime-packages/ui/src/components/scroll-area.tsx +58 -0
  93. package/dist/runtime-packages/ui/src/components/select.tsx +187 -0
  94. package/dist/runtime-packages/ui/src/components/separator.tsx +28 -0
  95. package/dist/runtime-packages/ui/src/components/skeleton.tsx +13 -0
  96. package/dist/runtime-packages/ui/src/components/sonner.tsx +25 -0
  97. package/dist/runtime-packages/ui/src/components/spinner.tsx +16 -0
  98. package/dist/runtime-packages/ui/src/components/switch.tsx +31 -0
  99. package/dist/runtime-packages/ui/src/components/table.tsx +137 -0
  100. package/dist/runtime-packages/ui/src/components/tabs.tsx +74 -0
  101. package/dist/runtime-packages/ui/src/components/textarea.tsx +23 -0
  102. package/dist/runtime-packages/ui/src/components/theme-provider.tsx +73 -0
  103. package/dist/runtime-packages/ui/src/components/tooltip.tsx +61 -0
  104. package/dist/runtime-packages/ui/src/components/ui/file-upload.tsx +195 -0
  105. package/dist/runtime-packages/ui/src/hooks/.gitkeep +0 -0
  106. package/dist/runtime-packages/ui/src/index.ts +68 -0
  107. package/dist/runtime-packages/ui/src/lib/utils.ts +6 -0
  108. package/dist/runtime-packages/ui-host-runtime/src/actor-principal.ts +68 -0
  109. package/dist/runtime-packages/ui-host-runtime/src/components/host-controls.tsx +359 -0
  110. package/dist/runtime-packages/ui-host-runtime/src/components/host-feedback-toaster.tsx +274 -0
  111. package/dist/runtime-packages/ui-host-runtime/src/components/host-feedback.tsx +214 -0
  112. package/dist/runtime-packages/ui-host-runtime/src/components/host-session-metadata.tsx +135 -0
  113. package/dist/runtime-packages/ui-host-runtime/src/components/index.ts +5 -0
  114. package/dist/runtime-packages/ui-host-runtime/src/components/perf-overlay.tsx +194 -0
  115. package/dist/runtime-packages/ui-host-runtime/src/host-controls.tsx +1 -0
  116. package/dist/runtime-packages/ui-host-runtime/src/host-feedback.tsx +1 -0
  117. package/dist/runtime-packages/ui-host-runtime/src/host-session-transport.ts +162 -0
  118. package/dist/runtime-packages/ui-host-runtime/src/index.ts +2 -0
  119. package/dist/runtime-packages/ui-host-runtime/src/logger.ts +11 -0
  120. package/dist/runtime-packages/ui-host-runtime/src/perf.ts +253 -0
  121. package/dist/runtime-packages/ui-host-runtime/src/plugin-bridge.ts +195 -0
  122. package/dist/runtime-packages/ui-host-runtime/src/plugin-health-check.ts +138 -0
  123. package/dist/runtime-packages/ui-host-runtime/src/plugin-messages.ts +159 -0
  124. package/dist/runtime-packages/ui-host-runtime/src/plugin-session-gateway.ts +524 -0
  125. package/dist/runtime-packages/ui-host-runtime/src/runtime/index.ts +12 -0
  126. package/dist/runtime-packages/ui-host-runtime/src/screenshot/projection-to-snapshot.ts +122 -0
  127. package/dist/runtime-packages/ui-host-runtime/src/screenshot/static-store-api.ts +26 -0
  128. package/dist/runtime-packages/ui-host-runtime/src/session-ingress-controller.ts +477 -0
  129. package/dist/runtime-packages/ui-host-runtime/src/session-ingress.ts +209 -0
  130. package/dist/runtime-packages/ui-host-runtime/src/session-live-runtime.ts +112 -0
  131. package/dist/runtime-packages/ui-host-runtime/src/session-model.ts +318 -0
  132. package/dist/runtime-packages/ui-host-runtime/src/session-projection.ts +211 -0
  133. package/dist/runtime-packages/ui-host-runtime/src/session-state-reducer.ts +814 -0
  134. package/dist/runtime-packages/ui-host-runtime/src/sse-manager.ts +334 -0
  135. package/dist/runtime-packages/ui-host-runtime/src/unified-session-store.ts +180 -0
  136. package/dist/scaffold/assets/static/app/tsconfig.framework.json +3 -1
  137. package/dist/scaffold/assets/static/ui/tsconfig.framework.json +7 -19
  138. package/package.json +52 -17
  139. package/skills/dreamboard/SKILL.md +41 -75
  140. package/LICENSE +0 -89
  141. package/NOTICE +0 -1
  142. package/dist/chunk-BMYC6772.js.map +0 -1
  143. package/dist/chunk-EYYWGWTO.js.map +0 -1
  144. package/dist/chunk-MOVHYB6E.js.map +0 -1
  145. package/dist/scaffold/assets/static/ui/App.tsx +0 -22
  146. package/dist/src-CUL7EGGG.js +0 -634
  147. package/dist/src-CUL7EGGG.js.map +0 -1
  148. package/skills/dreamboard/references/authoring-lifecycle.md +0 -102
  149. package/skills/dreamboard/references/board-surfaces.md +0 -36
  150. package/skills/dreamboard/references/board-topology.md +0 -443
  151. package/skills/dreamboard/references/boards-and-topology.md +0 -100
  152. package/skills/dreamboard/references/card-actions.md +0 -107
  153. package/skills/dreamboard/references/cli.md +0 -120
  154. package/skills/dreamboard/references/core-concepts.md +0 -514
  155. package/skills/dreamboard/references/custom-renderers.md +0 -26
  156. package/skills/dreamboard/references/derived-values.md +0 -55
  157. package/skills/dreamboard/references/effects.md +0 -111
  158. package/skills/dreamboard/references/game-contract.md +0 -89
  159. package/skills/dreamboard/references/game-definition.md +0 -89
  160. package/skills/dreamboard/references/game-shell.md +0 -80
  161. package/skills/dreamboard/references/hand-surfaces.md +0 -33
  162. package/skills/dreamboard/references/index.md +0 -112
  163. package/skills/dreamboard/references/inputs-and-targets.md +0 -160
  164. package/skills/dreamboard/references/interactions.md +0 -158
  165. package/skills/dreamboard/references/manifest-fields.md +0 -137
  166. package/skills/dreamboard/references/manifest.md +0 -81
  167. package/skills/dreamboard/references/package-surfaces.md +0 -69
  168. package/skills/dreamboard/references/phases.md +0 -145
  169. package/skills/dreamboard/references/prompts-and-choices.md +0 -55
  170. package/skills/dreamboard/references/quickstart.md +0 -71
  171. package/skills/dreamboard/references/rule-authoring.md +0 -144
  172. package/skills/dreamboard/references/setup-bootstrap.md +0 -78
  173. package/skills/dreamboard/references/stages-and-zones.md +0 -59
  174. package/skills/dreamboard/references/static-views.md +0 -67
  175. package/skills/dreamboard/references/table-queries-and-ops.md +0 -103
  176. package/skills/dreamboard/references/testing-bases.md +0 -147
  177. package/skills/dreamboard/references/testing-generated-contracts.md +0 -142
  178. package/skills/dreamboard/references/testing-runtime-assertions.md +0 -124
  179. package/skills/dreamboard/references/testing-scenarios.md +0 -148
  180. package/skills/dreamboard/references/testing-ui-tests.md +0 -174
  181. package/skills/dreamboard/references/testing.md +0 -161
  182. package/skills/dreamboard/references/ui-architecture.md +0 -137
  183. package/skills/dreamboard/references/ui-components.md +0 -34
  184. package/skills/dreamboard/references/views.md +0 -72
  185. package/skills/dreamboard/references/workspace-layout.md +0 -136
  186. /package/dist/{dist-WJRJNFLI.js.map → dist-FRURQI7Q.js.map} +0 -0
@@ -0,0 +1,162 @@
1
+ import {
2
+ disconnectSessionEvents,
3
+ getSessionByShortCode,
4
+ getSessionSnapshot,
5
+ restoreHistory,
6
+ startGame,
7
+ submitPlayerAction,
8
+ subscribeToSessionEvents,
9
+ } from "@dreamboard/api-client";
10
+ import type {
11
+ HostActionSubmitWireResponse,
12
+ HostSessionWireEvent,
13
+ HostSessionWireSnapshot,
14
+ } from "./session-ingress.js";
15
+
16
+ export interface HostSessionStream {
17
+ stream: AsyncIterable<HostSessionWireEvent | null | undefined>;
18
+ }
19
+
20
+ export interface HostSessionTransport {
21
+ loadSessionByShortCode(input: {
22
+ shortCode: string;
23
+ requestedPlayerId?: string | null;
24
+ }): Promise<HostSessionWireSnapshot>;
25
+ loadSessionSnapshot(input: {
26
+ sessionId: string;
27
+ requestedPlayerId?: string | null;
28
+ }): Promise<HostSessionWireSnapshot>;
29
+ startSession(input: { sessionId: string }): Promise<HostSessionWireSnapshot>;
30
+ createDevSessionSnapshot?(input: {
31
+ seed?: number | null;
32
+ }): Promise<HostSessionWireSnapshot>;
33
+ submitInteraction(input: {
34
+ sessionId: string;
35
+ playerId: string;
36
+ interactionId: string;
37
+ expectedVersion: number;
38
+ actionSetVersion: string;
39
+ params: unknown;
40
+ clientActionId?: string | null;
41
+ }): Promise<HostActionSubmitWireResponse>;
42
+ restoreHistory(input: { sessionId: string; entryId: string }): Promise<void>;
43
+ subscribeToSessionEvents(input: {
44
+ sessionId: string;
45
+ clientId: string;
46
+ connectionAttemptId: string;
47
+ clientSource?: string;
48
+ playerId?: string;
49
+ signal?: AbortSignal;
50
+ onSseError?: (error: unknown) => void;
51
+ }): Promise<HostSessionStream>;
52
+ disconnectSessionEvents(input: {
53
+ sessionId: string;
54
+ clientId: string;
55
+ connectionAttemptId: string;
56
+ playerId?: string;
57
+ }): Promise<void>;
58
+ }
59
+
60
+ const CLIENT_ACTION_ID_HEADER = "X-Dreamboard-Client-Action-Id";
61
+
62
+ function asInputs(params: unknown): Record<string, unknown> {
63
+ return params && typeof params === "object" && !Array.isArray(params)
64
+ ? (params as Record<string, unknown>)
65
+ : {};
66
+ }
67
+
68
+ async function requireData<T>(
69
+ result: { data?: T; error?: unknown },
70
+ message: string,
71
+ ): Promise<T> {
72
+ if (result.error || !result.data) {
73
+ throw result.error ?? new Error(message);
74
+ }
75
+ return result.data;
76
+ }
77
+
78
+ export const defaultHostSessionTransport: HostSessionTransport = {
79
+ async loadSessionByShortCode(input) {
80
+ return requireData(
81
+ await getSessionByShortCode({
82
+ path: { shortCode: input.shortCode },
83
+ query: input.requestedPlayerId
84
+ ? { playerId: input.requestedPlayerId }
85
+ : undefined,
86
+ }),
87
+ "Failed to load session by short code",
88
+ );
89
+ },
90
+ async loadSessionSnapshot(input) {
91
+ return requireData(
92
+ await getSessionSnapshot({
93
+ path: { sessionId: input.sessionId },
94
+ query: input.requestedPlayerId
95
+ ? { playerId: input.requestedPlayerId }
96
+ : undefined,
97
+ }),
98
+ "Failed to load session snapshot",
99
+ );
100
+ },
101
+ async startSession(input) {
102
+ return requireData(
103
+ await startGame({ path: { sessionId: input.sessionId } }),
104
+ "Failed to start session",
105
+ );
106
+ },
107
+ async submitInteraction(input) {
108
+ const headers = input.clientActionId
109
+ ? { [CLIENT_ACTION_ID_HEADER]: input.clientActionId }
110
+ : undefined;
111
+ return requireData(
112
+ await submitPlayerAction({
113
+ path: {
114
+ sessionId: input.sessionId,
115
+ playerId: input.playerId,
116
+ interactionId: input.interactionId,
117
+ },
118
+ body: {
119
+ expectedVersion: input.expectedVersion,
120
+ actionSetVersion: input.actionSetVersion,
121
+ inputs: asInputs(input.params) as never,
122
+ },
123
+ ...(headers ? { headers } : {}),
124
+ }),
125
+ "Failed to submit interaction",
126
+ );
127
+ },
128
+ async restoreHistory(input) {
129
+ const { error } = await restoreHistory({
130
+ path: { sessionId: input.sessionId },
131
+ body: { entryId: input.entryId },
132
+ });
133
+ if (error) {
134
+ throw error;
135
+ }
136
+ },
137
+ async subscribeToSessionEvents(input) {
138
+ const result = await subscribeToSessionEvents({
139
+ path: { sessionId: input.sessionId },
140
+ query: {
141
+ clientId: input.clientId,
142
+ connectionAttemptId: input.connectionAttemptId,
143
+ clientSource: input.clientSource,
144
+ playerId: input.playerId,
145
+ },
146
+ sseMaxRetryAttempts: 0,
147
+ signal: input.signal,
148
+ onSseError: input.onSseError,
149
+ });
150
+ return { stream: result.stream };
151
+ },
152
+ async disconnectSessionEvents(input) {
153
+ await disconnectSessionEvents({
154
+ path: { sessionId: input.sessionId },
155
+ query: {
156
+ clientId: input.clientId,
157
+ connectionAttemptId: input.connectionAttemptId,
158
+ playerId: input.playerId,
159
+ },
160
+ });
161
+ },
162
+ };
@@ -0,0 +1,2 @@
1
+ export * from "./components/index.js";
2
+ export * from "./runtime/index.js";
@@ -0,0 +1,11 @@
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
+ };
@@ -0,0 +1,253 @@
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
+ interface PerfBuffer {
69
+ entries: PerfEntry[];
70
+ versionIndex: Map<number, string>;
71
+ syncIdIndex: Map<number, string>;
72
+ }
73
+
74
+ const GLOBAL_KEY = "__dreamboardPerf__";
75
+ const DUMP_KEY = "__dreamboardPerfDump__";
76
+ const MAX_ENTRIES = 50;
77
+
78
+ type PerfGlobal = typeof globalThis & {
79
+ [GLOBAL_KEY]?: PerfBuffer;
80
+ [DUMP_KEY]?: () => PerfEntry[];
81
+ };
82
+
83
+ function getBuffer(): PerfBuffer | null {
84
+ if (typeof globalThis === "undefined") {
85
+ return null;
86
+ }
87
+ const scope = globalThis as PerfGlobal;
88
+ const existing = scope[GLOBAL_KEY];
89
+ if (existing) {
90
+ return existing;
91
+ }
92
+ const buffer: PerfBuffer = {
93
+ entries: [],
94
+ versionIndex: new Map(),
95
+ syncIdIndex: new Map(),
96
+ };
97
+ scope[GLOBAL_KEY] = buffer;
98
+ scope[DUMP_KEY] = () => buffer.entries.slice();
99
+ return buffer;
100
+ }
101
+
102
+ /**
103
+ * Perf marks + HUD are off by default in prod. Enabled in dev builds
104
+ * or when a maintainer opts in via `localStorage.dreamboard.perf = 1`.
105
+ */
106
+ export function isPerfEnabled(): boolean {
107
+ if (typeof window === "undefined") {
108
+ return false;
109
+ }
110
+ try {
111
+ const env = (import.meta as unknown as { env?: { DEV?: boolean } }).env;
112
+ if (env?.DEV) {
113
+ return true;
114
+ }
115
+ } catch {
116
+ // import.meta may be unavailable in non-Vite contexts; fall through
117
+ }
118
+ try {
119
+ return window.localStorage.getItem("dreamboard.perf") === "1";
120
+ } catch {
121
+ return false;
122
+ }
123
+ }
124
+
125
+ function nowMs(): number {
126
+ return Date.now();
127
+ }
128
+
129
+ function findOrCreateEntry(
130
+ buffer: PerfBuffer,
131
+ clientActionId: string,
132
+ ): PerfEntry {
133
+ const existing = buffer.entries.find(
134
+ (entry) => entry.clientActionId === clientActionId,
135
+ );
136
+ if (existing) {
137
+ return existing;
138
+ }
139
+ const created: PerfEntry = {
140
+ clientActionId,
141
+ createdAtMs: nowMs(),
142
+ marks: [],
143
+ };
144
+ buffer.entries.push(created);
145
+ while (buffer.entries.length > MAX_ENTRIES) {
146
+ const evicted = buffer.entries.shift();
147
+ if (!evicted) break;
148
+ if (evicted.version !== undefined) {
149
+ buffer.versionIndex.delete(evicted.version);
150
+ }
151
+ if (evicted.syncId !== undefined) {
152
+ buffer.syncIdIndex.delete(evicted.syncId);
153
+ }
154
+ }
155
+ return created;
156
+ }
157
+
158
+ export interface RecordMarkOptions {
159
+ timestampMs?: number;
160
+ extra?: Record<string, unknown>;
161
+ }
162
+
163
+ /**
164
+ * Record a perf mark against a `clientActionId`. No-ops silently when
165
+ * perf is disabled or the runtime has no usable window/globalThis.
166
+ */
167
+ export function recordMark(
168
+ clientActionId: string | undefined | null,
169
+ name: string,
170
+ options: RecordMarkOptions = {},
171
+ ): void {
172
+ if (!clientActionId) return;
173
+ if (!isPerfEnabled()) return;
174
+ const buffer = getBuffer();
175
+ if (!buffer) return;
176
+
177
+ const timestampMs = options.timestampMs ?? nowMs();
178
+ const entry = findOrCreateEntry(buffer, clientActionId);
179
+ entry.marks.push({
180
+ name,
181
+ timestampMs,
182
+ extra: options.extra,
183
+ });
184
+
185
+ if (
186
+ typeof performance !== "undefined" &&
187
+ typeof performance.mark === "function"
188
+ ) {
189
+ try {
190
+ performance.mark(`dreamboard.${name}.${clientActionId}`, {
191
+ detail: { clientActionId, ...options.extra },
192
+ });
193
+ } catch {
194
+ // performance.mark detail arg not supported in older browsers; ignore
195
+ }
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Associate the server-returned `version` (from SubmitInputResponse)
201
+ * with a client-minted actionId so downstream SSE marks keyed by
202
+ * `version` can resolve back to the original action.
203
+ */
204
+ export function correlateVersion(
205
+ clientActionId: string,
206
+ version: number,
207
+ ): void {
208
+ if (!isPerfEnabled()) return;
209
+ const buffer = getBuffer();
210
+ if (!buffer) return;
211
+ const entry = findOrCreateEntry(buffer, clientActionId);
212
+ entry.version = version;
213
+ buffer.versionIndex.set(version, clientActionId);
214
+ }
215
+
216
+ /**
217
+ * Associate the local `syncId` assigned when the store applies the
218
+ * gameplay.updated with a clientActionId, so plugin-side state-ack /
219
+ * state-rendered callbacks (which carry syncId rather than actionId)
220
+ * can resolve back to the original action.
221
+ */
222
+ export function correlateSyncId(clientActionId: string, syncId: number): void {
223
+ if (!isPerfEnabled()) return;
224
+ const buffer = getBuffer();
225
+ if (!buffer) return;
226
+ const entry = findOrCreateEntry(buffer, clientActionId);
227
+ entry.syncId = syncId;
228
+ buffer.syncIdIndex.set(syncId, clientActionId);
229
+ }
230
+
231
+ export function findActionIdByVersion(version: number): string | undefined {
232
+ const buffer = getBuffer();
233
+ return buffer?.versionIndex.get(version);
234
+ }
235
+
236
+ export function findActionIdBySyncId(syncId: number): string | undefined {
237
+ const buffer = getBuffer();
238
+ return buffer?.syncIdIndex.get(syncId);
239
+ }
240
+
241
+ export function getPerfEntries(): PerfEntry[] {
242
+ const buffer = getBuffer();
243
+ return buffer ? buffer.entries.slice() : [];
244
+ }
245
+
246
+ /** Drop every recorded entry; used by tests and the HUD "Clear" action. */
247
+ export function clearPerfEntries(): void {
248
+ const buffer = getBuffer();
249
+ if (!buffer) return;
250
+ buffer.entries = [];
251
+ buffer.versionIndex.clear();
252
+ buffer.syncIdIndex.clear();
253
+ }
@@ -0,0 +1,195 @@
1
+ import type { PluginStateSnapshot } from "@dreamboard/ui-sdk/reducer";
2
+ import {
3
+ MainToPluginMessageSchema,
4
+ PluginToMainMessageSchema,
5
+ type MainToPluginMessage,
6
+ type PluginToMainMessage,
7
+ } from "./plugin-messages.js";
8
+ import type { LoggerLike } from "./logger.js";
9
+ import { consoleLogger } from "./logger.js";
10
+
11
+ type MessageHandler<T> = (data: T) => void;
12
+
13
+ export interface PluginBridgeOptions {
14
+ logger?: LoggerLike;
15
+ }
16
+
17
+ export class PluginBridge {
18
+ private iframe: HTMLIFrameElement;
19
+ private targetOrigin: string;
20
+ private allowedOrigins: Set<string>;
21
+ private handlers: Map<
22
+ PluginToMainMessage["type"],
23
+ Set<MessageHandler<PluginToMainMessage>>
24
+ > = new Map();
25
+ private messageListener: ((event: MessageEvent) => void) | null = null;
26
+ private logger: LoggerLike;
27
+
28
+ constructor(
29
+ iframe: HTMLIFrameElement,
30
+ targetOrigin: string,
31
+ options: PluginBridgeOptions = {},
32
+ ) {
33
+ if (!iframe.contentWindow) {
34
+ throw new Error("iframe.contentWindow is not available");
35
+ }
36
+
37
+ this.iframe = iframe;
38
+ this.targetOrigin = targetOrigin;
39
+ this.allowedOrigins = resolveAllowedOrigins(iframe);
40
+ this.logger = options.logger ?? consoleLogger;
41
+ this.setupMessageListener();
42
+ }
43
+
44
+ private validateOrigin(origin: string): boolean {
45
+ return this.allowedOrigins.has(origin);
46
+ }
47
+
48
+ private setupMessageListener(): void {
49
+ this.messageListener = (event: MessageEvent) => {
50
+ if (event.source !== this.iframe.contentWindow) {
51
+ return;
52
+ }
53
+ if (!this.validateOrigin(event.origin)) {
54
+ return;
55
+ }
56
+
57
+ const result = PluginToMainMessageSchema.safeParse(event.data);
58
+ if (!result.success) {
59
+ return;
60
+ }
61
+
62
+ this.emit(result.data.type, result.data);
63
+ };
64
+
65
+ window.addEventListener("message", this.messageListener);
66
+ }
67
+
68
+ sendInit(
69
+ sessionId: string,
70
+ controllablePlayerIds: string[],
71
+ controllingPlayerId: string,
72
+ userId: string | null,
73
+ ): void {
74
+ this.sendToPlugin({
75
+ type: "init",
76
+ sessionId,
77
+ controllablePlayerIds,
78
+ controllingPlayerId,
79
+ userId,
80
+ });
81
+ }
82
+
83
+ sendPing(): void {
84
+ this.sendToPlugin({
85
+ type: "ping",
86
+ });
87
+ }
88
+
89
+ sendStateSync(syncId: number, state: PluginStateSnapshot): void {
90
+ this.sendToPlugin({
91
+ type: "state-sync",
92
+ syncId,
93
+ state,
94
+ });
95
+ }
96
+
97
+ sendSubmitResult(result: {
98
+ messageId: string;
99
+ accepted: boolean;
100
+ errorCode?: string;
101
+ message?: string;
102
+ }): void {
103
+ this.sendToPlugin({
104
+ type: "submit-result",
105
+ ...result,
106
+ });
107
+ }
108
+
109
+ send(message: Record<string, unknown>): void {
110
+ if (!this.iframe.contentWindow) {
111
+ this.logger.error(
112
+ "[PluginBridge] iframe contentWindow not available for message:",
113
+ message.type,
114
+ );
115
+ return;
116
+ }
117
+
118
+ this.iframe.contentWindow.postMessage(message, this.targetOrigin);
119
+ }
120
+
121
+ private sendToPlugin(message: MainToPluginMessage): void {
122
+ if (!this.iframe.contentWindow) {
123
+ this.logger.error(
124
+ "[PluginBridge] iframe contentWindow not available for message:",
125
+ message.type,
126
+ );
127
+ return;
128
+ }
129
+
130
+ const result = MainToPluginMessageSchema.safeParse(message);
131
+ if (!result.success) {
132
+ this.logger.error(
133
+ "[PluginBridge] Failed to validate outgoing message:",
134
+ message,
135
+ );
136
+ return;
137
+ }
138
+
139
+ this.iframe.contentWindow.postMessage(result.data, this.targetOrigin);
140
+ }
141
+
142
+ onPluginMessage<T extends PluginToMainMessage["type"]>(
143
+ type: T,
144
+ handler: MessageHandler<Extract<PluginToMainMessage, { type: T }>>,
145
+ ): () => void {
146
+ if (!this.handlers.has(type)) {
147
+ this.handlers.set(type, new Set());
148
+ }
149
+
150
+ const handlers = this.handlers.get(type);
151
+ if (!handlers) {
152
+ throw new Error(`Handler for type ${type} not found`);
153
+ }
154
+
155
+ handlers.add(handler as MessageHandler<PluginToMainMessage>);
156
+
157
+ return () => {
158
+ handlers.delete(handler as MessageHandler<PluginToMainMessage>);
159
+ };
160
+ }
161
+
162
+ private emit(
163
+ type: PluginToMainMessage["type"],
164
+ data: PluginToMainMessage,
165
+ ): void {
166
+ const handlers = this.handlers.get(type);
167
+ if (handlers) {
168
+ handlers.forEach((handler) => handler(data));
169
+ }
170
+ }
171
+
172
+ disconnect(): void {
173
+ if (this.messageListener) {
174
+ window.removeEventListener("message", this.messageListener);
175
+ this.messageListener = null;
176
+ }
177
+
178
+ this.handlers.clear();
179
+ }
180
+ }
181
+
182
+ function resolveAllowedOrigins(iframe: HTMLIFrameElement): Set<string> {
183
+ const allowedOrigins = new Set<string>(["null"]);
184
+
185
+ try {
186
+ const iframeUrl = new URL(iframe.src, window.location.href);
187
+ if (iframeUrl.origin && iframeUrl.origin !== "null") {
188
+ allowedOrigins.add(iframeUrl.origin);
189
+ }
190
+ } catch {
191
+ // Ignore invalid or unset iframe URLs and keep the sandboxed null origin.
192
+ }
193
+
194
+ return allowedOrigins;
195
+ }