Machine versions
Each machine definition may have many machine versions associated with it.
The most important aspect of a machine version is your actual code for your authorizer and state machines.
Machine versions can also provide a version specifier
to help you link
a version to your own systems. We recommend a semantic version, timestamp,
or git commit sha for a version specifier.
You won't be able to create an instance of a machine until you create at least one version for it.
Code structure
Your code will run in a web standards-like environment. The code that is uploaded to State Backed must be a self-contained javascript bundle (no external dependencies) in ECMAScript module format and it must export 3 things:
- Your code should default export an XState machine, (e.g.
export default createMachine({...})
) - Your code should export an
allowRead
function that will be called to authorize all requests to read the state of an instance of your machine - Your code should export an
allowWrite
function that will be called to authorize all requests to send events to an instance of your machine
Example
- Typescript
- Javascript
import {createMachine, assign} from "xstate";
import type {AllowRead, AllowWrite, AnonymousAuthContext} from "@statebacked/machine";
// shape of your machine's context
type Context = {};
// State Backed will call allowRead to determine whether a request to read
// the state of an instance of this machine will be allowed or not.
// authContext contains claims about your end-user that you include in the
// auth token for the request.
//
// In this case, we use anonymous sessions and allow users to read from any
// machine instance that is named with their session id.
export const allowRead: AllowRead<Context, AnonymousAuthContext> = ({machineInstanceName, authContext}) =>
machineInstanceName === authContext.sid;
// Similarly, State Backed calls allowWrite to determine whether a request
// to send an event to an instance of this machine will be allowed or not.
//
// In this case, we allow users to write to any machine instance that
// is named with their session id.
export const allowWrite: AllowWrite<Context, AnonymousAuthContext> = ({machineInstanceName, authContext}) =>
machineInstanceName === authContext.sid;
type Context = {
public: {
toggleCount ?: number;
}
};
// this is just a regular XState state machine
export default createMachine<Context>({
predictableActionArguments: true,
initial: "on",
states: {
on: {
on: {
toggle: {
target: "off",
actions: assign({
// any context under the `public` key will be visible to authorized clients
public: (ctx) => ({
...ctx.public,
toggleCount: (ctx.public?.toggleCount ?? 0) + 1
})
}),
},
},
},
off: {
on: {
toggle: "on",
},
},
},
});
import {createMachine} from "xstate";
// State Backed will call allowRead to determine whether a request to read
// the state of an instance of this machine will be allowed or not.
// authContext contains claims about your end-user that you include in the
// auth token for the request.
//
// In this case, we allow users to read from any machine instance that
// is named with their user id.
export const allowRead = ({machineInstanceName, authContext}) =>
machineInstanceName === authContext.sid;
// Similarly, State Backed calls allowWrite to determine whether a request
// to send an event to an instance of this machine will be allowed or not.
//
// In this case, we allow users to write to any machine instance that
// is named with their user id.
export const allowWrite = ({machineInstanceName, authContext}) =>
machineInstanceName === authContext.sid;
// this is just a regular XState state machine
export default createMachine({
predictableActionArguments: true,
initial: "on",
states: {
on: {
on: {
toggle: {
target: "off",
actions: assign({
// any context under the `public` key will be visible to authorized clients
public: (ctx) => ({
...ctx.public,
toggleCount: (ctx.public.toggleCount || 0) + 1
})
}),
},
},
},
off: {
on: {
toggle: "on",
},
},
},
});
Version upgrades
Existing instances may be upgraded between machine versions via migrations. As long as there is a path from the current instance version to the desired instance version via some set of migrations, once you set a desired version for an instance, State Backed will execute the sequence of migrations to bring the instance state and context to that version.
Builds
The smply
CLI can attempt to build a suitable State Backed bundle for you
from your javascript or typescript code or you can build your bundle yourself.
If you choose to build your own bundle, make sure you target a web
standards-like environment and emit an ECMAScript module (esm).
We also highly recommend treating xstate
as an externally-loaded dependency, referenced
Deno-style as npm:xstate
. State Backed internally maps this to the latest 4.x version of
XState to ensure that only one XState dependency is used. If you choose to bundle xstate
instead of treating it as an external, you will need to use spawnEphemeralInstance
from
@statebacked/machine
instead of the native
xstate spawn
to spawn ephemeral
(i.e. non-persistent instances).
If you elect to have smply
build your bundle, it ensures xstate is treated as an external
library and executes builds with esbuild using (essentially):
esbuild <your-file.(js|ts)> \
--bundle \
--platform=browser \
--define:process.env.NODE_ENV=\"production\" \
--format=esm \
--minify \
--keep-names \
--legal-comments=none \
--drop:debugger \
--external:npm:xstate \
--alias:xstate=npm:xstate
For convenience, smply
supports node or deno dependency resolution.
Node dependency resolution requires a package.json
and running npm install
prior to building and will take dependencies from the node_modules
folder.
Deno dependency resolution allows for a fully self-contained machine definition,
without managing a package.json
or running npm
but requires that any
dependencies are imported with Deno-style ESM specifiers. You can
import { createMachine } from "npm:xstate";
to access XState.
Web dashboard
You can view and create machine versions by tapping into a machine in the web dashboard.
Use our in-browser code editor and visualizer to define and deploy versions.
CLI
Creating a machine version
Creating a version by building your-machine.(ts|js)
using node dependency resolution
(requires a package.json
and npm install
)
smply machine-versions create \
--machine <your machine name> \
--version-reference 0.2.1 \
--node <your-machine.(ts|js)>
Deno-style, self-contained dependency support is coming soon.
Creating a version by building your-machine.(ts|js)
using deno dependency resolution
(requires using deno-style imports)
smply machine-versions create \
--machine <your machine name> \
--version-reference 0.2.1 \
--deno <your-machine.(ts|js)>
Creating a version by bundling yourself
- npm
- Yarn
- pnpm
npm run build
smply machine-versions create \
--machine <your machine name> \
--version-reference 0.2.1 \
--js <./dist/your-bundle.js>
yarn build
smply machine-versions create \
--machine <your machine name> \
--version-reference 0.2.1 \
--js <./dist/your-bundle.js>
pnpm run build
smply machine-versions create \
--machine <your machine name> \
--version-reference 0.2.1 \
--js <./dist/your-bundle.js>
Listing the versions for a machine
smply machine-versions list --machine <your machine name>
Client SDK
Creating a machine version
You will generally want to use the smply CLI to create machine versions.
import { StateBackedClient } from "@statebacked/client";
const client = new StateBackedClient(token);
await client.machineVersions.create(
"my-machine",
{
makeCurrent: true,
clientInfo: "v0.1.1",
code: `
// JS code for your machine version
`
}
)