├── dist ├── interfaces.js ├── util │ ├── isAction.d.ts │ ├── isAction.js │ ├── combineReducers.d.ts │ └── combineReducers.js ├── createStore.d.ts ├── index.d.ts ├── createStore.js ├── interfaces.d.ts ├── index.js ├── Store.d.ts └── Store.js ├── src ├── util │ ├── isAction.ts │ └── combineReducers.ts ├── index.spec.ts ├── createStore.ts ├── index.ts ├── interfaces.ts ├── ng2 │ ├── provideStore.ts │ └── StoreService.ts ├── Store.subscription.spec.ts ├── Store.ts ├── Store.scheduling.spec.ts └── Store.spec.ts ├── typings.json ├── .gitignore ├── tsconfig.json ├── webpack.config.js ├── karma.conf.js ├── package.json ├── tslint.json └── README.md /dist/interfaces.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /dist/util/isAction.d.ts: -------------------------------------------------------------------------------- 1 | /** Created by ge on 12/27/16. */ 2 | export declare function isAction(obj?: any): boolean; 3 | -------------------------------------------------------------------------------- /src/util/isAction.ts: -------------------------------------------------------------------------------- 1 | /** Created by ge on 12/27/16. */ 2 | export function isAction(obj?: any): boolean { 3 | return (!!obj && !!(obj.type)); 4 | } 5 | -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": {}, 3 | "devDependencies": {}, 4 | "ambientDependencies": { 5 | "es6-shim": "registry:dt/es6-shim#0.31.2+20160317120654" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /dist/createStore.d.ts: -------------------------------------------------------------------------------- 1 | /** Created by ge on 12/17/15. */ 2 | import { Reducer, Hash } from "./interfaces"; 3 | export declare function createStore(reducer: Reducer | Hash, initialState: TState): any; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # editor 2 | .idea 3 | 4 | # test generated files 5 | src/**.js 6 | typings 7 | 8 | # packages 9 | node_modules 10 | 11 | # log files contain sensitive directory information. 12 | **.log 13 | **.logs 14 | -------------------------------------------------------------------------------- /dist/util/isAction.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | /** Created by ge on 12/27/16. */ 4 | function isAction(obj) { 5 | return (!!obj && !!(obj.type)); 6 | } 7 | exports.isAction = isAction; 8 | -------------------------------------------------------------------------------- /src/index.spec.ts: -------------------------------------------------------------------------------- 1 | /** Created by ge on 12/6/15. */ 2 | declare var describe:any, it:any, expect:any, console:Console, require:any; 3 | 4 | require('./Store.spec.ts'); 5 | require('./Store.subscription.spec.ts'); 6 | require('./Store.scheduling.spec.ts'); 7 | -------------------------------------------------------------------------------- /dist/util/combineReducers.d.ts: -------------------------------------------------------------------------------- 1 | import { Hash, Reducer } from "../interfaces"; 2 | export declare function combineReducers(reducers: Hash): Reducer; 3 | export declare function passOrCombineReducers(reducers: Reducer | Hash): Reducer; 4 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | /** Created by ge on 12/4/15. */ 2 | export * from "./interfaces"; 3 | export { combineReducers, passOrCombineReducers } from "./util/combineReducers"; 4 | export { Store, INIT_STORE, INIT_STORE_ACTION } from "./Store"; 5 | export { createStore } from "./createStore"; 6 | -------------------------------------------------------------------------------- /src/createStore.ts: -------------------------------------------------------------------------------- 1 | /** Created by ge on 12/17/15. */ 2 | import {Reducer, Hash} from "./interfaces"; 3 | import {Store} from "./Store"; 4 | export function createStore (reducer:Reducer|Hash, initialState:TState):any { 5 | return () => { 6 | return new Store(reducer, initialState); 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /dist/createStore.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var Store_1 = require("./Store"); 4 | function createStore(reducer, initialState) { 5 | return function () { 6 | return new Store_1.Store(reducer, initialState); 7 | }; 8 | } 9 | exports.createStore = createStore; 10 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** Created by ge on 12/4/15. */ 2 | export * from "./interfaces"; 3 | export {combineReducers, passOrCombineReducers} from "./util/combineReducers"; 4 | export {Store, INIT_STORE, INIT_STORE_ACTION} from "./Store"; 5 | export {createStore} from "./createStore"; 6 | /*remove dependency on angular2*/ 7 | //export {provideStore} from "./ng2/provideStore"; 8 | //export {StoreService} from "./ng2/StoreService"; 9 | -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | /** Created by ge on 12/6/15. */ 2 | export interface Action { 3 | type: string; 4 | } 5 | 6 | export interface Thunk { 7 | (): Action | void ; 8 | } 9 | export interface Hash { 10 | [key:string]:TS; 11 | } 12 | export interface Reducer { 13 | (state:TState, action:Action, callback?:(state:TState)=>void):TState; 14 | } 15 | export interface StateActionBundle { 16 | state: TState; 17 | action: Action 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "noImplicitAny": true, 7 | "experimentalDecorators": true, 8 | "emitDecoratorMetadata": true, 9 | "outDir": "dist", 10 | "declaration": true 11 | }, 12 | "files": [ 13 | "src/index.ts", 14 | "typings/browser/ambient/es6-shim/index.d.ts" 15 | ], 16 | "exclude": [ 17 | "node_modules" 18 | ] 19 | } -------------------------------------------------------------------------------- /dist/interfaces.d.ts: -------------------------------------------------------------------------------- 1 | /** Created by ge on 12/6/15. */ 2 | export interface Action { 3 | type: string; 4 | } 5 | export interface Thunk { 6 | (): Action | void; 7 | } 8 | export interface Hash { 9 | [key: string]: TS; 10 | } 11 | export interface Reducer { 12 | (state: TState, action: Action, callback?: (state: TState) => void): TState; 13 | } 14 | export interface StateActionBundle { 15 | state: TState; 16 | action: Action; 17 | } 18 | -------------------------------------------------------------------------------- /src/ng2/provideStore.ts: -------------------------------------------------------------------------------- 1 | /** Created by ge on 12/17/15. */ 2 | import {Reducer, Hash} from "./../interfaces"; 3 | import {Store} from "./../Store"; 4 | import {createStore} from "./../createStore"; 5 | 6 | // for angular2 7 | import "reflect-metadata"; 8 | import {provide} from "angular2/core"; 9 | 10 | export function provideStore(reducer:Reducer|Hash, initialState:TState):any[] { 11 | return [ 12 | provide(Store, {useFactory: createStore(reducer, initialState)}) 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: [ __dirname + '/src/index.ts' ], 3 | devtool: 'source-map', 4 | output: { 5 | path: __dirname, 6 | filename: 'bundle.js' 7 | }, 8 | module: { 9 | loaders: [ 10 | { 11 | test: /\.ts?$/, 12 | loader: 'awesome-typescript-loader', 13 | exclude: [ /node_modules/ ] 14 | } 15 | ] 16 | }, 17 | resolve: { 18 | extensions: ['', '.ts', '.webpack.js', '.web.js', '.js', 'html'], 19 | modulesDirectories: ['node_modules'] 20 | }, 21 | exclude: [ 22 | "example" 23 | ] 24 | 25 | }; -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var combineReducers_1 = require("./util/combineReducers"); 4 | exports.combineReducers = combineReducers_1.combineReducers; 5 | exports.passOrCombineReducers = combineReducers_1.passOrCombineReducers; 6 | var Store_1 = require("./Store"); 7 | exports.Store = Store_1.Store; 8 | exports.INIT_STORE = Store_1.INIT_STORE; 9 | exports.INIT_STORE_ACTION = Store_1.INIT_STORE_ACTION; 10 | var createStore_1 = require("./createStore"); 11 | exports.createStore = createStore_1.createStore; 12 | /*remove dependency on angular2*/ 13 | //export {provideStore} from "./ng2/provideStore"; 14 | //export {StoreService} from "./ng2/StoreService"; 15 | -------------------------------------------------------------------------------- /dist/Store.d.ts: -------------------------------------------------------------------------------- 1 | /** Created by ge on 12/4/15. */ 2 | import { BehaviorSubject, Subject, Observable } from 'rxjs'; 3 | import { Action, Thunk, Reducer, Hash, StateActionBundle } from "./interfaces"; 4 | export declare const INIT_STORE = "@@luna/INIT_STORE"; 5 | export declare const INIT_STORE_ACTION: { 6 | type: string; 7 | }; 8 | export declare class Store extends BehaviorSubject { 9 | rootReducer: Reducer; 10 | dispatch: (action: Action | Thunk) => void; 11 | update$: Subject>; 12 | action$: Subject; 13 | constructor(rootReducer: Reducer | Hash, initialState?: TState); 14 | _dispatch(action: Action | Thunk): void; 15 | getState(): TState; 16 | select(key: string): Observable; 17 | destroy: () => void; 18 | } 19 | -------------------------------------------------------------------------------- /src/ng2/StoreService.ts: -------------------------------------------------------------------------------- 1 | /** Created by ge on 12/19/15. 2 | * 3 | * # NOTE: 4 | * 5 | * This is the parent class for store services in an angular2 project. 6 | * 7 | * I found it easier to organize the reducer and types as angular2 classes, and use 8 | * the dependency injection to automatically setup the rootStoreService. 9 | */ 10 | import {Reducer} from "./../interfaces"; 11 | 12 | export class StoreService { 13 | initialState:TState; 14 | reducer:Reducer; 15 | types:any; 16 | $:any; 17 | actions:any; 18 | 19 | constructor() { 20 | this.$ = {}; 21 | this.types = {}; 22 | this.actions = {}; 23 | 24 | // # Typical coding patterns in the constructor: 25 | // 26 | // 1. Compose the reducer of your dependencies and save it to this.reducer 27 | // 2. Collect all streams from lower level dependencies to this.$ 28 | // 3. Now initialize your store if this is the root store, or assemble the child states 29 | // and assign it to `this.initialState`. 30 | // 4. It is also convenient to collect actionCreators. They will be dispatched with 31 | // `this` keyword bound to the rootStore object. 32 | } 33 | 34 | onStoreInit (store:TState):void { 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Sun Dec 06 2015 01:12:24 GMT-0600 (CST) 3 | var webpackConfig = require('./webpack.config'); 4 | webpackConfig.devtool = 'inline-source-map'; 5 | webpackConfig.stats = { colors: true, reasons: true }; 6 | 7 | module.exports = function (config) { 8 | config.set({ 9 | basePath: '', 10 | frameworks: ['jasmine'], 11 | files: [ 12 | 'src/index.spec.ts' // just use index to import everything 13 | //'src/*.spec.ts' 14 | ], 15 | // note: necessary for karma script to execute properly in Chrome. 16 | // Otherwise mime type is recognized as video, result in an error message. 17 | mime: { 18 | 'text/x-typescript': ['ts','tsx'] 19 | }, 20 | exclude: [], 21 | preprocessors: { 22 | "**/*.spec.ts": ["webpack", "sourcemap"] 23 | }, 24 | webpack: webpackConfig, 25 | webpackMiddleware: { noInfo: true }, 26 | reporters: ['progress'], 27 | port: 9876, 28 | colors: true, 29 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 30 | logLevel: config.LOG_INFO, 31 | autoWatch: true, 32 | browsers: ['Chrome'], 33 | singleRun: false, 34 | // Concurrency level 35 | // how many browser should be started simultanous 36 | concurrency: Infinity 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /dist/util/combineReducers.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | // helper function 4 | function pickReducers(reducers) { 5 | var initialResult = {}; 6 | return Object 7 | .keys(reducers) 8 | .reduce(function (finalReducer, key) { 9 | if (typeof reducers[key] === 'function') { 10 | finalReducer[key] = reducers[key]; 11 | } 12 | return finalReducer; 13 | }, initialResult); 14 | } 15 | // mixed reducer type is not supported, but I want to add them later on. 16 | function combineReducers(reducers) { 17 | var finalReducers = pickReducers(reducers); 18 | var keys = Object.keys(finalReducers); 19 | var combinedReducer = function (state, action) { 20 | if (typeof state === "undefined") 21 | state = {}; 22 | var hasChanged = false; 23 | var finalState = keys.reduce(function (_state, key) { 24 | var nextState; 25 | var previousStateForKey = _state[key]; 26 | var nextStateForKey = finalReducers[key](_state[key], action); 27 | hasChanged = hasChanged || previousStateForKey !== nextStateForKey; 28 | if (!hasChanged) { 29 | return _state; 30 | } 31 | else { 32 | nextState = Object.assign({}, _state); 33 | nextState[key] = nextStateForKey; 34 | return nextState; 35 | } 36 | }, state); 37 | return hasChanged ? finalState : state; 38 | }; 39 | return combinedReducer; 40 | } 41 | exports.combineReducers = combineReducers; 42 | function passOrCombineReducers(reducers) { 43 | if (typeof reducers !== 'function') { 44 | return combineReducers(reducers); 45 | } 46 | else { 47 | return reducers; 48 | } 49 | } 50 | exports.passOrCombineReducers = passOrCombineReducers; 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "luna", 3 | "version": "1.6.3", 4 | "description": "a reactive redux-like store", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "directories": { 8 | "example": "example", 9 | "test": "tests" 10 | }, 11 | "homepage": "https://github.com/escherpad/luna#readme", 12 | "keywords": [ 13 | "Redux", 14 | "Store", 15 | "RxJS", 16 | "Angular2", 17 | "ng2", 18 | "TypeScript", 19 | "ts" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/escherpad/luna.git" 24 | }, 25 | "bugs": { 26 | "url": "https://github.com/escherpad/luna/issues" 27 | }, 28 | "scripts": { 29 | "v": "tsc -v", 30 | "test": "karma start", 31 | "clean": "rimraf dist", 32 | "build:src": "tsc", 33 | "clean+build": "npm run clean && npm run build:src", 34 | "publish:patch": "npm run clean+build && git add . && git ci -m \"BUILD\" && npm version patch && npm publish && git push", 35 | "publish:minor": "npm run clean+build && git add . && git ci -m \"BUILD\" && npm version minor && npm publish && git push", 36 | "prepublish": "npm run clean+build" 37 | }, 38 | "author": "Ge Yang ", 39 | "license": "MIT", 40 | "peerDependencies": { 41 | "rxjs": "^5.5.5" 42 | }, 43 | "devDependencies": { 44 | "awesome-typescript-loader": "^0.15.9", 45 | "browserify": "^12.0.1", 46 | "es6-shim": "^0.35.0", 47 | "jasmine-core": "^2.4.1", 48 | "karma": "^0.13.22", 49 | "karma-browserify": "^4.4.2", 50 | "karma-chrome-launcher": "^0.2.2", 51 | "karma-jasmine": "^0.3.8", 52 | "karma-sourcemap-loader": "^0.3.6", 53 | "karma-typescript-preprocessor": "0.0.21", 54 | "karma-webpack": "^1.7.0", 55 | "rimraf": "^2.4.4", 56 | "rxjs": "^5.5.5", 57 | "sourcemap": "^0.1.0", 58 | "ts-loader": "^0.7.2", 59 | "tsify": "^0.13.1", 60 | "typescript": "^2.6.2", 61 | "typings": "^0.7.9", 62 | "webpack": "^1.12.9" 63 | }, 64 | "dependencies": {} 65 | } 66 | -------------------------------------------------------------------------------- /src/util/combineReducers.ts: -------------------------------------------------------------------------------- 1 | import {Action, Hash, Reducer} from "../interfaces"; 2 | // helper function 3 | function pickReducers(reducers:Hash):Hash { 4 | var initialResult:Hash = {}; 5 | return Object 6 | .keys(reducers) 7 | .reduce((finalReducer:Hash, key:string):Hash => { 8 | if (typeof reducers[key] === 'function') { 9 | finalReducer[key] = reducers[key]; 10 | } 11 | return finalReducer; 12 | }, initialResult); 13 | } 14 | 15 | 16 | // mixed reducer type is not supported, but I want to add them later on. 17 | export function combineReducers(reducers:Hash):Reducer { 18 | const finalReducers:Hash = pickReducers(reducers); 19 | const keys = Object.keys(finalReducers); 20 | 21 | var combinedReducer = >(state:TState, action:Action) => { 22 | if (typeof state === "undefined") state = {}; 23 | var hasChanged:boolean = false; 24 | var finalState:TState = keys.reduce((_state:TState, key:string):TState => { 25 | var nextState:TState; 26 | var previousStateForKey:any = _state[key]; 27 | var nextStateForKey:any = finalReducers[key]( 28 | _state[key], 29 | action 30 | ); 31 | hasChanged = hasChanged || previousStateForKey !== nextStateForKey; 32 | if (!hasChanged) { 33 | return _state; 34 | } else { 35 | nextState = Object.assign({}, _state); 36 | nextState[key] = nextStateForKey; 37 | return nextState; 38 | } 39 | }, state); 40 | 41 | return hasChanged ? finalState : state; 42 | }; 43 | return combinedReducer as Reducer; 44 | } 45 | 46 | export function passOrCombineReducers(reducers:Reducer|Hash):Reducer { 47 | if (typeof reducers !== 'function') { 48 | return combineReducers(reducers as Hash); 49 | } else { 50 | return reducers as Reducer; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Store.subscription.spec.ts: -------------------------------------------------------------------------------- 1 | /** Created by ge on 3/26/16. */ 2 | 3 | /* to make this script a module */ 4 | export default {} 5 | 6 | import {Action, Hash, Reducer, Store} from "./index"; 7 | let reducer = function (state:number = 0, action:Action, callback:(state:number)=>void):number { 8 | if (action.type === "INC") { 9 | return state + 1; 10 | } else if (action.type === "DEC") { 11 | return state - 1; 12 | } else { 13 | return state; 14 | } 15 | }; 16 | 17 | import {Observable} from 'rxjs'; 18 | 19 | describe("store$", function () { 20 | it("can get updated state", function () { 21 | 22 | let store$:Store = new Store(reducer) as Store; 23 | expect(store$.value).toEqual(0); 24 | store$.subscribe(state => console.log("test 1: ", state)); 25 | 26 | }); 27 | it("can get updated state as well as actions", function () { 28 | 29 | let store$:Store = new Store(reducer, 10) as Store; 30 | store$ 31 | .update$ 32 | .subscribe((_) => console.log("test 2: ", _)); 33 | 34 | }); 35 | it("can subscribe to actions", function () { 36 | let store$:Store = new Store(reducer, 10); 37 | 38 | let testAction$ = Observable.from([ 39 | {type: "INC"}, 40 | {type: "DEC"}, 41 | {type: "INC"}, 42 | {type: "DEC"} 43 | ]); 44 | store$ 45 | .update$ 46 | .subscribe(_ => console.log("test 3: ", _)); 47 | 48 | testAction$ 49 | .subscribe((action:Action) => store$.dispatch(action)); 50 | }); 51 | it("can subscribe to actions directly", function () { 52 | let store$:Store = new Store(reducer, 10) as Store; 53 | 54 | let testAction$ = Observable.from([ 55 | {type: "INC"}, 56 | {type: "DEC"}, 57 | {type: "INC"}, 58 | {type: "DEC"} 59 | ]); 60 | store$ 61 | .update$ 62 | .subscribe(_ => console.log("test 4: ", _)); 63 | 64 | testAction$ 65 | .subscribe(store$.action$); 66 | }); 67 | }); 68 | 69 | -------------------------------------------------------------------------------- /src/Store.ts: -------------------------------------------------------------------------------- 1 | /** Created by ge on 12/4/15. */ 2 | import {BehaviorSubject, Subject, Observable} from 'rxjs'; 3 | import {passOrCombineReducers} from './util/combineReducers'; 4 | import {Action, Thunk, Reducer, Hash, StateActionBundle} from "./interfaces"; 5 | import {isAction} from "./util/isAction"; 6 | 7 | export const INIT_STORE = '@@luna/INIT_STORE'; 8 | export const INIT_STORE_ACTION = {type: INIT_STORE}; 9 | 10 | export class Store extends BehaviorSubject { 11 | public rootReducer: Reducer; 12 | public dispatch: (action: Action|Thunk) => void; 13 | public update$: Subject>; 14 | public action$: Subject; 15 | 16 | constructor(rootReducer: Reducer | Hash, 17 | initialState?: TState) { 18 | // this is a stream for the states of the store, call BehaviorSubject constructor 19 | super(passOrCombineReducers(rootReducer)(initialState, INIT_STORE_ACTION)); 20 | this.dispatch = this._dispatch.bind(this); 21 | this.rootReducer = passOrCombineReducers(rootReducer); 22 | 23 | // action$ is a stream for action objects 24 | this.action$ = new Subject(); 25 | this.update$ = new Subject>(); 26 | this.action$ 27 | .subscribe( 28 | (action) => { 29 | let currentState: TState = this.getValue(); 30 | let newState: TState = this.rootReducer(currentState, action); 31 | this.next(newState); 32 | this.update$.next({state: newState, action}) 33 | }, 34 | (error) => console.log('dispatcher$ Error: ', error.toString()), 35 | () => console.log('dispatcher$ completed') 36 | ); 37 | 38 | this.action$.next(INIT_STORE_ACTION); 39 | } 40 | 41 | _dispatch(action: Action|Thunk) { 42 | let _action: Action, 43 | _actionThunk: Thunk, 44 | newAction: Action; 45 | if (typeof action === 'function') { 46 | _actionThunk = action as Thunk; 47 | newAction = _actionThunk.apply(this); 48 | if (isAction(newAction)) return this.action$.next(newAction); 49 | } else if (!isAction(action)) { 50 | console.error("action object ill-defined: ", action, "will not pass in."); 51 | } else { 52 | _action = action as Action; 53 | this.action$.next(_action); 54 | } 55 | } 56 | 57 | // this method is just a wrapper function to make it compatible with redux convention. 58 | getState(): TState { 59 | return this.getValue(); 60 | } 61 | 62 | select (key: string): Observable { 63 | return this 64 | .map((state: any) => { 65 | var rState: TRState = state[key] as TRState; 66 | return rState; 67 | }) 68 | .distinctUntilChanged(); 69 | } 70 | 71 | destroy = () => { 72 | this.action$.complete(); 73 | this.complete(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "align": [ 4 | true, 5 | "parameters", 6 | "arguments", 7 | "statements" 8 | ], 9 | "ban": false, 10 | "class-name": true, 11 | "comment-format": [ 12 | false, 13 | "check-space", 14 | "check-lowercase" 15 | ], 16 | "curly": true, 17 | "eofline": true, 18 | "forin": true, 19 | "indent": [ 20 | true, 21 | "spaces" 22 | ], 23 | "interface-name": false, 24 | "jsdoc-format": true, 25 | "label-position": true, 26 | "label-undefined": true, 27 | "max-line-length": [ 28 | true, 29 | 140 30 | ], 31 | "member-access": false, 32 | "member-ordering": [ 33 | true, 34 | "public-before-private", 35 | "static-before-instance", 36 | "variables-before-functions" 37 | ], 38 | "no-any": false, 39 | "no-arg": true, 40 | "no-bitwise": true, 41 | "no-conditional-assignment": true, 42 | "no-console": [ 43 | true, 44 | "debug", 45 | "info", 46 | "time", 47 | "timeEnd", 48 | "trace" 49 | ], 50 | "no-construct": true, 51 | "no-constructor-vars": false, 52 | "no-debugger": true, 53 | "no-duplicate-key": true, 54 | "no-shadowed-variable": true, 55 | "no-duplicate-variable": true, 56 | "no-empty": true, 57 | "no-eval": true, 58 | "no-internal-module": true, 59 | "no-require-imports": false, 60 | "no-string-literal": true, 61 | "no-switch-case-fall-through": true, 62 | "no-trailing-comma": true, 63 | "no-trailing-whitespace": true, 64 | "no-unreachable": true, 65 | "no-unused-expression": true, 66 | "no-unused-variable": true, 67 | "no-use-before-declare": true, 68 | "no-var-keyword": false, 69 | "no-var-requires": true, 70 | "one-line": [ 71 | true, 72 | "check-open-brace", 73 | "check-catch", 74 | "check-else", 75 | "check-whitespace" 76 | ], 77 | "quotemark": [ 78 | false, 79 | "double" 80 | ], 81 | "radix": true, 82 | "semicolon": true, 83 | "sort-object-literal-keys": true, 84 | "switch-default": true, 85 | "triple-equals": [ 86 | true, 87 | "allow-null-check" 88 | ], 89 | "typedef": [ 90 | false, 91 | "call-signature", 92 | "parameter", 93 | "property-declaration", 94 | "variable-declaration", 95 | "member-variable-declaration" 96 | ], 97 | "typedef-whitespace": [ 98 | false, 99 | { 100 | "call-signature": "nospace", 101 | "index-signature": "nospace", 102 | "parameter": "nospace", 103 | "property-declaration": "nospace", 104 | "variable-declaration": "nospace" 105 | } 106 | ], 107 | "use-strict": [ 108 | true, 109 | "check-module", 110 | "check-function" 111 | ], 112 | "variable-name": false, 113 | "whitespace": [ 114 | true, 115 | "check-branch", 116 | "check-decl", 117 | "check-operator", 118 | "check-separator" 119 | ] 120 | } 121 | } -------------------------------------------------------------------------------- /dist/Store.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __extends = (this && this.__extends) || (function () { 3 | var extendStatics = Object.setPrototypeOf || 4 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 5 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; 6 | return function (d, b) { 7 | extendStatics(d, b); 8 | function __() { this.constructor = d; } 9 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 10 | }; 11 | })(); 12 | Object.defineProperty(exports, "__esModule", { value: true }); 13 | /** Created by ge on 12/4/15. */ 14 | var rxjs_1 = require("rxjs"); 15 | var combineReducers_1 = require("./util/combineReducers"); 16 | var isAction_1 = require("./util/isAction"); 17 | exports.INIT_STORE = '@@luna/INIT_STORE'; 18 | exports.INIT_STORE_ACTION = { type: exports.INIT_STORE }; 19 | var Store = /** @class */ (function (_super) { 20 | __extends(Store, _super); 21 | function Store(rootReducer, initialState) { 22 | var _this = 23 | // this is a stream for the states of the store, call BehaviorSubject constructor 24 | _super.call(this, combineReducers_1.passOrCombineReducers(rootReducer)(initialState, exports.INIT_STORE_ACTION)) || this; 25 | _this.destroy = function () { 26 | _this.action$.complete(); 27 | _this.complete(); 28 | }; 29 | _this.dispatch = _this._dispatch.bind(_this); 30 | _this.rootReducer = combineReducers_1.passOrCombineReducers(rootReducer); 31 | // action$ is a stream for action objects 32 | _this.action$ = new rxjs_1.Subject(); 33 | _this.update$ = new rxjs_1.Subject(); 34 | _this.action$ 35 | .subscribe(function (action) { 36 | var currentState = _this.getValue(); 37 | var newState = _this.rootReducer(currentState, action); 38 | _this.next(newState); 39 | _this.update$.next({ state: newState, action: action }); 40 | }, function (error) { return console.log('dispatcher$ Error: ', error.toString()); }, function () { return console.log('dispatcher$ completed'); }); 41 | _this.action$.next(exports.INIT_STORE_ACTION); 42 | return _this; 43 | } 44 | Store.prototype._dispatch = function (action) { 45 | var _action, _actionThunk, newAction; 46 | if (typeof action === 'function') { 47 | _actionThunk = action; 48 | newAction = _actionThunk.apply(this); 49 | if (isAction_1.isAction(newAction)) 50 | return this.action$.next(newAction); 51 | } 52 | else if (!isAction_1.isAction(action)) { 53 | console.error("action object ill-defined: ", action, "will not pass in."); 54 | } 55 | else { 56 | _action = action; 57 | this.action$.next(_action); 58 | } 59 | }; 60 | // this method is just a wrapper function to make it compatible with redux convention. 61 | Store.prototype.getState = function () { 62 | return this.getValue(); 63 | }; 64 | Store.prototype.select = function (key) { 65 | return this 66 | .map(function (state) { 67 | var rState = state[key]; 68 | return rState; 69 | }) 70 | .distinctUntilChanged(); 71 | }; 72 | return Store; 73 | }(rxjs_1.BehaviorSubject)); 74 | exports.Store = Store; 75 | -------------------------------------------------------------------------------- /src/Store.scheduling.spec.ts: -------------------------------------------------------------------------------- 1 | /** Created by ge on 3/26/16. */ 2 | /* so that this show up as a module */ 3 | export default {}; 4 | 5 | /** Created by ge on 12/6/15. */ 6 | import {Action, Hash, Reducer, Store, INIT_STORE_ACTION} from "./index"; 7 | 8 | interface TestAction extends Action { 9 | payload?:any; 10 | } 11 | interface TState { 12 | counter: number; 13 | name: string; 14 | } 15 | 16 | describe("store thread schedule", function () { 17 | it("the dispatch calls should run in a different thread", function (done:()=>void) { 18 | let counterReducer = function (state:number = 0, action:TestAction):number { 19 | if (action.type === "INC") { 20 | return state + 1; 21 | } else if (action.type === "DEC") { 22 | return state - 1; 23 | } else { 24 | return state; 25 | } 26 | }; 27 | let stringReducer = function (state:string = "", action:TestAction):string { 28 | if (action.type === "SET") { 29 | return action.payload; 30 | } else if (action.type === "CAPITALIZE") { 31 | return state.toUpperCase(); 32 | } else if (action.type === "LOWERING") { 33 | return state.toLowerCase(); 34 | } else { 35 | return state; 36 | } 37 | }; 38 | var rootReducer:Hash = { 39 | counter: counterReducer, 40 | name: stringReducer 41 | }; 42 | 43 | var store = new Store(rootReducer); // does not need to pass in a inital state 44 | // you can not capture the INIT_STORE action, 45 | // because the .action$ stream is HOT 46 | // and the initialization happens synchronously. 47 | expect(store.value).toEqual({counter: 0, name: ""}); 48 | 49 | store.subscribe( 50 | (state)=> { 51 | console.log('spec state: ', state) 52 | }, 53 | error=> console.log('error ', error), 54 | () => console.log('completed.') 55 | ); 56 | store.dispatch({type: "SET", payload: "episodeyang"}); 57 | expect(store.value).toEqual({counter: 0, name: "episodeyang"}); 58 | store.dispatch({type: "CAPITALIZE"}); 59 | expect(store.value).toEqual({counter: 0, name: "EPISODEYANG"}); 60 | store.dispatch({type: "LOWERING"}); 61 | expect(store.value).toEqual({counter: 0, name: "episodeyang"}); 62 | store.dispatch({type: "INC"}); 63 | expect(store.value).toEqual({counter: 1, name: "episodeyang"}); 64 | 65 | /*Now let's do something complicated*/ 66 | var subscription = store.select('counter').subscribe(count=> store.dispatch({type: "CAPITALIZE"})); 67 | store.dispatch({type: "INC"}); 68 | expect(store.value).toEqual({counter: 2, name: "EPISODEYANG"}); 69 | subscription.unsubscribe(); 70 | 71 | /* an anti-pattern */ 72 | var subscription = store.select('name') 73 | .subscribe(name=> { 74 | store.dispatch({type: "INC"}) 75 | }); 76 | store.dispatch({type: "SET", payload: "Ge Yang"}); 77 | /* the counter increase fires twice, once on subscription, once on update because 78 | * the store is a behavioralSubject. It is equivalent to ReplaySubject with a buffer 79 | * size of 2 in this regard. 80 | * If you want to avoid triggering on subscription, use update$ or action$ */ 81 | expect(store.value).toEqual({counter: 4, name: "Ge Yang"}); 82 | subscription.unsubscribe(); 83 | 84 | /* Now let's add some async subscription */ 85 | var subscription = store.select('name').subscribe(name=> { 86 | setTimeout(()=> { 87 | var currentCount = store.value.counter; 88 | store.dispatch({type: "INC"}); 89 | expect(store.value.counter).toBe(currentCount + 1); 90 | }, 10); 91 | }); 92 | store.dispatch({type: "CAPITALIZE"}); 93 | expect(store.value).toEqual({counter: 4, name: "GE YANG"}); 94 | setTimeout(()=> { 95 | expect(store.value).toEqual({counter: 6, name: "GE YANG"}); 96 | subscription.unsubscribe(); 97 | store.destroy(); 98 | done() 99 | }, 100); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /src/Store.spec.ts: -------------------------------------------------------------------------------- 1 | /** Created by ge on 3/26/16. */ 2 | 3 | /* so that this show up as a module */ 4 | import {isAction} from "./util/isAction"; 5 | export default {}; 6 | /** Created by ge on 12/6/15. */ 7 | import {Action, Hash, Reducer} from "./index"; 8 | 9 | // the Stat interface need to extend Hash so that the index keys are available. 10 | 11 | let reducer = function (state: number = 0, action: Action, callback: (state: number) => void): number { 12 | if (action.type === "INC") { 13 | return state + 1; 14 | } else if (action.type === "DEC") { 15 | return state - 1; 16 | } else if (action.type === "ASYNC_INC") { 17 | setTimeout(() => { 18 | callback(state + 1); 19 | }, 10); 20 | return undefined; 21 | } else if (action.type === "ASYNC_DEC") { 22 | setTimeout(() => { 23 | callback(state - 1); 24 | }, 10); 25 | return undefined; 26 | } else { 27 | return state; 28 | } 29 | }; 30 | 31 | describe("interfaces", function () { 32 | it("Reducer can be a function", function () { 33 | let state: number = undefined; 34 | state = reducer(state, {type: "INC"}); 35 | expect(state).toBe(1); 36 | state = reducer(state, {type: "DEC"}); 37 | expect(state).toBe(0); 38 | }); 39 | it("reducer should contain initial state", function () { 40 | let state: number; 41 | expect(state).toBeUndefined(); 42 | state = reducer(state, {type: "INC"}); 43 | expect(state).toBe(1); 44 | state = reducer(state, {type: "DEC"}); 45 | expect(state).toBe(0); 46 | }); 47 | }); 48 | 49 | import {Store} from "./index"; 50 | describe("store", function () { 51 | it("should contain action$ and update$ stream", function () { 52 | let state: number = 10; 53 | let store = new Store(reducer, state); 54 | // store should contain action$ and update$ stream. 55 | expect(store.action$).toBeDefined(); 56 | expect(store.update$).toBeDefined(); 57 | }); 58 | it("sync reducers should work", function () { 59 | let state: number = 10; 60 | let store = new Store(reducer, state); 61 | 62 | store.subscribe( 63 | (state) => { 64 | console.log('spec state: ', state) 65 | }, 66 | error => console.log('error ', error), 67 | () => console.log('completed.') 68 | ); 69 | store.dispatch({type: "INC"}); 70 | expect(store.value).toEqual(11); 71 | store.dispatch({type: "DEC"}); 72 | expect(store.value).toEqual(10); 73 | store.destroy(); 74 | }); 75 | }); 76 | describe("dispatch function", function () { 77 | 78 | it("support action creator", function () { 79 | let state: number = 30; 80 | let store = new Store(reducer, state); 81 | 82 | function increase(): Action { 83 | return { 84 | type: "INC" 85 | }; 86 | } 87 | 88 | store.subscribe( 89 | (state) => { 90 | console.log('spec state: ', state) 91 | }, 92 | error => console.log('error ', error), 93 | () => console.log('completed.') 94 | ); 95 | store.dispatch(increase()); 96 | store.dispatch({type: "DEC"}); 97 | store.destroy(); 98 | }); 99 | it("support thunk; properly handle null or undefined actions from thunk.", function () { 100 | let state: number = 40; 101 | let store = new Store(reducer, state); 102 | 103 | function increase(): Action { 104 | return { 105 | type: "INC" 106 | }; 107 | } 108 | 109 | store.subscribe( 110 | (state) => { 111 | console.log('spec state: ', state) 112 | }, 113 | error => console.log('error ', error), 114 | () => console.log('completed.') 115 | ); 116 | 117 | store.update$.subscribe( 118 | ({state, action}) => { 119 | if (!isAction(action)) throw Error("ill-formed action is allowed to get dispatched"); 120 | } 121 | ); 122 | // null or undefined results from thunk should not be passed through by dispatch 123 | store.dispatch(() => null); 124 | store.dispatch(() => undefined); 125 | 126 | // dispatching typical thunks 127 | store.dispatch(increase); 128 | store.dispatch({type: "DEC"}); 129 | store.destroy(); 130 | }); 131 | it("thunk have access to dispatch", function () { 132 | let state: number = 40; 133 | let store = new Store(reducer, state); 134 | 135 | function increase(): void { 136 | let _store: Store = this; 137 | setTimeout(function (): void { 138 | let action: Action = { 139 | type: "INC" 140 | }; 141 | _store.dispatch(action) 142 | }, 200); 143 | } 144 | 145 | store.subscribe( 146 | (state) => { 147 | console.log('spec state: ', state) 148 | }, 149 | error => console.log('error ', error), 150 | () => console.log('completed.') 151 | ); 152 | store.dispatch(increase); 153 | store.dispatch({type: "DEC"}); 154 | setTimeout(() => { 155 | store.destroy(); 156 | }, 210) 157 | }) 158 | 159 | }); 160 | describe("store with hash type", function () { 161 | it("can accept actions without initial state (an properly handle the initialization)", function () { 162 | interface TState extends Hash { 163 | } 164 | 165 | let reducer = function (state: number = 0, action: Action): number { 166 | if (action.type === "INC") { 167 | return state + 1; 168 | } else if (action.type === "DEC") { 169 | return state - 1; 170 | } else { 171 | return state; 172 | } 173 | }; 174 | let rootReducer: Hash = { 175 | counter: reducer 176 | }; 177 | 178 | let store = new Store(rootReducer); 179 | 180 | function increase(): void { 181 | let _store: Store = this; 182 | setTimeout(function (): void { 183 | let action: Action = { 184 | type: "INC" 185 | }; 186 | _store.dispatch(action); 187 | }, 200); 188 | } 189 | 190 | store.subscribe( 191 | (state) => { 192 | console.log('spec state: ', state); 193 | }, 194 | error => console.log('error ', error), 195 | () => console.log('completed.') 196 | ); 197 | store.dispatch(increase); 198 | store.dispatch({type: "DEC"}); 199 | setTimeout(() => { 200 | store.destroy(); 201 | }, 210) 202 | }); 203 | it("can take initial value", function () { 204 | interface TState extends Hash { 205 | } 206 | let state: TState = { 207 | counter: 40 208 | }; 209 | 210 | let reducer = function (state: number = 0, action: Action): number { 211 | if (action.type === "INC") { 212 | return state + 1; 213 | } else if (action.type === "DEC") { 214 | return state - 1; 215 | } else { 216 | return state; 217 | } 218 | }; 219 | let rootReducer: Hash = { 220 | counter: reducer 221 | }; 222 | 223 | let store = new Store(rootReducer, state); 224 | 225 | function increase(): void { 226 | let _store: Store = this; 227 | setTimeout(function (): void { 228 | let action: Action = { 229 | type: "INC" 230 | }; 231 | _store.dispatch(action); 232 | }, 200); 233 | } 234 | 235 | store.subscribe( 236 | (state) => { 237 | console.log('spec state: ', state); 238 | }, 239 | error => console.log('error ', error), 240 | () => console.log('completed.') 241 | ); 242 | store.dispatch(increase); 243 | store.dispatch({type: "DEC"}); 244 | setTimeout(() => { 245 | store.destroy(); 246 | }, 210); 247 | }); 248 | it("accept reducers of different types", function () { 249 | interface TState { 250 | counter: number; 251 | name: string; 252 | } 253 | let state: TState = { 254 | counter: 40, 255 | name: 'Captain Kirk' 256 | }; 257 | 258 | let counterReducer = function (state: number, action: Action): number { 259 | if (action.type === "INC") { 260 | return state + 1; 261 | } else if (action.type === "DEC") { 262 | return state - 1; 263 | } else { 264 | return state; 265 | } 266 | }; 267 | let stringReducer = function (state: string, action: Action): string { 268 | if (action.type === "CAPITALIZE") { 269 | return state.toUpperCase(); 270 | } else if (action.type === "LOWERING") { 271 | return state.toLowerCase(); 272 | } else { 273 | return state; 274 | } 275 | }; 276 | let rootReducer: Hash = { 277 | counter: counterReducer, 278 | name: stringReducer 279 | }; 280 | 281 | let store = new Store(rootReducer, state); 282 | 283 | store.subscribe( 284 | (state) => { 285 | console.log('spec state: ', state) 286 | }, 287 | error => console.log('error ', error), 288 | () => console.log('completed.') 289 | ); 290 | store.dispatch({type: "CAPITALIZE"}); 291 | store.dispatch({type: "LOWERING"}); 292 | store.dispatch({type: "INC"}); 293 | store.destroy(); 294 | }); 295 | it("should allow filtered partial states in a stream", function () { 296 | interface TState { 297 | counter: number; 298 | name: string; 299 | } 300 | let state: TState = { 301 | counter: 40, 302 | name: 'Captain Kirk' 303 | }; 304 | 305 | let counterReducer = function (state: number, action: Action): number { 306 | if (action.type === "INC") { 307 | return state + 1; 308 | } else if (action.type === "DEC") { 309 | return state - 1; 310 | } else { 311 | return state; 312 | } 313 | }; 314 | let stringReducer = function (state: string, action: Action): string { 315 | if (action.type === "CAPITALIZE") { 316 | return state.toUpperCase(); 317 | } else if (action.type === "LOWERING") { 318 | return state.toLowerCase(); 319 | } else { 320 | return state; 321 | } 322 | }; 323 | let rootReducer: Hash = { 324 | counter: counterReducer, 325 | name: stringReducer 326 | }; 327 | 328 | let store = new Store(rootReducer, state); 329 | 330 | store.select('name').subscribe( 331 | (state) => { 332 | console.log('spec state: ', state); 333 | }, 334 | error => console.log('error ', error), 335 | () => console.log('completed.') 336 | ); 337 | 338 | // mock persistent storage example 339 | store 340 | .select('counter') 341 | .subscribe((count: number): void => console.log('counter saving event: ', count)); 342 | 343 | store.dispatch({type: "CAPITALIZE"}); 344 | store.dispatch({type: "LOWERING"}); 345 | store.dispatch({type: "INC"}); 346 | store.dispatch({type: "DEC"}); 347 | 348 | 349 | store.destroy(); 350 | }); 351 | 352 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Luna, a reactive redux library with built-in support for async action creator and Thunks 2 | 3 | [![Join the chat at https://gitter.im/escherpad/luna](https://img.shields.io/badge/GITTER-join%20chat-green.svg?style=flat-square)](https://gitter.im/escherpad/luna?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | Luna is a reactive redux implementation based on the Reactive-Extension (Rxjs@v5.0-beta) and build with Typescript. Luna try to improve upon existing implementations of redux by providing out of the box support for async actions, ways to access child components of the root store as a stream and more. Luna drops verbatim compatibility with the original `redux` to give rise to a more coherent, reactive API. 6 | 7 | ## Features 8 | 1. **Luna `store$` object is reactive**. You can subscribe to it and use all of `rxjs`'s operators, such as `debounceTime`, `throttleTime`, `map`, `flatMap`, `buffer` etc. 9 | 2. Luna `dispatch` function takes in both `thunk`s and `action`. As well as a batch(array) of thunks and actions, and update the store state in a single batch. Since dispatching and rendering in react are synchronous 10 | 11 | ## Installing Luna 12 | 13 | Just do `npm install luna` to install from npm. Or you can install from github: 14 | 15 | ```shell 16 | npm install luna@git+https://git@github.com/escherpad/luna.git 17 | ``` 18 | 19 | ## To Use: 20 | 21 | ```javascript 22 | import {Store} from "luna"; 23 | 24 | let reducer = function (state = 0, action){// default value here would be 0. 25 | if (action.type === "INC") { 26 | return state + 1; 27 | } else { 28 | return state; 29 | } 30 | } 31 | 32 | let store$ = new Store(reducer); 33 | 34 | store$.dispatch({type: "INC"}) 35 | store$.subscribe((state)=>console.log(state)); 36 | // 0 37 | // 1 38 | ``` 39 | 40 | ## The `store` instance 41 | 42 | calling `new Store(reducer[, initialState])` returns a `store$` instance. Luna `Store` is a subclass of the BehaviorSubject from rx. So it takes an intialstate at instantiation, and emits a stream of state objects that you can subscribe to. 43 | 44 | the `store$.action$` is a (publish) subject for the actions. The store internally subscribes to this stream and executes the reducers on the store state in response to events from this action stream. Because it is a (publish) subject, it does not trigger event on subscription. It also does not have the `getValue()` method. 45 | 46 | The `store$.update$` is a (publish) subject for `{state, action}` bundle. It receives updated state/action bundle after the action has been applied to the store. This stream is used as a `post-action` hook for middlewares such as [luna-saga](https://github.com/escherpad/luna-saga). Because it is a (publish) subject, it does not trigger event on subscription. It also does not have the `getValue()` method. 47 | 48 | ## Wanna use Reactive-Extention (Rxjs) and redux in your project, but don't know how? 49 | 50 | Luna is the easiest way to get started. 51 | 52 | Redux is a very simple library with a small API footprint. It is written without taking advantage of the Reactive-Extension. Now using Rxjs as the message/middleware layer, we make redux even more powerful and natural to use. Luna is written with redux's spirit and design pattern in mind, but it includes all of the different parts in one place, and make it easy to start developing in a redux-like way right away. Luna replaces `redux`, `redux-thunk`, `redux-middleware`, and allows you to use asynchronous `actionCreators` directly in the dispatcher. 53 | 54 | ### Start by Designing Your Store Tree 55 | 56 | For an application, Redux provides a single data store for the entire application. To architect your app, you first start with designing the structure of the store object. Then you can write reducers for those sub-part of the root store, and use the `combineReducer` function to combine those into the root reducer. 57 | 58 | ### `map =>` Array Composition Pattern 59 | 60 | To deal with arrays (collections of documents for instance), you use the array composition pattern. Dan has a very nice vieo on Egghead.io \([link](https://egghead.io/lessons/javascript-redux-reducer-composition-with-arrays)\) 61 | 62 | 63 | ### `()=>` Action Creator Pattern 64 | 65 | Another useful patter is action creators. You write a simple function that returns an action object. \([link: action creators](https://egghead.io/lessons/javascript-redux-extracting-action-creators)\) 66 | 67 | ### `Thunk` and Async Actions 68 | 69 | **Note: Async Action is going to be deprecated.** 70 | 71 | Now what about async operations such as network calls? Redux thinks that the `store` object should only be mutated synchronously. This makes everything easier to understand and sequential. To allow async operations, you then rely on a concept called 'thunk'. In simple words, because action objects are not enough, you dispatch functions that contains a certain execution context. With redux you need to use the `redux-thunk` middleware. It patches the redux `store` class, and makes the dispatch method accept `thunks`. This sytanx is slightly strange, so with Luna I decided to support dispatching `thunks` out of the box and avoid the monkey patching. 72 | 73 | ### Middlewares (don't need anymore) 74 | 75 | You don't need middleware anymore now you have Rxjs. In Rxjs, an observable is a stream of data that you can subscribe to. In Luna, the `Store` is a subclass of the `Rxjs.BehaviorSubject`, which is an extension of the `Observable` class. In addition, Luna `Store` also has a property called `Store.action$`, which is a `Rx.Subject` for all of the actions the store accepts. In a reactive paradigm, if you want to log all of the actions for instance, you can just subscribe to the `Store.action$` stream. 76 | 77 | ### Persistent Storage and Children of the Root Store 78 | 79 | Luna also provides a convenient method called `Store.select`. It allows you to pass in the key of a child of the root store object, and returns a stream for that child part of the model. 80 | 81 | Therefore, if you have an app, and you want to save only part of the app state in the localStorage, you can just do the following: 82 | 83 | ```typescript 84 | interface State { 85 | loginSession: User, 86 | notes: Note[], 87 | otherData: Blah... 88 | } 89 | 90 | const rootReducer:Reducer = { 91 | loginSession: loginSessionReducer, 92 | notes: notesReducer, 93 | ... 94 | } 95 | 96 | const initialState:State = { 97 | loginSession: [], 98 | notes: [], 99 | ... 100 | } 101 | 102 | var store = new Store(rootReducer, intialState); 103 | 104 | // To save the loginSession in the localStorage of the 105 | // browser, you can just subscribe like this: 106 | 107 | store 108 | .select('loginSession') 109 | .subscribe(loginSession => { 110 | window.localStorage.setItem('loginSession', loginSession); 111 | }) 112 | ``` 113 | example here: [line in test file](https://github.com/escherpad/luna/commit/e0741cc4ca8af2ad4d3a38e08c5681838f342ed4#diff-6b623d6bcf5e7f06c466aa060ec9c4b6R279) 114 | 115 | and I personally find this very powerful! 116 | 117 | 118 | ## About this library and What's different about Luna (from redux)? 119 | 120 | - Luna is written using the reactive-extension, so that both the state of the store and the actions are streams. 121 | - Luna `dispatch` method supports `actionCreator` and asynchronous actions out of the box. 122 | - Luna's `Thunks` take no input arguments. The Store object is accessed as `this` keyword in the thunk. I'm experimenting with this as a cleaner syntax. 123 | - Luna is written for an Angular2 project, and provide dependency injection (DI) via a 'provideStore' 124 | function, and a StoreService class that you can extend in your angular2 applications. 125 | 126 | ## For Angular2 Developers Out There~ 127 | 128 | Angular2 Beta just came out, and a lot of us are still figuring out the best practices. Getting redux, Typescript and angular2 dependency injection to work well together was a challenge. 129 | 130 | To make getting started easy for you, Luna includes a simple class called `StoreService` in Luna. In escherpad the application I'm working on, Each child of the root store has a store service class that extends `StoreService` class. This service class include the reducer, action types, as well as the action creators inside, and use the angular2 dependency injection to connect everything together. 131 | 132 | This way, script for each child store only need to know what it depends on itself. Components become truly composable. 133 | 134 | I currently do not use the `provideStore` provider for the reason given above. 135 | 136 | ## Developing Luna 137 | 138 | - you need to have `karma-cli` installed globally. (do `npm install -g karma-cli`) 139 | - to build, run `npm run build`. This just calls `tsc` in project root. 140 | - to test, you can use `karma start`. I use webStorm's karma integration to run the tests. 141 | 142 | ```typescript 143 | /** Created by ge on 12/6/15. */ 144 | import {Action, Hash, Reducer} from "luna"; 145 | 146 | // the Stat interface need to extend Hash so that the index keys are available. 147 | interface TestState extends Hash { 148 | counter:number; 149 | } 150 | 151 | // Reducer example 152 | const reducer = function (state:TestState, action:Action, callback:(state:TestState)=>void):TestState { 153 | if (action.type === "INC") { 154 | state.counter += 1; 155 | return state 156 | } else if (action.type === "DEC") { 157 | state.counter -= 1; 158 | return state 159 | } else { 160 | return state; 161 | } 162 | }; 163 | 164 | // define the initial state of store 165 | var state:TestState = { 166 | counter: 20 167 | }; 168 | 169 | // now create store 170 | var store = new Store(reducer, state); 171 | 172 | // stream states to view 173 | store.subscribe( 174 | (state)=> { 175 | console.log('spec state: ', state) 176 | }, 177 | error=> console.log('error ', error), 178 | () => { 179 | console.log('completed.'); 180 | done(); 181 | } 182 | ); 183 | 184 | // dispatch actions using the dispatcher$ BehaviorSubject 185 | var action = { 186 | type: "INC" 187 | } 188 | store.dispatcher$.next(action); 189 | 190 | ``` 191 | 192 | ## Luna's different signature for asynchronous actions 193 | 194 | Here is the syntax with redux-thunk: 195 | 196 | ```typescript 197 | // But what do you do when you need to start an asynchronous action, 198 | // such as an API call, or a router transition? 199 | 200 | // Meet thunks. 201 | // A thunk is a function that returns a function. 202 | // This is a thunk. 203 | 204 | function makeASandwichWithSecretSauce(forPerson) { 205 | 206 | // Invert control! 207 | // Return a function that accepts `dispatch` so we can dispatch later. 208 | // Thunk middleware knows how to turn thunk async actions into actions. 209 | 210 | return function (dispatch) { 211 | return fetchSecretSauce().then( 212 | sauce => dispatch(makeASandwich(forPerson, sauce)), 213 | error => dispatch(apologize('The Sandwich Shop', forPerson, error)) 214 | ); 215 | }; 216 | } 217 | 218 | // Thunk middleware lets me dispatch thunk async actions 219 | // as if they were actions! 220 | 221 | store.dispatch( 222 | makeASandwichWithSecretSauce('Me') 223 | ); 224 | ``` 225 | 226 | The signature for the returned thunk has `dispatch` and `getValue`. I find this signature kind of arbitrary. Here with Luna, you can do: 227 | 228 | ```typescript 229 | // But what do you do when you need to start an asynchronous action, 230 | // such as an API call, or a router transition? 231 | 232 | // Meet thunks. 233 | // A thunk is a function that returns a function. 234 | // This is a thunk. 235 | 236 | function makeASandwichWithSecretSauce(forPerson) { 237 | 238 | // Invert control! 239 | // Return a function that accepts `dispatch` so we can dispatch later. 240 | // Thunk middleware knows how to turn thunk async actions into actions. 241 | 242 | return function () { 243 | var _store = this; 244 | return fetchSecretSauce().then( 245 | sauce => _store.dispatch(makeASandwich(forPerson, sauce)), 246 | error => _store.dispatch(apologize('The Sandwich Shop', forPerson, error)) 247 | ); 248 | }; 249 | } 250 | 251 | // Thunk middleware lets me dispatch thunk async actions 252 | // as if they were actions! 253 | 254 | store.dispatch( 255 | makeASandwichWithSecretSauce('Me') 256 | ); 257 | ``` 258 | 259 | ## Plans next 260 | 261 | Personally I think documentation is the most important part of a library, and for making everyone's life easier. Bad documentation wastes people's time. 262 | 263 | If you would like to help, besides code you can create even larger impact by writing up examples. Redux (and luna) is a simple idea. Let's make it easier for people to understand the concept and start doing things that they set-out to do asap. 264 | 265 | ### Todo List 266 | 267 | - [ ] use immutable in the test instead. Current form is too sloppy! 268 | - [ ] more testing cases with complicated stores 269 | - better store life-cycle support 270 | 271 | ## Acknowledgement 272 | 273 | This library is influenced by @jas-chen's work on redux-core, and received help from @fxck and @robwormald. 274 | 275 | Luna is part of my effort on re-writting [escherpad](http://www.escherpad.com), a beautiful real-time collaborative notebook supporting real-time LaTeX, collaborative Jupyter notebook, and a WYSIWYG rich-text editor. 276 | 277 | ## About Ge 278 | 279 | I'm a graduate student studying quantum information and quantum computer at University of Chicago. When I'm not tolling away in a cleanroom or working on experiments, I write `(java|type)script` to relax. You can find my publications here: [google scholar](https://scholar.google.com/citations?user=kNtDoX8AAAAJ&hl=en) 280 | 281 | ## LICENSING 282 | 283 | MIT. 284 | --------------------------------------------------------------------------------