<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>JSDoc: Source: Updux.js</title> <script src="scripts/prettify/prettify.js"> </script> <script src="scripts/prettify/lang-css.js"> </script> <!--[if lt IE 9]> <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> <![endif]--> <link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css"> <link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css"> </head> <body> <div id="main"> <h1 class="page-title">Source: Updux.js</h1> <section> <article> <pre class="prettyprint source linenums"><code>/* TODO change * for leftovers to +, change subscriptions to reactions */ import moize from 'moize'; import u from '@yanick/updeep'; import { createStore as reduxCreateStore, applyMiddleware } from 'redux'; import { get, map, mapValues, merge, difference } 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'; /** * @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 { /** @type { unknown } */ #initial = {}; #subduxes = {}; /** @type Record<string,Function> */ #actions = {}; #selectors = {}; #mutations = {}; #effects = []; #subscriptions = []; #splatSelector = undefined; #splatReaction = undefined; constructor(config) { this.#initial = config.initial ?? {}; this.#subduxes = config.subduxes ?? {}; if (config.subduxes) { this.#subduxes = mapValues(config.subduxes, (sub) => sub instanceof Updux ? sub : new Updux(sub) ); } 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 ?? {}; this.#splatSelector = config.splatSelector; 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 ?? []; this.#splatReaction = config.splatReaction; } #memoInitial = moize(buildInitial); #memoActions = moize(buildActions); #memoSelectors = moize(buildSelectors); #memoUpreducer = moize(buildUpreducer); #memoMiddleware = moize(buildMiddleware); get subscriptions() { return this.#subscriptions; } setSplatSelector(name, f) { this.#splatSelector = [name, f]; } get middleware() { return this.#memoMiddleware( this.#effects, this.actions, this.selectors, this.#subduxes ); } /** @return { import('./Updux').What } */ get initial() { return this.#memoInitial(this.#initial, this.#subduxes); } /** * @return {Record<string,Function>} */ get actions() { return this.#memoActions(this.#actions, this.#subduxes); } get selectors() { return this.#memoSelectors( this.#selectors, this.#splatSelector, 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]]; } splatSubscriber(store, inner, splatReaction) { const cache = {}; return () => (state, previous, unsub) => { const cacheKeys = Object.keys(cache); const newKeys = difference(Object.keys(state), cacheKeys); for (const slice of newKeys) { let localStore = { ...store, getState: () => store.getState()[slice], }; cache[slice] = []; if (typeof splatReaction === 'function') { localStore = { ...localStore, ...splatReaction(localStore, slice), }; } const { unsub, subscriber, subscriberRaw } = inner.subscribeAll(localStore); cache[slice].push({ unsub, subscriber, subscriberRaw }); subscriber(); } const deletedKeys = difference(cacheKeys, Object.keys(state)); for (const deleted of deletedKeys) { for (const inner of cache[deleted]) { inner.subscriber(); inner.unsub(); } delete cache[deleted]; } }; } subscribeTo(store, subscription, setupArgs = []) { const localStore = augmentMiddlewareApi( { ...store, subscribe: (subscriber) => this.subscribeTo(store, () => subscriber), }, this.actions, this.selectors ); const subscriber = subscription(localStore, ...setupArgs); let previous; const memoSub = () => { const state = store.getState(); if (state === previous) return; let p = previous; previous = state; subscriber(state, p, unsub); }; let ret = store.subscribe(memoSub); const unsub = typeof ret === 'function' ? ret : ret.unsub; return { unsub, subscriber: memoSub, subscriberRaw: subscriber, }; } subscribeAll(store) { let results = this.#subscriptions.map((sub) => this.subscribeTo(store, sub) ); for (const subdux in this.#subduxes) { if (subdux !== '*') { const localStore = { ...store, getState: () => get(store.getState(), subdux), }; results.push(this.#subduxes[subdux].subscribeAll(localStore)); } } if (this.#splatReaction) { results.push( this.subscribeTo( store, this.splatSubscriber( store, this.#subduxes['*'], this.#splatReaction ) ) ); } return { unsub: () => results.forEach(({ unsub }) => unsub()), subscriber: () => results.forEach(({ subscriber }) => subscriber()), subscriberRaw: (...args) => results.forEach(({ subscriberRaw }) => subscriberRaw(...args)), }; } createStore(initial) { const store = reduxCreateStore( this.reducer, initial ?? this.initial, applyMiddleware(this.middleware) ); store.actions = this.actions; store.selectors = this.selectors; merge( store.getState, 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.subscribeAll(store); return store; } } const x = new Updux(); x.selectors; </code></pre> </article> </section> </div> <nav> <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Updux.html">Updux</a></li></ul> </nav> <br class="clear"> <footer> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.7</a> on Tue Oct 12 2021 10:16:59 GMT-0400 (Eastern Daylight Time) </footer> <script> prettyPrint(); </script> <script src="scripts/linenumber.js"> </script> </body> </html>