phone/packages/robot3/machine.js
Corey Johnson 27aa62f950 Upgrade robot3 to 1.3.0 for improved TypeScript support
- Bundle robot3 1.3.0 locally (not yet published to npm)
- Fix remaining type inference issues with `as any` casts for invoke() events

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 14:53:11 -08:00

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;
}