redux-sigma
Version:
A state machine library for redux and redux-saga.
441 lines (428 loc) • 14.8 kB
JavaScript
'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;