@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,7 @@
|
|
|
1
|
+
import { type MutableRefObject } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Mirror the latest render's `value` into a ref so a stable callback can read
|
|
4
|
+
* it synchronously without re-subscribing. The ref is updated on every render
|
|
5
|
+
* (not in an effect), so it is current the moment the callback runs.
|
|
6
|
+
*/
|
|
7
|
+
export declare function useLatestRef<T>(value: T): MutableRefObject<T>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useRef } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Mirror the latest render's `value` into a ref so a stable callback can read
|
|
4
|
+
* it synchronously without re-subscribing. The ref is updated on every render
|
|
5
|
+
* (not in an effect), so it is current the moment the callback runs.
|
|
6
|
+
*/
|
|
7
|
+
export function useLatestRef(value) {
|
|
8
|
+
const ref = useRef(value);
|
|
9
|
+
ref.current = value;
|
|
10
|
+
return ref;
|
|
11
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ContextDeletePath } from '../../build/context-merge.js';
|
|
2
|
+
import type { MutationCommit } from '../mutation-commit.js';
|
|
3
|
+
/**
|
|
4
|
+
* The optimistic record-merge shared by the participant and session
|
|
5
|
+
* wrappers. Mirrors the latest `value`, deep-merges an update into it
|
|
6
|
+
* synchronously (matching the host's §9.5 merge semantics, so a minimal
|
|
7
|
+
* nested patch never blanks untouched siblings locally), pushes the merged
|
|
8
|
+
* value into local state, and forwards only the update to `send` — the host
|
|
9
|
+
* applies it to its current context. Deletions are applied locally in the same
|
|
10
|
+
* order the host applies them, after the merge, so the optimistic view and the
|
|
11
|
+
* host's context agree about a removed key as well as a changed one. Each call
|
|
12
|
+
* sends its own delta;
|
|
13
|
+
* deltas are never accumulated across calls, so a rejected write is not
|
|
14
|
+
* resent inside later payloads. The returned callback's identity depends only
|
|
15
|
+
* on `send`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function useOptimisticMerge<T extends Record<string, unknown>>(value: T, setValue: (next: T) => void, send: (update: Partial<T>, deletes?: ContextDeletePath[]) => Promise<MutationCommit>): (update: Partial<T>, options?: {
|
|
18
|
+
deletes?: ContextDeletePath[];
|
|
19
|
+
}) => Promise<MutationCommit>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { applyContextUpdate } from '../../build/context-merge.js';
|
|
3
|
+
import { useLatestRef } from './useLatestRef.js';
|
|
4
|
+
/**
|
|
5
|
+
* The optimistic record-merge shared by the participant and session
|
|
6
|
+
* wrappers. Mirrors the latest `value`, deep-merges an update into it
|
|
7
|
+
* synchronously (matching the host's §9.5 merge semantics, so a minimal
|
|
8
|
+
* nested patch never blanks untouched siblings locally), pushes the merged
|
|
9
|
+
* value into local state, and forwards only the update to `send` — the host
|
|
10
|
+
* applies it to its current context. Deletions are applied locally in the same
|
|
11
|
+
* order the host applies them, after the merge, so the optimistic view and the
|
|
12
|
+
* host's context agree about a removed key as well as a changed one. Each call
|
|
13
|
+
* sends its own delta;
|
|
14
|
+
* deltas are never accumulated across calls, so a rejected write is not
|
|
15
|
+
* resent inside later payloads. The returned callback's identity depends only
|
|
16
|
+
* on `send`.
|
|
17
|
+
*/
|
|
18
|
+
export function useOptimisticMerge(value, setValue, send) {
|
|
19
|
+
const valueRef = useLatestRef(value);
|
|
20
|
+
return useCallback((update, options) => {
|
|
21
|
+
const merged = applyContextUpdate(valueRef.current, update, options?.deletes);
|
|
22
|
+
valueRef.current = merged;
|
|
23
|
+
setValue(merged);
|
|
24
|
+
return send(update, options?.deletes);
|
|
25
|
+
},
|
|
26
|
+
// valueRef is stable; setValue is a React state setter (stable).
|
|
27
|
+
[send, setValue, valueRef]);
|
|
28
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { HostOverlay } from '../../build/callbacks.js';
|
|
2
|
+
/**
|
|
3
|
+
* The timer/lifecycle control messages on the session channel. A structural
|
|
4
|
+
* subset of that iframe→host union, so the wrapper's typed `sendMutation`
|
|
5
|
+
* accepts it. Every one is awaitable.
|
|
6
|
+
*/
|
|
7
|
+
type SurfaceControlMessage = {
|
|
8
|
+
type: 'pauseTimer';
|
|
9
|
+
requestId?: string;
|
|
10
|
+
teamId?: string;
|
|
11
|
+
} | {
|
|
12
|
+
type: 'resumeTimer';
|
|
13
|
+
requestId?: string;
|
|
14
|
+
teamId?: string;
|
|
15
|
+
} | {
|
|
16
|
+
type: 'resetGame';
|
|
17
|
+
requestId?: string;
|
|
18
|
+
teamId?: string;
|
|
19
|
+
} | {
|
|
20
|
+
type: 'undoTick';
|
|
21
|
+
requestId?: string;
|
|
22
|
+
teamId?: string;
|
|
23
|
+
} | {
|
|
24
|
+
type: 'extendTick';
|
|
25
|
+
requestId?: string;
|
|
26
|
+
teamId?: string;
|
|
27
|
+
byMs: number;
|
|
28
|
+
} | {
|
|
29
|
+
type: 'openHostOverlay';
|
|
30
|
+
requestId?: string;
|
|
31
|
+
overlay: HostOverlay;
|
|
32
|
+
target?: string;
|
|
33
|
+
};
|
|
34
|
+
interface SurfaceControlChannel {
|
|
35
|
+
sendMutation: (message: SurfaceControlMessage) => Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
export interface SurfaceControls {
|
|
38
|
+
pauseTimer: (teamId?: string) => Promise<void>;
|
|
39
|
+
resumeTimer: (teamId?: string) => Promise<void>;
|
|
40
|
+
resetGame: (teamId?: string) => Promise<void>;
|
|
41
|
+
undoTick: (teamId?: string) => Promise<void>;
|
|
42
|
+
extendTick: (byMs: number, teamId?: string) => Promise<void>;
|
|
43
|
+
openHostOverlay: (overlay: HostOverlay, target?: string) => Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The pause/resume/reset/undo controls on the session wrapper. Each carries an
|
|
47
|
+
* optional `teamId`; omitting it applies the action to every team. Each control
|
|
48
|
+
* is independently memoised, so consumers' dependency arrays see stable
|
|
49
|
+
* identities.
|
|
50
|
+
*/
|
|
51
|
+
export declare function useSurfaceControls(channel: SurfaceControlChannel): SurfaceControls;
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* The pause/resume/reset/undo controls on the session wrapper. Each carries an
|
|
4
|
+
* optional `teamId`; omitting it applies the action to every team. Each control
|
|
5
|
+
* is independently memoised, so consumers' dependency arrays see stable
|
|
6
|
+
* identities.
|
|
7
|
+
*/
|
|
8
|
+
export function useSurfaceControls(channel) {
|
|
9
|
+
const { sendMutation } = channel;
|
|
10
|
+
const pauseTimer = useCallback((teamId) => sendMutation({ type: 'pauseTimer', teamId }), [sendMutation]);
|
|
11
|
+
const resumeTimer = useCallback((teamId) => sendMutation({ type: 'resumeTimer', teamId }), [sendMutation]);
|
|
12
|
+
const resetGame = useCallback((teamId) => sendMutation({ type: 'resetGame', teamId }), [sendMutation]);
|
|
13
|
+
const undoTick = useCallback((teamId) => sendMutation({ type: 'undoTick', teamId }), [sendMutation]);
|
|
14
|
+
// `byMs` leads: it is the argument every caller passes, where teamId is
|
|
15
|
+
// the one most omit.
|
|
16
|
+
const extendTick = useCallback((byMs, teamId) => sendMutation({ type: 'extendTick', byMs, teamId }), [sendMutation]);
|
|
17
|
+
const openHostOverlay = useCallback((overlay, target) => sendMutation({ type: 'openHostOverlay', overlay, target }), [sendMutation]);
|
|
18
|
+
return { pauseTimer, resumeTimer, resetGame, undoTick, extendTick, openHostOverlay };
|
|
19
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PastStateEntry, PastStateQuery } from '../../build/callbacks.js';
|
|
2
|
+
type RawPastStateReader = (query: PastStateQuery<Record<string, unknown>>) => Promise<PastStateEntry<Record<string, unknown>>[]>;
|
|
3
|
+
/**
|
|
4
|
+
* Re-expose the channel's untyped `getPastState` as the generic callback a
|
|
5
|
+
* surface declares, so `getPastState<Context>(query)` narrows both halves:
|
|
6
|
+
* each entry's `context`, and the `capabilityIds` the query may name.
|
|
7
|
+
*
|
|
8
|
+
* The channel is untyped because it does not know the game's `Context`; the
|
|
9
|
+
* cast is the one place that knowledge is reintroduced, and it is safe because
|
|
10
|
+
* the host projects onto the same declarations the type was generated from.
|
|
11
|
+
*
|
|
12
|
+
* One hook serves both surfaces. The player and session variants differ only
|
|
13
|
+
* in what `teams` defaults to, which the host resolves, so the signature is
|
|
14
|
+
* identical on both.
|
|
15
|
+
*/
|
|
16
|
+
export declare function useTypedPastState(raw: RawPastStateReader): <T = Record<string, unknown>>(query: PastStateQuery<T>) => Promise<PastStateEntry<T>[]>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Re-expose the channel's untyped `getPastState` as the generic callback a
|
|
4
|
+
* surface declares, so `getPastState<Context>(query)` narrows both halves:
|
|
5
|
+
* each entry's `context`, and the `capabilityIds` the query may name.
|
|
6
|
+
*
|
|
7
|
+
* The channel is untyped because it does not know the game's `Context`; the
|
|
8
|
+
* cast is the one place that knowledge is reintroduced, and it is safe because
|
|
9
|
+
* the host projects onto the same declarations the type was generated from.
|
|
10
|
+
*
|
|
11
|
+
* One hook serves both surfaces. The player and session variants differ only
|
|
12
|
+
* in what `teams` defaults to, which the host resolves, so the signature is
|
|
13
|
+
* identical on both.
|
|
14
|
+
*/
|
|
15
|
+
export function useTypedPastState(raw) {
|
|
16
|
+
return useCallback((query) => raw(query), [raw]);
|
|
17
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a committed mutation reports back.
|
|
3
|
+
*
|
|
4
|
+
* The surface holds the tick it was showing when the participant composed the
|
|
5
|
+
* write, and the host answers with the tick it committed into. When those
|
|
6
|
+
* differ the round advanced first, so the write landed in the round *after*
|
|
7
|
+
* the one on screen: not lost, but not the round the participant meant either.
|
|
8
|
+
*
|
|
9
|
+
* Only the committed tick crosses the boundary. The composed tick is the
|
|
10
|
+
* surface's own state, so it never has to be sent and the host is never asked
|
|
11
|
+
* to trust it.
|
|
12
|
+
*
|
|
13
|
+
* That is worth telling them about, and before this there was no way to know.
|
|
14
|
+
* Both serialisation strategies produce it — one queues the write past the
|
|
15
|
+
* tick, the other re-applies it against the tick's result — and neither said
|
|
16
|
+
* so (SPECIFICATION.md §9.5).
|
|
17
|
+
*
|
|
18
|
+
* Only the participant context write reports the pair; every other mutation
|
|
19
|
+
* resolves with `late: false` and no ticks.
|
|
20
|
+
*/
|
|
21
|
+
export type MutationCommit = {
|
|
22
|
+
/** The tick the write committed into, when the host reported one. */
|
|
23
|
+
committedAtTick?: number;
|
|
24
|
+
/** The tick the surface was showing when the write was composed. */
|
|
25
|
+
composedAtTick?: number;
|
|
26
|
+
/**
|
|
27
|
+
* True when the round advanced between composing the write and committing
|
|
28
|
+
* it, so the write counts for the next round rather than the one the
|
|
29
|
+
* participant was looking at.
|
|
30
|
+
*/
|
|
31
|
+
late: boolean;
|
|
32
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed outcomes for fallible UI state mutations.
|
|
3
|
+
*
|
|
4
|
+
* Mutating callbacks (`setData`, `updateSettings`, `requestTick`, `undoTick`,
|
|
5
|
+
* `resetGame`) return a promise that resolves once the operation has committed
|
|
6
|
+
* and rejects with a {@link MutationError} carrying a discriminated
|
|
7
|
+
* {@link MutationOutcome}. Games branch on `outcome.kind` to tell the
|
|
8
|
+
* participant what happened; `useMutationReporter` does this for them.
|
|
9
|
+
*
|
|
10
|
+
* A surface never retries a mutation on its own (SPECIFICATION.md §12.5.1). A
|
|
11
|
+
* `conflict` means the host has already retried and given up, so the most a
|
|
12
|
+
* surface does is offer the participant a 'Try again' control, which
|
|
13
|
+
* `useMutationReporter` provides as `failure.retry`.
|
|
14
|
+
*/
|
|
15
|
+
import type { ValidationRejection } from '../build/validators.js';
|
|
16
|
+
/**
|
|
17
|
+
* The reason a mutation failed.
|
|
18
|
+
*
|
|
19
|
+
* - `conflict` — the write kept losing a race with a tick and the host, having
|
|
20
|
+
* retried it itself, gave up. A person may try again; a surface MUST NOT
|
|
21
|
+
* retry it automatically. The dev server serialises writes against ticks, so
|
|
22
|
+
* it never emits this unless one is armed deliberately.
|
|
23
|
+
* - `superseded` — what the operation addressed has already moved on, because
|
|
24
|
+
* somebody else did the same thing. Terminal, and not a failure: the caller
|
|
25
|
+
* got the state they asked for. A double-pressed tick button is the archetype.
|
|
26
|
+
* - `validation-rejected` — a validator rejected the write; carries the same
|
|
27
|
+
* rejections delivered to {@link useUserInterfaceValidationRejections}.
|
|
28
|
+
* - `denied` — the operation was not permitted (e.g. a write of a capability
|
|
29
|
+
* the surface does not declare in `writes`).
|
|
30
|
+
* - `tick-failed` — the tick could not run (tick limit reached, game complete,
|
|
31
|
+
* or the executor threw).
|
|
32
|
+
* - `error` — anything else unexpected.
|
|
33
|
+
*/
|
|
34
|
+
export type MutationOutcome = {
|
|
35
|
+
kind: 'conflict';
|
|
36
|
+
message?: string;
|
|
37
|
+
} | {
|
|
38
|
+
kind: 'superseded';
|
|
39
|
+
message: string;
|
|
40
|
+
committedAtTick?: number;
|
|
41
|
+
} | {
|
|
42
|
+
kind: 'validation-rejected';
|
|
43
|
+
rejections: ValidationRejection[];
|
|
44
|
+
message?: string;
|
|
45
|
+
} | {
|
|
46
|
+
kind: 'denied';
|
|
47
|
+
message: string;
|
|
48
|
+
} | {
|
|
49
|
+
kind: 'tick-failed';
|
|
50
|
+
message: string;
|
|
51
|
+
} | {
|
|
52
|
+
kind: 'error';
|
|
53
|
+
message: string;
|
|
54
|
+
};
|
|
55
|
+
/** The message a {@link MutationError} carries for an outcome. */
|
|
56
|
+
export declare function describeMutationOutcome(outcome: MutationOutcome): string;
|
|
57
|
+
/**
|
|
58
|
+
* Error a fallible mutation promise rejects with. Inspect {@link outcome} (or
|
|
59
|
+
* the shorthand {@link kind}) to branch on the failure reason.
|
|
60
|
+
*/
|
|
61
|
+
export declare class MutationError extends Error {
|
|
62
|
+
readonly outcome: MutationOutcome;
|
|
63
|
+
constructor(outcome: MutationOutcome);
|
|
64
|
+
/** Shorthand for `outcome.kind`. */
|
|
65
|
+
get kind(): MutationOutcome['kind'];
|
|
66
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed outcomes for fallible UI state mutations.
|
|
3
|
+
*
|
|
4
|
+
* Mutating callbacks (`setData`, `updateSettings`, `requestTick`, `undoTick`,
|
|
5
|
+
* `resetGame`) return a promise that resolves once the operation has committed
|
|
6
|
+
* and rejects with a {@link MutationError} carrying a discriminated
|
|
7
|
+
* {@link MutationOutcome}. Games branch on `outcome.kind` to tell the
|
|
8
|
+
* participant what happened; `useMutationReporter` does this for them.
|
|
9
|
+
*
|
|
10
|
+
* A surface never retries a mutation on its own (SPECIFICATION.md §12.5.1). A
|
|
11
|
+
* `conflict` means the host has already retried and given up, so the most a
|
|
12
|
+
* surface does is offer the participant a 'Try again' control, which
|
|
13
|
+
* `useMutationReporter` provides as `failure.retry`.
|
|
14
|
+
*/
|
|
15
|
+
/** The message a {@link MutationError} carries for an outcome. */
|
|
16
|
+
export function describeMutationOutcome(outcome) {
|
|
17
|
+
switch (outcome.kind) {
|
|
18
|
+
case 'conflict':
|
|
19
|
+
return outcome.message ?? 'The write conflicted with a concurrent tick';
|
|
20
|
+
case 'validation-rejected':
|
|
21
|
+
return (outcome.message ??
|
|
22
|
+
outcome.rejections[0]?.message ??
|
|
23
|
+
'The write was rejected by a validator');
|
|
24
|
+
case 'superseded':
|
|
25
|
+
case 'denied':
|
|
26
|
+
case 'tick-failed':
|
|
27
|
+
case 'error':
|
|
28
|
+
return outcome.message;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Error a fallible mutation promise rejects with. Inspect {@link outcome} (or
|
|
33
|
+
* the shorthand {@link kind}) to branch on the failure reason.
|
|
34
|
+
*/
|
|
35
|
+
export class MutationError extends Error {
|
|
36
|
+
outcome;
|
|
37
|
+
constructor(outcome) {
|
|
38
|
+
super(describeMutationOutcome(outcome));
|
|
39
|
+
this.name = 'MutationError';
|
|
40
|
+
this.outcome = outcome;
|
|
41
|
+
// Restore the prototype chain so `instanceof` works after transpilation
|
|
42
|
+
// to ES5-targeted output.
|
|
43
|
+
Object.setPrototypeOf(this, MutationError.prototype);
|
|
44
|
+
}
|
|
45
|
+
/** Shorthand for `outcome.kind`. */
|
|
46
|
+
get kind() {
|
|
47
|
+
return this.outcome.kind;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A correlation id for one host↔iframe request, in any browsing context.
|
|
3
|
+
*
|
|
4
|
+
* `crypto.randomUUID` exists only in a **secure context**: HTTPS, or an origin
|
|
5
|
+
* the browser treats as potentially trustworthy, which `localhost` is and a LAN
|
|
6
|
+
* address is not. So a surface opened on a phone at `http://192.168.1.157:3000`
|
|
7
|
+
* — the normal way anyone tests a participant screen on a real device — threw
|
|
8
|
+
* `crypto.randomUUID is not a function` on mount, before it had painted
|
|
9
|
+
* anything. The dev server serves plain HTTP by design, and the platform serves
|
|
10
|
+
* HTTPS, which is why this only ever failed on the one setup nobody runs CI on.
|
|
11
|
+
*
|
|
12
|
+
* `crypto.getRandomValues` is **not** gated on a secure context, so the
|
|
13
|
+
* fallback is a real version 4 UUID rather than a weaker shape. The last resort
|
|
14
|
+
* exists for a context with no `crypto` at all (an old embedded webview, a test
|
|
15
|
+
* double): these ids correlate a request with its reply within one page, so
|
|
16
|
+
* uniqueness is what they need and unpredictability is a bonus.
|
|
17
|
+
*/
|
|
18
|
+
export declare function newRequestId(): string;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A correlation id for one host↔iframe request, in any browsing context.
|
|
3
|
+
*
|
|
4
|
+
* `crypto.randomUUID` exists only in a **secure context**: HTTPS, or an origin
|
|
5
|
+
* the browser treats as potentially trustworthy, which `localhost` is and a LAN
|
|
6
|
+
* address is not. So a surface opened on a phone at `http://192.168.1.157:3000`
|
|
7
|
+
* — the normal way anyone tests a participant screen on a real device — threw
|
|
8
|
+
* `crypto.randomUUID is not a function` on mount, before it had painted
|
|
9
|
+
* anything. The dev server serves plain HTTP by design, and the platform serves
|
|
10
|
+
* HTTPS, which is why this only ever failed on the one setup nobody runs CI on.
|
|
11
|
+
*
|
|
12
|
+
* `crypto.getRandomValues` is **not** gated on a secure context, so the
|
|
13
|
+
* fallback is a real version 4 UUID rather than a weaker shape. The last resort
|
|
14
|
+
* exists for a context with no `crypto` at all (an old embedded webview, a test
|
|
15
|
+
* double): these ids correlate a request with its reply within one page, so
|
|
16
|
+
* uniqueness is what they need and unpredictability is a bonus.
|
|
17
|
+
*/
|
|
18
|
+
const HEX = Array.from({ length: 256 }, (_, byte) => byte.toString(16).padStart(2, '0'));
|
|
19
|
+
export function newRequestId() {
|
|
20
|
+
const api = globalThis.crypto;
|
|
21
|
+
if (typeof api?.randomUUID === 'function')
|
|
22
|
+
return api.randomUUID();
|
|
23
|
+
if (typeof api?.getRandomValues === 'function') {
|
|
24
|
+
return uuidFrom(api.getRandomValues(new Uint8Array(16)));
|
|
25
|
+
}
|
|
26
|
+
const bytes = new Uint8Array(16);
|
|
27
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
28
|
+
bytes[index] = Math.floor(Math.random() * 256);
|
|
29
|
+
}
|
|
30
|
+
return uuidFrom(bytes);
|
|
31
|
+
}
|
|
32
|
+
/** RFC 9562 §5.4: sixteen random bytes, with the version and variant stamped in. */
|
|
33
|
+
function uuidFrom(bytes) {
|
|
34
|
+
bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40;
|
|
35
|
+
bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
|
|
36
|
+
const hex = (index) => HEX[bytes[index] ?? 0];
|
|
37
|
+
const group = (from, to) => {
|
|
38
|
+
let out = '';
|
|
39
|
+
for (let index = from; index < to; index += 1)
|
|
40
|
+
out += hex(index);
|
|
41
|
+
return out;
|
|
42
|
+
};
|
|
43
|
+
return `${group(0, 4)}-${group(4, 6)}-${group(6, 8)}-${group(8, 10)}-${group(10, 16)}`;
|
|
44
|
+
}
|