diff --git a/example/parallelExecution.ts b/example/parallelExecution.ts index 1010ee8..73dd8c6 100644 --- a/example/parallelExecution.ts +++ b/example/parallelExecution.ts @@ -1,18 +1,19 @@ -import { Command, Executor, handleCommand } from 'redux-executor'; +import { Action } from 'redux'; +import { Executor, handleCommand } from 'redux-executor'; /** * ParallelCommand is a command that will execute all commands from payload in parallel. * As a payload of ParallelCommand is a list of commands, you can put there another * ParallelCommand or SequenceCommand and build nested tree of execution. */ -interface ParallelCommand extends Command { - type: 'PARALLEL'; - payload: Command[]; +interface ParallelCommand extends Action { + type: 'PARALLEL()'; + payload: Action[]; } export const parallelCommandExecutor: Executor = handleCommand( - 'PARALLEL', + 'PARALLEL()', (command, dispatch) => Promise.all( command.payload.map(command => dispatch(command).promise || Promise.resolve()) ).then(() => undefined) diff --git a/example/sequenceExecution.ts b/example/sequenceExecution.ts index f03a1b7..32a05a6 100644 --- a/example/sequenceExecution.ts +++ b/example/sequenceExecution.ts @@ -1,18 +1,19 @@ -import { Command, Executor, handleCommand } from 'redux-executor'; +import { Action } from 'redux'; +import { Executor, handleCommand } from 'redux-executor'; /** * SequenceCommand is a command that will execute all commands from payload in given order. * As a payload of SequenceCommand is a list of commands, you can put there another * SequenceCommand or ParallelCommand and build nested tree of execution. */ -interface SequenceCommand extends Command { - type: 'SEQUENCE'; - payload: Command[]; +interface SequenceCommand extends Action { + type: 'SEQUENCE()'; + payload: Action[]; } export const sequenceCommandExecutor: Executor = handleCommand( - 'SEQUENCE', + 'SEQUENCE()', (command, dispatch) => command.payload.reduce( (promise, command) => promise.then(() => dispatch(command).promise || Promise.resolve()), Promise.resolve() diff --git a/package.json b/package.json index 9e412d6..94f84f9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "redux-executor", - "version": "0.5.1", + "version": "0.5.2", "description": "Redux enhancer for handling side effects.", "main": "lib/index.js", "types": "lib/index.d.ts", diff --git a/src/Command.ts b/src/Command.ts deleted file mode 100644 index 05054cb..0000000 --- a/src/Command.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Action } from 'redux'; - -export interface Command extends Action { - command: true; -} diff --git a/src/ExecutableDispatch.ts b/src/ExecutableDispatch.ts index 20c2333..0e560d1 100644 --- a/src/ExecutableDispatch.ts +++ b/src/ExecutableDispatch.ts @@ -1,5 +1,9 @@ import { Dispatch, Action } from 'redux'; +/** + * Dispatch method that adds promise field for commands dispatches results. + * It allows executors to synchronize dispatches. + */ export interface ExecutableDispatch extends Dispatch { (action: A): A & { promise?: Promise }; } diff --git a/src/ExecutableStore.ts b/src/ExecutableStore.ts index 0e3229f..912fab9 100644 --- a/src/ExecutableStore.ts +++ b/src/ExecutableStore.ts @@ -2,6 +2,9 @@ import { Store } from 'redux'; import { Executor } from './Executor'; import { ExecutableDispatch } from './ExecutableDispatch'; +/** + * Store that can handle commands and executors. + */ export interface ExecutableStore extends Store { dispatch: ExecutableDispatch; replaceExecutor(nextExecutor: Executor): void; diff --git a/src/Executor.ts b/src/Executor.ts index 0ceb5e1..401149c 100644 --- a/src/Executor.ts +++ b/src/Executor.ts @@ -1,5 +1,13 @@ -import { Command } from "./Command"; -import { ExecutableDispatch } from "./ExecutableDispatch"; +import { Action } from 'redux'; +import { ExecutableDispatch } from './ExecutableDispatch'; -export type Executor = (command: C, dispatch: ExecutableDispatch, state: S) => Promise | void; -export type NarrowExecutor = (command: C, dispatch: ExecutableDispatch, state: S) => Promise | void; +/** + * Executor is an simple function that executes some side effects on given command. + * They can return promise if side effect is asynchronous. + */ +export type Executor = (command: A, dispatch: ExecutableDispatch, state: S) => Promise | void; + +/** + * It's executor limited to given action (command) type. + */ +export type NarrowExecutor = (command: A, dispatch: ExecutableDispatch, state: S) => Promise | void; diff --git a/src/combineExecutors.ts b/src/combineExecutors.ts index 48ac5fa..875d55e 100644 --- a/src/combineExecutors.ts +++ b/src/combineExecutors.ts @@ -1,7 +1,13 @@ -import { Command } from './Command'; +import { Action } from 'redux'; import { Executor } from './Executor'; import { ExecutableDispatch } from './ExecutableDispatch'; +/** + * Combine executors to get one that will call wrapped. + * + * @param executors Executors to combine + * @returns Executor that wraps all given executors + */ export function combineExecutors(...executors: Executor[]): Executor { // check executors type in runtime const invalidExecutorsIndexes: number[] = executors @@ -16,7 +22,7 @@ export function combineExecutors(...executors: Executor[]): Executor { ); } - return function combinedExecutor(command: C, dispatch: ExecutableDispatch, state: S): Promise { + return function combinedExecutor(command: A, dispatch: ExecutableDispatch, state: S): Promise { return Promise.all( executors .map(executor => executor(command, dispatch, state)) diff --git a/src/createExecutableStore.ts b/src/createExecutableStore.ts index b45a901..8a7d841 100644 --- a/src/createExecutableStore.ts +++ b/src/createExecutableStore.ts @@ -3,6 +3,15 @@ import { Executor } from './Executor'; import { ExecutableStore } from './ExecutableStore'; import { createExecutorEnhancer } from './createExecutorEnhancer'; +/** + * Create store that will be able to handle executors and commands. + * + * @param reducer Main redux reducer + * @param executor Main redux executor + * @param preloadedState State that should be initialized + * @param enhancer Additional enhancer + * @returns Executable Store that will handle commands and executors + */ export function createExecutableStore( reducer: Reducer, executor: Executor, diff --git a/src/createExecutorEnhancer.ts b/src/createExecutorEnhancer.ts index fd758e4..b83b689 100644 --- a/src/createExecutorEnhancer.ts +++ b/src/createExecutorEnhancer.ts @@ -3,11 +3,17 @@ import { Executor } from './Executor'; import { ExecutableStore } from './ExecutableStore'; import { isCommand } from './isCommand'; -export const EXECUTOR_INIT: '@@executor/INIT' = '@@executor/INIT'; +export const EXECUTOR_INIT: string = '@@executor/INIT()'; export type StoreExecutableEnhancer = (next: StoreEnhancerStoreCreator) => StoreEnhancerStoreExecutableCreator; export type StoreEnhancerStoreExecutableCreator = (reducer: Reducer, preloadedState: S) => ExecutableStore; +/** + * Create enhacer for redux store. This enhancer adds commands and executors support. + * + * @param executor Main redux executor + * @returns Store enhancer + */ export function createExecutorEnhancer(executor: Executor): StoreExecutableEnhancer { if (typeof executor !== 'function') { throw new Error('Expected the executor to be a function.'); @@ -31,7 +37,7 @@ export function createExecutorEnhancer(executor: Executor): StoreExecutabl } currentExecutor = nextExecutor; - executableStore.dispatch({ type: EXECUTOR_INIT, command: true }); + executableStore.dispatch({ type: EXECUTOR_INIT }); } }; diff --git a/src/handleCommand.ts b/src/handleCommand.ts index c82dc5f..214fdad 100644 --- a/src/handleCommand.ts +++ b/src/handleCommand.ts @@ -1,12 +1,27 @@ +import { Action } from 'redux'; import { Executor, NarrowExecutor } from './Executor'; -import { Command } from './Command'; import { ExecutableDispatch } from './ExecutableDispatch'; -export function handleCommand(type: C['type'], executor: NarrowExecutor): Executor { - return function wideExecutor(command: Command, dispatch: ExecutableDispatch, state: S): Promise | void { +/** + * Wraps executor to handle only one type of command. + * + * @param type Type which our target commands should have. + * @param executor Wrapped executor + * @returns Executor that runs wrapped executor only for commands with given type. + */ +export function handleCommand(type: string, executor: NarrowExecutor): Executor { + if (!type || type.length < 3 || ')' !== type[type.length - 1] || '(' !== type[type.length - 2]) { + throw new Error(`Expected type to be valid command type with '()' ending. Given '${type}' type. Maybe typo?`); + } + + if (typeof executor !== 'function') { + throw new Error('Expected the executor to be a function.'); + } + + return function wideExecutor(command: Action, dispatch: ExecutableDispatch, state: S): Promise | void { if (command && command.type === type) { - return executor(command as C, dispatch, state); + return executor(command as A, dispatch, state); } }; } diff --git a/src/index.ts b/src/index.ts index f089cc8..78dfaec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,4 @@ // typings -export { Command } from './Command'; export { ExecutableDispatch } from './ExecutableDispatch'; export { ExecutableStore } from './ExecutableStore'; export { Executor, NarrowExecutor } from './Executor'; diff --git a/src/isCommand.ts b/src/isCommand.ts index 5d98568..e14add2 100644 --- a/src/isCommand.ts +++ b/src/isCommand.ts @@ -1,4 +1,15 @@ -export function isCommand(action: any): boolean { - return !!(action && action.type && true === action.command); +/** + * Checks if given object is command (action with type that ends with () substring). + * + * @param object Object to check + */ +export function isCommand(object: any): boolean { + return !!( + object && + object.type && + object.type.length >= 3 && + ')' === object.type[object.type.length - 1] && // last char is ) + '(' === object.type[object.type.length - 2] // and before it there is ( char + ); } diff --git a/src/mountExecutor.ts b/src/mountExecutor.ts index 173f43a..b8f2f3f 100644 --- a/src/mountExecutor.ts +++ b/src/mountExecutor.ts @@ -1,9 +1,24 @@ +import { Action } from 'redux'; import { Executor } from './Executor'; -import { Command } from './Command'; import { ExecutableDispatch } from './ExecutableDispatch'; +/** + * Mount executor to operate on some substate. + * + * @param selector Selector to map state to substate + * @param executor Executor that runs on substate + * @returns Executor that runs on state + */ export function mountExecutor(selector: (state: S1) => S2, executor: Executor): Executor { - return function mountedExecutor(command: C, dispatch: ExecutableDispatch, state: S1): Promise | void { + if (typeof selector !== 'function') { + throw new Error('Expected the selector to be a function.'); + } + + if (typeof executor !== 'function') { + throw new Error('Expected the executor to be a function.'); + } + + return function mountedExecutor(command: A, dispatch: ExecutableDispatch, state: S1): Promise | void { return executor(command, dispatch, selector(state)); }; } diff --git a/test/combineExecutors.spec.ts b/test/combineExecutors.spec.ts index 4e67f4a..36ef210 100644 --- a/test/combineExecutors.spec.ts +++ b/test/combineExecutors.spec.ts @@ -26,7 +26,7 @@ describe('combineExecutors', () => { const executorAB = combineExecutors(executorA, executorB); expect(executorAB).to.be.function; - let promise = executorAB({ type: 'FOO', command: true }, () => {}, {}); + let promise = executorAB({ type: 'FOO()' }, () => {}, {}); let thenSpy = chai.spy(); let catchSpy = chai.spy(); @@ -67,7 +67,7 @@ describe('combineExecutors', () => { const executorAB = combineExecutors(executorA, executorB); expect(executorAB).to.be.function; - let promise = executorAB({ type: 'FOO', command: true }, () => {}, {}); + let promise = executorAB({ type: 'FOO()' }, () => {}, {}); let thenSpy = chai.spy(); let catchSpy = chai.spy(); diff --git a/test/createExecutorEnhancer.spec.ts b/test/createExecutorEnhancer.spec.ts index 4ac9c95..ab8dec8 100644 --- a/test/createExecutorEnhancer.spec.ts +++ b/test/createExecutorEnhancer.spec.ts @@ -87,7 +87,7 @@ describe('createExecutorEnhancer', () => { expect(dispatchSpy).to.not.have.been.called; - const commandResult = executableStore.dispatch({ type: 'DETECTOR_COMMAND', command: true }); + const commandResult = executableStore.dispatch({ type: 'DETECTOR_COMMAND()' }); expect(dispatchSpy).to.not.have.been.called; expect(commandResult).to.exist; expect(commandResult.promise).to.exist; @@ -95,15 +95,15 @@ describe('createExecutorEnhancer', () => { executableStore.replaceExecutor(nextExecutorSpy); expect(dispatchSpy).to.not.have.been.called; - expect(nextExecutorSpy).to.have.been.called.with({ type: EXECUTOR_INIT, command: true }, executableStore.dispatch, {}); + expect(nextExecutorSpy).to.have.been.called.with({ type: EXECUTOR_INIT }, executableStore.dispatch, {}); - const nextCommandResult = executableStore.dispatch({ type: 'NEXT_DETECTOR_COMMAND', command: true }); + const nextCommandResult = executableStore.dispatch({ type: 'NEXT_DETECTOR_COMMAND()' }); expect(dispatchSpy).to.not.have.been.called; expect(nextCommandResult).to.exist; expect(nextCommandResult.promise).to.exist; expect(nextCommandResult.promise.then).to.be.function; - expect(nextExecutorSpy).to.have.been.called.with({ type: 'NEXT_DETECTOR_COMMAND', command: true }, executableStore.dispatch, {}); + expect(nextExecutorSpy).to.have.been.called.with({ type: 'NEXT_DETECTOR_COMMAND()' }, executableStore.dispatch, {}); expect(dispatchSpy).to.not.have.been.called; executableStore.dispatch({ type: 'NON_COMMAND' }); diff --git a/test/handleCommand.spec.ts b/test/handleCommand.spec.ts index c1e8722..4228049 100644 --- a/test/handleCommand.spec.ts +++ b/test/handleCommand.spec.ts @@ -16,19 +16,31 @@ describe('combineExecutors', () => { const dispatchSpy = chai.spy(); const dumbState = {}; - const targetedExecutor = handleCommand('COMMAND_TYPE', executorSpy); + const targetedExecutor = handleCommand('COMMAND_TYPE()', executorSpy); expect(targetedExecutor).to.be.function; expect(executorSpy).to.not.have.been.called; // expect that executor will bypass this command - targetedExecutor({ type: 'ANOTHER_COMMAND_TYPE', command: true }, dispatchSpy, dumbState); + targetedExecutor({ type: 'ANOTHER_COMMAND_TYPE()' }, dispatchSpy, dumbState); + expect(executorSpy).to.not.have.been.called; + expect(dispatchSpy).to.not.have.been.called; + + // expect that executor will bypass similar non command + targetedExecutor({ type: 'COMMAND_TYPE' }, dispatchSpy, dumbState); expect(executorSpy).to.not.have.been.called; expect(dispatchSpy).to.not.have.been.called; // expect that executor will call wrapped executor - targetedExecutor({ type: 'COMMAND_TYPE', command: true }, dispatchSpy, dumbState); - expect(executorSpy).to.have.been.called.with({ type: 'COMMAND_TYPE', command: true }, dispatchSpy, dumbState); + targetedExecutor({ type: 'COMMAND_TYPE()' }, dispatchSpy, dumbState); + expect(executorSpy).to.have.been.called.with({ type: 'COMMAND_TYPE()' }, dispatchSpy, dumbState); expect(dispatchSpy).to.have.been.called; }); + + it('should throw an exception for call with invalid argument', () => { + expect(() => { (handleCommand as any)('TEST()', undefined); }).to.throw(Error); + expect(() => { (handleCommand as any)('TEST()', 123); }).to.throw(Error); + expect(() => { (handleCommand as any)('TEST', () => {}); }).to.throw(Error); + expect(() => { (handleCommand as any)('TEST', undefined); }).to.throw(Error); + }); }); diff --git a/test/isCommand.spec.ts b/test/isCommand.spec.ts index e8bef0f..9d3b85b 100644 --- a/test/isCommand.spec.ts +++ b/test/isCommand.spec.ts @@ -17,9 +17,11 @@ describe('combineExecutors', () => { expect(isCommand(0)).to.be.false; expect(isCommand({})).to.be.false; expect(isCommand({ type: 'SOME_TYPE' })).to.be.false; - expect(isCommand({ type: 'SOME_TYPE', payload: { command: true } })).to.be.false; - expect(isCommand({ type: 'SOME_TYPE', meta: { command: true } })).to.be.false; + expect(isCommand({ type: 'SOME_TYPE( )' })).to.be.false; + expect(isCommand({ type: 'SOME_TYPE)' })).to.be.false; + expect(isCommand({ type: 'SOME_TYPE(' })).to.be.false; + expect(isCommand({ type: 'SOME_TYP(E)' })).to.be.false; - expect(isCommand({ type: 'SOME_TYPE', command: true })).to.be.true; + expect(isCommand({ type: 'SOME_TYPE()' })).to.be.true; }); }); diff --git a/test/mountExecutor.spec.ts b/test/mountExecutor.spec.ts index b49ede1..8d10503 100644 --- a/test/mountExecutor.spec.ts +++ b/test/mountExecutor.spec.ts @@ -23,7 +23,7 @@ describe('mountExecutor', () => { function dumbDispatch(action) { } function executor(command, dispatch, state) { - if (state && state.value === 1 && command && command.type === 'COMMAND_THROUGH_MOUNT') { + if (state && state.value === 1 && command && command.type === 'COMMAND_THROUGH_MOUNT()') { dispatch({type: 'SELECTORS_WORKED'}); } } @@ -37,11 +37,18 @@ describe('mountExecutor', () => { expect(mountedExecutor).to.be.function; mountedExecutor( - { type: 'COMMAND_THROUGH_MOUNT', command: true }, + { type: 'COMMAND_THROUGH_MOUNT()' }, dumbDispatchSpy, state ); expect(dumbDispatchSpy).to.have.been.called.once.with({type: 'SELECTORS_WORKED'}); }); + + it('should throw an exception for call with invalid argument', () => { + expect(() => { (mountExecutor as any)(() => {}, undefined); }).to.throw(Error); + expect(() => { (mountExecutor as any)(undefined, () => {}); }).to.throw(Error); + expect(() => { (mountExecutor as any)({}, () => {}); }).to.throw(Error); + expect(() => { (mountExecutor as any)('test', 'test'); }).to.throw(Error); + }); }); diff --git a/webpack.config.js b/webpack.config.js index e9f44da..10c2600 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -47,7 +47,12 @@ var config = { }, externals: { - 'redux': 'Redux' + 'redux': { + root: 'Redux', + commonjs2: 'redux', + commonjs: 'redux', + amd: 'redux' + } } };