matthewp / robot Goto Github PK
View Code? Open in Web Editor NEW๐ค A functional, immutable Finite State Machine library
Home Page: https://thisrobot.life
License: BSD 2-Clause "Simplified" License
๐ค A functional, immutable Finite State Machine library
Home Page: https://thisrobot.life
License: BSD 2-Clause "Simplified" License
Document all of the methods.
When a parent invokes a child machine, the event that led to the invoke should be threaded to the child.
I'll start with the disclaimer that I'm just dipping my toes with robot, but here goes.
Currently the documentation for actions states that:
action takes a function that will be run during a transition. The primary purpose of using action is to perform side-effects.
Why are actions allowed only within transitions? What if I want to invoke an action every time the machine enters a specific state, regardless of the previous state? With the current API, I have to define that action in all the transitions that lead to that specific state.
Allow actions to be defined also as children of state.
const machine = createMachine({
idle: state(transition("submit", "validate")),
validate: state(
action(ctx => log('Validation attempt', ctx))
),
// ...gazillion other states that might lead to 'validate'
});
Now the action get's invoked every time the machine enters validate-state.
To take it one step further, there could also be an option to define whether the action should be triggered upon entry or exit of state.
When ready to go live.
Hi Matthew,
Following up on the Twitter conversation, I'm opening this issue about packaging for use in Node and (older) browsers.
I can add a Rollup config to build CJS / UMD bundles in a PR, but I noticed the use of "type": "module"
in package.json (TIL!), and I'm not sure how making the main
field point to CJS and module
point to ES can impact people's setups...
Hello! I've been loving this library but I discovered some unexpected behavior with immediate
(long post ahead).
Original code snippet:
import { createMachine, immediate, interpret, invoke, reduce, state, state as final, transition } from 'robot3';
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const load = async () => {
await wait(1000);
return 'content';
};
const machine = createMachine({
ready: state(immediate("loading")),
loading: invoke(load, transition('done', 'loaded')),
loaded: final(),
});
console.log(`state: ${machine.current}`);
interpret(machine, service => {
console.log(`state: ${service.machine.current}`);
});
Expected:
state: ready
state: loading
state: loaded
What actually happens:
state: ready
state: loaded
The state machine never actually has the loading
state when transitioned to from an immediate
state! All other kinds of transitions to loading
would actually have loading
be the state rather than skipping over it (I've actually had to retitle and rewrite this post a few times because I keep misidentifying what the pattern / problem is with the interaction of invoke
and immediate
).
One implication here is that, if you are using this to render a UI, your application can't know from the state machine that that 1 second is spent in loading
; it'll still show its ready
state. You would have to add a side effect (in the load
function) that reaches outside the state machine to inform the outside world that there is loading taking place.
Without changing how immediate
works (i.e. in the current version of the library), you can use a recipe like this to get my originally expected behavior (at least in this situation; I haven't tested it in the infinite other ways you could):
// all the codes from the original snippet except for the machine definition
const immediately = () => wait(0);
const machine = createMachine({
ready: invoke(immediately, transition('done', 'loading')),
loading: invoke(load, transition('done', 'loaded')),
loaded: final(),
});
// same interpreter from the original snippet
However, this has problems like
wait
and immediately
) to be written in userspaceimmediate
without any purpose (as far as I'm aware?)I think some solutions could be
immediate
works this way very explicitlyexport const immediately = next => invoke(() => wait(0), transition('done', next));
immediate
to work in some way based off of the function in 2.Is there a way to extend machines like in xstate: https://xstate.js.org/docs/guides/machines.html#extending-machines. I would specifically be interested in overriding my invoked functions so i can reuse my machines.
My example use case is a form machines fetching data from different apis that have very different reponse formats.
I apologize if this is a silly question, I just got started with robot.
I wrote this state machine (codesandbox here) and it didn't work as I expected:
const canSubmit = () => false; // will never approve!
const machine = createMachine({
idle: state(transition("submit", "validate")),
validate: state(
immediate("submission", guard(canSubmit)),
transition("revalidate", "submission", guard(canSubmit))
),
submission: state()
});
After sending submit
, I expected the machine to be in validate
state, waiting for the revalidate
transition.
Machine went back to idle
In the guide, there's this code:
const machine = createMachine({
idle: state(
transition('submit', 'validate')
),
validate: state(
immediate('submission', guard(canSubmit)),
immediate('idle') // <--- you can omit this line if you want to?
),
submission: state()
});
...but to go back to idle
then the guard prevents proceeding to 'submission', it's completely unnecessary to call immediate('idle')
. The machine will go back to idle
without it, too.
This would be a breaking change, but I think it's the correct one.
Hey there hello!
Package.json specifies "type": "module"
so Node,js (>=13.5.0, which have esm support) tries to load robot3 as a module.
But in the build dist/machine.js file, the first line is Object.defineProperty(exports, '__esModule', { value: true });
.
This breaks Node.js, which issues: Uncaught ReferenceError: exports is not defined
.
import("robot3/machine.js")
works, as this file is esm.
Pretty cool lib!
Is it possible to have guards returning promises, so it will only transition if the promise guard resolves?
e.g:
// Only allow submission of a login and password is entered.
async function canSubmit(ctx) {
return fetch(`/api/verify/${ctx.login}`);
}
const machine = createMachine({
idle: state(
transition('submit', 'complete',
guard(canSubmit)
)
),
complete: state()
});
I understand that Actions are the place where we are supposed to do side effects, but sometimes we have side effects which can fail and if we allow the transition, our machine would be in an invalid state.
Our use case is media playback. From idle
to loaded
you can try loading the video but if that fails you actually need to be back in the idle state.
maybe there is already a way to do this, but how do I set the initial state for my machine ?
In the first toggle example in the docs(https://thisrobot.life), what to do if I want to start for example at the active state ?
right now it always starts at the first state the machine encounters, which might work in most cases
I'm trying to run the scenario from the "Nested Guide" test, with Typescript.
Copying the exemple, I get some errors:
context
when calling createMachine
, otherwise the compiler complains with: robot3.spec.ts:258:29 - error TS2345: Argument of type 'Machine<{ green: MachineState; yellow: MachineState; red: MachineState; }, unknown, "yellow" | "green" | "red">' is not assignable to parameter of type 'Machine<{}, {}, string>'.
Type 'unknown' is not assignable to type '{}'.
258 let service = interpret(stoplight, () => {});
service.send
function:let child = service.send;
console.log(child.machine.current); // walk
robot3.spec.ts:267:23 - error TS2339: Property 'machine' does not exist on type 'SendFunction<string>'.
267 console.log(child.machine.current); // walk
~~~~~~~
robot3.spec.ts:269:11 - error TS2339: Property 'send' does not exist on type 'SendFunction<string>'.
269 child.send('toggle');
However, I suspect this might be a typo in the Guide, since the child machine seems to be accessible at runtime by doing this:
let child = service.child;
console.log(child.machine.current); // walk
Is it a problem with the documentation ?
Service
typescript type does not seem to have the child
property:robot3.spec.ts:267:23 - error TS2339: Property 'machine' does not exist on type 'SendFunction<string>'.
267 console.log(child.machine.current); // walk
~~~~~~~
it("can nest machines", async () => {
const stopwalk = createMachine({
walk: state(
transition('toggle', 'dontWalk')
),
dontWalk: state(
transition('toggle', 'walk')
)
}, () : any => {} );
const stoplight = createMachine({
green: state(
transition('next', 'yellow')
),
yellow: state(
transition('next', 'red')
),
red: invoke(stopwalk,
transition('done', 'green')
)
},() : any => {}); // Required "context" for typing
let service = interpret(stoplight, () => {});
service.send('next');
console.log(service.machine.current); // yellow
service.send('next');
console.log(service.machine.current); // red
let child = (service as any).child; // Need to cast, and access child instead of send
console.log(child.machine.current); // walk
child.send('toggle');
console.log(child.machine.current); // dontWalk
console.log(service.machine.current); // green
})
Is there a cleaner way ?
Thanks !
const three = createMachine({
threeInit: state(
immediate('threeDone')
),
threeDone: state(),
})
const two = createMachine({
twoInit: state(
immediate('twoStart'),
),
twoStart: invoke(three,
transition('done', 'twoDone'),
),
twoDone: state(),
})
const one = createMachine({
oneInit: state(
transition('START', 'oneStart'),
),
oneStart: invoke(two,
transition('done', 'oneDone'),
),
oneDone: state(),
})
const interpretedMachine = interpret(one, (service) => {
console.log(service.machine.current)
});
interpretedMachine.send('START')
Output
oneStart
Expected output
oneStart
twoInit
twoStart
threeInit
threeDone
twoDone
oneDone
Example: https://codesandbox.io/s/robot-playground-xzfr0
For LitElement, it may be useful to provide easy access to the element instance so folks can do things like set properties during invoke()
.
Currently, the docs outline the current workflow for using the nested stoplight example:
let service = interpret(stoplight, () => {});
service.send('next');
console.log(service.machine.current); // yellow
service.send('next');
console.log(service.machine.current); // red
let child = service.send;
console.log(child.machine.current); // walk
child.send('toggle');
console.log(child.machine.current); // dontWalk
console.log(service.machine.current); // green
It seems like let child = service.send;
should be let child = service.child;
, right? If so, easy enough to change.
However, when applying the documented code, the final console.log
returns red
for me. https://stackblitz.com/edit/robot-stoplight?file=index.js There seems to be some possibilities here, but the ones I can see are lacking:
It is possible that the child machine should be:
const stopwalk = createMachine({
walk: state(
transition('toggle', 'dontWalk')
),
dontWalk: state(), // <--- makes it "final"?
});
And, the docs should run two paths through the machine, getting the "new" child the second time?
let service = interpret(stoplight, () => {});
service.send('next');
console.log(service.machine.current); // yellow
service.send('next');
console.log(service.machine.current); // red
let child = service.child;
console.log(child.machine.current); // walk
child.send('toggle');
console.log(child.machine.current); // dontWalk
console.log(service.machine.current); // green
service.send('next');
console.log(service.machine.current); // yellow
service.send('next');
console.log(service.machine.current); // red
child = service.child; // <--- magic happens here when you get the NEW child machine
console.log(child.machine.current); // walk
child.send('toggle');
console.log(child.machine.current); // dontWalk
console.log(service.machine.current); // green
Code for alternate versions: https://stackblitz.com/edit/robot-stoplight?file=alt.js
Currently it doesn't look right in mobile view even though we have the breakpoints, not sure why.
Probably using bundlesize
Would you be opposed to a PR that implements the library directly in TypeScript with an extra build step? It would ensure that the typings stay up to date and as exhaustive as it can be and add a level of safety to the actual library code. Wanted to ask before I put any time trying to do something like that.
Hi, I've been using this module for a little while but just figured out that the app we are working on is not working on old browsers because the es2015+ syntax is not being compiled down to support those browsers, It would be possible to configure the build to generate es5 compatible code?, I can make a PR for that (this is also happening on the integrations libraries).
Thanks.
All libraries supporting useMachine
can take a second argument that is the initial context. Should update the docs to reflect this.
Based on this: https://twitter.com/Rich_Harris/status/1175415662757535746
Link to the official docs, etc.
I was feeling too guilty to open a "TypeScript herp derp?" issue when I found this project, but then I found this tweet, so here we are.
There are two main ways to add types โ convert the source to TypeScript, and generate the JS and type file from that, or leave the source as the current JS, and add an index.d.ts with separate tests for the types.
Do you feel a preference for one or the other for this project?
I am building an API server to serve xState statecharts. For that I need to know which events are generally possible in the current state of the machine.
xState has a way to tell me with nextEvents
. It seems robot does not provide such view on the state yet?
In xState I can also try to perform a state change without actually changing the state machines current state using service.machine.transition(current, event).changed
Is something like that possible with robot?
I'd like to add robot
support to my API server.
Are there any plans to build something similar to @statecharts/xstate-viz
and @xstate/test
?
Hey everyone, I'm thinking about giving Robot a logo that's not just the robot emoji.
I created this, derived from an open license artwork I found online:
I like it, but if other more artistic people want to jump in that would be great too. Or just give your opinions.
Personally I like the idea of a robot head. A full robot would be nice too, but for stickers the head seems like the right dimensions to me. Would love to hear what you think.
Link to the official docs, etc.
const first = createMachine({
start: state(
transition('DONE', 'done'),
),
done: state(),
})
const second = createMachine({
start: state(
transition('DONE', 'done'),
),
done: state(),
})
const machine = createMachine({
start: state(
transition('FIRST', 'first'),
transition('SECOND', 'second'),
),
first: invoke(first,
transition('done', 'start'),
),
second: invoke(second,
transition('done', 'start'),
),
})
when calling interpretedMachine.send('SECOND')
get an error
uncaughtException TypeError: Cannot read property 'call' of undefined
at Object.enter (\node_modules\robot3\dist\machine.js:90:13)
at transitionTo (\node_modules\robot3\dist\machine.js:153:20)
at send (\node_modules\robot3\dist\machine.js:164:12)
at Object.send (\node_modules\robot3\dist\machine.js:171:20)
This is particularly useful for child machines. Prevents the need for an initial state to handle receiving the event.
const child = createMachine({
one: state()
}, (ctx, ev) => ( { files: ev.files }));
Make it mostly just link to the website.
I can't seem to get the exports from the published module at
import { createMachine } from "https://unpkg.com/[email protected]/dist/machine.js"
It errors with:
Uncaught SyntaxError: The requested module 'https://unpkg.com/[email protected]/dist/machine.js?module' does not provide an export named 'createMachine'
Maybe the module should export using module.exports
instead of just exports
?
I got it working using:
import { createMachine, state, transition } from "https://unpkg.com/[email protected]/machine.js"
However preact-robot
is not properly hosted on unpkg (it tries to import from preact-hooks
). So is there a way to get robot running with webmodules instead of transpiling?
It would be nice of invoke could take a machine instead of a promise.
import { createMachine, invoke, state, transition } from '@matthewp/robot';
const machine = createMachine({
idle: invoke(otherMachine,
transition('done', 'next')
),
next: state()
});
For this to work we need the concept of a final
state.
I have a state machine where I want a a transition only to occur when the event has a specific form, e.g. a provided value is in a specific range. My understanding is that guards would be the right approach here, but it looks like events are currently not passed to guards.
When there is a tree of nested state machines, when the very child final state is reached it goes straight to the root machine
Link to the official docs, etc.
The goal of this issue is to find an immutable replacement for services.
Many parts of Robot are already immutable, machines and their state for example, but services are the one glaring exception. This causes a number of problems internally, mostly around services carrying around stale state in the middle of transitions.
I would like to research this topic a bit more and look to the wider software world for inspiration. It's important to first consider why do we have services in the first place? There could be a send()
function that takes a machine and returns a new machine (this is kind of how it already works internally).
onChange
function that gets called when send() is called. How can we replace this?context
object.child
which is a child service. I think we can move this to the machine where child
is a child machine but I'm not totally sure about this, this is sort of stateful.In this branch I implemented matches()
which works like in XState: https://github.com/matthewp/robot/tree/matches
That is, you can do something like this:
service.matches('foo.bar');
Which will return true of the state is foo
and the child machine's state is bar
. This is pretty nice.
However, one thing I've wanted for a while is the ability to do unions for matching. In JavaScript you can use bitwise operators to do union matching, like so:
const Red = 1;
const Yellow = 1 << 1;
const Green = 1 << 2;
const CanGo = Yellow | Green;
It would be cool to incorporate this into Robot some how. What if you could pass such a union into a service.matches()
API like so:
service.machine.current; // "yellow";
service.matches(Yellow); // true
service.matches(Green); // false
service.matches(CanGo); // true
I'd like to explore possibly allowing for such a thing. This would likely mean creating the state bits so users don't have to. Here's a possibly api:
const { yellow, green } = machine.states;
service.matches(yellow | green);
This seems fine. What about child states though? I would want to incorporate these some how as well so you can do the sort of service.matches('foo.bar')
check that's shown at the top of this issue.
This is possible today.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.