import moize from 'moize'; import u from '@yanick/updeep'; import { createStore as reduxCreateStore, applyMiddleware } from 'redux'; import { map, mapValues } from 'lodash-es'; import { buildInitial } from './buildInitial/index.js'; import { buildActions } from './buildActions/index.js'; import { buildSelectors } from './buildSelectors/index.js'; import { action } from './actions.js'; import { buildUpreducer } from './buildUpreducer.js'; import { buildMiddleware, augmentMiddlewareApi, } from './buildMiddleware/index.js'; function _subscribeToStore(store, subscriptions) { for (const sub of subscriptions) { const subscriber = sub({ ...store, subscribe(subscriber) { let previous; const unsub = store.subscribe(() => { const state = store.getState(); if (state === previous) return; let p = previous; previous = state; subscriber(state, p, unsub); }); }, }); let unsub = store.subscribe(() => subscriber(store.getState(), unsub)); } } const sliceSubscriber = (slice, subdux) => (subscription) => (store) => { let localStore = augmentMiddlewareApi( { ...store, getState: () => store.getState()[slice], }, subdux.actions, subdux.selectors ); return (state, previous, unsub) => subscription(localStore)( state[slice], previous && previous[slice], unsub ); }; const memoizeSubscription = (subscription) => (store) => { let previous = undefined; const subscriber = subscription(store); return (state, unsub) => { if (state === previous) return; let p = previous; previous = state; subscriber(state, p, unsub); }; }; /** * @public * `Updux` is a way to minimize and simplify the boilerplate associated with the * creation of a `Redux` store. It takes a shorthand configuration * object, and generates the appropriate reducer, actions, middleware, etc. * In true `Redux`-like fashion, upduxes can be made of sub-upduxes (`subduxes` for short) for different slices of the root state. */ export class Updux { #initial = {}; #subduxes = {}; /** @type Record */ #actions = {}; #selectors = {}; #mutations = {}; #effects = []; #subscriptions = []; constructor(config) { this.#initial = config.initial ?? {}; this.#subduxes = config.subduxes ?? {}; if (config.actions) { for (const [type, actionArg] of Object.entries(config.actions)) { if (typeof actionArg === 'function' && actionArg.type) { this.#actions[type] = actionArg; } else { this.#actions[type] = action(type, actionArg); } } } this.#selectors = config.selectors ?? {}; this.#mutations = config.mutations ?? {}; Object.keys(this.#mutations) .filter((action) => action !== '*') .filter((action) => !this.actions.hasOwnProperty(action)) .forEach((action) => { throw new Error(`action '${action}' is not defined`); }); if (config.effects) { this.#effects = Object.entries(config.effects); } this.#subscriptions = config.subscriptions ?? []; } #memoInitial = moize(buildInitial); #memoActions = moize(buildActions); #memoSelectors = moize(buildSelectors); #memoUpreducer = moize(buildUpreducer); #memoMiddleware = moize(buildMiddleware); get subscriptions() { return this.#subscriptions; } get middleware() { return this.#memoMiddleware( this.#effects, this.actions, this.selectors, this.#subduxes ); } get initial() { return this.#memoInitial(this.#initial, this.#subduxes); } /** * @return {Record} */ get actions() { return this.#memoActions(this.#actions, this.#subduxes); } get selectors() { return this.#memoSelectors(this.#selectors, this.#subduxes); } get upreducer() { return this.#memoUpreducer( this.initial, this.#mutations, this.#subduxes ); } get reducer() { return (state, action) => this.upreducer(action)(state); } addSubscription(subscription) { this.#subscriptions = [...this.#subscriptions, subscription]; } addAction(type, payloadFunc) { const theAction = action(type, payloadFunc); this.#actions = { ...this.#actions, [type]: theAction }; return theAction; } addSelector(name, func) { this.#selectors = { ...this.#selectors, [name]: func, }; return func; } addMutation(name, mutation) { if (typeof name === 'function') name = name.type; this.#mutations = { ...this.#mutations, [name]: mutation, }; return this; } addEffect(action, effect) { this.#effects = [...this.#effects, [action, effect]]; } subscribeTo(store, subscription) { const localStore = augmentMiddlewareApi( { ...store, subscribe: (subscriber) => this.subscribeTo(store, () => subscriber), }, this.actions, this.selectors ); const subscriber = subscription(localStore); let previous; const unsub = store.subscribe(() => { const state = store.getState(); if (state === previous) return; let p = previous; previous = state; subscriber(state, p, unsub); }); } createStore() { const store = reduxCreateStore( this.reducer, this.initial, applyMiddleware(this.middleware) ); store.actions = this.actions; store.selectors = mapValues(this.selectors, (selector) => { return (...args) => { let result = selector(store.getState()); if (typeof result === 'function') return result(...args); return result; }; }); for (const action in this.actions) { store.dispatch[action] = (...args) => { return store.dispatch(this.actions[action](...args)); }; } this.#subscriptions.forEach((sub) => this.subscribeTo(store, sub)); for (const subdux in this.#subduxes) { const localStore = { ...store, getState: () => store.getState()[subdux], }; for (const subscription of this.#subduxes[subdux].subscriptions) { this.#subduxes[subdux].subscribeTo(localStore, subscription); } } return store; } }