@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,372 @@
|
|
|
1
|
+
import { Participant, SignatureCallbacks } from './callbacks.js';
|
|
2
|
+
import type { ContextDeletePath } from './context-merge.js';
|
|
3
|
+
import type { GameTeamMode } from './resource-schema.js';
|
|
4
|
+
/**
|
|
5
|
+
* Which kind of run a function is executing in.
|
|
6
|
+
*
|
|
7
|
+
* `'session'` is the game being played. `'walkthrough'` is a disposable run
|
|
8
|
+
* alongside it, used to demonstrate the game before or during play: one team,
|
|
9
|
+
* one participant, always from tick 0, isolated from the session it sits
|
|
10
|
+
* beside.
|
|
11
|
+
*
|
|
12
|
+
* A walkthrough is a solo run of something built for a group. Logic may use
|
|
13
|
+
* this to synthesise the social context that is genuinely absent (other teams
|
|
14
|
+
* on a leaderboard, teammates in a presence list), and to suppress spoilers and
|
|
15
|
+
* side effects. It must not change the mechanics of the team being driven: a
|
|
16
|
+
* walkthrough that plays differently teaches the wrong game.
|
|
17
|
+
*/
|
|
18
|
+
export type RunKind = 'session' | 'walkthrough';
|
|
19
|
+
/**
|
|
20
|
+
* How a session runs its teams (§5.2): the game's `teams.mode`.
|
|
21
|
+
*
|
|
22
|
+
* Only the two the game declares, and no third for a session that happens to
|
|
23
|
+
* hold one team: §20.1 admits only these, and a one-team session is read off
|
|
24
|
+
* `teams.length`, not off the mode.
|
|
25
|
+
*/
|
|
26
|
+
export type SessionMode = GameTeamMode;
|
|
27
|
+
/**
|
|
28
|
+
* Which viewer a player surface is being rendered for (§13.3).
|
|
29
|
+
*
|
|
30
|
+
* - `'participant'`: a team member who joined the session. Has a caller
|
|
31
|
+
* identity and appears in the team roster.
|
|
32
|
+
* - `'observer'`: a facilitator peeking at a team's live player surface without
|
|
33
|
+
* joining. Has no caller identity and is absent from the roster and
|
|
34
|
+
* presence. Still sees the live surface and, on a permissive host, may
|
|
35
|
+
* drive it.
|
|
36
|
+
*/
|
|
37
|
+
export type ViewerRole = 'participant' | 'observer';
|
|
38
|
+
/**
|
|
39
|
+
* The facts true of the whole run, delivered to every call into game logic
|
|
40
|
+
* whether it runs inside the tick loop or outside it.
|
|
41
|
+
*
|
|
42
|
+
* {@link RunMeta} adds the tick for logic that runs as part of one. Hooks,
|
|
43
|
+
* endpoints and validators receive this shape alone: none of them runs as part
|
|
44
|
+
* of a tick, so there is no tick being executed to name.
|
|
45
|
+
*/
|
|
46
|
+
export interface LogicMeta {
|
|
47
|
+
/**
|
|
48
|
+
* The active session locale (e.g. 'en', 'ar'). Session-wide, not per
|
|
49
|
+
* participant: every participant in a session shares one locale. Use it
|
|
50
|
+
* when generating prose (for example via `callbacks.generateText`) that
|
|
51
|
+
* should be produced in the session's language.
|
|
52
|
+
*/
|
|
53
|
+
locale: string;
|
|
54
|
+
/**
|
|
55
|
+
* A session-wide random seed, stable for the life of the session and across
|
|
56
|
+
* a re-run of the same tick.
|
|
57
|
+
*
|
|
58
|
+
* Logic functions may be invoked more than once for one logical tick, so a
|
|
59
|
+
* live `Math.random()` is unsafe: two runs of the same tick disagree and
|
|
60
|
+
* only the last is durable.
|
|
61
|
+
*
|
|
62
|
+
* Derive instead. {@link deriveSeed} combines this with whatever identifies
|
|
63
|
+
* the thing being seeded, and {@link seededRandom} turns the result into a
|
|
64
|
+
* generator:
|
|
65
|
+
*
|
|
66
|
+
* ```ts
|
|
67
|
+
* const random = seededRandom(deriveSeed(meta.seed, meta.index, 'opening-hand'));
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* Freeze a derived value into context if later ticks need the same one,
|
|
71
|
+
* rather than re-deriving it from parts that may have changed.
|
|
72
|
+
*/
|
|
73
|
+
seed: string;
|
|
74
|
+
/**
|
|
75
|
+
* Whether this is the game being played or a walkthrough beside it.
|
|
76
|
+
*
|
|
77
|
+
* Host-supplied and identical on every surface of one run, so a decision
|
|
78
|
+
* the engine takes on it and a decision a user interface takes on it cannot
|
|
79
|
+
* disagree. A host that predates walkthroughs reports `'session'`, which is
|
|
80
|
+
* also the value every existing game behaves correctly on.
|
|
81
|
+
*
|
|
82
|
+
* ```ts
|
|
83
|
+
* const rivals = meta.runKind === 'walkthrough'
|
|
84
|
+
* ? demonstrationRivals(meta.seed)
|
|
85
|
+
* : context.market.rivals;
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
runKind: RunKind;
|
|
89
|
+
/**
|
|
90
|
+
* When this operation started, in milliseconds since the epoch: the tick,
|
|
91
|
+
* the hook dispatch, the endpoint call or the validator run this call is
|
|
92
|
+
* part of. Fixed by the host before the first logic call of the operation,
|
|
93
|
+
* so every call in it (every team's `Run` in one tick, and the tick
|
|
94
|
+
* controller after it) reads the same value, and a re-run of the same
|
|
95
|
+
* operation reads it again.
|
|
96
|
+
*
|
|
97
|
+
* Use it instead of `Date.now()`, which differs between a tick and its
|
|
98
|
+
* re-run and is not offered by every runtime (§3.2):
|
|
99
|
+
*
|
|
100
|
+
* ```ts
|
|
101
|
+
* const placedAt = meta.now;
|
|
102
|
+
* const ageMs = meta.now - context.orders.PlacedAt;
|
|
103
|
+
* ```
|
|
104
|
+
*
|
|
105
|
+
* Freeze it into context if a later operation needs the same instant.
|
|
106
|
+
*/
|
|
107
|
+
now: number;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* `meta` for logic that runs as part of a tick: the engine's `Init` and `Run`,
|
|
111
|
+
* the aggregate forms of both, and tick-controller logic.
|
|
112
|
+
*/
|
|
113
|
+
export interface RunMeta extends LogicMeta {
|
|
114
|
+
/**
|
|
115
|
+
* The tick being executed, resolved before the function runs: `0` in
|
|
116
|
+
* `Init`, and 1-based in `Run`, so the first `Run` of a session sees `1`
|
|
117
|
+
* and the last `Run` of a `ticks: 10` game sees `10`.
|
|
118
|
+
*
|
|
119
|
+
* Deliberately one ahead of the `completedTicks` a user interface reads,
|
|
120
|
+
* which counts the ticks already completed (§9.3).
|
|
121
|
+
*/
|
|
122
|
+
tick: number;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* `meta` for a function that runs against one team's context: `Init`, `Run`,
|
|
126
|
+
* and tick-controller logic.
|
|
127
|
+
*
|
|
128
|
+
* The rule is one sentence: **`teamId` and `index` name the team whose context
|
|
129
|
+
* is in `context`.** In independent mode that is the team being ticked. In
|
|
130
|
+
* shared-state mode the tick controller runs once for the session with team 0's
|
|
131
|
+
* context, so it reads team 0's identity, which is what it was given.
|
|
132
|
+
*
|
|
133
|
+
* `RunAggregate` and `InitAggregate` receive plain {@link RunMeta}: they run
|
|
134
|
+
* across every team at once, so there is no single team to name, and each
|
|
135
|
+
* team's identity is already on its entry in `teams`.
|
|
136
|
+
*/
|
|
137
|
+
export interface TeamRunMeta extends RunMeta {
|
|
138
|
+
/**
|
|
139
|
+
* The team this call is running for, e.g. `'team-1'`. The same identifier
|
|
140
|
+
* `RunAggregate` sees in its `teams` array, and the same one a session
|
|
141
|
+
* surface keys `SessionData` by. Opaque and stable for the session: never
|
|
142
|
+
* parse it or build one from an index, and key ordinal logic on `index`
|
|
143
|
+
* instead (§5.6). A single-team game's one team has an ordinary id too:
|
|
144
|
+
* one id space, so a value keyed off this in the engine can be looked up
|
|
145
|
+
* by a surface.
|
|
146
|
+
*/
|
|
147
|
+
teamId: string;
|
|
148
|
+
/**
|
|
149
|
+
* The team's stable, session-lifetime index. Assigned at team creation
|
|
150
|
+
* (dense 0..N-1 at session start) and never reassigned: a removal leaves a
|
|
151
|
+
* gap, and a team added later takes the lowest unused index. Use it, not
|
|
152
|
+
* array position, to key anything per team.
|
|
153
|
+
*/
|
|
154
|
+
index: number;
|
|
155
|
+
}
|
|
156
|
+
export interface LogicFunctionArgs<TContext, TSettings, TCallbacks = SignatureCallbacks> {
|
|
157
|
+
context: TContext;
|
|
158
|
+
/**
|
|
159
|
+
* The settings resources this resource declared in `settings`, keyed by
|
|
160
|
+
* identifier: resolved from their defaults, the active scenario and the
|
|
161
|
+
* session's overrides, and fixed when play starts (§8). Read-only.
|
|
162
|
+
*/
|
|
163
|
+
settings: TSettings;
|
|
164
|
+
meta: TeamRunMeta;
|
|
165
|
+
/**
|
|
166
|
+
* The participants on the team this call is running for, taken when the
|
|
167
|
+
* operation started (§10.2). A tick controller sees the participants of
|
|
168
|
+
* every team in its `teams`. Read-only, and a copy per call, so changing
|
|
169
|
+
* it changes nothing: only a lifecycle operation (a join, a rename, a
|
|
170
|
+
* move) changes the roster.
|
|
171
|
+
*
|
|
172
|
+
* ```ts
|
|
173
|
+
* const present = participants.filter((p) => p.connected).length;
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
participants: readonly Readonly<Participant>[];
|
|
177
|
+
callbacks: TCallbacks;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* What engine and hook logic returns: the state it writes, named by kind.
|
|
181
|
+
*
|
|
182
|
+
* `capabilities` is a patch over the context the function received, merged one
|
|
183
|
+
* level deep, exactly as the whole result was before it was wrapped. Naming the
|
|
184
|
+
* kind leaves room for other outputs beside it, and the runtime refuses any
|
|
185
|
+
* top-level key it does not know, so a result written in the old flat shape
|
|
186
|
+
* fails rather than losing its writes.
|
|
187
|
+
*/
|
|
188
|
+
export interface LogicResult<TContext> {
|
|
189
|
+
capabilities: TContext;
|
|
190
|
+
/**
|
|
191
|
+
* Paths to remove once `capabilities` is folded in, with the same rules as
|
|
192
|
+
* a UI's `deletes`: segments joined by `.` (`'orders.Open.o-17'`), or an
|
|
193
|
+
* array of segments where a key could contain a dot. A path that does not
|
|
194
|
+
* resolve is not an error. Each must sit under a capability the resource
|
|
195
|
+
* declares in `writes`; one under a capability it only reads fails the
|
|
196
|
+
* call. Use it to drop an entry from a keyed map without rebuilding the
|
|
197
|
+
* whole variable.
|
|
198
|
+
*/
|
|
199
|
+
deletes?: ContextDeletePath[];
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* The arguments of the engine's own functions: everything logic receives, plus
|
|
203
|
+
* the engine's private memory.
|
|
204
|
+
*
|
|
205
|
+
* `state` is whatever the engine last returned under `state`, and `undefined`
|
|
206
|
+
* until it first returns one (so always in `Init`). The engine is its only
|
|
207
|
+
* writer and its only reader: no surface, hook, validator or endpoint ever
|
|
208
|
+
* sees it. It is snapshotted and rewound with the tick, so undo and reset put
|
|
209
|
+
* it back exactly as they put back capabilities.
|
|
210
|
+
*/
|
|
211
|
+
export interface EngineFunctionArgs<TContext, TSettings, TCallbacks = SignatureCallbacks, TState = never> extends LogicFunctionArgs<TContext, TSettings, TCallbacks> {
|
|
212
|
+
/**
|
|
213
|
+
* Optional in the type so a test calling `Run` directly need not pass it;
|
|
214
|
+
* the runtime always does, as `undefined` before the first write.
|
|
215
|
+
*/
|
|
216
|
+
state?: TState;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* What the engine's `Init` and `Run` return: the capabilities they write and,
|
|
220
|
+
* optionally, the engine's private memory.
|
|
221
|
+
*
|
|
222
|
+
* `state` replaces the previous state whole; leave it out to keep the previous
|
|
223
|
+
* one. It must be JSON-serialisable (plain objects, arrays, strings, finite
|
|
224
|
+
* numbers, booleans and `null`), which the runtime checks.
|
|
225
|
+
*
|
|
226
|
+
* Typed by `TState` rather than a schema: the engine is the only writer and the
|
|
227
|
+
* only reader, so the compiler checks it end to end.
|
|
228
|
+
*/
|
|
229
|
+
export interface EngineResult<TContext, TState = never> extends LogicResult<TContext> {
|
|
230
|
+
state?: TState;
|
|
231
|
+
}
|
|
232
|
+
export type InitializationFunction<TContext, TSettings, TCallbacks = SignatureCallbacks, TState = never> = (args: EngineFunctionArgs<TContext, TSettings, TCallbacks, TState>) => Promise<EngineResult<TContext, TState>>;
|
|
233
|
+
/**
|
|
234
|
+
* What the engine's `Run` returns: an {@link EngineResult}, plus whether the
|
|
235
|
+
* game is now over for this team.
|
|
236
|
+
*/
|
|
237
|
+
export interface RunResult<TContext, TState = never> extends EngineResult<TContext, TState> {
|
|
238
|
+
/**
|
|
239
|
+
* `true` ends the game for the team this call ran for: no further ticks,
|
|
240
|
+
* as a tick controller's `complete` does. Per team in independent mode;
|
|
241
|
+
* in shared-state mode one team's completion ends it for every team,
|
|
242
|
+
* because they share one clock. An undo of the completing tick clears it.
|
|
243
|
+
* `false` and absence are the same: completion cannot be withdrawn here.
|
|
244
|
+
*/
|
|
245
|
+
complete?: boolean;
|
|
246
|
+
}
|
|
247
|
+
export type RunFunction<TContext, TSettings, TCallbacks = SignatureCallbacks, TState = never> = (args: EngineFunctionArgs<TContext, TSettings, TCallbacks, TState>) => Promise<RunResult<TContext, TState>>;
|
|
248
|
+
/**
|
|
249
|
+
* One step of an engine that splits its `Init` or `Run` into several functions.
|
|
250
|
+
*
|
|
251
|
+
* A step takes the engine's arguments and returns the next context, flat, so
|
|
252
|
+
* steps chain by passing one's output to the next; only the exported `Init` or
|
|
253
|
+
* `Run` wraps the final context in `{ capabilities }`, and returns any `state`.
|
|
254
|
+
* A step may read `state`; its type defaults to `unknown`, so a step that
|
|
255
|
+
* ignores it accepts whatever the engine keeps.
|
|
256
|
+
*/
|
|
257
|
+
export type StepFunction<TContext, TSettings, TCallbacks = SignatureCallbacks, TState = unknown> = (args: EngineFunctionArgs<TContext, TSettings, TCallbacks, TState>) => Promise<TContext>;
|
|
258
|
+
export interface TickControllerResult {
|
|
259
|
+
/** False disables automatic ticking: ticks are manual only. */
|
|
260
|
+
enabled: boolean;
|
|
261
|
+
/**
|
|
262
|
+
* Milliseconds until the next automatic tick, a whole number within the
|
|
263
|
+
* §11.8 bounds (30 seconds to 7 days). Needed when `enabled` is true and
|
|
264
|
+
* there is no `nextTickAt`; beside a `nextTickAt` it is only a display
|
|
265
|
+
* hint, and a controller with the timer off may leave it out.
|
|
266
|
+
*/
|
|
267
|
+
intervalMs?: number;
|
|
268
|
+
/**
|
|
269
|
+
* When the next tick should fire (epoch milliseconds, UTC). When set and
|
|
270
|
+
* `enabled` is true, the runtime uses it instead of `intervalMs`. If it is
|
|
271
|
+
* in the past the runtime fires one tick immediately, then re-evaluates.
|
|
272
|
+
*/
|
|
273
|
+
nextTickAt?: number;
|
|
274
|
+
/** When true, the game is permanently complete: no further ticks will be allowed. */
|
|
275
|
+
complete?: boolean;
|
|
276
|
+
}
|
|
277
|
+
export interface TickControllerFunctionArgs<TContext, TSettings, TCallbacks = SignatureCallbacks> extends LogicFunctionArgs<TContext, TSettings, TCallbacks> {
|
|
278
|
+
/**
|
|
279
|
+
* The teams this evaluation covers, sorted by stable index: every team in
|
|
280
|
+
* a shared-state game, the one team being evaluated in independent mode,
|
|
281
|
+
* and the one team in a single-team game. Always present, so a controller
|
|
282
|
+
* can say which team it is waiting on by `teamId` or `index` even after a
|
|
283
|
+
* team has been removed and the indexes have a gap.
|
|
284
|
+
*/
|
|
285
|
+
teams: AggregateFunctionTeam<TContext>[];
|
|
286
|
+
/**
|
|
287
|
+
* Whether the session's waiting room is currently open — players are seeing
|
|
288
|
+
* the game's waiting room UI instead of its game UIs.
|
|
289
|
+
*
|
|
290
|
+
* The flag pauses nothing by itself. A game that wants automatic ticks to
|
|
291
|
+
* stop while players wait returns `enabled: false` when this is true;
|
|
292
|
+
* a game whose simulation should keep advancing in the background ignores
|
|
293
|
+
* it. Deciding here rather than in the runtime keeps the policy with the
|
|
294
|
+
* author, who is the only one who knows which of the two their game is.
|
|
295
|
+
*/
|
|
296
|
+
waitingRoomOpen: boolean;
|
|
297
|
+
}
|
|
298
|
+
export type GetTickControllerFunction<TContext, TSettings, TCallbacks = SignatureCallbacks> = (args: TickControllerFunctionArgs<TContext, TSettings, TCallbacks>) => Promise<TickControllerResult>;
|
|
299
|
+
export interface AggregateFunctionTeam<TTeamContext, TTeamState = never> {
|
|
300
|
+
teamId: string;
|
|
301
|
+
/**
|
|
302
|
+
* Platform-assigned, session-stable team index. Assigned at team creation
|
|
303
|
+
* (dense 0..N-1 at session start) and never reassigned: removals leave a
|
|
304
|
+
* gap, later-added teams take the lowest unused index. The same team has
|
|
305
|
+
* the same index here, in the player teams context, and in the
|
|
306
|
+
* session teams context. `teams.defaultNames` entries are applied
|
|
307
|
+
* by this index.
|
|
308
|
+
*/
|
|
309
|
+
index: number;
|
|
310
|
+
/**
|
|
311
|
+
* This team's context. For an aggregate engine function, its own
|
|
312
|
+
* capabilities (team and private scope): shared-scope capabilities are not
|
|
313
|
+
* repeated here, and arrive once, in `shared`. For a tick controller, the
|
|
314
|
+
* team's context as the controller's own `context` would hold it.
|
|
315
|
+
*/
|
|
316
|
+
context: TTeamContext;
|
|
317
|
+
/** This team's engine state, as `Run` sees it (see {@link EngineFunctionArgs}). */
|
|
318
|
+
state?: TTeamState;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* The session's shared store, as an aggregate function sees it: one value per
|
|
322
|
+
* shared-scope capability the engine declared, whatever the number of teams.
|
|
323
|
+
*/
|
|
324
|
+
export interface AggregateFunctionShared<TSharedContext, TSharedState = never> {
|
|
325
|
+
context: TSharedContext;
|
|
326
|
+
/**
|
|
327
|
+
* The engine's session-wide private memory: one value for the session,
|
|
328
|
+
* written under `shared.state` by an aggregate function in a shared-state
|
|
329
|
+
* or single-team session.
|
|
330
|
+
*/
|
|
331
|
+
state?: TSharedState;
|
|
332
|
+
}
|
|
333
|
+
export interface RunAggregateFunctionArgs<TTeamContext, TSharedContext, TSettings, TCallbacks = SignatureCallbacks, TTeamState = never, TSharedState = never> {
|
|
334
|
+
shared: AggregateFunctionShared<TSharedContext, TSharedState>;
|
|
335
|
+
teams: AggregateFunctionTeam<TTeamContext, TTeamState>[];
|
|
336
|
+
/**
|
|
337
|
+
* The settings resources this resource declared in `settings`, keyed by
|
|
338
|
+
* identifier: resolved from their defaults, the active scenario and the
|
|
339
|
+
* session's overrides, and fixed when play starts (§8). Read-only.
|
|
340
|
+
*/
|
|
341
|
+
settings: TSettings;
|
|
342
|
+
meta: RunMeta;
|
|
343
|
+
/**
|
|
344
|
+
* The participants of every team in `teams`, taken when the operation
|
|
345
|
+
* started (§10.2): every participant in the session whenever every team
|
|
346
|
+
* is passed. Group by `teamId` to join them to their team. Read-only.
|
|
347
|
+
*/
|
|
348
|
+
participants: readonly Readonly<Participant>[];
|
|
349
|
+
callbacks: TCallbacks;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* An aggregate engine's result. Each level names what it writes the same way a
|
|
353
|
+
* team's `Run` does: every `teams` entry has the shape of a `Run` result, in
|
|
354
|
+
* the order the teams were passed, and `shared` carries the session's
|
|
355
|
+
* shared-scope capabilities. A shared capability is written through `shared`
|
|
356
|
+
* alone; one in a `teams` entry fails the call.
|
|
357
|
+
*/
|
|
358
|
+
export interface RunAggregateResult<TTeamContext, TSharedContext, TTeamState = never, TSharedState = never> {
|
|
359
|
+
shared: EngineResult<Partial<TSharedContext>, TSharedState>;
|
|
360
|
+
teams: EngineResult<TTeamContext, TTeamState>[];
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* `RunAggregate`'s result: each level may also report completion, as `Run`'s
|
|
364
|
+
* `complete` does. A `teams` entry's ends the game for that team; `shared`'s
|
|
365
|
+
* for every team at once.
|
|
366
|
+
*/
|
|
367
|
+
export interface RunAggregateTickResult<TTeamContext, TSharedContext, TTeamState = never, TSharedState = never> {
|
|
368
|
+
shared: RunResult<Partial<TSharedContext>, TSharedState>;
|
|
369
|
+
teams: RunResult<TTeamContext, TTeamState>[];
|
|
370
|
+
}
|
|
371
|
+
export type RunAggregateFunction<TTeamContext, TSharedContext, TSettings, TCallbacks = SignatureCallbacks, TTeamState = never, TSharedState = never> = (args: RunAggregateFunctionArgs<TTeamContext, TSharedContext, TSettings, TCallbacks, TTeamState, TSharedState>) => Promise<RunAggregateTickResult<TTeamContext, TSharedContext, TTeamState, TSharedState>>;
|
|
372
|
+
export type InitAggregateFunction<TTeamContext, TSharedContext, TSettings, TCallbacks = SignatureCallbacks, TTeamState = never, TSharedState = never> = (args: RunAggregateFunctionArgs<TTeamContext, TSharedContext, TSettings, TCallbacks, TTeamState, TSharedState>) => Promise<RunAggregateResult<TTeamContext, TSharedContext, TTeamState, TSharedState>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Speech Generation Types
|
|
3
|
+
*
|
|
4
|
+
* Provides provider-agnostic text-to-speech for the engine, hooks, tick controllers
|
|
5
|
+
* and user interfaces. Game code describes the utterance via a small set of
|
|
6
|
+
* role-based voice presets; the runtime decides which provider, model and
|
|
7
|
+
* voice executes the call. The synthesised audio is persisted as a blob and
|
|
8
|
+
* the returned asset reference (`blob:<id>`) can be played from a UI
|
|
9
|
+
* (`getAssetUrl`) or read in the engine (`readAsset`).
|
|
10
|
+
*/
|
|
11
|
+
import type { LLMTier } from './llm.js';
|
|
12
|
+
/**
|
|
13
|
+
* Role-based voice preset. The runtime maps the preset to a concrete provider
|
|
14
|
+
* voice. Game code only ever passes the preset name. The five values below are
|
|
15
|
+
* the recommended starting set; runtimes may accept additional preset names.
|
|
16
|
+
*
|
|
17
|
+
* - `neutral` — warm, balanced, unobtrusive
|
|
18
|
+
* - `narrator` — engaging, gravitas, draws the listener in
|
|
19
|
+
* - `announcer` — clear, authoritative, broadcast-style
|
|
20
|
+
* - `executive` — confident, weighty, commands attention
|
|
21
|
+
* - `coach` — encouraging, warm, reassuring
|
|
22
|
+
*
|
|
23
|
+
* If a requested preset is not configured, the runtime falls back to
|
|
24
|
+
* `neutral`; if that is also unconfigured, it falls back to the provider's
|
|
25
|
+
* own default voice.
|
|
26
|
+
*/
|
|
27
|
+
export type GenerateSpeechVoice = 'neutral' | 'narrator' | 'announcer' | 'executive' | 'coach';
|
|
28
|
+
/**
|
|
29
|
+
* Audio container format for synthesised speech.
|
|
30
|
+
*/
|
|
31
|
+
export type GenerateSpeechFormat = 'mp3' | 'wav' | 'opus';
|
|
32
|
+
/**
|
|
33
|
+
* Options for synthesising speech.
|
|
34
|
+
*/
|
|
35
|
+
export interface GenerateSpeechRequest {
|
|
36
|
+
/**
|
|
37
|
+
* The text to read aloud. Plain text — SSML and provider-specific tags
|
|
38
|
+
* are intentionally not supported in v1.
|
|
39
|
+
*/
|
|
40
|
+
text: string;
|
|
41
|
+
/**
|
|
42
|
+
* Voice preset name. Defaults to `'neutral'`. See {@link GenerateSpeechVoice}.
|
|
43
|
+
* Any string is accepted; unknown presets fall back to `neutral`.
|
|
44
|
+
*/
|
|
45
|
+
voice?: GenerateSpeechVoice | string;
|
|
46
|
+
/**
|
|
47
|
+
* Capability tier (`quick` / `standard` / `expert`). Mirrors {@link LLMTier};
|
|
48
|
+
* the runtime maps each tier to a provider model. Defaults to `'quick'`.
|
|
49
|
+
*/
|
|
50
|
+
tier?: LLMTier;
|
|
51
|
+
/**
|
|
52
|
+
* Output audio container. Defaults to `'mp3'`.
|
|
53
|
+
*/
|
|
54
|
+
format?: GenerateSpeechFormat;
|
|
55
|
+
/**
|
|
56
|
+
* Optional BCP-47 language hint (e.g. `'en-GB'`, `'fr-FR'`). Advisory; the
|
|
57
|
+
* provider may ignore it.
|
|
58
|
+
*/
|
|
59
|
+
languageHint?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Playback speed multiplier in the range `0.5` to `2.0`. The runtime
|
|
62
|
+
* clamps to whatever range the configured provider supports.
|
|
63
|
+
*/
|
|
64
|
+
speed?: number;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Response from a speech-synthesis call. The audio bytes are persisted as a
|
|
68
|
+
* blob; play them from a UI via `<audio src={callbacks.getAssetUrl(reference)} />`
|
|
69
|
+
* or read the raw bytes in the engine via `callbacks.readAsset(reference)`.
|
|
70
|
+
*/
|
|
71
|
+
export interface GenerateSpeechResponse {
|
|
72
|
+
/**
|
|
73
|
+
* Asset reference for the synthesised audio, of the form `blob:<id>`.
|
|
74
|
+
* Resolve via `getAssetUrl` (UI) or `readAsset` (engine).
|
|
75
|
+
*/
|
|
76
|
+
reference: string;
|
|
77
|
+
/**
|
|
78
|
+
* MIME type of the audio (e.g. `'audio/mpeg'`, `'audio/wav'`, `'audio/ogg'`).
|
|
79
|
+
*/
|
|
80
|
+
mimeType: string;
|
|
81
|
+
/**
|
|
82
|
+
* Best-effort audio duration in milliseconds. Not all providers return this.
|
|
83
|
+
*/
|
|
84
|
+
durationMs?: number;
|
|
85
|
+
/**
|
|
86
|
+
* Number of characters the provider billed for this synthesis. Useful for
|
|
87
|
+
* observability and budgeting.
|
|
88
|
+
*/
|
|
89
|
+
charactersBilled?: number;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Function for synthesising speech.
|
|
93
|
+
*
|
|
94
|
+
* Can be called with a bare string (shorthand for `{ text: '...' }`) or a
|
|
95
|
+
* full options object:
|
|
96
|
+
*
|
|
97
|
+
* ```typescript
|
|
98
|
+
* await callbacks.generateSpeech('Welcome to the simulation.');
|
|
99
|
+
* await callbacks.generateSpeech({ text: '...', voice: 'narrator' });
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export type GenerateSpeechFunction = (request: GenerateSpeechRequest | string) => Promise<GenerateSpeechResponse>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Speech Generation Types
|
|
3
|
+
*
|
|
4
|
+
* Provides provider-agnostic text-to-speech for the engine, hooks, tick controllers
|
|
5
|
+
* and user interfaces. Game code describes the utterance via a small set of
|
|
6
|
+
* role-based voice presets; the runtime decides which provider, model and
|
|
7
|
+
* voice executes the call. The synthesised audio is persisted as a blob and
|
|
8
|
+
* the returned asset reference (`blob:<id>`) can be played from a UI
|
|
9
|
+
* (`getAssetUrl`) or read in the engine (`readAsset`).
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { TickControllerResult } from './signatures.js';
|
|
2
|
+
/**
|
|
3
|
+
* Options for {@link validateTickControllerResult}.
|
|
4
|
+
*/
|
|
5
|
+
export interface ValidateTickResultOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Override for "now" in ms since epoch, used to evaluate the effective
|
|
8
|
+
* cadence of a scheduled (`nextTickAt`) result. Defaults to `Date.now()`.
|
|
9
|
+
*/
|
|
10
|
+
now?: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Validate a runtime `TickControllerResult` against the §11.8–§11.9 contract.
|
|
14
|
+
*
|
|
15
|
+
* Returns `null` when the result is valid, or a human-readable error message
|
|
16
|
+
* that names the limit, cites SPECIFICATION.md §11, and points at the helper to
|
|
17
|
+
* use (never at an exported symbol). The message is suitable for surfacing to
|
|
18
|
+
* authors at authoring time, in the dev server, or in the build step.
|
|
19
|
+
*
|
|
20
|
+
* The builders in `scheduling.ts` call this, so an invalid result is refused
|
|
21
|
+
* at authoring time rather than at the first tick.
|
|
22
|
+
*/
|
|
23
|
+
export declare function validateTickControllerResult(result: Pick<TickControllerResult, 'intervalMs' | 'nextTickAt'>, options?: ValidateTickResultOptions): string | null;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effective tick interval bounds, derived from SPECIFICATION.md §11.8: every
|
|
3
|
+
* conformant runtime guarantees a 30-second floor and a 7-day ceiling on the
|
|
4
|
+
* *effective cadence* (the gap until the next automatic tick), not merely on
|
|
5
|
+
* the `intervalMs` display hint.
|
|
6
|
+
*
|
|
7
|
+
* These constants are deliberately module-private. Authors build valid results
|
|
8
|
+
* with `intervalTick()` / `scheduledTick()`, and the contract surface for
|
|
9
|
+
* runtime implementers is the spec, never an exported magic number.
|
|
10
|
+
*
|
|
11
|
+
* This module is internal to the SDK: it is what the builders validate with,
|
|
12
|
+
* and no entry point exports it. The runtime's copy lives in
|
|
13
|
+
* `@seriousgaming/sdk-tools/runtime`, and
|
|
14
|
+
* `tick-validation.parity.test.ts` there holds the two to one table of cases.
|
|
15
|
+
*/
|
|
16
|
+
const MIN_EFFECTIVE_TICK_INTERVAL_MS = 30_000;
|
|
17
|
+
const MAX_EFFECTIVE_TICK_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000; // 604,800,000 (7 days)
|
|
18
|
+
/**
|
|
19
|
+
* Validate a runtime `TickControllerResult` against the §11.8–§11.9 contract.
|
|
20
|
+
*
|
|
21
|
+
* Returns `null` when the result is valid, or a human-readable error message
|
|
22
|
+
* that names the limit, cites SPECIFICATION.md §11, and points at the helper to
|
|
23
|
+
* use (never at an exported symbol). The message is suitable for surfacing to
|
|
24
|
+
* authors at authoring time, in the dev server, or in the build step.
|
|
25
|
+
*
|
|
26
|
+
* The builders in `scheduling.ts` call this, so an invalid result is refused
|
|
27
|
+
* at authoring time rather than at the first tick.
|
|
28
|
+
*/
|
|
29
|
+
export function validateTickControllerResult(result, options) {
|
|
30
|
+
const { intervalMs, nextTickAt } = result;
|
|
31
|
+
if (nextTickAt !== undefined &&
|
|
32
|
+
(typeof nextTickAt !== 'number' || !Number.isFinite(nextTickAt))) {
|
|
33
|
+
return (`Tick controller nextTickAt must be a finite timestamp in milliseconds; got ${describe(nextTickAt)}. ` +
|
|
34
|
+
`See the tick controller guide.`);
|
|
35
|
+
}
|
|
36
|
+
const now = options?.now ?? Date.now();
|
|
37
|
+
// The effective cadence is the gap the runtime will actually schedule:
|
|
38
|
+
// nextTickAt − now in scheduled mode, intervalMs in interval mode (§11.8).
|
|
39
|
+
const effectiveMs = nextTickAt !== undefined ? nextTickAt - now : (intervalMs ?? NaN);
|
|
40
|
+
// A nextTickAt at or in the past yields a 0 delay: firing one immediate
|
|
41
|
+
// catch-up tick is explicitly allowed and exempt from both the floor and the
|
|
42
|
+
// intervalMs shape rule, since `intervalMs` is only a display hint here
|
|
43
|
+
// (§11.8/§11.3). The tick controller re-evaluates after the catch-up fires.
|
|
44
|
+
if (nextTickAt !== undefined && effectiveMs <= 0) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
// Beside a nextTickAt, intervalMs is a display hint and may be absent; when
|
|
48
|
+
// it is there it is held to the same shape as in interval mode.
|
|
49
|
+
if ((nextTickAt === undefined || intervalMs !== undefined) &&
|
|
50
|
+
(typeof intervalMs !== 'number' ||
|
|
51
|
+
!Number.isFinite(intervalMs) ||
|
|
52
|
+
!Number.isInteger(intervalMs) ||
|
|
53
|
+
intervalMs < 1)) {
|
|
54
|
+
return (`Tick intervalMs must be a finite, positive, whole number of milliseconds; got ${describe(intervalMs)}. ` +
|
|
55
|
+
`Build interval-based results with intervalTick(intervalMs). See the tick controller guide.`);
|
|
56
|
+
}
|
|
57
|
+
if (effectiveMs < MIN_EFFECTIVE_TICK_INTERVAL_MS) {
|
|
58
|
+
return (`Effective tick cadence of ${duration(effectiveMs)} is below the 30-second minimum ` +
|
|
59
|
+
`(${MIN_EFFECTIVE_TICK_INTERVAL_MS} ms). Use intervalTick(intervalMs) with at least ` +
|
|
60
|
+
`${MIN_EFFECTIVE_TICK_INTERVAL_MS}, or schedule nextTickAt further out. See the tick controller guide.`);
|
|
61
|
+
}
|
|
62
|
+
if (effectiveMs > MAX_EFFECTIVE_TICK_INTERVAL_MS) {
|
|
63
|
+
return (`Effective tick cadence of ${duration(effectiveMs)} exceeds the 7-day maximum ` +
|
|
64
|
+
`(${MAX_EFFECTIVE_TICK_INTERVAL_MS} ms). For longer gaps, set enabled: false and ` +
|
|
65
|
+
`drive ticks by another means. See the tick controller guide.`);
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
function describe(value) {
|
|
70
|
+
if (value === undefined)
|
|
71
|
+
return 'undefined';
|
|
72
|
+
if (typeof value === 'number') {
|
|
73
|
+
if (Number.isNaN(value))
|
|
74
|
+
return 'NaN';
|
|
75
|
+
if (!Number.isFinite(value))
|
|
76
|
+
return value > 0 ? 'Infinity' : '-Infinity';
|
|
77
|
+
return String(value);
|
|
78
|
+
}
|
|
79
|
+
return typeof value;
|
|
80
|
+
}
|
|
81
|
+
/** `12000 ms (12 seconds)`: the field's own unit, then a reading of it. */
|
|
82
|
+
function duration(ms) {
|
|
83
|
+
const rounded = Number.isInteger(ms) ? String(ms) : ms.toFixed(1);
|
|
84
|
+
const seconds = ms / 1000;
|
|
85
|
+
const human = seconds >= 86_400
|
|
86
|
+
? `${round(seconds / 86_400)} days`
|
|
87
|
+
: seconds >= 3_600
|
|
88
|
+
? `${round(seconds / 3_600)} hours`
|
|
89
|
+
: seconds >= 1
|
|
90
|
+
? `${round(seconds)} seconds`
|
|
91
|
+
: 'under a second';
|
|
92
|
+
return `${rounded} ms (${human})`;
|
|
93
|
+
}
|
|
94
|
+
function round(value) {
|
|
95
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(1);
|
|
96
|
+
}
|