- Bundle robot3 1.3.0 locally (not yet published to npm) - Fix remaining type inference issues with `as any` casts for invoke() events - Resolves state machine transition type errors Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
208 lines
5.8 KiB
JavaScript
208 lines
5.8 KiB
JavaScript
function valueEnumerable(value) {
|
|
return { enumerable: true, value };
|
|
}
|
|
|
|
function valueEnumerableWritable(value) {
|
|
return { enumerable: true, writable: true, value };
|
|
}
|
|
|
|
export let d = {};
|
|
let truthy = () => true;
|
|
let empty = () => ({});
|
|
let identity = a => a;
|
|
let callBoth = (par, fn, self, args) => par.apply(self, args) && fn.apply(self, args);
|
|
let callForward = (par, fn, self, [a, b]) => fn.call(self, par.call(self, a, b), b);
|
|
let create = (a, b) => Object.freeze(Object.create(a, b));
|
|
|
|
function stack(fns, def, caller) {
|
|
return fns.reduce((par, fn) => {
|
|
return function(...args) {
|
|
return caller(par, fn, this, args);
|
|
};
|
|
}, def);
|
|
}
|
|
|
|
function fnType(fn) {
|
|
return create(this, { fn: valueEnumerable(fn) });
|
|
}
|
|
|
|
let reduceType = {};
|
|
export let reduce = fnType.bind(reduceType);
|
|
export let action = fn => reduce((ctx, ev) => !!~fn(ctx, ev) && ctx);
|
|
|
|
let guardType = {};
|
|
export let guard = fnType.bind(guardType);
|
|
|
|
function filter(Type, arr) {
|
|
return arr.filter(value => Type.isPrototypeOf(value));
|
|
}
|
|
|
|
function makeTransition(from, to, ...args) {
|
|
let guards = stack(filter(guardType, args).map(t => t.fn), truthy, callBoth);
|
|
let reducers = stack(filter(reduceType, args).map(t => t.fn), identity, callForward);
|
|
return create(this, {
|
|
from: valueEnumerable(from),
|
|
to: valueEnumerable(to),
|
|
guards: valueEnumerable(guards),
|
|
reducers: valueEnumerable(reducers)
|
|
});
|
|
}
|
|
|
|
let transitionType = {};
|
|
let immediateType = {};
|
|
export let transition = makeTransition.bind(transitionType);
|
|
export let immediate = makeTransition.bind(immediateType, null);
|
|
|
|
function enterImmediate(machine, service, event) {
|
|
return transitionTo(service, machine, event, this.immediates) || machine;
|
|
}
|
|
|
|
function transitionsToMap(transitions) {
|
|
let m = new Map();
|
|
for(let t of transitions) {
|
|
if(!m.has(t.from)) m.set(t.from, []);
|
|
m.get(t.from).push(t);
|
|
}
|
|
return m;
|
|
}
|
|
|
|
let stateType = { enter: identity };
|
|
export function state(...args) {
|
|
let transitions = filter(transitionType, args);
|
|
let immediates = filter(immediateType, args);
|
|
let desc = {
|
|
final: valueEnumerable(args.length === 0),
|
|
transitions: valueEnumerable(transitionsToMap(transitions))
|
|
};
|
|
if(immediates.length) {
|
|
desc.immediates = valueEnumerable(immediates);
|
|
desc.enter = valueEnumerable(enterImmediate);
|
|
}
|
|
return create(stateType, desc);
|
|
}
|
|
|
|
let invokeFnType = {
|
|
enter(machine2, service, event) {
|
|
let rn = this.fn.call(service, service.context, event)
|
|
if(machine.isPrototypeOf(rn))
|
|
return create(invokeMachineType, {
|
|
machine: valueEnumerable(rn),
|
|
transitions: valueEnumerable(this.transitions)
|
|
}).enter(machine2, service, event)
|
|
rn
|
|
.then(data => {
|
|
if (machine2 === service.machine)
|
|
return service.send({ type: 'done', data });
|
|
})
|
|
.catch(error => {
|
|
if (machine2 === service.machine)
|
|
return service.send({ type: 'error', error });
|
|
});
|
|
return machine2;
|
|
}
|
|
};
|
|
let invokeMachineType = {
|
|
enter(machine, service, event) {
|
|
service.child = interpret(this.machine, s => {
|
|
service.onChange(s);
|
|
if(service.child == s && s.machine.state.value.final) {
|
|
delete service.child;
|
|
service.send({ type: 'done', data: s.context });
|
|
}
|
|
}, service.context, event);
|
|
if(service.child.machine.state.value.final) {
|
|
let data = service.child.context;
|
|
delete service.child;
|
|
return transitionTo(service, machine, { type: 'done', data }, this.transitions.get('done'));
|
|
}
|
|
return machine;
|
|
}
|
|
};
|
|
export function invoke(fn, ...transitions) {
|
|
let t = valueEnumerable(transitionsToMap(transitions));
|
|
return machine.isPrototypeOf(fn) ?
|
|
create(invokeMachineType, {
|
|
machine: valueEnumerable(fn),
|
|
transitions: t
|
|
}) :
|
|
create(invokeFnType, {
|
|
fn: valueEnumerable(fn),
|
|
transitions: t
|
|
});
|
|
}
|
|
|
|
let machine = {
|
|
get state() {
|
|
return {
|
|
name: this.current,
|
|
value: this.states[this.current]
|
|
};
|
|
}
|
|
};
|
|
|
|
export function createMachine(current, states, contextFn = empty) {
|
|
if(typeof current !== 'string') {
|
|
contextFn = states || empty;
|
|
states = current;
|
|
current = Object.keys(states)[0];
|
|
}
|
|
if(d._create) d._create(current, states);
|
|
return create(machine, {
|
|
context: valueEnumerable(contextFn),
|
|
current: valueEnumerable(current),
|
|
states: valueEnumerable(states)
|
|
});
|
|
}
|
|
|
|
function transitionTo(service, machine, fromEvent, candidates) {
|
|
let { context } = service;
|
|
for(let { to, guards, reducers } of candidates) {
|
|
if(guards(context, fromEvent)) {
|
|
service.context = reducers.call(service, context, fromEvent);
|
|
|
|
let original = machine.original || machine;
|
|
let newMachine = create(original, {
|
|
current: valueEnumerable(to),
|
|
original: { value: original }
|
|
});
|
|
|
|
if (d._onEnter) d._onEnter(machine, to, service.context, context, fromEvent);
|
|
let state = newMachine.state.value;
|
|
service.machine = newMachine;
|
|
let ret = state.enter(newMachine, service, fromEvent);
|
|
service.onChange(service);
|
|
return ret;
|
|
}
|
|
}
|
|
}
|
|
|
|
function send(service, event) {
|
|
let eventName = event.type || event;
|
|
let { machine } = service;
|
|
let { value: state, name: currentStateName } = machine.state;
|
|
|
|
if(state.transitions.has(eventName)) {
|
|
return transitionTo(service, machine, event, state.transitions.get(eventName)) || machine;
|
|
} else {
|
|
if(d._send) d._send(eventName, currentStateName);
|
|
}
|
|
return machine;
|
|
}
|
|
|
|
let service = {
|
|
send(event) {
|
|
send(this, event);
|
|
}
|
|
};
|
|
|
|
export function interpret(machine, onChange, initialContext, event) {
|
|
let s = Object.create(service, {
|
|
machine: valueEnumerableWritable(machine),
|
|
context: valueEnumerableWritable(machine.context(initialContext, event)),
|
|
onChange: valueEnumerable(onChange)
|
|
});
|
|
s.send = s.send.bind(s);
|
|
s.machine = s.machine.state.value.enter(s.machine, s, event);
|
|
return s;
|
|
}
|