Generic state-machine mutations
Source GenericContracts.cs, GenericTrains.cs, StateMachineOptions.cs
Six mutations under dispatch { stateMachine { ... } } that take the machine as a runtime argument (the machine
field) and reach every registered user-owned machine. They are not in the schema unless the host opts in. A
machine's own typed operations are the machines root; moving a client
from these to it is covered in
Generic State-Machine Mutations.
Opt-in
| Call | Effect |
|---|---|
StateMachineOptions.ExposeGenericStateMachineMutations(o => o.Policy = "..."), in the configure argument of AddStateMachines | Serves the six mutations, each behind GenericStateMachineMutationOptions.Policy. |
The same call with no Policy, or a blank one | AddStateMachines throws InvalidOperationException. |
| No call | None of the six is in the schema. The trains behind them are still mediator routes, so host code and a machine's typed surface dispatch them. |
While the opt-in is on, every user-owned machine is reachable through the mutations, so each must declare a rule for
every event, its start and its reads, under the same startup check as a machine marked [TraxMachine].
Authorization
| Check | Refusal |
|---|---|
Each mutation's train carries [TraxAuthorize], and the opt-in's policy gates its field | TRAX_AUTHORIZATION GraphQL error ("Not authorized."), before the mutation runs |
ISnapshotPrincipal.CurrentUserKey is null | UNAUTHENTICATED problem |
| The machine's own rule for the event, start or read | FORBIDDEN problem |
Mutations
| Mutation | Train | Does |
|---|---|---|
startSnapshot | IStartSnapshot | Creates an instance from the machine's declared start. The server mints the id and builds the first snapshot. |
saveSnapshot | ISaveSnapshot | Soft path: validates a whole client snapshot and stores it. Refused as SAVE_NOT_ALLOWED for a machine with server-side behaviour (a gated event or start, a timer, an invoked train, an effect). |
advanceSnapshot | IAdvanceSnapshot | Authoritative path: fires a trigger on the stored draft, server-side. |
loadSnapshot | ILoadSnapshot | Reads the caller's stored draft, held to the machine's get rule. A missing draft, and one the rule does not let the caller read, answer the same problem. |
sendSnapshot | ISendSnapshot | Runs the machine's one irreversible effect exactly once and advances with its receipt. |
discardSnapshot | IDiscardSnapshot | Deletes one of the caller's instances, cancelling a run its state invoked first. |
All six are mutations, loadSnapshot included. Each takes one input argument and answers output { ... }. The
snapshot crosses the wire as a string of canonical JSON in both directions. Each train withholds its output from the
lifecycle hooks, so the broadcaster and SignalR never carry an instance's snapshot.
Inputs
! is required.
| Input type | Fields |
|---|---|
StartSnapshotInput | machine: String!, requestId: String!, input: String (the start input as a JSON object), sensitiveInput: String, schemaHash: String |
SaveSnapshotInput | machine: String!, id: UUID!, snapshot: String! (the whole snapshot as JSON), schemaHash: String |
AdvanceSnapshotInput | machine: String!, id: UUID!, trigger: String! (a trigger name, "Coin"), input: String (the trigger input as JSON), sensitiveInput: String, requestId: String, schemaHash: String, clientResult: String |
LoadSnapshotInput | machine: String!, id: UUID!, schemaHash: String |
SendSnapshotInput | machine: String!, id: UUID!, requestId: String, schemaHash: String |
DiscardSnapshotInput | machine: String!, id: UUID!, requestId: String! |
| Field | Meaning |
|---|---|
sensitiveInput | Fields of the input recorded masked, as a JSON object merged into input before it is applied. Every copy of the run's input Trax keeps writes them as {"_redacted": true}. A field must not be in both. |
schemaHash | The client's machine schema hash. A mismatch is refused as SCHEMA_MISMATCH before anything runs. Absent: no check. See runtime integrity. |
clientResult | The snapshot the client's twin computed for the advance. A different server result is refused as CLIENT_DIVERGENCE and nothing is written. Absent: no check. |
Outputs
| Output | Fields |
|---|---|
startSnapshot | id, created (false on a retry of the start that created it), snapshot, revision, dueAt, problem |
saveSnapshot, loadSnapshot | snapshot, revision, rowId, dueAt, problem |
advanceSnapshot, sendSnapshot | snapshot, revision, rowId, dueAt, dueAtKnown, problem |
discardSnapshot | discarded (true when the instance is gone, already or by this request; another user's id answers the same), problem |
Exactly one of snapshot and problem is set (discarded and problem for a discard). When problem is set,
nothing was written. revision orders the writes of one rowId, which names one incarnation of the instance.
Request ids
| Mutation | requestId |
|---|---|
startSnapshot | required. The same id and input return the instance the first request created; the same id with another input is REQUEST_ID_REUSED. |
advanceSnapshot | optional. The same id with the same trigger replays the result instead of firing again; with another trigger, REQUEST_ID_REUSED. |
sendSnapshot | optional; send:{id} when absent. Shares one id space with advanceSnapshot, so an id an advance used is REQUEST_ID_REUSED, before the effect runs. |
discardSnapshot | required. |
A blank id, one holding a control character, or one longer than 200 characters is INVALID_REQUEST_ID. The full
matching rule is under
persistence ports.
Limits
A snapshot, a trigger or start input, a clientResult, or an advanced snapshot larger than 64 KiB
(SnapshotLimits.MaxSnapshotBytes) is TOO_LARGE, and nothing is written.
sendSnapshot
sendSnapshot takes no trigger. It fires the one transition the machine binds its effect to, and only from that
transition's source state; from any other state it is NO_TRANSITION. The receipt the effect returns reaches the
transition's reducer as input["receipt"]. An effect that throws is DELIVERY_FAILED, and the draft is not
advanced, so the send can be retried.
Example
mutation Advance($i: AdvanceSnapshotInput!) {
dispatch { stateMachine { advanceSnapshot(input: $i) {
output { snapshot problem { code message } }
} } }
}{ "i": { "machine": "turnstile", "id": "33333333-3333-3333-3333-333333333333",
"trigger": "Coin", "input": "{\"coin\":\"quarter\"}" } }{ "data": { "dispatch": { "stateMachine": { "advanceSnapshot": { "output": {
"snapshot": "{\"machine\":\"turnstile\",\"version\":1,\"state\":\"Unlocked\",\"context\":{\"paidWith\":\"quarter\"}}",
"problem": null } } } } } }