@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,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
+ }