npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

txsm

v1.0.4

Published

Transactional state machines for backend (but not only) solutions.

Downloads

13

Readme

txsm

Transactional state machines for backend (but not only) solutions.

Features

  • Awaitable transitions (support for async operations across the functionality)
  • Transactional operations (the machine cannot land in incorrect/not-expected state)
  • Easy machine definition that works close with JavaScript
  • Multiple transitions from a state with one event via guards
  • Persistance and restoration
  • Actions (side effects) on transitions
  • Ability to provide actions implementation separately from the machine definition (via actors)
  • Full and strict TypeScript support
  • History of states

Quick start

$ pnpm add txsm
$ npm install --save txsm
$ yarn add txsm
export const lightMachine = Txsm
  .create({
    transitions: [
      // calling `stop` command will transition from green to yellow if current state is `green`
      { from: 'green', to: 'yellow', with: 'stop' },
      // automated transition - no command needs to be called
      { from: 'yellow', to: 'red' },
      // calling `walk` command will transition from red to green if current state is `red`
      { from: 'red', to: 'green', with: 'walk' }, 
    ],
    // new machine should start from `red`, and there is no final state, because it loops forever
    config: { initial: 'red', final: [] },
  })
  .setTypes({ // For TypeScript users define command's payload and context type
    context: {} as { // define context type
      enteredYellowCounter: number
    },
    commands: {} as { // define commands payload
      stop: {},
      walk: {}
    },
  })
  .addEffect({ enter: 'yellow' }, { // after entering yellow
    action: ({ from, assign, context }) =>
      // update context that yellow state was entered
      assign({ enteredYellowCounter: context.enteredYellowCounter + 1 })
      // wait for 500ms (then automated transition to red will happen)
      .then(() => waitFor(500))
  });

const lightRuntime = lightMachine.run({
  // context is required, because it was defined in the machine configuration
  context: { enteredYellowCounter: 0 }
});

// enters initial `red` state and waits
await lightRuntime.start();
// logs `[{ type: 'walk' }]`
console.log(lightRuntime.getAcceptableCommands());

// transitions to green
await lightRuntime.execute({ type: 'walk' });
// logs `[{ type: 'stop' }]`
console.log(lightRuntime.getAcceptableCommands());

// transitions to yellow, waits 500ms, and transitions to red.
// After all of that the `.execute` promise is resolved
await lightRuntime.execute({ type: 'stop' });

console.log(lightRuntime.getState()); // logs `red`
console.log(lightRuntime.getContext()); // logs `{ enteredYellowCounter: 1 }`

Examples

Examples can be found here: machines.mts

Transitions

Transition describes possible change from one state to another. All transitions are defined during machine creation:

.create({
  transitions: [
    // calling `stop` command will transition from green to yellow if current state is `green`
    { from: 'green', to: 'yellow', with: 'stop' },
    // automated transition - no command needs to be called
    { from: 'yellow', to: 'red' },
    // calling `walk` command will transition from red to green if current state is `red`
    { from: 'red', to: 'green', with: 'walk' }, 
  ],
})

Because transition describes state change it requires two properties: from and to. However in most cases one also wants to determine when the transition should happen. For that with property is defined, that creates a command with the given name.

Transition can happen only if the machine is in the transition's from state.

Transitions are always respected in the order they are defined. If there are two transitions applicable, the first one will be executed.

Automated transitions

If the command (with) is not defined, then the transition is automated. Upon entering from state machine automatically goes to to state.

Current state / any state transition

Transition can also happen from any state (current state) to any state (current state):

// As long as force-stop command is executed, it doesn't matter in which state the machine is, it will go to `red` state
{ from: Transition.CURRENT_STATE, to: 'red', with: 'force-stop' },
// It doesn't matter in which state the transition is, it will go back to itself after executing `go-to-self`
{ from: Transition.CURRENT_STATE, to: Transition.CURRENT_STATE, with: 'go-to-self' },

Commands

Commands are explicitly invoked transitions. You define (names) command directly on the transition using with property:

{ from: 'green', to: 'yellow', with: 'stop' },

At this point stop command is created. Commands can carry a payload, so one can define it in .setTypes if TypeScript is used:

.setTypes({
  commands: {} as { // define commands payload
    stop: {
      stopReason: string
    },
  },
})

Commands and their payload can be accessed in actions.

Upon creating, the command can be called on the machine using .execute function:

await lightRuntime.execute({ type: 'stop', stopReason: 'Pedestrian pressed a button' });

Because commands depend on the transitions, and transitions depend on current state, a list of currently executable commands can be checked at any point:

// returns true/false if there is a transition for given command in current state
lightRuntime.canAcceptCommand({ type: 'stop' })

// return true/false if there is a transition for given command, that considering all the checks (state, guards etc) can be executed
// @NOTE: because it executes all checks it requires the actual command payload
await lightRuntime.canExecuteCommand({ type: 'stop', stopReason: '...' }) 

// returns array of { type: '...' } objects/commands that can be executed (this function is counterpart of `canAcceptCommand`)
lightRuntime.getAcceptableCommands();

Effects

Effects are run when transition happens, a state is entered or a state if exited.

When adding an effect user must first decide when it should trigger:

.addEffect({ from: 'stateName', to: 'stateName' }, ...) // transition effect
.addEffect({ enter: 'stateName' }, ...) // enter state effect
.addEffect({ exit: 'stateName' }, ...) // exit state effect

and then what should trigger:

.addEffect({ from: 'stateName', to: 'stateName' }, { // transition effect
  guard: ...,
  action: ...,
})
.addEffect({ enter: 'stateName' }, { // enter state effect
  guard: ...,
  action: ...,
})
.addEffect({ exit: 'stateName' }, { // exit state effect
  guard: ...,
  action: ...,
})

Both guard and action are optional.

Guard describes whether the effect that should be triggered. Guard on an transition effect additionaly might prevent transition from happening if guard condition is not met. Actions can invoke Actors, update context, call any function (even an async one). In future its capabilities will be expanded.

All states defined in an effect need to match available states (therefore configured transitions). Transition effect (from/to) has to describe correct transition. Typescript will help a user with that as all available transition are narrowed down after setting from.

Additionaly, if the effect is configured for a transition (from/to), and a transition is triggered using a command (with in transition definition), the command will be available both in guard and action. This is useful if user wants to use command's payload for some reason (updating context, checking a condition etc):

.setTypes({
  commands: {} as {
    myCommand: { value: boolean }
  }
})
.addEffect({ from: 'state1', to: 'state2' }, {
  guard: ({ command }) => command.value === true, // allow transition from state1 to state2 only if command's payload `.value` is equal to true
})

See Actions and Guards for more details and examples.

Guards

Guards are defined on an effect. They inform the runtime whether the effect can be executed. Without a guard, an effect is always run when the condition is met. Guard on an transition effect additionaly prevents transition from happening if guard condition is not met.

Guards take a form of a callback, that receives current machine's context, and payload (if an effect is defined for transition that is triggered by a command):

.addEffect({ from: 'state1', to: 'state2' }, {
  guard: ({ context, command }) => c, // command is available only if a transition is trigger by a command
})

Defining guards on transition effects might guide the flow to reach proper state. Consider the following example:

Txsm.create({
  transitions: [
    { from: 'pending', to: 'true' },
    { from: 'pending', to: 'false' },
  ],
  config: { initial: 'pending', final: ['true', 'false'] },
}).setTypes({
  context: {} as { value: boolean }
}).addEffect({ from: 'pending', to: 'true' }, {
  guard: ({ context }) => context.value === true,
});

We define an automated transition pending->true, and automated pending->false. We define a context { value: boolean }. pending->true is defined as a first transition. Upon machine starting, it will try to execute first matching transition. However, because there is a guard defined for that transition, the context.value is taken into consideration. If current machine's context.value is true, then the machine indeed go to true state. However, if machine's context.value is false, then the pending->true transition will not be executed, and runtime will check next matching transition: pending->false. It does not have any guard, therefore it will be executed, and machine will reach state false.

Actions

Action says what an effect does (besides guarding a transition).

You start defining an action by writing a callback:

.addEffect({ from: 'state1', to: 'state2' }, {
  action: ({ ... }) => ...
});

The callback accepts few initial starting points you might want to use:

.addEffect({ from: 'state1', to: 'state2' }, {
  action: ({ assign, context, command, invoke, from }) => ...
});
  • context is the machine's context (a state)
  • assign allows you to update the context (immediately): assign({ newContextValue: 123 })
  • command is a command that triggered the transition. It works only if you define an effect for a particular transition that is triggered with a command (see: #Commands)
  • invoke is a method to call an actor (see: #Actors)
  • from allows you to write your own action with your own code (you can call external function, log something etc)

Actions are executed one by one, even if they are asynchronous. Therefore, there is a special way for chaining them using .then operator (similar to Promises, but it is not a Promise):

.addEffect({ from: 'pending', to: 'end' }, {
  action: ({ assign }) =>
    assign({ value1: true })
      .then(assign({ value2: [true] }))
      .then(assign({ value3: { subValue2: true }})),
});

Any effect / Any state / Current state

In certain cases one might want to be able to execute effect from any state.

For that one can set from as current state:

.addEffect({ enter: Transition.CURRENT_STATE }),

CURRENT_STATE can be used for to:

.addEffect({ exit: Transition.CURRENT_STATE })

It is useful to execute an effect without knowing current state.

Actors

Actors are a way for functions to be injected into machine's runtime. That way, if you have multiple instances of the same machine, each can have it's own dedicated of actors.

Actors are set during machine's initialization:

const runtime = myMachine.run({
  actors: {
    executeSideEffect: () => {
      console.log('My side effect')
    },
  },
});

For TypeScript users actors signature is defined in machine's configuration:

.setTypes({
  ...
  actors: {} as {
    executeSideEffect: () => void, // Actors can return a Promise (that will be properly awaited), and even some result.
  },
})

Actor can be called in an action via invoke function:

.addEffect({ ... }, {
  action: ({ invoke }) => invoke('executeSideEffect'),
});

Actor's function input is provided to invoke, and its result is returned in action chain:

.setTypes({
  context: {} as {
    value: number,
  },
  actors: {} as {
    multiplyBy2: (value: number) => Promise<{ newValue: boolean }>,
  },
})
.addEffect({ ... }, {
  action: ({ invoke, context }) =>
    invoke('multiplyBy2', context.value)
    .then(({ result, assign }) => assign({ value: result.newValue })),
});

Snapshots / persistance

If a machine's state and context needs to be persisted its snapshot can be retrieved, and later restored:

const snapshot = runtimeBeforeSnapshot.getSnapshot();
const runtimeAfterSnapshot = snapshotMachine.restoreRuntime({ snapshot });

How it compares to xstate

  1. Txsm does not have parallel and history states
  2. Txsm does not have delayed events, timers etc as I consider them a bloat. If needed similar thing can be implemented manually by the user using actions.
  3. Txsm supports awaitable transitions and performs them in a transactional way. That means if a command is called, and it results in going through multiple states, if at some point execution fails, it automatically rollbacks to a state from before calling the command. It makes it perfect for backend use.
  4. The API is oriented around transitions, not around states. Transitions define what is possible, and which states are achievable. Calling actions and checking guards is just an addition. That leads to machine being much easier to read and reason about.
  5. Txsm does not support nested state machines - yet. This is very important feature that I want to implement.
  6. Txsm is less fragile when it comes to state machine changes. In xstate when you have a snapshot persisted, changes to a machine definition might very easily lead to a bug. In txsm it is theoretically possible to rebuild entire logic and bring snapshot back to live, as long as the context and the persisted state of a machine is compatible.
  7. Many things achievable in xstate should be achievable in txsm in some way. If not - please open an issue.

License explanation

txsm uses LGPLv3 license. It means, that You can:

  1. use library in closed-source projects
  2. redistribute the code (preserving authorship)
  3. make changes to the code

However, if you decide to make changes to the library code, You has to publish them under LGPLv3 license. This way library legally always stays open source and free.

The best way to make changes is to create public fork of the library.

If You don't plan to add any malicious behaviour to the library, this license should not be harmful for You in any way.

It is also expected, that any plugins (extensions or modules) added to library, are respecting final user freedom, and are not spying on his actions performed over such module without his knowledge and approval.

Development notes / contributing

Txsm doesn't use scripts from package.json. It instead uses Just - much more flexible command runner. Upon installing just you can check available recipes in justfile or by running just --list.

It also uses pnpm instead of npm.

It requires fairly modern node js version (at least 21.2.0), because it uses node's test runner and assert library for tests.

Roadmap/planned features

See TODO.md