All Projects → JavaScriptRegenerated → yieldmachine

JavaScriptRegenerated / yieldmachine

Licence: MIT license
Components for State Machines, using Generator Functions

Programming Languages

typescript
32286 projects

Projects that are alternatives of or similar to yieldmachine

Awesome Fsm
🤖 A curated list of awesome resources related to finite state machines and statecharts.
Stars: ✭ 189 (+721.74%)
Mutual labels:  state-machine
Api Generator
PHP-code generator for Laravel framework, with complete support of JSON-API data format
Stars: ✭ 244 (+960.87%)
Mutual labels:  state-machine
StateArts
Intellij plugin that creates state machine diagram from state machine
Stars: ✭ 87 (+278.26%)
Mutual labels:  state-machine
Liquidstate
Efficient asynchronous and synchronous state machines for .NET
Stars: ✭ 197 (+756.52%)
Mutual labels:  state-machine
Use Machine
React Hook for using Statecharts powered by XState. use-machine.
Stars: ✭ 226 (+882.61%)
Mutual labels:  state-machine
Stateless
Go library for creating state machines
Stars: ✭ 247 (+973.91%)
Mutual labels:  state-machine
Modernavplayer
ModernAVPlayer is a persistence AVPlayer wrapper
Stars: ✭ 179 (+678.26%)
Mutual labels:  state-machine
vrrm
rough code for running consensus
Stars: ✭ 18 (-21.74%)
Mutual labels:  state-machine
Vue Save State
A Vue mixin to save the state of a component to local storage
Stars: ✭ 243 (+956.52%)
Mutual labels:  state-machine
SwiftElm
Reactive + Automaton + VTree in Swift, inspired by Elm.
Stars: ✭ 97 (+321.74%)
Mutual labels:  state-machine
Ui Router
The de-facto solution to flexible routing with nested views in AngularJS
Stars: ✭ 13,738 (+59630.43%)
Mutual labels:  state-machine
Laravel State Machine
Winzou State Machine service provider for Laravel
Stars: ✭ 213 (+826.09%)
Mutual labels:  state-machine
Xstate
State machines and statecharts for the modern web.
Stars: ✭ 18,300 (+79465.22%)
Mutual labels:  state-machine
Fluent State Machine
Fluent API for creating state machines in C#
Stars: ✭ 195 (+747.83%)
Mutual labels:  state-machine
Text101-Original
A basic state-machine based text adventure exercise as part of our Complete Unity C# Developer course (http://gdev.tv/cudgithub)
Stars: ✭ 43 (+86.96%)
Mutual labels:  state-machine
Python Statemachine
Python Finite State Machines made easy.
Stars: ✭ 184 (+700%)
Mutual labels:  state-machine
Aws Etl Orchestrator
A serverless architecture for orchestrating ETL jobs in arbitrarily-complex workflows using AWS Step Functions and AWS Lambda.
Stars: ✭ 245 (+965.22%)
Mutual labels:  state-machine
react-transition-state
Zero dependency React transition state machine.
Stars: ✭ 239 (+939.13%)
Mutual labels:  state-machine
xoid
Framework-agnostic state management library designed for simplicity and scalability ⚛
Stars: ✭ 96 (+317.39%)
Mutual labels:  state-machine
xstate-react-router
XState connector to React Router.
Stars: ✭ 23 (+0%)
Mutual labels:  state-machine

👑 ⚙️ Yield Machine

Components for State Machines, using Generator Functions

Goals

  • States and machines can be reused — components for state machines.
  • Nest machines inside one another — aid reuse and clarity.
  • Interops with native JavaScript & browser features such as Promise, AbortSignal, and EventTarget.
  • Consistently use built-in browser features such as offline status, promises, fetch, IntersectionObserver, ResizeObserver, window.location. Manage these things in a consistent way with a consistent interface.

Problems that state machines solve

  • Making sure my code is 100% robust and doesn't fall into inconsistent states is hard.
  • It's easy to forget about error handling.
  • Built-in browser features (such as InteractionObserver) are powerful but a pain to manage correctly.
  • Managing various flavors of state is hard: the current URL, local storage, focused element, fetch response, caches, offline/online.

Install

Requires Node.js 14 and up.

npm add yieldmachine

Overview

You define your machine using a function. For example, you could define a state machine representing a light switch. We’ll name our function Switch.

function Switch() {

}

Inside you declare each state you want as a generator function.

Our Switch will have two states: Off and On. We return Off as that’s what we want as our initial state to be — our light is off by default.

import { on, start } from "yieldmachine";

function Switch() {
  function* Off() {
  }
  function* On() {
  }

  return Off;
}

Our Switch can be flicked on and off. The string "FLICK" is our event that will represent the flicking on and off of our switch.

When our Switch is Off and it is sent a FLICK event, it transitions to On.

And, when our Switch is On and it is sent a FLICK event, it transitions back to Off.

import { on, start } from "yieldmachine";

function Switch() {
  function* Off() {
    yield on("FLICK", On);
  }
  function* On() {
    yield on("FLICK", Off);
  }

  return Off;
}

Now our machine is ready to be run. We pass our Switch to the start function we import from yieldmachine, and it will run an instance of our machine. And as we send it "FLICK" message, you’ll see the value of our machine instance change.

const machine = start(Switch);
machine.value; // { state: "Off", change: 0 }
machine.next("FLICK");
machine.value; // { state: "On", change: 1 }
machine.next("FLICK");
machine.value; // { state: "Off", change: 2 }

Benefits of Generator Functions

  • Generator Functions are a built-in feature of JavaScript and TypeScript.
  • They have built-in syntax highlighting, autocompletion, and general rich language support in editors like Visual Studio Code.
  • Our states are represented by actual JavaScript functions.
    • This means if we pass a state that’s either spelled incorrectly or isn’t in scope, our editor will tell us.
    • Our states use the name of the function.
    • Generator Functions can be reused, composed, and partially applied like any function. This increases the modularity and reuse of our machine parts.
  • Coming soon: our machine definitions can be serialized and deserialized. This means they could be generated on a back-end and sent to the front-end. They could be stored away in a database. They could even be generated dynamically on the fly.

Documentation

start(machineDefinition: Function | GeneratorFunction, options: { signal?: AbortSignal })

Starts a machine, transitioning to its initially returned state.

.value

.value.state: string | Record<string, unknown>

The current state of the machine. If machines were nested then an object is returned with the parent machine as the key, and its current state as the value.

.value.change: number

The number of times this machine has transitioned. Useful for consumers updating only when changes have been made.

.value.results: Promise<unknown>

The result of calling functions passed to entry() or exit().

.next(eventName: string | symbol)

Sends an event to the machine, transitioning if the event was recognised. Unrecognised events are ignored.

.stop()

Cleans up the machine.

Messages

on(eventName: string | symbol, target: GeneratorFunction | Cond | Mapper)

Transitions to the target state when the given event occurs.

import { on, start } from "yieldmachine";

function Switch() {
  function* Off() {
    yield on("FLICK", On);
    yield on("TOGGLE", On);
  }
  function* On() {
    yield on("FLICK", Off);
    yield on("TOGGLE", Off);
  }

  return Off;
}

const machine = start(Switch);
machine.value.state; // "Off"
machine.next("FLICK");
machine.value.state; // "On"
machine.next("TOGGLE");
machine.value.state; // "Off"

cond(predicate: (readContext: ReadContextCallback) => boolean, target: GeneratorFunction)

Passed as the 2nd argument to on() to conditionally transition to an event. Can read from context to help make its decision.

entry(action: ({ signal }: { signal: AbortSignal }) => undefined | unknown | Promise<unknown>)

Runs the provided function when this state is entered. If the function returns a promise, its value is made available in the .results property of the machine, keyed by the name of this passed function.

A signal is provided which is useful for passing to fetch() or eventTarget.addEventListener(). This signal is aborted on exit.

import { start, on, enter } from "yieldmachine";

let onCount = 0;
function recordOn() {
  onCount++;
}

function Switch() {
  function* Off() {
    yield on("FLICK", On);
  }
  function* On() {
    yield entry(recordOn);
    yield on("FLICK", Off);
  }

  return Off;
}

const machine = start(Switch);
machine.next("FLICK");
console.log(onCount, machine.value.state); // 1, "ON"
machine.next("FLICK");
console.log(onCount, machine.value.state); // 1, "OFF"
machine.next("FLICK");
console.log(onCount, machine.value.state); // 2, "ON"

exit(action: () => undefined | unknown | Promise<unknown>)

Runs the provided function when this state is exited.

import { start, on, exit } from "yieldmachine";

let lastSessionEnded = null;
function recordSessionEnd() {
  lastSessionEnded = new Date();
}

function Session() {
  function* SignedOut() {
    yield on("AUTHENTICATE", SignedIn);
  }
  function* SignedIn() {
    yield exit(recordSessionEnd);
    yield on("LOG_OFF", SignedOut);
  }

  return SignedOut;
}

const machine = start(Switch);
console.log(lastSessionEnded, machine.value.state); // null, "SignedOut"
machine.next("AUTHENTICATE");
console.log(lastSessionEnded, machine.value.state); // null, "SignedIn"
machine.next("LOG_OFF");
console.log(lastSessionEnded, machine.value.state); // (current time), "SignedOut"

listenTo(sender: EventTarget, eventName: string | string[])

Listens to an EventTarget — for example, an HTMLElement like a button.

Uses .addEventListener() to listen to the event. The listener is removed when transitioning to a different state or when the machine is stopped, so no extra clean up is necessary.

function ButtonClickListener(button: HTMLButtonElement) {
  function* initial() {
    yield on("click", clicked);
    yield listenTo(button, "click");
  }
  function* clicked() {}

  return initial;
}

const button = document.createElement('button');
const machine = start(ButtonClickListener.bind(null, button));

machine.value; // { state: "initial", change: 0 }
button.click();
machine.value; // { state: "clicked", change: 1 }
button.click();
machine.value; // { state: "initial", change: 1 }

Examples

HTTP Loader

import { entry, on, start } from "yieldmachine";

const exampleURL = new URL("https://example.org/");
function fetchData() {
  return fetch(exampleURL);
}

// Define a machine just using functions
function Loader() {
  // Each state is a generator function
  function* idle() {
    yield on("FETCH", loading);
  }
  // This is the ‘loading’ state
  function* loading() {
    // This function will be called when this state is entered.
    // Its return value is available at `loader.results.fetchData`
    yield entry(fetchData);
    // If the promise succeeds, we will transition to the `success` state
    // If the promise fails, we will transition to the `failure` state
    yield on("SUCCESS", success);
    yield on("FAILURE", failure);
  }
  // States that don’t yield anything are final
  function* success() {}
  // Or they can define transitions to other states
  function* failure() {
    // When the RETRY event happens, we transition from ‘failure’ to ‘loading’
    yield on("RETRY", loading);
  }

  // Return the initial state from your machine definition
  return idle;
}

const loader = start(Loader);
loader.value; // { state: "idle", change: 0 }

loader.next("FETCH");
loader.value; // { state: "loading", change: 1, results: Promise }

loader.value.results.then((results) => {
  console.log("Fetched", results.fetchData); // Use response of fetch()
  loader.value.state; // "success"
});

/* Or with await: */
// const { fetchData } = await loader.value.results;
// loader.value.state; // "success"

Passing parameters to a machine with closures

import { entry, on, start } from "yieldmachine";

// Function taking as many arguments as you like
function GenericLoader(url) {
  function fetchData() {
    return fetch(url);
  }

  function* idle() {
    yield on("FETCH", loading);
  }
  function* loading() {
    yield entry(fetchData);
    yield on("SUCCESS", success);
    yield on("FAILURE", failure);
  }
  function* success() {}
  function* failure() {
    yield on("RETRY", loading);
  }

  return idle;
}

// Function taking no arguments that will define our machine
function SpecificLoader() {
  const exampleURL = new URL("https://example.org/");
  return GenericLoader(exampleURL);
}

// Start our specific loader machine
const loader = start(SpecificLoader);
loader.value; // { state: "idle", change: 0 }

loader.next("FETCH");
loader.value; // { state: "loading", change: 1, results: Promise }

loader.value.results.then((results) => {
  console.log("Fetched", results.fetchData); // Use response of fetch()
  loader.value.state; // "success"
});

AbortController wrapper that listens to "abort" event

function* AbortListener(controller: AbortController) {
  function* Initial() {
    yield on("abort", Aborted);
    yield listenTo(controller.signal, ["abort"]);
  }
  function* Aborted() {}

  return new Map([
    [() => controller.signal.aborted, Aborted],
    [null, Initial],
  ]);
}

const aborter = new AbortController();
const machine = start(AbortListener.bind(null, aborter));

machine.value; // { state: "initial", change: 0 }
aborter.abort();
machine.value; // { state: "aborted", change: 1 }

Minifiers

If you use a minifier then your function name will be changed to a short name like d instead of On. To get around this, you can specify your states as methods (which are not usually minified) like so:

function SwitchMachine() {
  const { On, Off } = {
    *Off() {
      yield on("FLICK", On);
    },
    *On() {
      yield on("FLICK", Off);
    }
  };
  return Off;
}

TODO

  • Parallel states by returning object for initial state
  • Assign data somehow?
  • Allow sending objects: Event | { type: string }
  • More examples!
  • Hook for React
  • Hook for Preact
  • Hook for Vue
function *Parallel() {
  function Light1() {
    function* Off() {
      yield on('toggle_switch_1', On);
    }
    function* On() {
      yield on('toggle_switch_1', Off);
    }
    return Off;
  }

  function Light2() {
    function* Off() {
      yield on('toggle_switch_2', On);
    }
    function* On() {
      yield on('toggle_switch_2', Off);
    }
    return Off;
  }

  return [
    Light1,
    Light2
  ];
}
function *ParallelWithANDState() {
  function Light1() {
    function* Off() {
      yield on('toggle_switch_1', On);
    }
    function* On() {
      yield on('toggle_switch_1', Off);
    }
    return Off;
  }

  function Light2() {
    function* Off() {
      yield on('toggle_switch_2', On);
    }
    function* On() {
      yield on('toggle_switch_2', Off);
    }
    return Off;
  }

  // function* Light3() {
  //   function* Off() {}
  //   function* On() {}

  //   return conds(new Map([
  //     [hasState(Light1, 'Off'), Off],
  //     [hasState(Light2, 'Off'), Off],
  //     [true, On],
  //   ]));
  // }

  function* Light3() {
    const light1Off = yield readHasState(Light1, 'Off');
    const light2Off = yield readHasState(Light2, 'Off');

    function* Off() {}
    function* On() {}
    function* checking() {
      yield cond(light1Off, Off);
      yield cond(light2Off, Off);
      yield always(On);
    }

    return checking;
  }

  // Alternative
  function* Light3() {
    const onCount = yield readCountSiblings('Off');

    function* Off() {}
    function* On() {}
    function* checking() {
      yield cond(onCount === 2, Off);
      // yield cond(`${onCount} === ${2}`, Off);
      // yield condIs(Off, onCount, 2);
      yield always(On);
    }

    return checking;
  }

  return [
    Light1,
    Light2,
    Light3
  ];
}

Further reading / inspiration:

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].