├── .babelrc ├── .gitignore ├── .npmignore ├── README.md ├── index.d.ts ├── index.es ├── package.json └── test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-2" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | index.js 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | index.es 2 | test.js 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PromisedReducer 2 | 3 | Fold promise queue and tell detect sync or async 4 | 5 | ``` 6 | npm install promised-reducer 7 | ``` 8 | 9 | ## Example 10 | 11 | ```js 12 | import PromisedReducer from "promised-reducer"; 13 | 14 | const reducer = new PromisedReducer({count: 0}); 15 | 16 | reducer.on(":process-updating", () => { 17 | // called by each queue 18 | }); 19 | 20 | reducer.on(":process-async-updating", () => { 21 | // called by each async queue (including synced update) 22 | }); 23 | 24 | reducer.on(":start-async-updating", () => { 25 | // called once on start async 26 | }); 27 | 28 | reducer.on(":end-anync-updating", () => { 29 | // called once on end async 30 | }); 31 | 32 | reducer.on(":update", state => { 33 | // emit on non-promise update or end-async-update 34 | console.log(state): 35 | }); 36 | 37 | reducer.update(({count}) => ({count: count + 1})) 38 | // => logging {count: 1} 39 | reducer.update(({count}) => Promise.resolve({count: count + 1})) 40 | reducer.update(({count}) => Promise.resolve({count: count + 1})) 41 | // => logging {count: 3} not 2: queuing promises are reduced to one result. 42 | ``` 43 | 44 | ## Middlewares 45 | 46 | Middleware function type is `(t: T | Promise) => T | Promise`; 47 | Handle Promise.resolve if you need to consider promise by upper middleware chain. 48 | 49 | ```js 50 | const logger = (state_or_promise) => { 51 | return Promise.resolve(state_or_promise) 52 | .then(state => { 53 | console.log("log:", state); 54 | return state; 55 | }) 56 | } 57 | 58 | const reducer = new PromisedReducer({count: 0}, [logger]); 59 | ``` 60 | 61 | ## With Rx 62 | 63 | PromisedReducer extends EventEmitter. Handle it as EventEmitter. 64 | 65 | ```js 66 | const reducer = new PromisedReducer({count: 0}); 67 | const updateStream: Rx.Observable<{count: number;}> = Rx.Observable.formEvent(reducer, ":update") 68 | ``` 69 | 70 | ## LICENSE 71 | 72 | MIT 73 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export default class PromisedReducer { 4 | constructor(initialState: State, middlewares: Function[]); 5 | state: State; 6 | on: (eventName: string, fn: Function) => void; 7 | update(updater: (s?: State) => State | Promise): Promise; 8 | subscribe(): void; 9 | } 10 | -------------------------------------------------------------------------------- /index.es: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from "events"; 2 | 3 | const applyMiddlewares = (middlewares, nextState) => { 4 | return middlewares.reduce((s, next) => { 5 | return next(s); 6 | }, nextState); 7 | } 8 | 9 | export default class PromisedReducer extends EventEmitter { 10 | constructor(initialState, middlewares = []) { 11 | super(); 12 | this.state = initialState ? initialState : {}; 13 | this.middlewares = middlewares ? middlewares : []; 14 | 15 | this.updating = false; 16 | this._updatingQueue = []; // TODO 17 | this._updatingPromise = null; 18 | } 19 | 20 | _finishUp(nextState) { 21 | const inAsync = !!this._updatingPromise; 22 | 23 | if (inAsync) { 24 | this._updatingQueue.length = 0; 25 | this._updatingPromise = null; 26 | this.updating = false; 27 | } 28 | 29 | this.state = nextState; 30 | this.emit(":update", this.state); 31 | 32 | if (inAsync) { 33 | this.emit(":end-async-updating"); 34 | } 35 | return Promise.resolve(); 36 | } 37 | 38 | update(nextStateFn) { 39 | // if app is updating, add fn to queues and return current promise; 40 | if (this.updating) { 41 | this._updatingQueue.push(nextStateFn); 42 | return this._updatingPromise; 43 | } 44 | 45 | // Create state 46 | const promiseOrState = applyMiddlewares(this.middlewares, nextStateFn(this.state)); 47 | 48 | // if state is not promise, exec and resolve at once. 49 | if (!(promiseOrState instanceof Promise)) { 50 | const oldState = this.state; 51 | this._finishUp(promiseOrState); 52 | this.emit(":process-updating", this.state, oldState); 53 | return Promise.resolve(); 54 | } 55 | 56 | // start async updating! 57 | this.updating = true; 58 | this.emit(":start-async-updating"); 59 | 60 | // create callback to response. 61 | // TODO: I want Promise.defer 62 | var endUpdate; 63 | this._updatingPromise = new Promise(done => { 64 | endUpdate = done; 65 | }); 66 | 67 | // drain first async 68 | const lastState = this.state; 69 | promiseOrState.then(nextState => { 70 | this.emit(":process-async-updating", nextState, lastState); 71 | 72 | // if there is left queue after first async, 73 | const updateLoop = (appliedState) => { 74 | const nextFn = this._updatingQueue.shift(); 75 | if (nextFn == null) { 76 | this._finishUp(appliedState); 77 | endUpdate(); 78 | return; 79 | } else { 80 | return Promise.resolve( 81 | applyMiddlewares( 82 | this.middlewares, 83 | nextFn(appliedState) 84 | ) 85 | ).then(s => { 86 | this.emit(":process-async-updating", s, appliedState, this._updatingQueue.length); 87 | this.emit(":process-updating", s, appliedState); 88 | updateLoop(s); // recursive loop 89 | }); 90 | } 91 | } 92 | updateLoop(nextState); 93 | }); 94 | 95 | return this._updatingPromise; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "promised-reducer", 3 | "version": "0.2.0", 4 | "description": "Fold promise queue and tell detect sync or async", 5 | "main": "index.js", 6 | "scripts": { 7 | "prepublish": "babel -o index.js index.es", 8 | "test": "babel-node test.js" 9 | }, 10 | "author": "mizchi", 11 | "license": "MIT", 12 | "directories": { 13 | "test": "test" 14 | }, 15 | "devDependencies": { 16 | "babel-cli": "^6.9.0", 17 | "babel-polyfill": "^6.3.14", 18 | "babel-preset-es2015": "^6.3.13", 19 | "babel-preset-stage-2": "^6.3.13" 20 | }, 21 | "dependencies": {}, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/mizchi/promised-reducer.git" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/mizchi/promised-reducer/issues" 28 | }, 29 | "homepage": "https://github.com/mizchi/promised-reducer#readme" 30 | } 31 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import PromisedReducer from "./index.es"; 2 | const {ok, equal} = require("assert"); 3 | 4 | // helper 5 | function wait(ms = 100) {return new Promise(done => setTimeout(done, ms));} 6 | 7 | (async () => { 8 | var app = new PromisedReducer({count: 0}, []) 9 | // case 1 10 | app.update(s => ({count: 1})); 11 | ok(!app.updating); 12 | console.log("pass case 1"); 13 | 14 | // case 2 15 | const p1 = app.update(async (s) => { 16 | await wait(100); 17 | return {count: 2} 18 | }); 19 | 20 | await p1; 21 | console.log("pass case 2"); 22 | 23 | // case 3 24 | const p2 = app.update(async (s) => { 25 | await wait(100); 26 | return {count: 3} 27 | }); 28 | 29 | const p3 = app.update(async (s) => { 30 | await wait(100); 31 | return {count: 4} 32 | }); 33 | ok(app._updatingQueue.length === 1); 34 | ok(p2 === p3); 35 | await p2; 36 | 37 | // case 4 38 | const p4 = app.update(async (s) => { 39 | await wait(100); 40 | return {count: 3} 41 | }); 42 | 43 | const p5 = app.update(async (s) => { 44 | await wait(100); 45 | return {count: 4} 46 | }); 47 | ok(app._updatingQueue.length === 1); 48 | const p6 = app.update(async (s) => { 49 | await wait(100); 50 | return {count: 2} 51 | }); 52 | ok(p4 === p5); 53 | ok(p5 === p6); 54 | 55 | ok(app._updatingQueue.length === 2); 56 | await p6; 57 | console.log("pass case 4"); 58 | 59 | // finish 60 | console.log("all test passed"); 61 | })().catch(e => { 62 | console.log("catched", e) 63 | throw e; 64 | }); 65 | --------------------------------------------------------------------------------