dreamboard 0.1.21 → 0.1.23
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/dist/{chunk-EYYWGWTO.js → chunk-2RCUHMGL.js} +343 -1911
- package/dist/chunk-2RCUHMGL.js.map +1 -0
- package/dist/{chunk-MOVHYB6E.js → chunk-GN7232BY.js} +1025 -695
- package/dist/chunk-GN7232BY.js.map +1 -0
- package/dist/{chunk-BMYC6772.js → chunk-H3O43F5P.js} +9765 -3068
- package/dist/chunk-H3O43F5P.js.map +1 -0
- package/dist/dev-host/dev-api-proxy-plugin.ts +330 -0
- package/dist/dev-host/dev-diagnostics.ts +62 -0
- package/dist/dev-host/dev-fallback-stylesheet.ts +50 -0
- package/dist/dev-host/dev-host-controller.ts +686 -0
- package/dist/dev-host/dev-host-player-query.ts +17 -0
- package/dist/dev-host/dev-host-session-transport.ts +52 -0
- package/dist/dev-host/dev-host-storage.ts +56 -0
- package/dist/dev-host/dev-log-relay-plugin.ts +469 -0
- package/dist/dev-host/dev-runtime-config.ts +14 -0
- package/dist/dev-host/dev-runtime-platform.ts +419 -0
- package/dist/dev-host/dev-virtual-modules-plugin.ts +63 -0
- package/dist/dev-host/host-main.css +182 -0
- package/dist/dev-host/host-main.tsx +754 -0
- package/dist/dev-host/index.html +56 -0
- package/dist/dev-host/plugin-main.ts +55 -0
- package/dist/dev-host/plugin.html +24 -0
- package/dist/dev-host/start-dev-server.ts +138 -0
- package/dist/dev-host/virtual-modules.d.ts +27 -0
- package/dist/dist-FEPN3BDN.js +16543 -0
- package/dist/dist-FEPN3BDN.js.map +1 -0
- package/dist/{dist-WJRJNFLI.js → dist-FRURQI7Q.js} +4 -2
- package/dist/index.js +3 -3
- package/dist/internal.js +159 -124
- package/dist/internal.js.map +1 -1
- package/dist/runtime-packages/tailwind-config/shared-styles.css +146 -0
- package/dist/runtime-packages/ui/src/components/.gitkeep +0 -0
- package/dist/runtime-packages/ui/src/components/accordion.tsx +66 -0
- package/dist/runtime-packages/ui/src/components/ai-elements/actions.tsx +65 -0
- package/dist/runtime-packages/ui/src/components/ai-elements/artifact.tsx +147 -0
- package/dist/runtime-packages/ui/src/components/ai-elements/branch.tsx +215 -0
- package/dist/runtime-packages/ui/src/components/ai-elements/canvas.tsx +22 -0
- package/dist/runtime-packages/ui/src/components/ai-elements/chain-of-thought.tsx +228 -0
- package/dist/runtime-packages/ui/src/components/ai-elements/code-block.tsx +179 -0
- package/dist/runtime-packages/ui/src/components/ai-elements/confirmation.tsx +158 -0
- package/dist/runtime-packages/ui/src/components/ai-elements/connection.tsx +28 -0
- package/dist/runtime-packages/ui/src/components/ai-elements/context.tsx +408 -0
- package/dist/runtime-packages/ui/src/components/ai-elements/controls.tsx +18 -0
- package/dist/runtime-packages/ui/src/components/ai-elements/conversation.tsx +97 -0
- package/dist/runtime-packages/ui/src/components/ai-elements/edge.tsx +140 -0
- package/dist/runtime-packages/ui/src/components/ai-elements/image.tsx +24 -0
- package/dist/runtime-packages/ui/src/components/ai-elements/inline-citation.tsx +287 -0
- package/dist/runtime-packages/ui/src/components/ai-elements/loader.tsx +96 -0
- package/dist/runtime-packages/ui/src/components/ai-elements/message.tsx +463 -0
- package/dist/runtime-packages/ui/src/components/ai-elements/node.tsx +71 -0
- package/dist/runtime-packages/ui/src/components/ai-elements/open-in-chat.tsx +365 -0
- package/dist/runtime-packages/ui/src/components/ai-elements/panel.tsx +15 -0
- package/dist/runtime-packages/ui/src/components/ai-elements/plan.tsx +142 -0
- package/dist/runtime-packages/ui/src/components/ai-elements/prompt-input.tsx +1380 -0
- package/dist/runtime-packages/ui/src/components/ai-elements/queue.tsx +274 -0
- package/dist/runtime-packages/ui/src/components/ai-elements/reasoning.tsx +182 -0
- package/dist/runtime-packages/ui/src/components/ai-elements/response.tsx +22 -0
- package/dist/runtime-packages/ui/src/components/ai-elements/shimmer.tsx +64 -0
- package/dist/runtime-packages/ui/src/components/ai-elements/sources.tsx +77 -0
- package/dist/runtime-packages/ui/src/components/ai-elements/suggestion.tsx +53 -0
- package/dist/runtime-packages/ui/src/components/ai-elements/task.tsx +87 -0
- package/dist/runtime-packages/ui/src/components/ai-elements/tool.tsx +165 -0
- package/dist/runtime-packages/ui/src/components/ai-elements/toolbar.tsx +16 -0
- package/dist/runtime-packages/ui/src/components/ai-elements/web-preview.tsx +263 -0
- package/dist/runtime-packages/ui/src/components/alert-dialog.tsx +157 -0
- package/dist/runtime-packages/ui/src/components/alert.tsx +66 -0
- package/dist/runtime-packages/ui/src/components/avatar.tsx +53 -0
- package/dist/runtime-packages/ui/src/components/badge.tsx +46 -0
- package/dist/runtime-packages/ui/src/components/button-group.tsx +83 -0
- package/dist/runtime-packages/ui/src/components/button.tsx +65 -0
- package/dist/runtime-packages/ui/src/components/card.tsx +92 -0
- package/dist/runtime-packages/ui/src/components/carousel.tsx +241 -0
- package/dist/runtime-packages/ui/src/components/collapsible.tsx +33 -0
- package/dist/runtime-packages/ui/src/components/command.tsx +184 -0
- package/dist/runtime-packages/ui/src/components/context-menu.tsx +252 -0
- package/dist/runtime-packages/ui/src/components/counter.tsx +11 -0
- package/dist/runtime-packages/ui/src/components/dialog.tsx +146 -0
- package/dist/runtime-packages/ui/src/components/drawer.tsx +132 -0
- package/dist/runtime-packages/ui/src/components/dropdown-menu.tsx +257 -0
- package/dist/runtime-packages/ui/src/components/form.tsx +168 -0
- package/dist/runtime-packages/ui/src/components/header.tsx +13 -0
- package/dist/runtime-packages/ui/src/components/hover-card.tsx +44 -0
- package/dist/runtime-packages/ui/src/components/input-group.tsx +178 -0
- package/dist/runtime-packages/ui/src/components/input.tsx +21 -0
- package/dist/runtime-packages/ui/src/components/item.tsx +193 -0
- package/dist/runtime-packages/ui/src/components/label.tsx +24 -0
- package/dist/runtime-packages/ui/src/components/navigation-menu.tsx +168 -0
- package/dist/runtime-packages/ui/src/components/popover.tsx +55 -0
- package/dist/runtime-packages/ui/src/components/progress.tsx +31 -0
- package/dist/runtime-packages/ui/src/components/radio-group.tsx +45 -0
- package/dist/runtime-packages/ui/src/components/resizable.tsx +56 -0
- package/dist/runtime-packages/ui/src/components/scroll-area.tsx +58 -0
- package/dist/runtime-packages/ui/src/components/select.tsx +187 -0
- package/dist/runtime-packages/ui/src/components/separator.tsx +28 -0
- package/dist/runtime-packages/ui/src/components/skeleton.tsx +13 -0
- package/dist/runtime-packages/ui/src/components/sonner.tsx +25 -0
- package/dist/runtime-packages/ui/src/components/spinner.tsx +16 -0
- package/dist/runtime-packages/ui/src/components/switch.tsx +31 -0
- package/dist/runtime-packages/ui/src/components/table.tsx +137 -0
- package/dist/runtime-packages/ui/src/components/tabs.tsx +74 -0
- package/dist/runtime-packages/ui/src/components/textarea.tsx +23 -0
- package/dist/runtime-packages/ui/src/components/theme-provider.tsx +73 -0
- package/dist/runtime-packages/ui/src/components/tooltip.tsx +61 -0
- package/dist/runtime-packages/ui/src/components/ui/file-upload.tsx +195 -0
- package/dist/runtime-packages/ui/src/hooks/.gitkeep +0 -0
- package/dist/runtime-packages/ui/src/index.ts +68 -0
- package/dist/runtime-packages/ui/src/lib/utils.ts +6 -0
- package/dist/runtime-packages/ui-host-runtime/src/actor-principal.ts +68 -0
- package/dist/runtime-packages/ui-host-runtime/src/components/host-controls.tsx +359 -0
- package/dist/runtime-packages/ui-host-runtime/src/components/host-feedback-toaster.tsx +274 -0
- package/dist/runtime-packages/ui-host-runtime/src/components/host-feedback.tsx +214 -0
- package/dist/runtime-packages/ui-host-runtime/src/components/host-session-metadata.tsx +135 -0
- package/dist/runtime-packages/ui-host-runtime/src/components/index.ts +5 -0
- package/dist/runtime-packages/ui-host-runtime/src/components/perf-overlay.tsx +194 -0
- package/dist/runtime-packages/ui-host-runtime/src/host-controls.tsx +1 -0
- package/dist/runtime-packages/ui-host-runtime/src/host-feedback.tsx +1 -0
- package/dist/runtime-packages/ui-host-runtime/src/host-session-transport.ts +162 -0
- package/dist/runtime-packages/ui-host-runtime/src/index.ts +2 -0
- package/dist/runtime-packages/ui-host-runtime/src/logger.ts +11 -0
- package/dist/runtime-packages/ui-host-runtime/src/perf.ts +253 -0
- package/dist/runtime-packages/ui-host-runtime/src/plugin-bridge.ts +195 -0
- package/dist/runtime-packages/ui-host-runtime/src/plugin-health-check.ts +138 -0
- package/dist/runtime-packages/ui-host-runtime/src/plugin-messages.ts +159 -0
- package/dist/runtime-packages/ui-host-runtime/src/plugin-session-gateway.ts +524 -0
- package/dist/runtime-packages/ui-host-runtime/src/runtime/index.ts +12 -0
- package/dist/runtime-packages/ui-host-runtime/src/screenshot/projection-to-snapshot.ts +122 -0
- package/dist/runtime-packages/ui-host-runtime/src/screenshot/static-store-api.ts +26 -0
- package/dist/runtime-packages/ui-host-runtime/src/session-ingress-controller.ts +477 -0
- package/dist/runtime-packages/ui-host-runtime/src/session-ingress.ts +209 -0
- package/dist/runtime-packages/ui-host-runtime/src/session-live-runtime.ts +112 -0
- package/dist/runtime-packages/ui-host-runtime/src/session-model.ts +318 -0
- package/dist/runtime-packages/ui-host-runtime/src/session-projection.ts +211 -0
- package/dist/runtime-packages/ui-host-runtime/src/session-state-reducer.ts +814 -0
- package/dist/runtime-packages/ui-host-runtime/src/sse-manager.ts +334 -0
- package/dist/runtime-packages/ui-host-runtime/src/unified-session-store.ts +180 -0
- package/dist/scaffold/assets/static/app/tsconfig.framework.json +3 -1
- package/dist/scaffold/assets/static/ui/tsconfig.framework.json +7 -19
- package/package.json +52 -17
- package/skills/dreamboard/SKILL.md +41 -75
- package/LICENSE +0 -89
- package/NOTICE +0 -1
- package/dist/chunk-BMYC6772.js.map +0 -1
- package/dist/chunk-EYYWGWTO.js.map +0 -1
- package/dist/chunk-MOVHYB6E.js.map +0 -1
- package/dist/scaffold/assets/static/ui/App.tsx +0 -22
- package/dist/src-CUL7EGGG.js +0 -634
- package/dist/src-CUL7EGGG.js.map +0 -1
- package/skills/dreamboard/references/authoring-lifecycle.md +0 -102
- package/skills/dreamboard/references/board-surfaces.md +0 -36
- package/skills/dreamboard/references/board-topology.md +0 -443
- package/skills/dreamboard/references/boards-and-topology.md +0 -100
- package/skills/dreamboard/references/card-actions.md +0 -107
- package/skills/dreamboard/references/cli.md +0 -120
- package/skills/dreamboard/references/core-concepts.md +0 -514
- package/skills/dreamboard/references/custom-renderers.md +0 -26
- package/skills/dreamboard/references/derived-values.md +0 -55
- package/skills/dreamboard/references/effects.md +0 -111
- package/skills/dreamboard/references/game-contract.md +0 -89
- package/skills/dreamboard/references/game-definition.md +0 -89
- package/skills/dreamboard/references/game-shell.md +0 -80
- package/skills/dreamboard/references/hand-surfaces.md +0 -33
- package/skills/dreamboard/references/index.md +0 -112
- package/skills/dreamboard/references/inputs-and-targets.md +0 -160
- package/skills/dreamboard/references/interactions.md +0 -158
- package/skills/dreamboard/references/manifest-fields.md +0 -137
- package/skills/dreamboard/references/manifest.md +0 -81
- package/skills/dreamboard/references/package-surfaces.md +0 -69
- package/skills/dreamboard/references/phases.md +0 -145
- package/skills/dreamboard/references/prompts-and-choices.md +0 -55
- package/skills/dreamboard/references/quickstart.md +0 -71
- package/skills/dreamboard/references/rule-authoring.md +0 -144
- package/skills/dreamboard/references/setup-bootstrap.md +0 -78
- package/skills/dreamboard/references/stages-and-zones.md +0 -59
- package/skills/dreamboard/references/static-views.md +0 -67
- package/skills/dreamboard/references/table-queries-and-ops.md +0 -103
- package/skills/dreamboard/references/testing-bases.md +0 -147
- package/skills/dreamboard/references/testing-generated-contracts.md +0 -142
- package/skills/dreamboard/references/testing-runtime-assertions.md +0 -124
- package/skills/dreamboard/references/testing-scenarios.md +0 -148
- package/skills/dreamboard/references/testing-ui-tests.md +0 -174
- package/skills/dreamboard/references/testing.md +0 -161
- package/skills/dreamboard/references/ui-architecture.md +0 -137
- package/skills/dreamboard/references/ui-components.md +0 -34
- package/skills/dreamboard/references/views.md +0 -72
- package/skills/dreamboard/references/workspace-layout.md +0 -136
- /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,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
|
+
}
|