# Source: Updux.js

    /* 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;