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

@tvaliasek/state-machine

v1.3.0

Published

Very basic framework for building state machines.

Downloads

35

Readme

Generic state machine

A simple but useful DIY framework for building state machines. We use it in several projects for complex user data exports to integrated business systems.

Basic concepts

Using this library, you can arrange multiple units of work - "steps" in processing pipeline - "process". Each step can be dependent on other steps state and produces its own state. This state is then persisted and retrieved by process state provider, which must be implemented. Each step can end in one of three states - success, skipped or failed.

Usage

  1. Install package
$ npm install @tvaliasek/state-machine
  1. Bring your own classes that implement the appropriate interface (see the docs below), you can extend generic abstract classes and run the process.
import { GenericProcess } from "../../src";
import { ExampleArrayItemStep } from "./ExampleArrayItemStep";
import { ExampleStep } from "./ExampleStep";
import { MemoryStepStateProvider } from './MemoryStepStateProvider'

class Process extends GenericProcess {}

const stateProvider = new MemoryStepStateProvider()

const instance = new Process(
    'exampleProcess', 
    [
        new ExampleStep('step1'),
        new ExampleStep('step2'),
        new ExampleArrayItemStep('arrayItemStep1', '1'),
        new ExampleArrayItemStep('arrayItemStep1', '2'),
        new ExampleArrayItemStep('arrayItemStep1', '3')
    ],
    stateProvider
)

instance.run()
    .then(() => {
        console.log('Process has been finished')
    }).catch((error) => {
        console.error(error)
    })

The docs below :)

Classes

There are several basic abstract classes to extend from.

GenericProcess

This is the main class containing all the logic needed to run all steps, validate and resolve their dependencies and retrieve and save step states. If you do not need anything custom, you can simply extend it.

You can pass the processed input as the last parameter of the constructor. Then the input will be accessible from all the steps via a process reference (this.process.getProcessedInput()).

The generic class is an event emitter, so you can listen for events:

| event | data | description | |-------|------|-------------| | start | { processName: string } | emitted on start of run | | step-done | { processName: string, stepName: string, itemIdentifier: string\|null, state: ProcessStepStateInterface } | emitted after successful doWork method call | | step-error | { processName: string, stepName: string, itemIdentifier: string\|null, error: Error } | emitted when any error is thrown from doWork method | | done | { processName: string } | emitted on end of run |

Example:

import { GenericProcess } from "@tvaliasek/state-machine"

class Process extends GenericProcess {}

GenericStep

Abstract class representing common step. You can customize inner logic to your needs, but you must implement at least doWork method. And you probably want to customize shouldRun method. Its state is maintained by combination of process name and step name.

If you define a dependency on the successful execution of the other steps (third constructor parameter), you can access it from context property on this.stateOfDependencies. This property contains Map<stringNameOfStep, ProcessStepStateInterface|ProcessStepStateInterface[]>.

You can also access the process on context property this.process.

Example:

import { GenericStep, StepInterface, ProcessStepStateInterface } from "@tvaliasek/state-machine"

export class ExampleStep extends GenericStep<Record<string, any>> implements StepInterface<Record<string, any>> {
    async doWork (): Promise<ProcessStepStateInterface> {
        try {
            if (!this.shouldRun()) {
                return this.getStepResult()
            }
            
            return await (new Promise((resolve, reject) => {
                setTimeout(
                    () => {
                        this.onSuccess(this.state)
                        console.log({ step: this.stepName, item: null })
                        resolve(this.getStepResult())
                    },
                    250
                )
            }))
        } catch (error) {
            this.onError(error.message)
            throw error
        }
    }
}

GenericArrayStep

GenericArrayStep is just like GenericStep, but it is meant to be used as one step repeatedly used on multiple items. For this reason, its state is maintained by combination of process name, step name and specific processed item identifier.

Example:

import { GenericArrayStep, ArrayItemStepInterface, ProcessStepStateInterface } from "@tvaliasek/state-machine"

export class ExampleArrayItemStep extends GenericArrayStep<Record<string, any>> implements ArrayItemStepInterface<Record<string, any>> {
    async doWork (): Promise<ProcessStepStateInterface> {
        try {
            if (!this.shouldRun()) {
                return this.getStepResult()
            }
            
            return await (new Promise((resolve, reject) => {
                setTimeout(
                    () => {
                        this.onSuccess(this.state)
                        console.log({ step: this.stepName, item: this.itemIdentifier })
                        resolve(this.getStepResult())
                    },
                    250
                )
            }))
        } catch (error) {
            this.onError(error.message)
            throw error
        }
    }
}

State provider

State provider could be instance of class implementing two methods:

export interface ProcessStateProviderInterface {
    getStepState (processName: string, stepName: string, itemIdentifier: string|null): Promise<ProcessStepStateInterface|null>
    setStepState (processName: string, stepName: string, itemIdentifier: string|null, stepState: ProcessStepStateInterface): Promise<void>
}

Example:

import { ProcessStepStateInterface } from "@tvaliasek/state-machine"

export class MemoryStepStateProvider {
    constructor (
        public state: Map<string, ProcessStepStateInterface> = new Map([])
    ) {}

    async getStepState (processName: string, stepName: string, itemIdentifier: string|null): Promise<ProcessStepStateInterface|null> {
        const entry = this.state.get(`${processName}_${stepName}_${itemIdentifier}`)
        return entry ?? null
    }

    async setStepState (processName: string, stepName: string, itemIdentifier: string|null, stepState: ProcessStepStateInterface): Promise<void> {
        console.log(`State for process ${processName}, step ${stepName}, item ${itemIdentifier} has been set.`)
        this.state.set(`${processName}_${stepName}_${itemIdentifier}`, stepState)
    }
}

In our case, we use factory methods to instantiate state providers scoped to specific record of processed items.

Example:

import { ProcessStepStateInterface } from "@tvaliasek/state-machine"

export class ScopedMemoryStepStateProvider {
    constructor (
        protected readonly processedItemId: string,
        public state: Map<string, ProcessStepStateInterface> = new Map([])
    ) {}

    async getStepState (processName: string, stepName: string, itemIdentifier: string|null): Promise<ProcessStepStateInterface|null> {
        const entry = this.state.get(`${this.processedItemId}_${processName}_${stepName}_${itemIdentifier}`)
        return entry ?? null
    }

    async setStepState (processName: string, stepName: string, itemIdentifier: string|null, stepState: ProcessStepStateInterface): Promise<void> {
        console.log(`State for process ${processName}, step ${stepName}, item ${itemIdentifier} has been set.`)
        this.state.set(`${this.processedItemId}_${processName}_${stepName}_${itemIdentifier}`, stepState)
    }

    // factory method
    public static createForRecord (id: string): ScopedMemoryStepStateProvider {
        return new ScopedMemoryStepStateProvider(id)
    }
}

const stateProvider = ScopedMemoryStepStateProvider.createForRecord('someId')