@zedux/machines
Version:
Simple native state machine implementation for Zedux atoms
147 lines (146 loc) • 6.76 kB
JavaScript
import { injectEffect, injectMemo, injectRef, injectSelf, zeduxTypes, } from '@zedux/atoms';
import { MachineStore } from './MachineStore.js';
/**
* Create a MachineStore. Pass a statesFactory
*
* The first state in the state list returned from your statesFactory will
* become the initial state (`.value`) of the store.
*
* Registers an effect that listens to all store changes and calls the
* configured listeners appropriately.
*
* ```ts
* const store = injectMachineStore(state => [
* state('a')
* .on('next', 'b', localGuard)
* .onEnter(enterListener)
* .onLeave(leaveListener),
* state('b').on('next', 'a')
* ], initialContext, { guard, onTransition })
* ```
*
* Set a universal transition guard via the 3rd `config` object param. This
* guard will be called every time a valid transition is about to occur. It will
* be called with the current `.context` value and should return a boolean.
* Return true to allow the transition, or any falsy value to deny it.
*
* Set a universal `onTransition` listener via the 3rd `config` object param.
* This listener will be called every time the machine transitions to a new
* state (after the state is updated). It will be called with 2 params: The
* current MachineStore and the storeEffect of the action that transitioned the
* store. For example, use `storeEffect.oldState.value` to see what state the
* machine just transitioned from.
*
* @param statesFactory Required. A function. Use the received state factory to
* create a list of states for the machine and specify their transitions,
* guards, and listeners.
* @param initialContext Optional. An object or undefined. Will be set as the
* initial `.context` value of the machine store's state.
* @param config Optional. An object with 2 additional properties: `guard` and
* `onTransition`.
*/
export const injectMachineStore = (...[statesFactory, initialContext, config]) => {
const instance = injectSelf();
const { enterHooks, leaveHooks, store } = injectMemo(() => {
var _a, _b, _c;
const enterHooks = {};
const leaveHooks = {};
const states = {};
const createState = (stateName) => {
const state = {
on: (eventName, nextState, guard) => {
if (!states[stateName]) {
states[stateName] = {};
}
if (!states[nextState]) {
states[nextState] = {};
}
states[stateName][eventName] = {
name: nextState,
guard,
};
return state;
},
onEnter: (callback) => {
if (!enterHooks[stateName]) {
enterHooks[stateName] = [];
}
enterHooks[stateName].push(callback);
return state;
},
onLeave: (callback) => {
if (!leaveHooks[stateName]) {
leaveHooks[stateName] = [];
}
leaveHooks[stateName].push(callback);
return state;
},
stateName,
};
return state;
};
const [initialState] = statesFactory(createState);
const hydration = (config === null || config === void 0 ? void 0 : config.hydrate) && ((_a = instance.ecosystem.hydration) === null || _a === void 0 ? void 0 : _a[instance.id]);
const store = new MachineStore((_b = hydration === null || hydration === void 0 ? void 0 : hydration.value) !== null && _b !== void 0 ? _b : initialState.stateName, states, (_c = hydration === null || hydration === void 0 ? void 0 : hydration.context) !== null && _c !== void 0 ? _c : initialContext, config === null || config === void 0 ? void 0 : config.guard);
return { enterHooks, leaveHooks, store };
}, []);
const subscribeRef = injectRef();
subscribeRef.current = config === null || config === void 0 ? void 0 : config.subscribe;
injectEffect(() => {
const subscription = store.subscribe({
effects: storeEffect => {
const { action, newState, oldState } = storeEffect;
if (newState.value === (oldState === null || oldState === void 0 ? void 0 : oldState.value))
return;
if (oldState && leaveHooks[oldState.value]) {
leaveHooks[oldState.value].forEach(callback => callback(store, storeEffect));
}
if (enterHooks[newState.value]) {
enterHooks[newState.value].forEach(callback => callback(store, storeEffect));
}
if (config === null || config === void 0 ? void 0 : config.onTransition) {
config.onTransition(store, storeEffect);
}
// Nothing to do if the state hasn't changed. Also ignore state updates
// during evaluation or that are caused by `zeduxTypes.ignore` actions
if (!subscribeRef.current ||
newState === oldState ||
instance._isEvaluating ||
(action === null || action === void 0 ? void 0 : action.meta) === zeduxTypes.ignore) {
return;
}
instance._scheduleEvaluation({
newState,
oldState,
operation: 'injectMachineStore',
reasons: [
{
action,
newState,
oldState,
operation: 'dispatch',
sourceType: 'Store',
type: 'state changed',
},
],
sourceType: 'Injector',
type: 'state changed',
}, false);
// run the scheduler synchronously after any store update
if ((action === null || action === void 0 ? void 0 : action.meta) !== zeduxTypes.batch) {
instance.ecosystem._scheduler.flush();
}
},
});
return () => subscription.unsubscribe();
}, [], { synchronous: true });
const currentState = store.getState();
if (enterHooks[currentState.value]) {
enterHooks[currentState.value].forEach(callback => callback(store, {
action: { type: zeduxTypes.prime },
newState: currentState,
store,
}));
}
return store;
};