import R from 'remeda'; import u from 'updeep'; import { createStore as reduxCreateStore } from 'redux'; import { buildSelectors } from './selectors.js'; import { buildUpreducer } from './upreducer.js'; import { action } from './actions.js'; function isActionGen(action) { return typeof action === 'function' && action.type; } /** * Updux configuration object * @typedef {Object} Updux config * @property {Object} actions - Actions to be made available to the updux. */ export class Updux { #localInitial = {}; #subduxes = {}; #actions; #mutations = {}; #config = {}; #selectors = {}; constructor(config = {}) { this.#config = config; this.#localInitial = config.initial ?? {}; this.#subduxes = config.subduxes ?? {}; this.#actions = R.mapValues(config.actions ?? {}, (arg, name) => isActionGen(arg) ? arg : action(name, arg), ); Object.entries(this.#subduxes).forEach(([slice, sub]) => this.#addSubduxActions(slice, sub), ); this.#selectors = buildSelectors( config.selectors, config.splatSelectors, this.#subduxes, ); } #addSubduxActions(_slice, subdux) { if (!subdux.actions) return; // TODO action 'blah' defined multiple times: Object.entries(subdux.actions).forEach(([action, gen]) => { if (this.#actions[action]) { if (this.#actions[action] === gen) return; throw new Error(`action '${action}' already defined`); } this.#actions[action] = gen; }); } get selectors() { return this.#selectors; } get actions() { return this.#actions; } get initial() { if (Object.keys(this.#subduxes).length === 0) return this.#localInitial; return Object.assign( {}, this.#localInitial, R.mapValues(this.#subduxes, ({ initial }) => initial), ); } get reducer() { return (state, action) => this.upreducer(action)(state); } get upreducer() { return buildUpreducer(this.#mutations, this.#subduxes); } /** * * @param {string | Function} action - Action triggering the mutation. If * the action is a string, it has to have been previously declared for this * updux, but if it's a function generator, it'll be automatically added to the * updux if not already present (the idea being that making a typo on a string * is easy, but passing a wrong function very more unlikely). * @param {Function} mutation - Mutating function. * @return {void} */ setMutation(action, mutation) { // TODO option strict: false to make it okay to auto-create // the actions as strings? if (action.type) { if (!this.#actions[action.type]) { this.#actions[action.type] = action; } else if (this.#actions[action.type] !== action) { throw new Error( `action '${action.type}' not defined for this updux or definition is different`, ); } action = action.type; } if (!this.#actions[action]) { throw new Error(`action '${action}' is not defined`); } this.#mutations[action] = mutation; } get mutations() { return this.#mutations; } createStore(initial = undefined, enhancerGenerator = undefined) { // const enhancer = (enhancerGenerator ?? applyMiddleware)( // this.middleware // ); const store = reduxCreateStore( this.reducer, initial ?? this.initial, //enhancer ); store.actions = this.actions; // store.selectors = this.selectors; // store.getState = R.merge( // store.getState, // R.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; } } export const dux = (config) => new Updux(config);