@seriousgaming/sdk 0.0.0-stage → 1.0.0-alpha.1
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/LICENSE +202 -0
- package/NOTICE +1 -0
- package/README.md +29 -2
- package/lib/build/callbacks.d.ts +998 -0
- package/lib/build/callbacks.js +32 -0
- package/lib/build/context-merge.d.ts +59 -0
- package/lib/build/context-merge.js +118 -0
- package/lib/build/document.d.ts +36 -0
- package/lib/build/document.js +8 -0
- package/lib/build/encoding.d.ts +26 -0
- package/lib/build/encoding.js +78 -0
- package/lib/build/endpoints.d.ts +421 -0
- package/lib/build/endpoints.js +46 -0
- package/lib/build/hooks.d.ts +74 -0
- package/lib/build/hooks.js +1 -0
- package/lib/build/index.d.ts +18 -0
- package/lib/build/index.js +16 -0
- package/lib/build/llm.d.ts +166 -0
- package/lib/build/llm.js +17 -0
- package/lib/build/resource-schema.d.ts +1048 -0
- package/lib/build/resource-schema.js +7 -0
- package/lib/build/scheduling.d.ts +112 -0
- package/lib/build/scheduling.js +217 -0
- package/lib/build/seed.d.ts +51 -0
- package/lib/build/seed.js +89 -0
- package/lib/build/signatures.d.ts +372 -0
- package/lib/build/signatures.js +1 -0
- package/lib/build/speech.d.ts +102 -0
- package/lib/build/speech.js +11 -0
- package/lib/build/tick-validation.d.ts +23 -0
- package/lib/build/tick-validation.js +96 -0
- package/lib/build/timer.d.ts +74 -0
- package/lib/build/timer.js +1 -0
- package/lib/build/validators.d.ts +116 -0
- package/lib/build/validators.js +1 -0
- package/lib/commands.d.ts +357 -0
- package/lib/commands.js +14 -0
- package/lib/e2e.d.ts +238 -0
- package/lib/e2e.js +248 -0
- package/lib/projection.d.ts +266 -0
- package/lib/projection.js +23 -0
- package/lib/testing/index.d.ts +13 -0
- package/lib/testing/index.js +13 -0
- package/lib/testing/logic-args.d.ts +177 -0
- package/lib/testing/logic-args.js +229 -0
- package/lib/testing/stub-services.d.ts +41 -0
- package/lib/testing/stub-services.js +176 -0
- package/lib/ui/CrossTeamDataContext.d.ts +36 -0
- package/lib/ui/CrossTeamDataContext.js +32 -0
- package/lib/ui/PresentationContext.d.ts +72 -0
- package/lib/ui/PresentationContext.js +13 -0
- package/lib/ui/PresentationWrapper.d.ts +12 -0
- package/lib/ui/PresentationWrapper.js +78 -0
- package/lib/ui/SessionUserInterfaceCallbacksContext.d.ts +3 -0
- package/lib/ui/SessionUserInterfaceCallbacksContext.js +7 -0
- package/lib/ui/SessionUserInterfaceDataContext.d.ts +103 -0
- package/lib/ui/SessionUserInterfaceDataContext.js +17 -0
- package/lib/ui/SessionUserInterfaceHookFailuresContext.d.ts +44 -0
- package/lib/ui/SessionUserInterfaceHookFailuresContext.js +15 -0
- package/lib/ui/SessionUserInterfaceHostOverlaysContext.d.ts +23 -0
- package/lib/ui/SessionUserInterfaceHostOverlaysContext.js +23 -0
- package/lib/ui/SessionUserInterfaceLocaleContext.d.ts +11 -0
- package/lib/ui/SessionUserInterfaceLocaleContext.js +13 -0
- package/lib/ui/SessionUserInterfaceMetaContext.d.ts +39 -0
- package/lib/ui/SessionUserInterfaceMetaContext.js +13 -0
- package/lib/ui/SessionUserInterfaceParticipantsContext.d.ts +12 -0
- package/lib/ui/SessionUserInterfaceParticipantsContext.js +16 -0
- package/lib/ui/SessionUserInterfaceTeamConfigurationContext.d.ts +53 -0
- package/lib/ui/SessionUserInterfaceTeamConfigurationContext.js +23 -0
- package/lib/ui/SessionUserInterfaceTeamLinksContext.d.ts +56 -0
- package/lib/ui/SessionUserInterfaceTeamLinksContext.js +12 -0
- package/lib/ui/SessionUserInterfaceTeamsContext.d.ts +13 -0
- package/lib/ui/SessionUserInterfaceTeamsContext.js +13 -0
- package/lib/ui/SessionUserInterfaceThemeContext.d.ts +9 -0
- package/lib/ui/SessionUserInterfaceThemeContext.js +7 -0
- package/lib/ui/SessionUserInterfaceThemePreferenceContext.d.ts +3 -0
- package/lib/ui/SessionUserInterfaceThemePreferenceContext.js +7 -0
- package/lib/ui/SessionUserInterfaceWalkthroughLinkContext.d.ts +42 -0
- package/lib/ui/SessionUserInterfaceWalkthroughLinkContext.js +26 -0
- package/lib/ui/SessionUserInterfaceWrapper.d.ts +8 -0
- package/lib/ui/SessionUserInterfaceWrapper.js +321 -0
- package/lib/ui/SettingsEditorCallbacksContext.d.ts +3 -0
- package/lib/ui/SettingsEditorCallbacksContext.js +7 -0
- package/lib/ui/SettingsEditorContext.d.ts +33 -0
- package/lib/ui/SettingsEditorContext.js +16 -0
- package/lib/ui/SettingsEditorLocaleContext.d.ts +13 -0
- package/lib/ui/SettingsEditorLocaleContext.js +15 -0
- package/lib/ui/SettingsEditorThemeContext.d.ts +9 -0
- package/lib/ui/SettingsEditorThemeContext.js +7 -0
- package/lib/ui/SettingsEditorThemePreferenceContext.d.ts +3 -0
- package/lib/ui/SettingsEditorThemePreferenceContext.js +7 -0
- package/lib/ui/SettingsEditorWrapper.d.ts +13 -0
- package/lib/ui/SettingsEditorWrapper.js +74 -0
- package/lib/ui/UserInterfaceCallbacksContext.d.ts +3 -0
- package/lib/ui/UserInterfaceCallbacksContext.js +7 -0
- package/lib/ui/UserInterfaceDataContext.d.ts +78 -0
- package/lib/ui/UserInterfaceDataContext.js +11 -0
- package/lib/ui/UserInterfaceLocaleContext.d.ts +13 -0
- package/lib/ui/UserInterfaceLocaleContext.js +15 -0
- package/lib/ui/UserInterfaceOwnTeamContext.d.ts +25 -0
- package/lib/ui/UserInterfaceOwnTeamContext.js +26 -0
- package/lib/ui/UserInterfaceOwnTeamJoinLinkContext.d.ts +52 -0
- package/lib/ui/UserInterfaceOwnTeamJoinLinkContext.js +13 -0
- package/lib/ui/UserInterfaceParticipantContext.d.ts +16 -0
- package/lib/ui/UserInterfaceParticipantContext.js +24 -0
- package/lib/ui/UserInterfaceParticipantsContext.d.ts +12 -0
- package/lib/ui/UserInterfaceParticipantsContext.js +13 -0
- package/lib/ui/UserInterfaceRunKindContext.d.ts +31 -0
- package/lib/ui/UserInterfaceRunKindContext.js +31 -0
- package/lib/ui/UserInterfaceTeamsContext.d.ts +12 -0
- package/lib/ui/UserInterfaceTeamsContext.js +13 -0
- package/lib/ui/UserInterfaceThemeContext.d.ts +9 -0
- package/lib/ui/UserInterfaceThemeContext.js +7 -0
- package/lib/ui/UserInterfaceThemePreferenceContext.d.ts +3 -0
- package/lib/ui/UserInterfaceThemePreferenceContext.js +7 -0
- package/lib/ui/UserInterfaceTimerContext.d.ts +10 -0
- package/lib/ui/UserInterfaceTimerContext.js +12 -0
- package/lib/ui/UserInterfaceValidationContext.d.ts +25 -0
- package/lib/ui/UserInterfaceValidationContext.js +14 -0
- package/lib/ui/UserInterfaceViewerRoleContext.d.ts +14 -0
- package/lib/ui/UserInterfaceViewerRoleContext.js +17 -0
- package/lib/ui/UserInterfaceWrapper.d.ts +8 -0
- package/lib/ui/UserInterfaceWrapper.js +219 -0
- package/lib/ui/expectedParentOrigin.d.ts +10 -0
- package/lib/ui/expectedParentOrigin.js +32 -0
- package/lib/ui/index.d.ts +50 -0
- package/lib/ui/index.js +52 -0
- package/lib/ui/internal/SurfaceGate.d.ts +19 -0
- package/lib/ui/internal/SurfaceGate.js +17 -0
- package/lib/ui/internal/SurfaceScope.d.ts +17 -0
- package/lib/ui/internal/SurfaceScope.js +16 -0
- package/lib/ui/internal/createContexts.d.ts +50 -0
- package/lib/ui/internal/createContexts.js +32 -0
- package/lib/ui/internal/document-attributes.d.ts +9 -0
- package/lib/ui/internal/document-attributes.js +87 -0
- package/lib/ui/internal/surface-timer.d.ts +3 -0
- package/lib/ui/internal/surface-timer.js +11 -0
- package/lib/ui/internal/useLatestRef.d.ts +7 -0
- package/lib/ui/internal/useLatestRef.js +11 -0
- package/lib/ui/internal/useOptimisticMerge.d.ts +19 -0
- package/lib/ui/internal/useOptimisticMerge.js +28 -0
- package/lib/ui/internal/useSurfaceControls.d.ts +52 -0
- package/lib/ui/internal/useSurfaceControls.js +19 -0
- package/lib/ui/internal/useTypedPastState.d.ts +17 -0
- package/lib/ui/internal/useTypedPastState.js +17 -0
- package/lib/ui/mutation-commit.d.ts +32 -0
- package/lib/ui/mutation-commit.js +1 -0
- package/lib/ui/mutation-outcome.d.ts +66 -0
- package/lib/ui/mutation-outcome.js +49 -0
- package/lib/ui/request-id.d.ts +18 -0
- package/lib/ui/request-id.js +44 -0
- package/lib/ui/testing.d.ts +365 -0
- package/lib/ui/testing.js +572 -0
- package/lib/ui/tick-snapshot.d.ts +28 -0
- package/lib/ui/tick-snapshot.js +34 -0
- package/lib/ui/ui-protocol.d.ts +799 -0
- package/lib/ui/ui-protocol.js +50 -0
- package/lib/ui/useAsset.d.ts +80 -0
- package/lib/ui/useAsset.js +139 -0
- package/lib/ui/useEndpointCallbacks.d.ts +31 -0
- package/lib/ui/useEndpointCallbacks.js +68 -0
- package/lib/ui/useHostChannel.d.ts +90 -0
- package/lib/ui/useHostChannel.js +338 -0
- package/lib/ui/useMutationReporter.d.ts +100 -0
- package/lib/ui/useMutationReporter.js +97 -0
- package/lib/ui/usePastState.d.ts +39 -0
- package/lib/ui/usePastState.js +63 -0
- package/lib/ui/useViewerStorage.d.ts +54 -0
- package/lib/ui/useViewerStorage.js +141 -0
- package/package.json +83 -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>;
|