UNPKG

redux-sigma

Version:

A state machine library for redux and redux-saga.

441 lines (428 loc) 14.8 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var produce = require('immer'); var effects = require('redux-saga/effects'); var reduxSaga = require('redux-saga'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var produce__default = /*#__PURE__*/_interopDefaultLegacy(produce); const startStmActionType = '@@redux-sigma/start-stm'; const stmStartedActionType = '@@redux-sigma/stm-started'; const stopStmActionType = '@@redux-sigma/stop-stm'; const stmStoppedActionType = '@@redux-sigma/stm-stopped'; const storeStmStateActionType = '@@redux-sigma/store-state'; const storeStmContextActionType = '@@redux-sigma/store-context'; const REACTION_POLICY_FIRST = 'REACTION_POLICY_FIRST'; const REACTION_POLICY_LAST = 'REACTION_POLICY_LAST'; const REACTION_POLICY_ALL = 'REACTION_POLICY_ALL'; function isStateTransition(value) { return typeof value === 'string'; } function isGuardedTransition(value) { return 'guard' in value; } function isGuardedTransitionArray(value) { return value instanceof Array; } function isSimpleTransition(value) { return (!isStateTransition(value) && !isGuardedTransition(value) && !isGuardedTransitionArray(value)); } function isReactionSpec(value) { return 'policy' in value; } function isFunction(value) { return typeof value === 'function'; } function isStarted(storage) { return storage.state !== null; } class StateMachine { constructor() { this.runningTasks = []; this.start = (context) => { const initialContext = produce__default['default'](null, () => context); return { type: startStmActionType, payload: { name: this.name, context: initialContext, }, }; }; this.stop = () => { return { type: stopStmActionType, payload: { name: this.name, }, }; }; this.started = (context) => { return { type: stmStartedActionType, payload: { name: this.name, context, }, }; }; this.stopped = () => { return { type: stmStoppedActionType, payload: { name: this.name, }, }; }; this.storeState = (state) => { return { type: storeStmStateActionType, payload: { name: this.name, state, }, }; }; this.storeContext = (context) => { return { type: storeStmContextActionType, payload: { name: this.name, context, }, }; }; this.stateReducer = (state = { state: null, context: undefined }, action) => { var _a; if (((_a = action.payload) === null || _a === void 0 ? void 0 : _a.name) !== this.name) { return state; } switch (action.type) { case stmStartedActionType: return { state: this.initialState, context: action.payload.context, }; case stopStmActionType: return { state: null, context: undefined, }; case storeStmContextActionType: if (!isStarted(state)) { return state; } else { return { state: state.state, context: action.payload.context, }; } case storeStmStateActionType: if (isStarted(state)) { return { state: action.payload.state, context: state.context, }; } else { return state; } default: return state; } }; } *setContext(newContext) { if (isFunction(newContext)) { this._context = produce__default['default'](this._context, newContext); } else { this._context = produce__default['default'](null, () => newContext); } yield effects.putResolve(this.storeContext(this._context)); } get context() { return this._context; } *starterSaga() { const startChannel = (yield effects.actionChannel((action) => action.type == startStmActionType && action.payload.name == this.name, reduxSaga.buffers.sliding(1))); while (true) { const action = (yield effects.take(startChannel)); yield effects.put(this.started(action.payload.context)); yield effects.call([this, this.run], action.payload.context); yield effects.put(this.stopped()); } } *run(context) { this._context = context; this.currentState = this.initialState; const stopChannel = (yield effects.actionChannel((action) => action.type == stopStmActionType && action.payload.name == this.name)); while (true) { const nextState = (yield effects.call([this, this.stateLoop], stopChannel)); if (!nextState) { return; } if (nextState.command) { if (Array.isArray(nextState.command)) { for (const saga of nextState.command) { yield effects.call([this, saga], nextState.event); } } else { yield effects.call([this, nextState.command], nextState.event); } } this.currentState = nextState.nextState; yield effects.put(this.storeState(this.currentState)); } } *stateLoop(stopChannel) { try { const { transitions } = this.spec[this.currentState]; const transitionEvents = transitions ? Object.keys(transitions) : []; this.transitionChannel = (yield effects.actionChannel(transitionEvents)); this.runningTasks.push((yield effects.fork([this, this.startOnEntryActivities]))); this.runningTasks.push((yield effects.fork([this, this.registerToReactions]))); yield effects.call([this, this.startSubMachines]); const { nextState } = (yield effects.race({ nextState: effects.call([this, this.getNextState]), stop: effects.take(stopChannel), })); return nextState; } finally { yield effects.call([this, this.cancelRunningTasks]); yield effects.call([this, this.stopSubMachines]); yield effects.call([this, this.runOnExitActivities]); } } *getNextState() { while (true) { const event = (yield effects.take(this.transitionChannel)); const transitionSpec = this.spec[this.currentState].transitions[event.type]; if (isStateTransition(transitionSpec)) { return { event, nextState: transitionSpec, }; } else if (isSimpleTransition(transitionSpec)) { return { event, nextState: transitionSpec.target, command: transitionSpec.command, }; } else if (isGuardedTransition(transitionSpec)) { if (yield effects.call(transitionSpec.guard, event, this.context)) { return { event, nextState: transitionSpec.target, command: transitionSpec.command, }; } } else { for (const transitionOption of transitionSpec) { if (yield effects.call(transitionOption.guard, event, this.context)) return { event, nextState: transitionOption.target, command: transitionOption.command, }; } } } } *startOnEntryActivities() { const { onEntry } = this.spec[this.currentState]; if (onEntry) { if (Array.isArray(onEntry)) { for (const saga of onEntry) { this.runningTasks.push((yield effects.fork([this, saga]))); } } else { this.runningTasks.push((yield effects.fork([this, onEntry]))); } } } *registerToReactions() { const { reactions } = this.spec[this.currentState]; if (reactions) { const eventTypes = Object.keys(reactions); for (const eventType of eventTypes) { const reaction = reactions[eventType]; const [activity, policy] = isReactionSpec(reaction) ? [reaction.activity, reaction.policy] : [reaction, REACTION_POLICY_ALL]; let task; switch (policy) { case REACTION_POLICY_LAST: { task = (yield effects.fork([this, this.takeLast], eventType, activity)); break; } case REACTION_POLICY_FIRST: { task = (yield effects.fork([this, this.takeFirst], eventType, activity)); break; } case REACTION_POLICY_ALL: { task = (yield effects.fork([this, this.takeAll], eventType, activity)); break; } } this.runningTasks.push(task); } } } *takeFirst(eventType, activity) { while (true) { const event = (yield effects.take(eventType)); yield effects.call([this, activity], event); } } *takeLast(eventType, activity) { const channel = (yield effects.actionChannel(eventType)); let task = null; while (true) { const event = (yield effects.take(channel)); if (task !== null) { yield effects.cancel(task); } task = (yield effects.fork([this, activity], event)); } } *takeAll(eventType, activity) { const channel = (yield effects.actionChannel(eventType)); while (true) { const event = (yield effects.take(channel)); yield effects.call([this, activity], event); } } *cancelRunningTasks() { yield effects.cancel(this.runningTasks); this.runningTasks = []; } *startSubMachines() { let { subMachines } = this.spec[this.currentState]; if (!subMachines) return; if (!Array.isArray(subMachines)) { subMachines = [subMachines]; } for (const subMachine of subMachines) { if ('stm' in subMachine) { const ctx = yield effects.call([this, subMachine.contextBuilder]); yield effects.put(subMachine.stm.start(ctx)); } else { yield effects.put(subMachine.start({})); } } } *stopSubMachines() { let { subMachines } = this.spec[this.currentState]; if (!subMachines) return; if (!Array.isArray(subMachines)) { subMachines = [subMachines]; } for (const subMachine of subMachines) { if ('stm' in subMachine) { yield effects.put(subMachine.stm.stop()); } else { yield effects.put(subMachine.stop()); } } } *runOnExitActivities() { const { onExit } = this.spec[this.currentState]; if (onExit) { if (Array.isArray(onExit)) { for (const saga of onExit) { yield effects.call([this, saga]); } } else { yield effects.call([this, onExit]); } } } } function not(f) { return (...args) => !f(...args); } function and(...fs) { return function (...args) { return fs.every(f => f(...args)); }; } function or(...fs) { return function (...args) { return fs.some(f => f(...args)); }; } function all(activity) { return { activity, policy: REACTION_POLICY_ALL, }; } function last(activity) { return { activity, policy: REACTION_POLICY_LAST, }; } function first(activity) { return { activity, policy: REACTION_POLICY_FIRST, }; } function bindStm(stm, contextBuilder) { return { stm, contextBuilder, }; } function* reportUnknownStateMachine(action) { yield effects.call(console.warn, `Unkwnown state machine ${action.payload.name}`); } function* stateMachineStarterSaga(...stms) { const duplicateStm = stms .map(stm => stm.name) .find((name, idx, arr) => arr.lastIndexOf(name) !== idx); if (duplicateStm) { throw new Error(`Duplicate STM detected with name ${duplicateStm}`); } for (const stm of stms) { yield effects.fork([stm, stm.starterSaga]); } if (process.env.NODE_ENV !== 'production') { const stmNames = stms.map(stm => stm.name); yield effects.takeEvery((action) => action.payload && action.payload.name && [ startStmActionType, stopStmActionType, storeStmContextActionType, storeStmStateActionType, ].includes(action.type) && !stmNames.includes(action.payload.name), reportUnknownStateMachine); } } exports.StateMachine = StateMachine; exports.all = all; exports.and = and; exports.bindStm = bindStm; exports.first = first; exports.last = last; exports.not = not; exports.or = or; exports.stateMachineStarterSaga = stateMachineStarterSaga;