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 #1 from piotr-oles/dev
Browse files Browse the repository at this point in the history
Initial implementation
  • Loading branch information
piotr-oles committed Feb 10, 2017
2 parents 206d90a + 9e93af0 commit 77300b0
Show file tree
Hide file tree
Showing 11 changed files with 153 additions and 2 deletions.
9 changes: 9 additions & 0 deletions src/Command.ts
@@ -0,0 +1,9 @@
import { Action } from 'redux';

export interface Command extends Action {
command: true;
}

export function isCommand(action: any): boolean {
return action && action.type && action.command;
}
5 changes: 5 additions & 0 deletions src/ExecutableDispatch.ts
@@ -0,0 +1,5 @@
import { Dispatch, Action } from 'redux';

export interface ExecutableDispatch<S> extends Dispatch<S> {
<A extends Action>(action: A): A & { promise?: Promise<void> };
}
8 changes: 8 additions & 0 deletions src/ExecutableStore.ts
@@ -0,0 +1,8 @@
import { Store } from 'redux';
import { Executor } from './Executor';
import { ExecutableDispatch } from './ExecutableDispatch';

export interface ExecutableStore<S> extends Store<S> {
dispatch: ExecutableDispatch<S>;
replaceExecutor(nextExecutor: Executor<S>): void;
}
4 changes: 4 additions & 0 deletions src/Executor.ts
@@ -0,0 +1,4 @@
import { Command } from "./Command";
import { ExecutableDispatch } from "./ExecutableDispatch";

export type Executor<S> = <C extends Command>(state: S, command: C, dispatch: ExecutableDispatch<S>) => Promise<void> | void;
26 changes: 26 additions & 0 deletions src/combineExecutors.ts
@@ -0,0 +1,26 @@
import { Command } from './Command';
import { Executor } from './Executor';
import { ExecutableDispatch } from './ExecutableDispatch';

export function combineExecutors<S>(...executors: Executor<S>[]): Executor<S> {
// check executors type in runtime
const invalidExecutorsIndexes: number[] = executors
.map((executor, index) => executor instanceof Function ? -1 : index)
.filter(index => index !== -1);

if (invalidExecutorsIndexes.length) {
throw new Error(
`Invalid arguments: ${invalidExecutorsIndexes.join(', ')} in combineExecutors call.\n` +
`Executors should be a 'function' type, ` +
`'${invalidExecutorsIndexes.map(index => typeof executors[index]).join(`', '`)}' types passed.`
);
}

return function combinedExecutor<C extends Command>(state: S, command: C, dispatch: ExecutableDispatch<S>): Promise<void> {
return Promise.all(
executors
.map(executor => executor(state, command, dispatch))
.filter(promise => !!promise)
) as Promise<any>;
};
}
19 changes: 19 additions & 0 deletions src/createExecutableStore.ts
@@ -0,0 +1,19 @@
import { StoreEnhancer, Reducer, compose, createStore } from 'redux';
import { Executor } from './Executor';
import { ExecutableStore } from './ExecutableStore';
import { createExecutorEnhancer } from './createExecutorEnhancer';

export function createExecutableStore<S>(
reducer: Reducer<S>,
executor: Executor<S>,
preloadedState?: S,
enhancer?: StoreEnhancer<S>
): ExecutableStore<S> {
if (enhancer) {
enhancer = compose(createExecutorEnhancer(executor), enhancer);
} else {
enhancer = createExecutorEnhancer(executor);
}

return createStore(reducer, preloadedState, enhancer) as ExecutableStore<S>;
}
51 changes: 51 additions & 0 deletions src/createExecutorEnhancer.ts
@@ -0,0 +1,51 @@
import { StoreEnhancerStoreCreator, Store, Reducer, Action } from 'redux';
import { Executor } from './Executor';
import { ExecutableStore } from './ExecutableStore';
import { isCommand } from './Command';

export type StoreExecutableEnhancer<S> = (next: StoreEnhancerStoreCreator<S>) => StoreEnhancerStoreExecutableCreator<S>;
export type StoreEnhancerStoreExecutableCreator<S> = (reducer: Reducer<S>, preloadedState: S) => ExecutableStore<S>;

export function createExecutorEnhancer<S>(executor: Executor<S>): StoreExecutableEnhancer<S> {
if (typeof executor !== 'function') {
throw new Error('Expected the executor to be a function.');
}

return function executorEnhancer(next: StoreEnhancerStoreCreator<S>): StoreEnhancerStoreExecutableCreator<S> {
return function detectableStoreCreator(reducer: Reducer<S>, preloadedState?: S): ExecutableStore<S> {
// first create basic store
const store: Store<S> = next(reducer, preloadedState);

// then set initial values in this scope
let prevState: S | undefined = preloadedState;
let currentExecutor: Executor<S> = executor;

// store executable adds `replaceExecutor` method to it's interface
const executableStore: ExecutableStore<S> = {
...store as any, // some bug in typescript object spread operator?
replaceExecutor: function replaceExecutor(nextExecutor: Executor<S>): void {
if (typeof nextExecutor !== 'function') {
throw new Error('Expected the nextExecutor to be a function.');
}

currentExecutor = nextExecutor;
}
};

// store executable overrides `dispatch` method to implement ExecutableDispatch interface
executableStore.dispatch = <A extends Action>(action: A): A & { promise?: Promise<void> } => {
if (isCommand(action)) {
// run executor instead of default dispatch
let promise: Promise<void> | void = currentExecutor(executableStore.getState(), action as any, executableStore.dispatch);

// return also promise to allow to synchronize dispatches
return Object.assign({}, action as any, { promise: promise || Promise.resolve() });
}

return store.dispatch(action);
};

return executableStore;
};
};
}
9 changes: 8 additions & 1 deletion src/index.ts
@@ -1,4 +1,11 @@
// typings
export { Command } from './Command';
export { ExecutableDispatch } from './ExecutableDispatch';
export { ExecutableStore } from './ExecutableStore';
export { Executor } from './Executor';

// implementation
console.log('hello world');
export { combineExecutors } from './combineExecutors';
export { createExecutableStore } from './createExecutableStore';
export { createExecutorEnhancer } from './createExecutorEnhancer';
export { mountExecutor } from './mountExecutor';
9 changes: 9 additions & 0 deletions src/mountExecutor.ts
@@ -0,0 +1,9 @@
import { Executor } from './Executor';
import { Command } from './Command';
import { ExecutableDispatch } from './ExecutableDispatch';

export function mountExecutor<S1, S2>(selector: (state: S1) => S2, executor: Executor<S2>): Executor<S1> {
return function mountedExecutor<C extends Command>(state: S1, command: C, dispatch: ExecutableDispatch<S1>): Promise<void> | void {
return executor(selector(state), command, dispatch);
};
}
9 changes: 9 additions & 0 deletions test/trivial.spec.ts
@@ -0,0 +1,9 @@
// it's test just to not fail build because there is no tests yet

import { assert } from 'chai';

describe('trivial', () => {
it('should check if true equals true', () => {
assert.isTrue(true);
});
});
6 changes: 5 additions & 1 deletion tsconfig.json
Expand Up @@ -10,7 +10,11 @@
"moduleResolution": "node",
"baseUrl": "./src",
"declaration": true,
"declarationDir": "./"
"declarationDir": "./",
"lib": [
"dom",
"es6"
]
},
"include": [
"./src/**/*"
Expand Down

0 comments on commit 77300b0

Please sign in to comment.