@seriousgaming/sdk 0.0.0-stage → 1.0.0-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 (170) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +1 -0
  3. package/README.md +33 -2
  4. package/lib/build/callbacks.d.ts +998 -0
  5. package/lib/build/callbacks.js +32 -0
  6. package/lib/build/context-merge.d.ts +59 -0
  7. package/lib/build/context-merge.js +118 -0
  8. package/lib/build/document.d.ts +36 -0
  9. package/lib/build/document.js +8 -0
  10. package/lib/build/encoding.d.ts +26 -0
  11. package/lib/build/encoding.js +78 -0
  12. package/lib/build/endpoints.d.ts +421 -0
  13. package/lib/build/endpoints.js +46 -0
  14. package/lib/build/hooks.d.ts +74 -0
  15. package/lib/build/hooks.js +1 -0
  16. package/lib/build/index.d.ts +18 -0
  17. package/lib/build/index.js +16 -0
  18. package/lib/build/llm.d.ts +166 -0
  19. package/lib/build/llm.js +17 -0
  20. package/lib/build/resource-schema.d.ts +1048 -0
  21. package/lib/build/resource-schema.js +7 -0
  22. package/lib/build/scheduling.d.ts +112 -0
  23. package/lib/build/scheduling.js +217 -0
  24. package/lib/build/seed.d.ts +51 -0
  25. package/lib/build/seed.js +89 -0
  26. package/lib/build/signatures.d.ts +372 -0
  27. package/lib/build/signatures.js +1 -0
  28. package/lib/build/speech.d.ts +102 -0
  29. package/lib/build/speech.js +11 -0
  30. package/lib/build/tick-validation.d.ts +23 -0
  31. package/lib/build/tick-validation.js +96 -0
  32. package/lib/build/timer.d.ts +74 -0
  33. package/lib/build/timer.js +1 -0
  34. package/lib/build/validators.d.ts +116 -0
  35. package/lib/build/validators.js +1 -0
  36. package/lib/commands.d.ts +357 -0
  37. package/lib/commands.js +14 -0
  38. package/lib/e2e.d.ts +238 -0
  39. package/lib/e2e.js +248 -0
  40. package/lib/projection.d.ts +266 -0
  41. package/lib/projection.js +23 -0
  42. package/lib/testing/index.d.ts +13 -0
  43. package/lib/testing/index.js +13 -0
  44. package/lib/testing/logic-args.d.ts +177 -0
  45. package/lib/testing/logic-args.js +229 -0
  46. package/lib/testing/stub-services.d.ts +41 -0
  47. package/lib/testing/stub-services.js +176 -0
  48. package/lib/ui/CrossTeamDataContext.d.ts +36 -0
  49. package/lib/ui/CrossTeamDataContext.js +32 -0
  50. package/lib/ui/PresentationContext.d.ts +72 -0
  51. package/lib/ui/PresentationContext.js +13 -0
  52. package/lib/ui/PresentationWrapper.d.ts +12 -0
  53. package/lib/ui/PresentationWrapper.js +78 -0
  54. package/lib/ui/SessionUserInterfaceCallbacksContext.d.ts +3 -0
  55. package/lib/ui/SessionUserInterfaceCallbacksContext.js +7 -0
  56. package/lib/ui/SessionUserInterfaceDataContext.d.ts +103 -0
  57. package/lib/ui/SessionUserInterfaceDataContext.js +17 -0
  58. package/lib/ui/SessionUserInterfaceHookFailuresContext.d.ts +44 -0
  59. package/lib/ui/SessionUserInterfaceHookFailuresContext.js +15 -0
  60. package/lib/ui/SessionUserInterfaceHostOverlaysContext.d.ts +23 -0
  61. package/lib/ui/SessionUserInterfaceHostOverlaysContext.js +23 -0
  62. package/lib/ui/SessionUserInterfaceLocaleContext.d.ts +11 -0
  63. package/lib/ui/SessionUserInterfaceLocaleContext.js +13 -0
  64. package/lib/ui/SessionUserInterfaceMetaContext.d.ts +39 -0
  65. package/lib/ui/SessionUserInterfaceMetaContext.js +13 -0
  66. package/lib/ui/SessionUserInterfaceParticipantsContext.d.ts +12 -0
  67. package/lib/ui/SessionUserInterfaceParticipantsContext.js +16 -0
  68. package/lib/ui/SessionUserInterfaceTeamConfigurationContext.d.ts +53 -0
  69. package/lib/ui/SessionUserInterfaceTeamConfigurationContext.js +23 -0
  70. package/lib/ui/SessionUserInterfaceTeamLinksContext.d.ts +56 -0
  71. package/lib/ui/SessionUserInterfaceTeamLinksContext.js +12 -0
  72. package/lib/ui/SessionUserInterfaceTeamsContext.d.ts +13 -0
  73. package/lib/ui/SessionUserInterfaceTeamsContext.js +13 -0
  74. package/lib/ui/SessionUserInterfaceThemeContext.d.ts +9 -0
  75. package/lib/ui/SessionUserInterfaceThemeContext.js +7 -0
  76. package/lib/ui/SessionUserInterfaceThemePreferenceContext.d.ts +3 -0
  77. package/lib/ui/SessionUserInterfaceThemePreferenceContext.js +7 -0
  78. package/lib/ui/SessionUserInterfaceWalkthroughLinkContext.d.ts +42 -0
  79. package/lib/ui/SessionUserInterfaceWalkthroughLinkContext.js +26 -0
  80. package/lib/ui/SessionUserInterfaceWrapper.d.ts +8 -0
  81. package/lib/ui/SessionUserInterfaceWrapper.js +321 -0
  82. package/lib/ui/SettingsEditorCallbacksContext.d.ts +3 -0
  83. package/lib/ui/SettingsEditorCallbacksContext.js +7 -0
  84. package/lib/ui/SettingsEditorContext.d.ts +33 -0
  85. package/lib/ui/SettingsEditorContext.js +16 -0
  86. package/lib/ui/SettingsEditorLocaleContext.d.ts +13 -0
  87. package/lib/ui/SettingsEditorLocaleContext.js +15 -0
  88. package/lib/ui/SettingsEditorThemeContext.d.ts +9 -0
  89. package/lib/ui/SettingsEditorThemeContext.js +7 -0
  90. package/lib/ui/SettingsEditorThemePreferenceContext.d.ts +3 -0
  91. package/lib/ui/SettingsEditorThemePreferenceContext.js +7 -0
  92. package/lib/ui/SettingsEditorWrapper.d.ts +13 -0
  93. package/lib/ui/SettingsEditorWrapper.js +74 -0
  94. package/lib/ui/UserInterfaceCallbacksContext.d.ts +3 -0
  95. package/lib/ui/UserInterfaceCallbacksContext.js +7 -0
  96. package/lib/ui/UserInterfaceDataContext.d.ts +78 -0
  97. package/lib/ui/UserInterfaceDataContext.js +11 -0
  98. package/lib/ui/UserInterfaceLocaleContext.d.ts +13 -0
  99. package/lib/ui/UserInterfaceLocaleContext.js +15 -0
  100. package/lib/ui/UserInterfaceOwnTeamContext.d.ts +25 -0
  101. package/lib/ui/UserInterfaceOwnTeamContext.js +26 -0
  102. package/lib/ui/UserInterfaceOwnTeamJoinLinkContext.d.ts +52 -0
  103. package/lib/ui/UserInterfaceOwnTeamJoinLinkContext.js +13 -0
  104. package/lib/ui/UserInterfaceParticipantContext.d.ts +16 -0
  105. package/lib/ui/UserInterfaceParticipantContext.js +24 -0
  106. package/lib/ui/UserInterfaceParticipantsContext.d.ts +12 -0
  107. package/lib/ui/UserInterfaceParticipantsContext.js +13 -0
  108. package/lib/ui/UserInterfaceRunKindContext.d.ts +31 -0
  109. package/lib/ui/UserInterfaceRunKindContext.js +31 -0
  110. package/lib/ui/UserInterfaceTeamsContext.d.ts +12 -0
  111. package/lib/ui/UserInterfaceTeamsContext.js +13 -0
  112. package/lib/ui/UserInterfaceThemeContext.d.ts +9 -0
  113. package/lib/ui/UserInterfaceThemeContext.js +7 -0
  114. package/lib/ui/UserInterfaceThemePreferenceContext.d.ts +3 -0
  115. package/lib/ui/UserInterfaceThemePreferenceContext.js +7 -0
  116. package/lib/ui/UserInterfaceTimerContext.d.ts +10 -0
  117. package/lib/ui/UserInterfaceTimerContext.js +12 -0
  118. package/lib/ui/UserInterfaceValidationContext.d.ts +25 -0
  119. package/lib/ui/UserInterfaceValidationContext.js +14 -0
  120. package/lib/ui/UserInterfaceViewerRoleContext.d.ts +14 -0
  121. package/lib/ui/UserInterfaceViewerRoleContext.js +17 -0
  122. package/lib/ui/UserInterfaceWrapper.d.ts +8 -0
  123. package/lib/ui/UserInterfaceWrapper.js +219 -0
  124. package/lib/ui/expectedParentOrigin.d.ts +10 -0
  125. package/lib/ui/expectedParentOrigin.js +32 -0
  126. package/lib/ui/index.d.ts +50 -0
  127. package/lib/ui/index.js +52 -0
  128. package/lib/ui/internal/SurfaceGate.d.ts +19 -0
  129. package/lib/ui/internal/SurfaceGate.js +17 -0
  130. package/lib/ui/internal/SurfaceScope.d.ts +17 -0
  131. package/lib/ui/internal/SurfaceScope.js +16 -0
  132. package/lib/ui/internal/createContexts.d.ts +50 -0
  133. package/lib/ui/internal/createContexts.js +32 -0
  134. package/lib/ui/internal/document-attributes.d.ts +9 -0
  135. package/lib/ui/internal/document-attributes.js +87 -0
  136. package/lib/ui/internal/surface-timer.d.ts +3 -0
  137. package/lib/ui/internal/surface-timer.js +11 -0
  138. package/lib/ui/internal/useLatestRef.d.ts +7 -0
  139. package/lib/ui/internal/useLatestRef.js +11 -0
  140. package/lib/ui/internal/useOptimisticMerge.d.ts +19 -0
  141. package/lib/ui/internal/useOptimisticMerge.js +28 -0
  142. package/lib/ui/internal/useSurfaceControls.d.ts +52 -0
  143. package/lib/ui/internal/useSurfaceControls.js +19 -0
  144. package/lib/ui/internal/useTypedPastState.d.ts +17 -0
  145. package/lib/ui/internal/useTypedPastState.js +17 -0
  146. package/lib/ui/mutation-commit.d.ts +32 -0
  147. package/lib/ui/mutation-commit.js +1 -0
  148. package/lib/ui/mutation-outcome.d.ts +66 -0
  149. package/lib/ui/mutation-outcome.js +49 -0
  150. package/lib/ui/request-id.d.ts +18 -0
  151. package/lib/ui/request-id.js +44 -0
  152. package/lib/ui/testing.d.ts +365 -0
  153. package/lib/ui/testing.js +572 -0
  154. package/lib/ui/tick-snapshot.d.ts +28 -0
  155. package/lib/ui/tick-snapshot.js +34 -0
  156. package/lib/ui/ui-protocol.d.ts +799 -0
  157. package/lib/ui/ui-protocol.js +50 -0
  158. package/lib/ui/useAsset.d.ts +80 -0
  159. package/lib/ui/useAsset.js +139 -0
  160. package/lib/ui/useEndpointCallbacks.d.ts +31 -0
  161. package/lib/ui/useEndpointCallbacks.js +68 -0
  162. package/lib/ui/useHostChannel.d.ts +90 -0
  163. package/lib/ui/useHostChannel.js +338 -0
  164. package/lib/ui/useMutationReporter.d.ts +100 -0
  165. package/lib/ui/useMutationReporter.js +97 -0
  166. package/lib/ui/usePastState.d.ts +39 -0
  167. package/lib/ui/usePastState.js +63 -0
  168. package/lib/ui/useViewerStorage.d.ts +54 -0
  169. package/lib/ui/useViewerStorage.js +141 -0
  170. package/package.json +89 -3
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The host ↔ iframe MessagePort protocol (§12), as the guest speaks it.
3
+ *
4
+ * The wrappers in this package import these unions (via
5
+ * {@link ./useHostChannel}). Game code never handles a wire message, so no
6
+ * entry point exports them: the host's definition of the same protocol lives
7
+ * with the host library in `@seriousgaming/sdk-tools/host`,
8
+ * and `protocol.parity.test.ts` there fails to compile the moment the two
9
+ * disagree. Extend a union here and extend the host's in the same change.
10
+ */
11
+ /**
12
+ * Version of the host↔iframe handshake protocol. Both ends stamp it on the
13
+ * `ready`/`init` window messages and warn (without blocking) when the peer's
14
+ * declared version differs, so a host and an SDK build that have drifted apart
15
+ * surface a diagnostic instead of failing silently. Bump on any
16
+ * backwards-incompatible change to the handshake or port message contract.
17
+ *
18
+ * Version 2 adds `updateTeamState`: a host may deliver an ongoing session-data
19
+ * change as one team's state rather than the whole session's, and a session
20
+ * wrapper merges it. `setSessionData` remains the full snapshot on handshake
21
+ * and on resync, so a host that only ever sends the snapshot is still
22
+ * conformant.
23
+ *
24
+ * Version 3 gives shared state its own channel: `SessionData` carries the
25
+ * shared store once as `shared`, each team's `context` carries team scope
26
+ * only, and `updateSharedState` delivers a change to the store as one message
27
+ * rather than one per team.
28
+ *
29
+ * Version 4 gives each concept one name (#634): the context push `setData`
30
+ * becomes `stateChanged`, carrying `completedTicks` where it carried `tick`;
31
+ * `updateConfig` becomes `updateSettings`; a settings editor receives
32
+ * `setSettings` in place of `setResolvedConfiguration`; `extendTick` carries
33
+ * `byMs`; `pauseTimer` and `resumeTimer` carry a `requestId`; timers are one
34
+ * `TimerState` in milliseconds; participants and teams share one shape; and
35
+ * a failed `callEndpoint:response` carries an `EndpointFailure` (`outcome`,
36
+ * `applied`, `failedStep`) where it carried an `error` string (#636); and
37
+ * configuration UIs become the `setup` and `scenario_editor` slots, which are
38
+ * sent no `setConfigurationSurface` (#637). `init` carries the `sessionKey` a
39
+ * surface namespaces its own storage by, and every theme colour carries a
40
+ * `contrastText` (#638).
41
+ * Version 3 never shipped, so version 4 is the version that ships with
42
+ * manifest version 3, and a host serving an earlier package speaks the
43
+ * version that package was built for.
44
+ *
45
+ * There is deliberately no capability-negotiation list here. The version alone
46
+ * is the gate, and it can be, because no published package predates the
47
+ * feature: it ships in the same release as manifest version 2, and a host
48
+ * serving a version 1 package sends full snapshots to it.
49
+ */
50
+ export const UI_PROTOCOL_VERSION = 4;
@@ -0,0 +1,80 @@
1
+ /** What a wrapper tells {@link useAssetUrl} and {@link useAsset} about its surface. */
2
+ export interface AssetScope {
3
+ /**
4
+ * Resolve an `asset:`, `shared:` or `blob:` reference to a URL, as the
5
+ * surface's `getAssetUrl` callback does. Undefined until the host has sent
6
+ * the surface its asset URLs.
7
+ */
8
+ getAssetUrl: ((reference: string) => string) | undefined;
9
+ /** The surface's locale, which {@link useAsset} looks for a localised copy in. */
10
+ locale: string | undefined;
11
+ }
12
+ /**
13
+ * Provided by every surface wrapper. Exported for the test helpers, which
14
+ * provide it without a host; game code reads it through {@link useAssetUrl}
15
+ * and {@link useAsset}.
16
+ */
17
+ export declare const AssetContext: import("react").Context<AssetScope | undefined>;
18
+ /**
19
+ * The URL an `asset:`, `shared:` or `blob:` reference resolves to, for an
20
+ * `<img>`, `<video>` or link.
21
+ *
22
+ * Undefined until the host has sent the surface its asset URLs, and for an
23
+ * absent reference, so a component renders a placeholder rather than catching
24
+ * a throw. A reference without one of the three prefixes still throws, as
25
+ * `getAssetUrl` does: it is a mistake in the game, not a state to wait out.
26
+ *
27
+ * ```tsx
28
+ * const src = useAssetUrl(settings.storyline.Avatar);
29
+ * return src ? <img src={src} alt="CEO" /> : null;
30
+ * ```
31
+ */
32
+ export declare function useAssetUrl(reference: string | null | undefined): string | undefined;
33
+ /** How {@link useAsset} reads the response body. */
34
+ export type AssetFormat = 'json' | 'text' | 'blob';
35
+ export interface UseAssetOptions<F extends AssetFormat = AssetFormat> {
36
+ /** `'json'` parses the body, `'text'` returns it as a string, `'blob'` as a `Blob`. */
37
+ as: F;
38
+ /**
39
+ * The locale to look for a localised copy in. Defaults to the surface's
40
+ * locale; `false` reads the reference as given and nothing else.
41
+ */
42
+ locale?: string | false;
43
+ }
44
+ export interface AssetState<T> {
45
+ /** The asset's contents. Undefined until loaded, and while a new reference loads. */
46
+ data: T | undefined;
47
+ /** Why the asset could not be read: no copy was found, or the body did not parse. */
48
+ error: Error | undefined;
49
+ loading: boolean;
50
+ }
51
+ /**
52
+ * The references {@link useAsset} tries, most specific first: the locale as a
53
+ * directory immediately before the file name, then its language alone, then
54
+ * the reference as given. `asset:documents/intro.md` in `fr-CA` tries
55
+ * `asset:documents/fr-CA/intro.md`, `asset:documents/fr/intro.md` and
56
+ * `asset:documents/intro.md`.
57
+ *
58
+ * A `blob:` reference names one uploaded file, so it has no localised copies.
59
+ */
60
+ export declare function localisedReferences(reference: string, locale: string | false): string[];
61
+ /**
62
+ * Fetch an asset and read its body, preferring a copy in the surface's locale.
63
+ *
64
+ * Looks for the locale's copy first, then its language's, then the reference
65
+ * as given (see {@link localisedReferences}), so the untranslated file is the
66
+ * fallback and a translation is added by dropping a file beside it. Waits for
67
+ * the host's asset URLs rather than throwing, refetches when the reference or
68
+ * locale changes, and never lets a slower, older request overwrite a newer one.
69
+ *
70
+ * ```tsx
71
+ * const { data: briefing, error, loading } = useAsset<Briefing>('asset:content/briefing.json', {
72
+ * as: 'json',
73
+ * });
74
+ * ```
75
+ *
76
+ * @param reference An `asset:`, `shared:` or `blob:` reference; nothing is fetched while it is absent
77
+ */
78
+ export declare function useAsset<T = unknown>(reference: string | null | undefined, options: UseAssetOptions<'json'>): AssetState<T>;
79
+ export declare function useAsset(reference: string | null | undefined, options: UseAssetOptions<'text'>): AssetState<string>;
80
+ export declare function useAsset(reference: string | null | undefined, options: UseAssetOptions<'blob'>): AssetState<Blob>;
@@ -0,0 +1,139 @@
1
+ import { createContext, useContext, useEffect, useMemo, useState } from 'react';
2
+ /**
3
+ * Provided by every surface wrapper. Exported for the test helpers, which
4
+ * provide it without a host; game code reads it through {@link useAssetUrl}
5
+ * and {@link useAsset}.
6
+ */
7
+ export const AssetContext = createContext(undefined);
8
+ function useAssetScope(hookName) {
9
+ const scope = useContext(AssetContext);
10
+ if (scope === undefined) {
11
+ throw new Error(`${hookName} must be used inside a surface wrapper`);
12
+ }
13
+ return scope;
14
+ }
15
+ /**
16
+ * The URL an `asset:`, `shared:` or `blob:` reference resolves to, for an
17
+ * `<img>`, `<video>` or link.
18
+ *
19
+ * Undefined until the host has sent the surface its asset URLs, and for an
20
+ * absent reference, so a component renders a placeholder rather than catching
21
+ * a throw. A reference without one of the three prefixes still throws, as
22
+ * `getAssetUrl` does: it is a mistake in the game, not a state to wait out.
23
+ *
24
+ * ```tsx
25
+ * const src = useAssetUrl(settings.storyline.Avatar);
26
+ * return src ? <img src={src} alt="CEO" /> : null;
27
+ * ```
28
+ */
29
+ export function useAssetUrl(reference) {
30
+ const { getAssetUrl } = useAssetScope('useAssetUrl');
31
+ if (!reference || !getAssetUrl)
32
+ return undefined;
33
+ return getAssetUrl(reference);
34
+ }
35
+ /**
36
+ * The references {@link useAsset} tries, most specific first: the locale as a
37
+ * directory immediately before the file name, then its language alone, then
38
+ * the reference as given. `asset:documents/intro.md` in `fr-CA` tries
39
+ * `asset:documents/fr-CA/intro.md`, `asset:documents/fr/intro.md` and
40
+ * `asset:documents/intro.md`.
41
+ *
42
+ * A `blob:` reference names one uploaded file, so it has no localised copies.
43
+ */
44
+ export function localisedReferences(reference, locale) {
45
+ if (locale === false || locale === '' || reference.startsWith('blob:'))
46
+ return [reference];
47
+ const colon = reference.indexOf(':');
48
+ if (colon < 0)
49
+ return [reference];
50
+ const scheme = reference.slice(0, colon + 1);
51
+ const path = reference.slice(colon + 1);
52
+ const slash = path.lastIndexOf('/');
53
+ const directory = slash < 0 ? '' : path.slice(0, slash + 1);
54
+ const file = path.slice(slash + 1);
55
+ const language = locale.split('-')[0];
56
+ const locales = language && language !== locale ? [locale, language] : [locale];
57
+ return [...locales.map((each) => `${scheme}${directory}${each}/${file}`), reference];
58
+ }
59
+ async function readBody(response, format) {
60
+ if (format === 'json')
61
+ return response.json();
62
+ if (format === 'text')
63
+ return response.text();
64
+ return response.blob();
65
+ }
66
+ export function useAsset(reference, options) {
67
+ const { getAssetUrl, locale: surfaceLocale } = useAssetScope('useAsset');
68
+ const format = options.as;
69
+ const locale = options.locale ?? surfaceLocale ?? false;
70
+ // Resolved during render and keyed by the URLs themselves, so a resolver
71
+ // whose identity changes without its prefixes changing fetches nothing.
72
+ const plan = useMemo(() => {
73
+ if (!reference || !getAssetUrl)
74
+ return undefined;
75
+ try {
76
+ return { urls: localisedReferences(reference, locale).map(getAssetUrl) };
77
+ }
78
+ catch (cause) {
79
+ return { error: cause instanceof Error ? cause : new Error(String(cause)) };
80
+ }
81
+ }, [reference, locale, getAssetUrl]);
82
+ const key = plan === undefined ? '' : 'urls' in plan ? `${format}\n${plan.urls.join('\n')}` : '';
83
+ const [loaded, setLoaded] = useState(undefined);
84
+ useEffect(() => {
85
+ if (key === '')
86
+ return;
87
+ const [, ...urls] = key.split('\n');
88
+ const controller = new AbortController();
89
+ void (async () => {
90
+ let failure;
91
+ for (const url of urls) {
92
+ let response;
93
+ try {
94
+ response = await fetch(url, {
95
+ credentials: 'include',
96
+ signal: controller.signal,
97
+ });
98
+ }
99
+ catch (cause) {
100
+ if (controller.signal.aborted)
101
+ return;
102
+ failure = cause instanceof Error ? cause : new Error(String(cause));
103
+ continue;
104
+ }
105
+ if (!response.ok) {
106
+ failure = new Error(`Failed to fetch asset (${response.status})`);
107
+ continue;
108
+ }
109
+ try {
110
+ const data = await readBody(response, format);
111
+ if (!controller.signal.aborted)
112
+ setLoaded({ key, data, error: undefined });
113
+ }
114
+ catch (cause) {
115
+ if (controller.signal.aborted)
116
+ return;
117
+ const error = cause instanceof Error ? cause : new Error(String(cause));
118
+ setLoaded({ key, data: undefined, error });
119
+ }
120
+ return;
121
+ }
122
+ if (!controller.signal.aborted) {
123
+ setLoaded({ key, data: undefined, error: failure ?? new Error('Asset not found') });
124
+ }
125
+ })();
126
+ return () => controller.abort();
127
+ // `format` is part of `key`.
128
+ // eslint-disable-next-line react-hooks/exhaustive-deps
129
+ }, [key]);
130
+ if (plan !== undefined && 'error' in plan) {
131
+ return { data: undefined, error: plan.error, loading: false };
132
+ }
133
+ if (!reference)
134
+ return { data: undefined, error: undefined, loading: false };
135
+ if (key === '' || loaded?.key !== key) {
136
+ return { data: undefined, error: undefined, loading: true };
137
+ }
138
+ return { data: loaded.data, error: loaded.error, loading: false };
139
+ }
@@ -0,0 +1,31 @@
1
+ import { type EndpointFailure } from '../build/endpoints.js';
2
+ /**
3
+ * Returned by {@link useEndpointCallbacks} — a proxy-backed map where any
4
+ * property access resolves to an async function that posts a `callEndpoint`
5
+ * message over the supplied port. Designed to mirror the typed `Endpoints`
6
+ * interface emitted by per-UI generated `types.ts`. A failed call rejects with
7
+ * an {@link EndpointError}.
8
+ */
9
+ export type EndpointCallable = (request: unknown) => Promise<unknown>;
10
+ export type EndpointCallbacksMap = Record<string, EndpointCallable>;
11
+ /**
12
+ * Build the host ↔ iframe endpoint plumbing shared across UI wrappers.
13
+ *
14
+ * Exposes:
15
+ * - `endpoints` — proxy-backed map used as `callbacks.endpoints[id](request)`.
16
+ * - `handleEndpointResponse(msg)` — call from the wrapper's port `onmessage`
17
+ * for `callEndpoint:response` payloads.
18
+ * - `resetPending()` — drop all in-flight calls on connection reset.
19
+ */
20
+ export declare function useEndpointCallbacks(getPort: () => MessagePort | null): {
21
+ endpoints: EndpointCallbacksMap;
22
+ handleEndpointResponse: (msg: {
23
+ requestId: string;
24
+ ok: true;
25
+ response: unknown;
26
+ } | ({
27
+ requestId: string;
28
+ ok: false;
29
+ } & EndpointFailure)) => void;
30
+ resetPending: () => void;
31
+ };
@@ -0,0 +1,68 @@
1
+ import { useCallback, useMemo, useRef } from 'react';
2
+ import { EndpointError } from '../build/endpoints.js';
3
+ import { newRequestId } from './request-id.js';
4
+ /** A failure the host never answered, so it reported nothing applied. */
5
+ function unreached(message) {
6
+ return new EndpointError({ outcome: { kind: 'error', message }, applied: [] });
7
+ }
8
+ /**
9
+ * Build the host ↔ iframe endpoint plumbing shared across UI wrappers.
10
+ *
11
+ * Exposes:
12
+ * - `endpoints` — proxy-backed map used as `callbacks.endpoints[id](request)`.
13
+ * - `handleEndpointResponse(msg)` — call from the wrapper's port `onmessage`
14
+ * for `callEndpoint:response` payloads.
15
+ * - `resetPending()` — drop all in-flight calls on connection reset.
16
+ */
17
+ export function useEndpointCallbacks(getPort) {
18
+ const pendingRef = useRef(new Map());
19
+ const callEndpoint = useCallback((endpointIdentifier, request) => {
20
+ const port = getPort();
21
+ if (!port) {
22
+ return Promise.reject(unreached('Not connected to host'));
23
+ }
24
+ return new Promise((resolve, reject) => {
25
+ const requestId = newRequestId();
26
+ pendingRef.current.set(requestId, { resolve, reject });
27
+ port.postMessage({
28
+ type: 'callEndpoint',
29
+ requestId,
30
+ endpointIdentifier,
31
+ request,
32
+ });
33
+ });
34
+ }, [getPort]);
35
+ const endpoints = useMemo(() => new Proxy({}, {
36
+ get: (_target, prop) => {
37
+ if (typeof prop !== 'string')
38
+ return undefined;
39
+ return (request) => callEndpoint(prop, request);
40
+ },
41
+ }), [callEndpoint]);
42
+ const handleEndpointResponse = useCallback((msg) => {
43
+ const pending = pendingRef.current.get(msg.requestId);
44
+ if (!pending)
45
+ return;
46
+ if (msg.ok) {
47
+ pending.resolve(msg.response);
48
+ }
49
+ else {
50
+ pending.reject(new EndpointError({
51
+ outcome: msg.outcome ?? {
52
+ kind: 'error',
53
+ message: 'The endpoint call failed',
54
+ },
55
+ applied: msg.applied ?? [],
56
+ ...(msg.failedStep ? { failedStep: msg.failedStep } : {}),
57
+ }));
58
+ }
59
+ pendingRef.current.delete(msg.requestId);
60
+ }, []);
61
+ const resetPending = useCallback(() => {
62
+ // Whether the host applied anything before the connection went is
63
+ // unknown here: `applied` lists what the host reported, which is none.
64
+ pendingRef.current.forEach(({ reject }) => reject(unreached('Connection reset')));
65
+ pendingRef.current.clear();
66
+ }, []);
67
+ return { endpoints, handleEndpointResponse, resetPending };
68
+ }
@@ -0,0 +1,90 @@
1
+ import { PastStateEntry, PastStateQuery, ReadAssetResponse, ThemePreference, Participant } from '../build/callbacks.js';
2
+ import type { MutationCommit } from './mutation-commit.js';
3
+ import { type AssetUrls } from './ui-protocol.js';
4
+ import { type EndpointCallbacksMap } from './useEndpointCallbacks.js';
5
+ /** Helpers passed to a wrapper's per-message handler. */
6
+ export interface HostChannelHelpers {
7
+ /** Record that a required message has arrived (drives `isInitialized`). */
8
+ markReceived: (type: string) => void;
9
+ /** Post a raw message back to the host over the active port. */
10
+ postMessage: (message: unknown) => void;
11
+ }
12
+ export interface UseHostChannelOptions<THostMessage extends {
13
+ type: string;
14
+ }> {
15
+ /** Window the iframe posts its `ready` handshake to (usually `window.parent`). */
16
+ targetWindow: Window;
17
+ /** Console label for handshake logging. */
18
+ label: string;
19
+ /** Message types that must all arrive before `isInitialized` becomes true. */
20
+ requiredMessages: readonly string[];
21
+ /**
22
+ * Handle a channel-specific host message (everything the hook does not own:
23
+ * `stateChanged`, `setSettings`, `setTheme`, `setLocale`,
24
+ * `validationRejected`, …). Call `helpers.markReceived(type)` for required
25
+ * messages.
26
+ */
27
+ onMessage?: (message: THostMessage, helpers: HostChannelHelpers) => void;
28
+ /** Called on every (re)connection, before the new port is wired. */
29
+ onReset?: () => void;
30
+ }
31
+ export interface UseHostChannelReturn<TIFrameMessage> {
32
+ expectedParentOrigin: string | null;
33
+ isInitialized: boolean;
34
+ /**
35
+ * The opaque key the host named this run by on `init` (§12.2), for
36
+ * namespacing the surface's own storage. Undefined until the handshake
37
+ * completes, and on a host that sends none.
38
+ */
39
+ sessionKey: string | undefined;
40
+ /** Post a typed iframe→host message over the active port. */
41
+ postMessage: (message: TIFrameMessage) => void;
42
+ /**
43
+ * Post a fallible mutation and await its commit. Resolves once the host
44
+ * confirms the write/operation; rejects with a {@link MutationError}.
45
+ */
46
+ sendMutation: (message: TIFrameMessage) => Promise<void>;
47
+ /**
48
+ * As {@link sendMutation}, but resolving with what the host reported about
49
+ * the commit. Only the context write needs it: a write carries the tick it
50
+ * was composed against, and the answer says which tick it landed in (§9.5).
51
+ */
52
+ sendMutationReportingCommit: (message: TIFrameMessage) => Promise<MutationCommit>;
53
+ themePreference: ThemePreference;
54
+ setThemePreference: (preference: ThemePreference) => void;
55
+ uploadBlob: (file: File) => Promise<string>;
56
+ readAsset: (reference: string) => Promise<ReadAssetResponse>;
57
+ /**
58
+ * Read committed past state. Untyped here: the channel does not know
59
+ * the game's `Context`, so each wrapper re-exposes it generically.
60
+ *
61
+ * The host resolves the query, including which teams `own` means for
62
+ * the surface, so one signature serves the participant and session
63
+ * channels alike.
64
+ */
65
+ getPastState: (query: PastStateQuery<Record<string, unknown>>) => Promise<PastStateEntry<Record<string, unknown>>[]>;
66
+ /**
67
+ * Read the current participant roster of every team. Resolves with one flat
68
+ * entry per participant across all teams (each tagged with its `teamId`);
69
+ * rejects on a transport/host fault. A point-in-time snapshot, not a live
70
+ * subscription. Available on the participant channel only.
71
+ */
72
+ getAllTeamsParticipants: () => Promise<Participant[]>;
73
+ getAssetUrl: (reference: string) => string;
74
+ assetUrls: AssetUrls | undefined;
75
+ endpoints: EndpointCallbacksMap;
76
+ }
77
+ /**
78
+ * Iframe-side counterpart to the host's `useIframeChannel`. Owns the half of the
79
+ * host↔iframe protocol every UI wrapper shares: the two-phase handshake, port
80
+ * lifecycle and HMR reset, the per-callback pending-promise maps (blob upload,
81
+ * endpoints, tick snapshots), asset-URL resolution and `refreshAssetAuth`,
82
+ * theme-preference relay, and required-message gating.
83
+ *
84
+ * Each wrapper supplies its `requiredMessages` and an `onMessage` handler for the
85
+ * channel-specific messages (`stateChanged`/`setSettings`/`setTheme`/…) and keeps its
86
+ * own state and context providers.
87
+ */
88
+ export declare function useHostChannel<THostMessage extends {
89
+ type: string;
90
+ }, TIFrameMessage>(options: UseHostChannelOptions<THostMessage>): UseHostChannelReturn<TIFrameMessage>;