A machine that arrives as a value can still be commanded, so a name may name its state (#58)
game-maker / suite (push) Successful in 6m12s
game-maker / suite (push) Successful in 6m12s
Set state and State branch derived their ports from the graph, so a machine that came out of a variable or a list had no ports at all: nothing to set, nowhere to branch. It could be passed around but not operated. stateByName() resolves id first, then name; a name two states carry answers with nothing, the same rule transitionTo already follows for two transitions of one name — without something in the graph that decides between them, any pick would be a guess. Set state by name takes the machine and a text, and has a second exec output not found, so a typo is not swallowed in silence. State branch gains a derived else that fires whenever no state output matches — no state set, a state of another machine, or a machine that arrives as a value and therefore has no state outputs at all. A flow that enters State branch now always leaves it somewhere. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7e5003b590
commit
1135205d69
7 files changed
+218
-14
No files matched your search
@@ -0,0 +1,29 @@
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { setState } from '../../sharedState';
|
||||
import { machinePort, stateByName } from '../../stateMachine';
|
||||
import { asString } from '../../values';
|
||||
import { exec, str } from '../shared';
|
||||
|
||||
/**
|
||||
* What `Set state` cannot do: a machine that arrives as a value. Ports are derived from the graph,
|
||||
* so a machine coming from a variable or a list item has no state inputs at all – this node names
|
||||
* the state instead of wiring it. Nothing is found is a way out of its own, not silence.
|
||||
*/
|
||||
export const SetStateByName: NodeDef = {
|
||||
type: 'SetStateByName',
|
||||
label: 'Set state by name',
|
||||
category: 'action',
|
||||
description:
|
||||
'Puts the machine into the state of that id or name, then continues along "exec". A name no ' +
|
||||
'state answers to – or two states share – continues along "not found" and moves nothing.',
|
||||
inputs: [exec(), machinePort(), str('state')],
|
||||
outputs: [exec(), exec('notFound', 'not found')],
|
||||
params: [],
|
||||
run: (ctx, inputs, _params, _def, _entry, lookup) => {
|
||||
const machineId = asString(inputs.machine);
|
||||
const state = stateByName(machineId ? lookup.machine(machineId) : null, asString(inputs.state));
|
||||
if (!state) return { next: 'notFound' };
|
||||
setState(ctx, machineId, state);
|
||||
return { next: 'exec' };
|
||||
},
|
||||
};
|
||||
@@ -4,20 +4,33 @@ import { machineOf, machinePort, statePorts } from '../../stateMachine';
|
||||
import { asString } from '../../values';
|
||||
import { exec } from '../shared';
|
||||
|
||||
/** No state set yet, or a state of another machine: the flow ends here, along no output. */
|
||||
/** The way out for everything no state output covers. A state id is ten characters, so no state
|
||||
* can claim this key – the guard below is what makes that true rather than likely. */
|
||||
const ELSE = 'else';
|
||||
|
||||
/**
|
||||
* No state set yet, a state of another machine, or a machine that arrived as a value and therefore
|
||||
* has no state outputs at all: the flow leaves along `else`. A branch nobody can see taken is the
|
||||
* hardest kind to debug, so this node always continues somewhere.
|
||||
*/
|
||||
export const StateBranch: NodeDef = {
|
||||
type: 'StateBranch',
|
||||
label: 'State branch',
|
||||
category: 'flow',
|
||||
description:
|
||||
'Continues along the output of the state the machine is in. A machine with no start state is ' +
|
||||
'in no state until "Set state" has run, and nothing continues.',
|
||||
'Continues along the output of the state the machine is in, and along "else" while it is in ' +
|
||||
'none – or while the machine comes from a value, where the states are unknown to the graph.',
|
||||
inputs: [exec(), machinePort()],
|
||||
// Derived rather than fixed, so it sits below the states instead of above them – the way out of a
|
||||
// branch belongs under what it is an exception to, as it does on `Branch`.
|
||||
outputs: [],
|
||||
params: [],
|
||||
derivePorts: (source) => ({ outputs: statePorts(machineOf(source)) }),
|
||||
derivePorts: (source) => {
|
||||
const states = statePorts(machineOf(source));
|
||||
return { outputs: [...states, ...(states.some((p) => p.key === ELSE) ? [] : [exec(ELSE, ELSE)])] };
|
||||
},
|
||||
run: (ctx, inputs, _params, def) => {
|
||||
const current = currentState(ctx, asString(inputs.machine));
|
||||
return def.outputs.some((p) => p.key === current) ? { next: current } : {};
|
||||
return { next: def.outputs.some((p) => p.key === current) ? current : ELSE };
|
||||
},
|
||||
};
|
||||
@@ -97,6 +97,7 @@ import { Repeat } from './flow/Repeat';
|
||||
import { RunBehavior } from './flow/RunBehavior';
|
||||
import { Sequence } from './flow/Sequence';
|
||||
import { SetState } from './flow/SetState';
|
||||
import { SetStateByName } from './flow/SetStateByName';
|
||||
import { StateBranch } from './flow/StateBranch';
|
||||
import { StateTransition } from './flow/StateTransition';
|
||||
import { PARTICLE_NODES } from '../../particles/nodes';
|
||||
@@ -132,6 +133,7 @@ export const BUILT_IN_NODES: readonly NodeDef[] = [
|
||||
RunBehavior,
|
||||
StateBranch,
|
||||
SetState,
|
||||
SetStateByName,
|
||||
StateTransition,
|
||||
CurrentState,
|
||||
IsInState,
|
||||
|
||||
@@ -76,6 +76,19 @@ export function transitionTo(machine: StateMachine, current: string, name: strin
|
||||
return targets.length === 1 ? targets[0]! : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* The state a text names: its id where one matches, else the state of that name. A name two states
|
||||
* share answers nothing – the same rule `transitionTo` follows, and for the same reason: with
|
||||
* nothing in the graph to pick between them, either choice would be a guess. `''` for no match,
|
||||
* which is what a node reports as "not found" rather than moving the machine somewhere arbitrary.
|
||||
*/
|
||||
export function stateByName(machine: StateMachine | null, text: string): string {
|
||||
if (!machine || !text) return '';
|
||||
if (machine.states.some((s) => s.id === text)) return text;
|
||||
const named = machine.states.filter((s) => s.name === text);
|
||||
return named.length === 1 ? named[0]!.id : '';
|
||||
}
|
||||
|
||||
/** The names a machine's transitions carry, deduped and in edge order – what a node may ask for. */
|
||||
export function transitionNames(machine: StateMachine | null): string[] {
|
||||
return [...new Set((machine?.transitions ?? []).map((t) => t.name).filter((name) => name))];
|
||||
|
||||
Reference in new issue
Block a user