UNPKG

minska

Version:

A simple flux like store with reducers and effects

192 lines (152 loc) 5.35 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); // Formats messages to be prefixed with `minska: ` // str => str const formatMsg = msg => `minska: ${msg}`; // Test if all keys in a object are functions // obj => bool const validateObjHasOnlyFunctions = obj => Object.keys(obj).map(item => typeof obj[item] === 'function').every(i => i === true); var u = { formatMsg, validateObjHasOnlyFunctions }; // Private properties/methods const State = Symbol('state'); const Emit = Symbol('emit'); class Store { constructor({ state = {}, reducers = {}, effects = {}, onError = () => {}, onAction = () => {}, onChange = () => {} } = {}) { // Test all the namespaced reducers are functions if (!u.validateObjHasOnlyFunctions(reducers)) { throw new Error(u.formatMsg('All reducers should be functions.')); } // Test all the namespaced effects are functions if (!u.validateObjHasOnlyFunctions(effects)) { throw new Error(u.formatMsg('All effects should be functions.')); } // Store model this[State] = state; this.reducers = reducers; this.effects = effects; // List of subscriptions this.subscriptions = []; // Events you can subscribe too this.events = ['onError', 'onAction', 'onChange']; // (error, state) this.onError = onError; // (action, data, state) this.onAction = onAction; // (nextState, state) this.onChange = onChange; } // Return the current store state // => obj get state() { return this[State]; } // Set the new store state // any => null set state(nextState) { this.onChange(this.state, nextState); this[State] = nextState; } // Update the state by using an effect/reducer that matches an action name // (str, any|!func) => promise|obj send(action, data) { if (typeof action !== 'string') { const error = new Error(u.formatMsg('Action name must be a string.')); this[Emit]('onError', error, this.state); throw error; } if (typeof data === 'function') { const error = new Error(u.formatMsg('Data must be a serializable value. A function was passed.')); this[Emit]('onError', error, this.state); throw error; } // Emit the `onAction` hook this[Emit]('onAction', action, data, this.state); // Get the namespace from the action if there is one const ns = action.includes(':') ? action.split(':')[0] : null; // Get the slice of state that matches the namespace let stateSlice = this.state; if (ns && stateSlice[ns]) { stateSlice = stateSlice[ns]; } else if (ns) { stateSlice = {}; } const effect = this.effects[action]; const reducer = this.reducers[action]; // If no effect or reducer can be found, then throw an error, // and also notify the `onError` hook if (!effect && !reducer) { const error = new Error(u.formatMsg(`Can't find reducer or effect with name: ${action}.`)); this[Emit]('onError', error, this.state); throw error; } // Call the matching effect. It should return a promise so they can do async things. if (effect) { return Promise.resolve(effect(this.state, data, this.send.bind(this))); } // Get the result of calling the reducer with the state slice const reduced = Object.assign({}, reducer(stateSlice, data)); // If a namespace is present, then we should add the slice back to the global state const nextState = ns ? Object.assign({}, this.state, { [ns]: reduced }) : reduced; // Emit the `onChange` hook this[Emit]('onChange', nextState, this.state); // Actually change the state this.state = nextState; // Return the next state so send can use it for something return nextState; } // Add subscriptions for events // (str, str|num, fn) => null subscribe(event, id, fn) { if (!this.events.includes(event)) { const error = new Error(u.formatMsg(`${event} is not a valid event you can subscribe to.`)); this[Emit]('onError', error, this.state); throw error; } this.subscriptions.push({ event, id, fn }); } // Remove subscriptions // str|num => null unsubscribe(id) { if (!this.subscriptions.find(s => s.id === id)) { const error = new Error(u.formatMsg(`Can't find subscriber with id "${id}".`)); this[Emit]('onError', error, this.state); throw error; } this.subscriptions = this.subscriptions.filter(s => s.id !== id); } // Notify subscriptions of any events they listen to // (str, any|!func) => null [Emit](event, ...data) { const hook = this[event]; if (hook) hook(...data); this.subscriptions.filter(s => s.event === event).forEach(sub => { sub.fn(...data); }); } } // Combines reducers/effects with namespaces to a flattened object // arr => obj const combine = (...list) => list.reduce((memo, curr) => { const namespace = curr.namespace; const nsKey = `${namespace ? `${namespace}:` : ''}`; Object.keys(curr).forEach(item => { if (item !== 'namespace') { memo[`${nsKey}${item}`] = curr[item]; // eslint-disable-line no-param-reassign } }); return memo; }, {}); exports.Store = Store; exports.combine = combine; exports.utils = u; //# sourceMappingURL=index.js.map