From 0ae39f1b67e269216ce3d45b870e448f6dbf21d7 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Wed, 13 Sep 2017 21:11:01 +0900 Subject: add simple store --- src/store/index.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/store/index.js (limited to 'src/store') diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 0000000..bcb65d6 --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,27 @@ +class Store { + constructor(reducer, catcher) { + this.reducer = reducer; + this.catcher = catcher; + this.state = this.reducer(undefined, {}); + } + + dispatch(action) { + if (action instanceof Promise) { + action.then((a) => { + this.state = this.reducer(this.state, a); + }).catch(this.catcher) + } else { + try { + this.state = this.reducer(this.state, action); + } catch (e) { + this.catcher(e); + } + } + } +} + +const empty = () => {}; + +export function createStore(reducer, catcher = empty) { + return new Store(reducer, catcher); +} -- cgit v1.2.3 From c42ac8fac48f9d56b54af4818917082fda9af21e Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Thu, 14 Sep 2017 21:14:13 +0900 Subject: improve store and reducers --- src/background/index.js | 8 +++----- src/reducers/index.js | 7 +++++++ src/store/index.js | 21 +++++++++++++++++++-- 3 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 src/reducers/index.js (limited to 'src/store') diff --git a/src/background/index.js b/src/background/index.js index 0c3adce..e9757b9 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -2,12 +2,12 @@ import * as keys from './keys'; import * as inputActions from '../actions/input'; import * as operationActions from '../actions/operation'; import backgroundReducers from '../reducers/background'; +import reducers from '../reducers'; import commandReducer from '../reducers/command'; import inputReducers from '../reducers/input'; import * as store from '../store' -const emptyReducer = (state) => state; -const emptyStore = store.createStore(emptyReducer, (e) => { +const backgroundStore = store.createStore(reducers, (e) => { console.error('Vim-Vixen:', e); }); let inputState = inputReducers(undefined, {}); @@ -27,9 +27,7 @@ const keyQueueChanged = (sender, prevState, state) => { return Promise.resolve(); } let action = keys.defaultKeymap[matched]; - emptyStore.dispatch(operationActions.exec(action, sender), (e) => { - console.error('Vim-Vixen:', e); - }); + backgroundStore.dispatch(operationActions.exec(action, sender)); return handleMessage(inputActions.clearKeys(), sender).then(() => { return backgroundReducers(undefined, action, sender).then(() => { return browser.tabs.sendMessage(sender.tab.id, action); diff --git a/src/reducers/index.js b/src/reducers/index.js new file mode 100644 index 0000000..d49af7d --- /dev/null +++ b/src/reducers/index.js @@ -0,0 +1,7 @@ +const defaultState = { +}; + +export default function reducer(state = defaultState/*, action = {}*/) { + return Object.assign({}, state, { + }); +} diff --git a/src/store/index.js b/src/store/index.js index bcb65d6..841fd17 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -3,21 +3,38 @@ class Store { this.reducer = reducer; this.catcher = catcher; this.state = this.reducer(undefined, {}); + this.subscribers = []; } dispatch(action) { if (action instanceof Promise) { action.then((a) => { - this.state = this.reducer(this.state, a); + this.transitNext(a); }).catch(this.catcher) } else { try { - this.state = this.reducer(this.state, action); + this.transitNext(action); } catch (e) { this.catcher(e); } } } + + getState() { + return this.state; + } + + subscribe(callback) { + this.subscribers.push(callback); + } + + transitNext(action) { + let newState = this.reducer(this.state, action); + if (JSON.stringify(this.state) !== JSON.stringify(newState)) { + this.state = newState; + this.subscribers.forEach(f => f.call()) + } + } } const empty = () => {}; -- cgit v1.2.3 From bf75603e1514e64cd608fb90f4c47afeec5c37f9 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sat, 16 Sep 2017 22:24:27 +0900 Subject: propagate sender object --- src/background/index.js | 63 +++++++++++++++++++++++++------------------------ src/store/index.js | 16 +++++++------ 2 files changed, 41 insertions(+), 38 deletions(-) (limited to 'src/store') diff --git a/src/background/index.js b/src/background/index.js index cbd4721..ef1b881 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -8,10 +8,13 @@ import messages from '../messages'; import * as store from '../store' let prevInput = []; -const backgroundStore = store.createStore(reducers, (e) => { +const backgroundStore = store.createStore(reducers, (e, sender) => { console.error('Vim-Vixen:', e); + if (sender) { + backgroundStore.dispatch(consoleActions.showError(e.message), sender); + } }); -backgroundStore.subscribe(() => { +backgroundStore.subscribe((sender) => { let currentInput = backgroundStore.getState().input if (JSON.stringify(prevInput) === JSON.stringify(currentInput)) { return @@ -21,54 +24,52 @@ backgroundStore.subscribe(() => { if (currentInput.keys.length === 0) { return; } - - browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => { - if (tabs.length > 0) { - return keyQueueChanged(tabs[0], backgroundStore.getState()); - } - }); + if (sender) { + return keyQueueChanged(backgroundStore.getState(), sender); + } }); -backgroundStore.subscribe(() => { - browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => { - if (tabs.length > 0) { - return browser.tabs.sendMessage(tabs[0].id, { - type: messages.STATE_UPDATE, - state: backgroundStore.getState() - }); - } - }); +backgroundStore.subscribe((sender) => { + if (sender) { + return browser.tabs.sendMessage(sender.tab.id, { + type: messages.STATE_UPDATE, + state: backgroundStore.getState() + }); + } }); -const keyQueueChanged = (sendToTab, state) => { +const keyQueueChanged = (state, sender) => { let prefix = keys.asKeymapChars(state.input.keys); let matched = Object.keys(keys.defaultKeymap).filter((keys) => { return keys.startsWith(prefix); }); if (matched.length == 0) { - backgroundStore.dispatch(inputActions.clearKeys()); + backgroundStore.dispatch(inputActions.clearKeys(), sender); return Promise.resolve(); } else if (matched.length > 1 || matched.length === 1 && prefix !== matched[0]) { return Promise.resolve(); } let action = keys.defaultKeymap[matched]; - backgroundStore.dispatch(operationActions.exec(action, sendToTab)); - backgroundStore.dispatch(inputActions.clearKeys()); - return browser.tabs.sendMessage(sendToTab.id, action); + backgroundStore.dispatch(operationActions.exec(action, sender.tab), sender); + backgroundStore.dispatch(inputActions.clearKeys(), sender); }; -browser.runtime.onMessage.addListener((message) => { +const handleMessage = (message, sender) => { switch (message.type) { case messages.KEYDOWN: - backgroundStore.dispatch(inputActions.keyPress(message.code, message.ctrl)); - break; + return backgroundStore.dispatch(inputActions.keyPress(message.code, message.ctrl), sender); case messages.CONSOLE_BLURRED: - backgroundStore.dispatch(consoleActions.hide()); - break; + return backgroundStore.dispatch(consoleActions.hide(), sender); case messages.CONSOLE_ENTERED: - backgroundStore.dispatch(commandActions.exec(message.text)); - break; + return backgroundStore.dispatch(commandActions.exec(message.text), sender); case messages.CONSOLE_CHANGEED: - backgroundStore.dispatch(commandActions.complete(message.text)); - break; + return backgroundStore.dispatch(commandActions.complete(message.text), sender); + } +} + +browser.runtime.onMessage.addListener((message, sender) => { + try { + handleMessage(message, sender); + } catch (e) { + backgroundStore.dispatch(consoleActions.showError(e.message), sender); } }); diff --git a/src/store/index.js b/src/store/index.js index 841fd17..a0a7791 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -6,16 +6,18 @@ class Store { this.subscribers = []; } - dispatch(action) { + dispatch(action, sender) { if (action instanceof Promise) { action.then((a) => { - this.transitNext(a); - }).catch(this.catcher) + this.transitNext(a, sender); + }).catch((e) => { + this.catcher(e, sender); + }); } else { try { - this.transitNext(action); + this.transitNext(action, sender); } catch (e) { - this.catcher(e); + this.catcher(e, sender); } } } @@ -28,11 +30,11 @@ class Store { this.subscribers.push(callback); } - transitNext(action) { + transitNext(action, sender) { let newState = this.reducer(this.state, action); if (JSON.stringify(this.state) !== JSON.stringify(newState)) { this.state = newState; - this.subscribers.forEach(f => f.call()) + this.subscribers.forEach(f => f(sender)); } } } -- cgit v1.2.3 From fb1d3b5962531a004bb14f7f78796f77149ee7e7 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sat, 16 Sep 2017 23:17:42 +0900 Subject: add store test and fix store --- src/store/index.js | 7 ++- test/store/index.test.js | 111 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 test/store/index.test.js (limited to 'src/store') diff --git a/src/store/index.js b/src/store/index.js index a0a7791..2d08296 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -2,8 +2,12 @@ class Store { constructor(reducer, catcher) { this.reducer = reducer; this.catcher = catcher; - this.state = this.reducer(undefined, {}); this.subscribers = []; + try { + this.state = this.reducer(undefined, {}); + } catch (e) { + catcher(e); + } } dispatch(action, sender) { @@ -20,6 +24,7 @@ class Store { this.catcher(e, sender); } } + return action } getState() { diff --git a/test/store/index.test.js b/test/store/index.test.js new file mode 100644 index 0000000..e19d50e --- /dev/null +++ b/test/store/index.test.js @@ -0,0 +1,111 @@ +import { expect } from "chai"; +import { createStore } from '../../src/store'; + +describe("Store class", () => { + const reducer = (state, action) => { + if (state == undefined) { + return 0; + } + return state + action; + }; + + describe("#dispatch", () => { + it('transit status by immediate action', () => { + let store = createStore(reducer); + store.dispatch(10); + expect(store.getState()).to.equal(10); + + store.dispatch(-20); + expect(store.getState()).to.equal(-10); + }); + + it('returns next state by immediate action', () => { + let store = createStore(reducer); + let dispatchedAction = store.dispatch(11); + expect(dispatchedAction).to.equal(11); + }); + + it('transit status by Promise action', () => { + let store = createStore(reducer); + let p1 = Promise.resolve(10); + + return store.dispatch(p1).then(() => { + expect(store.getState()).to.equal(10); + }).then(() => { + store.dispatch(Promise.resolve(-20)); + }).then(() => { + expect(store.getState()).to.equal(-10); + }); + }); + + it('returns next state by promise action', () => { + let store = createStore(reducer); + let dispatchedAction = store.dispatch(Promise.resolve(11)); + return dispatchedAction.then((value) => { + expect(value).to.equal(11); + }); + }); + }); + + describe("#subscribe", () => { + it('invoke callback', (done) => { + let store = createStore(reducer); + store.subscribe(() => { + expect(store.getState()).to.equal(15); + done(); + }); + store.dispatch(15); + }); + + it('propagate sender object', (done) => { + let store = createStore(reducer); + store.subscribe((sender) => { + expect(sender).to.equal('sender'); + done(); + }); + store.dispatch(15, 'sender'); + }); + }) + + describe("catcher", () => { + it('catch an error in reducer on initializing by immediate action', (done) => { + let store = createStore(() => { + throw new Error(); + }, (e) => { + expect(e).to.be.an('error'); + done(); + }); + }); + + it('catch an error in reducer on initializing by immediate action', (done) => { + let store = createStore((state, action) => { + if (state === undefined) return 0; + throw new Error(); + }, (e) => { + expect(e).to.be.an('error'); + done(); + }); + store.dispatch(20); + }); + + it('catch an error in reducer on initializing by promise action', (done) => { + let store = createStore((state, action) => { + if (state === undefined) return 0; + throw new Error(); + }, (e) => { + expect(e).to.be.an('error'); + done(); + }); + store.dispatch(Promise.resolve(20)); + }); + + it('catch an error in promise action', (done) => { + let store = createStore((state, action) => 0, (e) => { + expect(e).to.be.an('error'); + done(); + }); + store.dispatch(new Promise(() => { throw new Error() })); + }); + }) +}); + -- cgit v1.2.3