yay-machine
Version:
A modern, simple, lightweight, zero-dependency, TypeScript state-machine library
302 lines (298 loc) • 9.84 kB
JavaScript
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __moduleCache = /* @__PURE__ */ new WeakMap;
var __toCommonJS = (from) => {
var entry = __moduleCache.get(from), desc;
if (entry)
return entry;
entry = __defProp({}, "__esModule", { value: true });
if (from && typeof from === "object" || typeof from === "function")
__getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
get: () => from[key],
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
}));
__moduleCache.set(from, entry);
return entry;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, {
get: all[name],
enumerable: true,
configurable: true,
set: (newValue) => all[name] = () => newValue
});
};
// src/index.ts
var exports_src = {};
__export(exports_src, {
defineMachine: () => defineMachine,
createMachine: () => createMachine,
YayMachine: () => YayMachine,
MDC: () => MDC
});
module.exports = __toCommonJS(exports_src);
// src/MachineDefinitionConfig.ts
var MDC = Symbol("MDC");
// src/defineMachine.ts
var defineMachine = (definitionConfig) => {
if (definitionConfig.states) {
for (const [name, config] of Object.entries(definitionConfig.states)) {
if (config.always) {
for (const transition of Array.isArray(config.always) ? config.always : [config.always]) {
if ("reenter" in transition) {
throw new Error(`Cannot use 'reenter' with immediate transitions in state "${name}"`);
}
}
}
if (config.on) {
for (const [type, tx] of Object.entries(config.on)) {
for (const transition of Array.isArray(tx) ? tx : [tx]) {
if ("reenter" in transition && transition.reenter === false) {
if (transition.to !== name) {
throw new Error(`Cannot use \`reenter: false\` to another state "${transition.to}" for transition in state "${name}" via event "${type}"`);
}
if (transition.data) {
throw new Error(`Cannot use \`reenter: false\` with \`data()\` for transition in state "${name}" via event "${type}"`);
}
}
}
}
}
}
}
return {
newInstance(instanceConfig) {
const enableCopyDataOnTransition = definitionConfig.enableCopyDataOnTransition;
const initialState = instanceConfig?.initialState ?? definitionConfig.initialState;
let currentState = initialState;
const getEffectParams = (event) => {
let disposed = false;
const dispose = () => {
disposed = true;
};
return [
{
state: currentState,
event,
send: (event2) => {
if (!disposed) {
machine.send(event2);
}
}
},
dispose
];
};
let disposeState;
const initState = (event) => {
const { onEnter, onExit } = definitionConfig.states?.[currentState.name] || {};
const [enterParams, disposeEnterParams] = getEffectParams(event);
const disposeEnter = onEnter?.(enterParams);
disposeState = (disposeEvent) => {
disposeEnterParams();
disposeEnter?.();
const [exitParams, disposeExitParams] = getEffectParams(disposeEvent);
onExit?.(exitParams)?.();
disposeExitParams();
};
};
const subscribers = [];
const transitionTo = (nextState, event, onTransition) => {
if (disposeState) {
disposeState(event);
disposeState = undefined;
}
if (onTransition) {
const [transitionParams, disposeTransitionParams] = getEffectParams(event);
onTransition({
...transitionParams,
next: nextState,
...event && { event }
})?.();
disposeTransitionParams();
}
currentState = nextState;
initState(event);
for (const subscriber of subscribers) {
subscriber({ state: currentState, event });
}
};
const applyAlwaysTransitions = () => {
const always = definitionConfig.states?.[currentState.name]?.always;
if (always) {
applyTransitions(undefined, always);
}
};
const applyTransitions = (event, transitions) => {
for (const transition of Array.isArray(transitions) ? transitions : [transitions]) {
if (tryTransition(event, transition)) {
applyAlwaysTransitions();
return true;
}
}
return false;
};
const tryTransition = (event, transition) => {
if ("when" in transition && !transition.when({ state: currentState, event })) {
return false;
}
if (isReenterTransitionFalse(transition)) {
const [transitionParams, disposeTransitionParams] = getEffectParams(event);
if (transition.onTransition) {
transition.onTransition({
...transitionParams,
event,
next: currentState
})?.();
disposeTransitionParams();
}
return true;
}
let nextState;
if (isTransitionData(transition)) {
const { name, ...nextData } = transition.data({
state: currentState,
event
});
nextState = {
name: transition.to ?? currentState.name,
...nextData
};
} else if (enableCopyDataOnTransition) {
const { name, ...nextData } = currentState;
nextState = {
name: transition.to ?? currentState.name,
...nextData
};
} else {
nextState = { name: transition.to ?? currentState.name };
}
transitionTo(nextState, event, transition.onTransition);
return true;
};
let handlingEvent = false;
const queuedEvents = [];
const handleEvent = (event) => {
if (handlingEvent) {
queuedEvents.push(event);
return;
}
handlingEvent = true;
try {
const { states, on } = definitionConfig;
const state = states?.[currentState.name];
if (state) {
const stateOnEvent = state.on?.[event.type];
if (stateOnEvent && applyTransitions(event, stateOnEvent)) {
return;
}
}
const anyStateOnEvent = on?.[event.type];
if (anyStateOnEvent && applyTransitions(event, anyStateOnEvent)) {
return;
}
} finally {
handlingEvent = false;
handleNextQueuedEvent();
}
};
const handleNextQueuedEvent = () => {
const event = queuedEvents.shift();
if (event) {
handleEvent(event);
}
};
let running = false;
let starting = false;
let stopping = false;
let disposeMachine;
const initMachine = () => {
const { onStart, onStop } = definitionConfig;
const [startParams, disposeStartParams] = getEffectParams(undefined);
const disposeStart = onStart?.(startParams);
disposeMachine = (disposeEvent) => {
disposeStartParams();
disposeStart?.();
const [stopParams, disposeStopParams] = getEffectParams(disposeEvent);
const disposeStop = onStop?.(stopParams);
disposeStopParams();
disposeStop?.();
};
};
const machine = {
get state() {
return currentState;
},
send(event) {
if (!running) {
throw new Error("Machine is not running");
}
handleEvent(event);
},
start() {
if (stopping) {
throw new Error("Machine is already stopping");
}
if (running) {
throw new Error("Machine is already running");
}
starting = true;
running = true;
handlingEvent = true;
initMachine();
initState(undefined);
applyAlwaysTransitions();
starting = false;
handlingEvent = false;
handleNextQueuedEvent();
return machine;
},
stop() {
if (!running) {
throw new Error("Machine is not running");
}
if (starting) {
throw new Error("Machine is already starting");
}
stopping = true;
handlingEvent = true;
disposeState?.(undefined);
disposeState = undefined;
disposeMachine?.(undefined);
disposeMachine = undefined;
running = false;
stopping = false;
handlingEvent = false;
queuedEvents.length = 0;
},
subscribe(callback) {
subscribers.push(callback);
callback({ state: currentState, event: undefined });
return () => {
const index = subscribers.indexOf(callback);
if (index !== -1) {
subscribers.splice(index, 1);
}
};
},
[MDC]: definitionConfig
};
return machine;
}
};
};
var isTransitionData = (transition) => ("data" in transition);
var isReenterTransitionFalse = (transition) => ("reenter" in transition) && transition.reenter === false;
// src/createMachine.ts
var createMachine = (definitionConfig) => {
return defineMachine(definitionConfig).newInstance();
};
// src/YayMachine.ts
var YayMachine = {
subtle: {
MDC
}
};