UNPKG

simred

Version:

A simple redux-like application state manager

295 lines (259 loc) 7.81 kB
/** * @module Simred * A global store manage */ import "core-js/stable" import "regenerator-runtime/runtime" import * as deep from './deep' let _state = {} let _reducers = {} let _listeners = [] let _middlewares = [] /** * Calls every registered listener to notify a state update * @param {State} state A copy of updated state * @private */ const onStateUpdate = (state) => { _listeners.forEach(listener => { listener(state) }) } /** * Updates the state by deeply merging the partial update into the original state * @param {State} stateCopy A Copy of the state * @param {State} partialState The updated data * @param {string} name The sub part of the state to update * @private */ const updateState = (stateCopy, partialState, name) => { let newState = {} if (partialState) { if (name) { newState = deep.copy(_state) newState[name] = deep.merge(newState[name], partialState) } else { newState = deep.merge(stateCopy, partialState) } } if (!deep.equal(_state, newState)) { _state = newState onStateUpdate(_state) } } /** * Calls every registered middleware to notify an action took place * This is called *after* the state was updated and listeners notified * @param {string} actionName * @param {object|array} payload * @private */ const applyMiddlewares = (store) => (actionName, payload) => { _middlewares.reverse().reduce((next, middleware) => { return () => { middleware(store)(next)(actionName, payload) } }, () => { })() } let _applyMiddlewares = () => { } /** * Bind an action to its state slice * Actions can return promises * @param {(state, actions, args...) => state} fn The action to bind * @param {string} parentName The name of the reducer this actions belongs to * @param {string} actionName The action name * @returns {Function} (args...) => void * @private */ const wrapFunction = (fn, parentName, actionName) => { return (...args) => { const stateCopy = deep.copy(parentName ? _state[parentName] : _state) const partialState = fn(stateCopy, _reducers)(...args) if (partialState instanceof Promise) { partialState.then(result => { updateState(stateCopy, result, parentName) _applyMiddlewares(`${parentName}.${actionName}`, args) }) } else { updateState(stateCopy, partialState, parentName) _applyMiddlewares(`${parentName}.${actionName}`, args) } } } /** * Binds all actions in a reducer to the state * @param {object} actions All functions to bind * @param {string} parentName Reducer name * @returns {object} * @private */ const wrapActions = (actions, parentName) => { return Object.keys(actions).reduce((acc, name) => { if (typeof actions[name] === 'function') { acc[name] = wrapFunction(actions[name], parentName, name) if (!_reducers[name]) { Object.defineProperty(_reducers, name, { configurable: false, enumerable: true, writable: false, value: acc[name] }) } else { console.warn(`${parentName}.${name}: Root reducer already has a function "${name}". Only the one assigned first will be available as "store.actions.${name}()". This one will be available under "store.actions.${parentName}.${name}()`) } } else { throw new Error(`Invalid reducer: ${name} is not a function`) } return acc }, {}) } /** * Creates a reducer * All actions in the reducer will be called with a state slice of the shape of `initialState`, * and will never see the global state. * If `initialState == undefined`, actions will be called with the whole global state. * This allows actions to handle many-to-many relations between state slices. * @param {object} actions * @param {object} [initialState] */ export const createReducer = (actions, initialState) => { const reducer = { actions } if (initialState != undefined) { reducer.state = initialState } return reducer } /** * Decorator to create a reducer * @example * const reducer = withInitialState(0)({ * increment: (state, actions) => state + 1, * decrement: (state, actions) => state - 1 * }) */ export const withInitialState = (initialState) => (actions) => { return createReducer(actions, initialState) } /** * @public * @static * @memberof Simred * Returns a composed reducer * @param {Reducer[]} reducers * @returns {Reducer} * @example * // has actions: add() remove(), and state: { items: [] } * const reducerA = createReducer(actionsA, stateA) * // has actions: setEditing(), and state: { is_editing: false } * const reducerB = createReducer(actionsB, stateB) * * // has actions: add() remove() setEditing(), and state: { is_editing: false, items: [] } * const composedReducer = composeReducer(reducerA, reducerB) */ export const composeReducers = (...reducers) => { return reducers.reduce((composed, reducer) => { composed = { actions: { ...composed.actions, ...reducer.actions }, state: { ...composed.state, ...reducer.state } } }, { actions: {}, state: {}}) } /** * @public * @static * @memberof Simred * Returns a copy of the global state * @returns {State} */ export function getState() { return deep.copy(_state) } /** * @public * @static * @memberof Simred * Returns all reducers * @returns {object} */ export function getActions() { return _reducers } export default { /** * Creates store * @param {object} reducers The combined reducers * @param {object} [state] An optional initial state to initialize the store with * @return {Store} */ createStore: (reducers, state, middlewares = []) => { if (!reducers) throw new Error('reducers cannot be undefined') // init globals _state = {} _reducers = {} // If createStore is called with a state, use this if (state) { _state = state } // build _state and _reducers try { Object.keys(reducers).forEach(key => { const { actions, state } = reducers[key] let stateName = undefined if (state) { // reducer is linked to a state slice if (_state[key]) { // global state received an initial value _state[key] = deep.merge(state, _state[key]) } else { // use reducer's initialState _state[key] = deep.copy(state) } stateName = key // the state slice name } const wrappedAction = wrapActions(actions, stateName) // Add the action to the rootReducer as a readonly property Object.defineProperty(_reducers, key, { configurable: false, enumerable: true, writable: false, value: wrappedAction }) }) } catch (e) { throw e } const store = { // Returns a copy of the current state getState: () => deep.copy(_state), // Suscribes to every state change subscribe: (listener) => { function unsubcribe() { const i = _listeners.indexOf(listener) _listeners.splice(i, 1) } _listeners.push(listener) return unsubcribe }, // Add a middleware that will called after every action was called addMiddleware: (middleware) => { _middlewares.push(middleware) } } _applyMiddlewares = applyMiddlewares(store) middlewares.forEach(middleware => _middlewares.push(middleware)) _applyMiddlewares('@@init', [_state]) Object.defineProperty(store, 'actions', { configurable: false, enumerable: false, set: () => { throw new Error('Stores actions are readonly') }, get: () => _reducers }) return store }, getState, getActions, }