Skip to content

Commit

Permalink
feat(createStore): Added easy action tunneling
Browse files Browse the repository at this point in the history
  • Loading branch information
isierra committed Oct 19, 2016
1 parent dfd061c commit 43e12f4
Show file tree
Hide file tree
Showing 2 changed files with 166 additions and 4 deletions.
101 changes: 101 additions & 0 deletions src/createStore.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/// <reference path="../typefix/jest.d.ts" />
"use strict";

import "jest";
Expand All @@ -6,6 +7,7 @@ require("babel-polyfill");
import { Observable } from "rxjs/Observable";
import "rxjs/add/observable/of";
import "rxjs/add/observable/empty";
import "rxjs/add/operator/delay";
import "rxjs/add/operator/first";
import "rxjs/add/operator/take";
import "rxjs/add/operator/timeout";
Expand Down Expand Up @@ -147,5 +149,104 @@ describe("createStore", () => {
it("it should call the given effects",
() => expect(effects).toBeCalledWith(store));
}); // describe When an action is dispatched in the store

describe("When a store is created with tunnel for all actions with no mapping", () => {
const reducer = jest.fn();
const state = { title: "hello" };
const dispatch = jest.fn();
const store = createStore(reducer, state, {
tunnel: {
actions: "all",
dispatch,
},
});
it("it should call the given tunnel dispatch",
() => {
store.dispatch({ type: "TEST1" });
const promise = Observable.of(1).delay(40)
.toPromise() as PromiseLike<any>;
return promise.then(() => {
expect(dispatch).toHaveBeenCalledTimes(1);
expect(dispatch).toBeCalledWith({ type: "TEST1" });
});
});
}); // describe When an action is dispatched in the store

describe("When a store is created with tunnel for all actions with mapping", () => {
const reducer = jest.fn();
const state = { title: "hello" };
const dispatch = jest.fn();
const store = createStore(reducer, state, {
tunnel: {
actions: a => ({ type: "WRAPPER", payload: a }),
dispatch,
},
});
it("it should call the given tunnel dispatch",
() => {
store.dispatch({ type: "TEST1" });
const promise = Observable.of(1).delay(40)
.toPromise() as PromiseLike<any>;
return promise.then(() => {
expect(dispatch).toHaveBeenCalledTimes(1);
expect(dispatch).toBeCalledWith({ type: "WRAPPER", payload: { type: "TEST1" } });
});
});
}); // describe When an action is dispatched in the store

describe("When a store is created with tunnel for a list of action names", () => {
const reducer = jest.fn();
const state = { title: "hello" };
const dispatch = jest.fn();
const store = createStore(reducer, state, {
tunnel: {
actions: ["TEST1", "TEST2"],
dispatch,
},
});
it("it should call the given tunnel dispatch",
() => {
store.dispatch({ type: "TEST1" });
store.dispatch({ type: "TEST2" });
store.dispatch({ type: "TEST3" });
const promise = Observable.of(1).delay(40)
.toPromise() as PromiseLike<any>;
return promise.then(() => {
expect(dispatch).toHaveBeenCalledTimes(2);
expect(dispatch).toBeCalledWith({ type: "TEST1" });
expect(dispatch).toBeCalledWith({ type: "TEST2" });
expect(dispatch).not.toBeCalledWith({ type: "TEST3" });
});
});
}); // describe When an action is dispatched in the store

describe("When a store is created with tunnel for some action mappings", () => {
const reducer = jest.fn();
const state = { title: "hello" };
const dispatch = jest.fn();
const store = createStore(reducer, state, {
tunnel: {
actions: {
["TEST1"]: a => ({ type: "WRAPPER1", payload: a }),
["TEST2"]: a => ({ type: "WRAPPER2", payload: a }),
},
dispatch,
},
});
it("it should call the given tunnel dispatch",
() => {
store.dispatch({ type: "TEST1" });
store.dispatch({ type: "TEST2" });
store.dispatch({ type: "TEST3" });
const promise = Observable.of(1).delay(40)
.toPromise() as PromiseLike<any>;
return promise.then(() => {
expect(dispatch).toHaveBeenCalledTimes(2);
expect(dispatch).toBeCalledWith({ type: "WRAPPER1", payload: { type: "TEST1" } });
expect(dispatch).toBeCalledWith({ type: "WRAPPER2", payload: { type: "TEST2" } });
expect(dispatch).not.toBeCalledWith({ type: "WRAPPER3", payload: { type: "TEST3" } });
});
});
}); // describe When an action is dispatched in the store
}); // describe Given a simple store
}); // describe createStore
69 changes: 65 additions & 4 deletions src/createStore.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
import { Observable } from "rxjs/Observable";
import "rxjs/add/operator/do";
import "rxjs/add/operator/first";
import "rxjs/add/operator/filter";
import "rxjs/add/operator/map";
import "rxjs/add/operator/publishReplay";
import "rxjs/add/operator/scan";
import "rxjs/add/operator/startWith";
import "rxjs/add/operator/subscribeOn";
import "rxjs/add/operator/switchMap";
import "rxjs/add/operator/takeUntil";
import { Subject } from "rxjs/Subject";
import { BehaviorSubject } from "rxjs/BehaviorSubject";
import { queue } from "rxjs/scheduler/queue";
import {
Store, Action, Reducer, StateUpdate, StoreActionsMap,
EffectsDisposer,
Dispatcher,
} from "./interfaces";
import "object-assign";
import objectAssign = require("object-assign");

export const STORE_ACTIONS = {
INIT: "RxStore@STORE@INIT",
FINISH: "RxStore@STORE@FINISH",
};

const scheduler = queue;

export const createStoreExtensions =
Expand All @@ -39,9 +47,19 @@ export const createStoreExtensions =
return result;
};

export type ActionMap = (a: Action) => Action;
export type ActionMapping = {
[name: string]: boolean | ActionMap;
};
export type ActionTunnel = {
dispatch: Dispatcher;
actions: "all" | string[] | ActionMap | ActionMapping;
};

export interface CreateStoreOptions<TState, TStore extends Store<TState>> {
extendWith?: (store: Store<TState>) => Object;
effects?: (store: TStore) => EffectsDisposer;
effects?: (store: TStore) => void;
tunnel?: ActionTunnel | ActionTunnel[];
}

export const createStore =
Expand All @@ -50,7 +68,11 @@ export const createStore =
initialState: TState,
options?: CreateStoreOptions<TState, TStore>
): TStore => {
const { extendWith = undefined, effects = undefined } = options || {};
const {
extendWith = undefined,
effects = undefined,
tunnel = undefined,
} = options || {};
const actionSubject$ = new Subject<Action>();
const action$ = actionSubject$.asObservable().subscribeOn(scheduler);
const connectableState$ = action$
Expand All @@ -63,7 +85,12 @@ export const createStore =
const update$ = action$.switchMap(action =>
state$.first()
.map(state => ({ action, state } as StateUpdate<TState>)));
const dispatch = (action: Action) => actionSubject$.next(action);
const dispatch = (action: Action) => {
actionSubject$.next(action);
if (action.type === STORE_ACTIONS.FINISH) {
actionSubject$.complete();
}
};

let store: TStore = {
action$,
Expand All @@ -80,6 +107,40 @@ export const createStore =
effects(store);
}

if (tunnel) {
const tunnels = Array.isArray(tunnel) ? tunnel : [tunnel];
tunnels.forEach(({ dispatch: disp, actions }) => {

if (actions === "all") {
store.action$.subscribe(disp);
} else if (Array.isArray(actions)) {
store.action$
.filter(a => actions.indexOf(a.type) >= 0)
.subscribe(disp);
} else if (typeof actions === "function") {
store.action$
.map(actions)
.subscribe(disp);
} else {
const filter = (a: Action) =>
(actions as Object).hasOwnProperty(a.type) &&
!!actions[a.type];
const map = (a: Action) => {
const act = actions[a.type];
if (typeof act === "function") {
return (act(a));
} else {
return a;
}
};
store.action$
.filter(filter)
.map(map)
.subscribe(disp);
}
});
}

return store;
};

Expand Down

0 comments on commit 43e12f4

Please sign in to comment.