Skip to content
This repository has been archived by the owner on Feb 16, 2022. It is now read-only.

Commit

Permalink
Merge pull request #3 from piotr-oles/dev
Browse files Browse the repository at this point in the history
0.5.2 - Change command definition + Redux external fix
  • Loading branch information
piotr-oles committed Mar 10, 2017
2 parents 29f9052 + fa67dc6 commit 3fd0001
Show file tree
Hide file tree
Showing 20 changed files with 148 additions and 49 deletions.
11 changes: 6 additions & 5 deletions 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<any> = handleCommand<any, ParallelCommand>(
'PARALLEL',
'PARALLEL()',
(command, dispatch) => Promise.all(
command.payload.map(command => dispatch(command).promise || Promise.resolve())
).then(() => undefined)
Expand Down
11 changes: 6 additions & 5 deletions 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<any> = handleCommand<any, SequenceCommand>(
'SEQUENCE',
'SEQUENCE()',
(command, dispatch) => command.payload.reduce(
(promise, command) => promise.then(() => dispatch(command).promise || Promise.resolve()),
Promise.resolve()
Expand Down
2 changes: 1 addition & 1 deletion 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",
Expand Down
5 changes: 0 additions & 5 deletions src/Command.ts

This file was deleted.

4 changes: 4 additions & 0 deletions 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<S> extends Dispatch<S> {
<A extends Action>(action: A): A & { promise?: Promise<void> };
}
3 changes: 3 additions & 0 deletions src/ExecutableStore.ts
Expand Up @@ -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<S> extends Store<S> {
dispatch: ExecutableDispatch<S>;
replaceExecutor(nextExecutor: Executor<S>): void;
Expand Down
16 changes: 12 additions & 4 deletions 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<S> = <C extends Command>(command: C, dispatch: ExecutableDispatch<S>, state: S) => Promise<void> | void;
export type NarrowExecutor<S, C extends Command> = (command: C, dispatch: ExecutableDispatch<S>, state: S) => Promise<void> | 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<S> = <A extends Action>(command: A, dispatch: ExecutableDispatch<S>, state: S) => Promise<void> | void;

/**
* It's executor limited to given action (command) type.
*/
export type NarrowExecutor<S, A extends Action> = (command: A, dispatch: ExecutableDispatch<S>, state: S) => Promise<void> | void;
10 changes: 8 additions & 2 deletions 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<S>(...executors: Executor<S>[]): Executor<S> {
// check executors type in runtime
const invalidExecutorsIndexes: number[] = executors
Expand All @@ -16,7 +22,7 @@ export function combineExecutors<S>(...executors: Executor<S>[]): Executor<S> {
);
}

return function combinedExecutor<C extends Command>(command: C, dispatch: ExecutableDispatch<S>, state: S): Promise<void> {
return function combinedExecutor<A extends Action>(command: A, dispatch: ExecutableDispatch<S>, state: S): Promise<void> {
return Promise.all(
executors
.map(executor => executor(command, dispatch, state))
Expand Down
9 changes: 9 additions & 0 deletions src/createExecutableStore.ts
Expand Up @@ -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<S>(
reducer: Reducer<S>,
executor: Executor<S>,
Expand Down
10 changes: 8 additions & 2 deletions src/createExecutorEnhancer.ts
Expand Up @@ -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<S> = (next: StoreEnhancerStoreCreator<S>) => StoreEnhancerStoreExecutableCreator<S>;
export type StoreEnhancerStoreExecutableCreator<S> = (reducer: Reducer<S>, preloadedState: S) => ExecutableStore<S>;

/**
* Create enhacer for redux store. This enhancer adds commands and executors support.
*
* @param executor Main redux executor
* @returns Store enhancer
*/
export function createExecutorEnhancer<S>(executor: Executor<S>): StoreExecutableEnhancer<S> {
if (typeof executor !== 'function') {
throw new Error('Expected the executor to be a function.');
Expand All @@ -31,7 +37,7 @@ export function createExecutorEnhancer<S>(executor: Executor<S>): StoreExecutabl
}

currentExecutor = nextExecutor;
executableStore.dispatch({ type: EXECUTOR_INIT, command: true });
executableStore.dispatch({ type: EXECUTOR_INIT });
}
};

Expand Down
23 changes: 19 additions & 4 deletions 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<S, C extends Command>(type: C['type'], executor: NarrowExecutor<S, C>): Executor<S> {
return function wideExecutor(command: Command, dispatch: ExecutableDispatch<S>, state: S): Promise<void> | 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<S, A extends Action>(type: string, executor: NarrowExecutor<S, A>): Executor<S> {
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<S>, state: S): Promise<void> | void {
if (command && command.type === type) {
return executor(command as C, dispatch, state);
return executor(command as A, dispatch, state);
}
};
}
1 change: 0 additions & 1 deletion 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';
Expand Down
15 changes: 13 additions & 2 deletions 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
);
}
19 changes: 17 additions & 2 deletions 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<S1, S2>(selector: (state: S1) => S2, executor: Executor<S2>): Executor<S1> {
return function mountedExecutor<C extends Command>(command: C, dispatch: ExecutableDispatch<S1>, state: S1): Promise<void> | 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<A extends Action>(command: A, dispatch: ExecutableDispatch<S1>, state: S1): Promise<void> | void {
return executor(command, dispatch, selector(state));
};
}
4 changes: 2 additions & 2 deletions test/combineExecutors.spec.ts
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
8 changes: 4 additions & 4 deletions test/createExecutorEnhancer.spec.ts
Expand Up @@ -87,23 +87,23 @@ 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;
expect(commandResult.promise.then).to.be.function;

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' });
Expand Down
20 changes: 16 additions & 4 deletions test/handleCommand.spec.ts
Expand Up @@ -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);
});
});
8 changes: 5 additions & 3 deletions test/isCommand.spec.ts
Expand Up @@ -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;
});
});
11 changes: 9 additions & 2 deletions test/mountExecutor.spec.ts
Expand Up @@ -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'});
}
}
Expand All @@ -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);
});
});
7 changes: 6 additions & 1 deletion webpack.config.js
Expand Up @@ -47,7 +47,12 @@ var config = {
},

externals: {
'redux': 'Redux'
'redux': {
root: 'Redux',
commonjs2: 'redux',
commonjs: 'redux',
amd: 'redux'
}
}
};

Expand Down

0 comments on commit 3fd0001

Please sign in to comment.