🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

react-machine

Package Overview
Dependencies
Maintainers
1
Versions
10
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

react-machine - npm Package Compare versions

Comparing version
0.1.0
to
0.2.0
+5
CHANGELOG.md
# Changelog
## 0.2.0
First release 🎉. Thanks to @tempname11 for the npm package name.
const hookKeys = {
guard: 'guards',
reduce: 'reducers',
effect: 'effects',
invoke: 'invokes',
}
const transitionHooks = ['assign', 'reduce', 'action', 'guard']
const enterHooks = ['assign', 'reduce', 'action', 'invoke', 'effect']
const exitHooks = ['assign', 'reduce', 'action']
const mappedHooks = {
assign: ['reduce', assignToReduce],
action: ['reduce', actionToReduce],
}
const ACTION = {}
const env = (process && process.env && process.env.NODE_ENV) || 'development'
function warn(msg) {
if (env !== 'production') {
console.warn(msg)
}
}
function arg(argument, type, error) {
if (type === 'string') {
if (typeof argument !== 'string') {
throw new Error(error)
}
}
}
/**
* Parse the machine DSL into a machine object.
*/
export function createMachine(create) {
const machine = { states: {} }
if (create) {
create({
state: (name, ...opts) => {
machine.states[name] = createState(name, ...opts)
},
enter: createEnter,
exit: createExit,
transition: createTransition,
immediate: createImmediate,
internal: createInternal,
})
}
validate(machine)
return machine
}
function validate(machine) {
for (const [, state] of Object.entries(machine.states)) {
for (const transition of state.immediates) {
if (!machine.states[transition.target]) {
throw new Error(`Invalid transition target '${transition.target}'`)
}
}
for (const transitions of Object.values(state.transitions)) {
for (const transition of transitions) {
if (!transition.internal && !machine.states[transition.target]) {
throw new Error(`Invalid transition target '${transition.target}'`)
}
}
}
}
}
/**
* Create a state node.
*/
function createState(name, ...opts) {
const enter = []
const exit = []
const transitions = {}
const immediates = []
for (const opt of opts) {
const { type, event } = opt
if (type === 'transition') {
if (!transitions[event]) transitions[event] = []
transitions[event].push(opt)
} else if (type === 'immediate') {
immediates.push(opt)
} else if (type === 'enter') {
enter.push(opt)
} else if (type === 'exit') {
exit.push(opt)
} else {
throw new Error(
`State '${name}' should be passed one of enter(), exit(), transition(), immediate() or internal()`
)
}
}
return {
name,
enter,
exit,
transitions,
immediates,
}
}
function createEnter(opts) {
return { type: 'enter', ...merge(opts, enterHooks) }
}
function createExit(opts) {
return { type: 'exit', ...merge(opts, exitHooks) }
}
function createTransition(event, target, opts) {
arg(event, 'string', 'First argument of the transition must be the name of the event')
arg(target, 'string', 'Second argument of the transition must be the name of the target state')
return { type: 'transition', event, target, ...merge(opts, transitionHooks) }
}
function createInternal(event, opts) {
arg(event, 'string', 'First argument of the internal transition must be the name of the event')
return {
type: 'transition',
event,
internal: true,
...merge(opts, transitionHooks),
}
}
function createImmediate(target, opts) {
arg(
target,
'string',
'First argument of the immediate transition must be the name of the target state'
)
return { type: 'immediate', target, ...merge(opts, transitionHooks) }
}
/**
* Transition the given machine, with the given state
* to the next state based on the event. Returns the tuple
* of the next state and any events to execute. In case no external
* transition took place, return null as effects, to indicate
* that the active effects should continue running.
*/
export function transition(machine = {}, state = {}, event) {
event = typeof event === 'string' ? { type: event } : event
// initial transition
if (!state.name && event && event.type === null) {
const stateNames = Object.keys(machine.states)
if (stateNames.length > 0) {
const initialStateName = stateNames[0]
const initialTransition = createImmediate(initialStateName)
return applyTransition(machine, state, event, initialTransition)
}
}
const currState = machine.states[state.name] || {}
const transitions = currState.transitions || {}
const candidates = transitions[event.type] || []
for (const candidate of candidates) {
if (checkGuards(state.context, event, candidate)) {
return applyTransition(machine, state, event, candidate)
}
}
return [state, null]
}
/**
* The logic of applying a transition to the machine. Exit states,
* apply transition hooks, enter states and collect any events. Do this
* recursively untill all immediate transitions settle.
*/
function applyTransition(machine, curr, event, transition) {
const next = { ...curr }
const target = transition.internal ? curr.name : transition.target
const currState = machine.states[curr.name]
const nextState = machine.states[target]
const effects = transition.internal ? null : []
if (currState && !transition.internal) {
for (const exit of currState.exit) {
applyReducers(next, event, exit.reducers)
}
}
next.name = target
applyReducers(next, event, transition.reducers)
if (!transition.internal) {
for (const enter of nextState.enter) {
applyReducers(next, event, enter.reducers)
}
}
for (const candidate of nextState.immediates) {
if (checkGuards(next.context, event, candidate)) {
return applyTransition(machine, next, event, candidate)
}
}
if (Object.keys(nextState.transitions).length === 0 && nextState.immediates.length === 0) {
next.final = true
}
if (!transition.internal) {
for (const enter of nextState.enter) {
for (const invoke of enter.invokes) {
effects.push({ run: promiseEffect(invoke), event })
}
for (const effect of enter.effects) {
effects.push({ run: effect, event })
}
}
}
return [next, effects]
}
function checkGuards(context, event, transition) {
return !transition.guards.length || transition.guards.every((g) => g(context, event))
}
function applyReducers(next, event, reducers) {
for (const reduce of reducers) {
const result = reduce(next.context, event)
if (result !== ACTION) {
next.context = result
}
}
}
/**
* A common operation is to assign event payload
* to the context, this allows to do in several ways:
* true - assign the full event payload to context
* fn - assign the result of the fn(context, data) to context
* val - assign the constant provided value to context
*/
function assignToReduce(assign) {
return (context, event) => {
const { type, ...data } = event
if (assign === true) {
return { ...context, ...data }
}
if (typeof assign === 'function') {
return { ...context, ...assign(context, data) }
}
return { ...context, ...assign }
}
}
function actionToReduce(action) {
return (context, event) => {
action(context, event)
return ACTION
}
}
/**
* Allow to pass each hook as a function, or a list of functions
* transition(..., { reduce: fn })
* transition(..., { reduce: [fn1, fn2] })
* Convert both of those into arrays, and also remap some of the
* hooks to different hooks (i.e. assign -> reduce)
*/
function merge(opts = {}, allowedHooks) {
const merged = {}
for (const hook of allowedHooks) {
add(hook)
}
function add(hook) {
let t = opts[hook] || []
t = Array.isArray(t) ? t : [t]
if (mappedHooks[hook]) {
const [newName, transform] = mappedHooks[hook]
hook = newName
t = t.map(transform)
}
const key = hookKeys[hook]
merged[key] = merged[key] || []
merged[key] = merged[key].concat(t)
}
return merged
}
/**
* Convert an async function into an effect
* that sends 'done' and 'error' events
*/
function promiseEffect(fn) {
return (curr, event, send) => {
let disposed = false
Promise.resolve(fn(curr, event))
.then((data) => {
if (!disposed) {
send({ type: 'done', data })
}
})
.catch((error) => {
if (!disposed) {
send({ type: 'error', error })
}
})
return () => {
disposed = true
}
}
}
/**
* createMachine and transition are pure, stateless functions. After
* transitioning the machine to the next state, the caller must clean
* up all of the running effects, and then run the newly provided effects.
*/
export function runEffects(effects = [], state, send) {
const runningEffects = []
for (const effect of effects) {
const safeSend = (...args) => {
if (effect.disposed) {
warn(
[
"Can't send events in an effect after it has been cleaned up.",
'This is a no-op, but indicates a memory leak in your application.',
"To fix, cancel all subscriptions and asynchronous tasks in the effect's cleanup function.",
].join(' ')
)
} else {
return send(...args)
}
}
const dispose = effect.run(state.context, effect.event, safeSend)
if (dispose && dispose.then) {
warn(
[
'Effect function must return a cleanup function or nothing.',
'Use invoke instead of effect for async functions, or call the async function inside the synchronous effect function.',
].join(' ')
)
} else if (dispose) {
effect.dispose = () => {
effect.disposed = true
return dispose()
}
runningEffects.push(effect)
}
}
return runningEffects
}
export function cleanEffects(runningEffects = []) {
for (const effect of runningEffects) {
effect.dispose()
}
return []
}
import { useReducer, useEffect, useCallback, useMemo, useRef } from 'react'
import { createMachine, transition, runEffects, cleanEffects } from './core.js'
function initial({ context, machine }) {
const initialState = { context }
const initialEvent = { type: null }
const [state, effects] = transition(machine, initialState, initialEvent)
const curr = {
machine,
effects: effects || [],
state,
}
return curr
}
function reduce(curr, action) {
if (action.type === 'send') {
const { event, machine } = action
const [state, effects] = transition(machine, curr.state, event)
return { ...curr, state, effects: effects || curr.effects }
}
}
export function useMachine(create, context = {}, options = {}) {
const { assign = 'assign', deps } = options
const runningEffects = useRef()
const firstRender = useRef(true)
const machine = useMemo(() => createMachine(create), [create])
const [curr, dispatch] = useReducer(reduce, { context, machine }, initial)
const send = useCallback((event) => dispatch({ type: 'send', event, machine }), [
machine,
dispatch,
])
useEffect(() => {
runningEffects.current = cleanEffects(runningEffects.current)
if (curr.effects.length) {
runningEffects.current = runEffects(curr.effects, curr.state, send)
}
}, [send, curr.effects])
useEffect(() => {
return () => {
runningEffects.current = cleanEffects(runningEffects.current)
}
}, [])
const assignEffectDeps = [send].concat(deps || (context ? Object.values(context) : []))
useEffect(() => {
if (firstRender.current) {
firstRender.current = false
return
}
if (assign) {
send({ type: assign, ...context })
}
}, assignEffectDeps)
return [curr.state, send, curr.machine]
}
export { createMachine, transition, runEffects, cleanEffects } from './core.js'
export { useMachine } from './hooks.js'
The MIT License (MIT)
Copyright (c) 2020 Karolis Narkevicius
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Sorry, the diff of this file is too big to display

import { createMachine, transition, runEffects as run, cleanEffects as clean } from './core.js'
export function createService(machineDescription, context = {}) {
const machine = createMachine(machineDescription)
// initial transition
const [state, effects] = transition(machine, { name: null, context }, { type: null })
let cbs = []
let running = true
const service = {
machine,
state,
prev: null,
pendingEffects: effects || [],
runningEffects: [],
send,
subscribe,
stop,
}
function runEffects() {
service.runningEffects = clean(service.runningEffects)
service.runningEffects = run(service.pendingEffects, service.state, service.send)
}
function stop() {
running = false
cbs = []
service.pendingEffects = []
service.runningEffects = clean(service.runningEffects)
}
function subscribe(fn) {
cbs.push(fn)
return () => {
cbs = cbs.filter((f) => f !== fn)
}
}
function send(event) {
if (!running) return
service.prev = service.state
const [state, effects] = transition(service.machine, service.state, event)
service.state = state
if (effects) {
service.pendingEffects = effects
runEffects()
}
for (const cb of cbs) {
cb(state)
}
}
runEffects()
return service
}
export type EventType = string;
export type MetaObject = Record<string, any>;
export interface EventObject {
type: string;
}
export type Event<TEvent extends EventObject> = TEvent['type'] | TEvent;
export interface StateObject<TContext = any> {
name: string
context: TContext
final?: true
}
export interface StateSchema<TContext = any> {
meta?: any;
context?: Partial<TContext>;
states?: {
[key: string]: StateSchema<TContext>;
};
}
export function useMachine<TContext, TStateSchema extends StateSchema, TEvent extends EventObject>(
description: MachineDescription<TContext, TStateSchema, TEvent>,
context?: TContext,
options?: MachineOptions
): [state: StateObject<TContext>, send: SendFunction<TEvent>]
export interface MachineOptions {
assign: string | boolean,
deps: string[]
}
export type MachineDescription<C, S, E> = ({ state, transition, immediate, internal, enter, exit }: {
state: StateFunction
transition: TransitionFunction<C, E>
immediate: ImmediateFunction<C, E>
internal: TransitionFunction<C, E>
enter: EnterFunction<C, E>
exit: ExitFunction<C, E>
}) => any
export type StateFunction = (
name: string,
...opts: (Transition | Immediate | Internal | Enter | Exit)[]
) => MachineState
/**
* A `transition` function is used to move from one state to another.
*
* @param event - This will give the name of the event that triggers this transition.
* @param target - The name of the destination state.
* @param opts - Transition hooks, one of reduce, assign, guard or action.
*/
export type TransitionFunction<C, E> = (
event: string,
target: string,
opts: TransitionOptions<C, E>
) => Transition
/**
* An `immediate` transition is triggered immediately upon entering the state.
*
* @param target - The name of the destination state.
* @param opts - Transition hooks, one of reduce, assign, guard or action.
*/
export type ImmediateFunction<C, E> = (
target: string,
opts: TransitionOptions<C, E>
) => Immediate
/**
* An `internal` transition will re-enter the same state, but without re-runing enter/exit hooks.
*
* @param event - This will give the name of the event that triggers this transition.
* @param opts - Transition hooks, one of reduce, assign, guard or action.
*/
export type InternalFunction<C, E> = (
target: string,
opts: TransitionOptions<C, E>
) => Internal
export type EnterFunction<C, E> = (
opts: EnterOptions<C, E>
) => Enter
export type ExitFunction<C, E> = (
opts: ExitOptions<C, E>
) => Exit
export interface MachineState {
name: string
transitions: Map<string, Transition[]>
immediates?: Map<string, Immediate[]>
enter: any[]
exit: any[]
final?: true
}
export interface Transition {
type: 'transition'
event: string
target: string
guards: any[]
reducers: any[]
}
export interface Immediate {
type: 'transition'
target: string
guards: any[]
reducers: any[]
}
export interface Internal {
type: 'transition'
internal: true
event: string
guards: any[]
reducers: any[]
}
export interface Enter {
type: 'enter'
reducers: any[]
effects: any[]
}
export interface Exit {
type: 'enter'
reducers: any[]
effects: any[]
}
export interface TransitionOptions<C, E> {
guard?: GuardFunction<C, E> | GuardFunction<C, E>[]
reduce?: ReduceFunction<C, E> | ReduceFunction<C, E>[]
assign?: Assign<C, E> | Assign<C, E>[]
action?: ActionFunction<C, E> | ActionFunction<C, E>[]
}
export interface EnterOptions<C, E> {
effect?: EffectFunction<C, E> | EffectFunction<C, E>[]
invoke?: InvokeFunction<C, E> | InvokeFunction<C, E>[]
reduce?: ReduceFunction<C, E> | ReduceFunction<C, E>[]
assign?: Assign<C, E> | Assign<C, E>[]
action?: ActionFunction<C, E> | ActionFunction<C, E>[]
}
export interface ExitOptions<C, E> {
reduce?: ReduceFunction<C, E> | ReduceFunction<C, E>[]
assign?: Assign<C, E> | Assign<C, E>[]
action?: ActionFunction<C, E> | ActionFunction<C, E>[]
}
export type ReduceFunction<C, E> = (context: C, event: E) => C
export type ActionFunction<C, E> = (context: C, event: E) => unknown
export type GuardFunction<C, E> = (context: C, event: E) => boolean
export type Assign<C, E> = true | Partial<C> | ((context: C, event: E) => Partial<C>)
export type InvokeFunction<C, E> = (context: C, event: E) => Promise<any>
export type EffectFunction<C, E> = (context: C, event: E) => CleanupFunction | void
export type CleanupFunction = () => void
export type SendFunction<TEvent extends EventObject> = (event: TEvent) => void
import { MachineDescription, useMachine } from 'react-machine'
interface LightStateSchema {
states: {
green: {};
yellow: {};
red: {
states: {
walk: {};
wait: {};
stop: {};
};
};
};
}
interface LightContext {
elapsed: number;
a: number
}
type LightEvent =
| { type: 'TIMER' }
| { type: 'POWER_OUTAGE' }
| { type: 'PED_COUNTDOWN', duration: number }
const machine: MachineDescription<LightContext, LightStateSchema, LightEvent> = ({ state, transition, enter }) => {
state('green',
transition('TIMER', 'yellow', {
reduce: (ctx, { type, ...data }) => {
if (type === 'PED_COUNTDOWN') {
return { ...ctx, a: data.duration }
}
return ctx
},
guard: (ctx) => true
}),
enter({ effect: (ctx, event) => {}})
)
}
export function TypoComponent () {
const [state, send] = useMachine(machine)
}
{
"compilerOptions": {
"module": "commonjs",
"lib": ["es6"],
"target": "es6",
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noEmit": true,
"baseUrl": ".",
"paths": { "react-machine": ["."] }
}
}
+69
-11
{
"name": "react-machine",
"version": "0.1.0",
"description": "",
"main": "index.js",
"version": "0.2.0",
"description": "A lightweight state machine for React applications",
"type": "module",
"exports": {
".": "./index.js",
"./core": "./core.js",
"./service": "./service.js",
"./hooks": "./hooks.js"
},
"types": "types",
"sideEffects": false,
"files": [
"*"
],
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "healthier && prettier --check '**/*.{js,css,yml}' && ava && npm run package-check",
"format": "prettier --write '**/*.{js,css,yml}'",
"package-check": "npm run build && cd dist && package-check",
"coverage": "c8 --reporter=html ava",
"build": "node ./build.js",
"watch": "nodemon --ignore dist ./build.js",
"version": "npm run build",
"release": "np --contents dist",
"release-beta": "np --tag=beta --contents=dist",
"types": "tsc"
},
"license": "MIT",
"author": "Karolis Narkevicius <hello@kn8.lt>",
"repository": {
"type": "git",
"url": "git+https://github.com/tempname11/react-machine.git"
"url": "git+https://github.com/humaans/react-machine.git"
},
"author": "",
"license": "MIT",
"bugs": {
"url": "https://github.com/tempname11/react-machine/issues"
"keywords": [
"react",
"state",
"effects",
"react hook",
"state machine",
"finite state machine",
"finite automata"
],
"dependencies": {},
"peerDependencies": {
"react": "^17.0.1"
},
"homepage": "https://github.com/tempname11/react-machine#readme"
}
"devDependencies": {
"@babel/cli": "^7.12.10",
"@babel/core": "^7.12.10",
"@babel/plugin-transform-modules-commonjs": "^7.12.1",
"@babel/plugin-transform-react-jsx": "^7.12.10",
"@babel/register": "^7.12.10",
"@skypack/package-check": "^0.2.2",
"ava": "^3.13.0",
"c8": "^7.3.5",
"esm": "^3.2.25",
"healthier": "^4.0.0",
"jsdom": "^16.4.0",
"np": "^7.0.0",
"prettier": "^2.2.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"typescript": "^4.1.2"
},
"ava": {
"files": [
"test/test-*.js"
],
"require": [
"@babel/register"
]
},
"np": {
"releaseDraft": false
}
}

Sorry, the diff of this file is not supported yet