updux/src/Updux.js

209 lines
5.5 KiB
JavaScript
Raw Normal View History

2021-09-28 22:13:22 +00:00
import moize from 'moize';
import u from '@yanick/updeep';
2021-10-07 19:08:21 +00:00
import { createStore as reduxCreateStore, applyMiddleware } from 'redux';
2021-10-08 23:56:55 +00:00
import { map, mapValues } from 'lodash-es';
2021-09-28 22:13:22 +00:00
import { buildInitial } from './buildInitial/index.js';
2021-10-07 16:04:15 +00:00
import { buildActions } from './buildActions/index.js';
import { buildSelectors } from './buildSelectors/index.js';
2021-09-28 22:13:22 +00:00
import { action } from './actions.js';
2021-10-07 16:04:15 +00:00
import { buildUpreducer } from './buildUpreducer.js';
2021-10-08 23:56:55 +00:00
import {
buildMiddleware,
augmentMiddlewareApi,
} from './buildMiddleware/index.js';
function _subscribeToStore(store, subscriptions) {
for (const sub of subscriptions) {
const subscriber = sub(store);
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, unsub) => subscription(localStore)(state[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);
};
};
2021-09-28 22:13:22 +00:00
/**
* @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 = {};
2021-10-07 16:04:15 +00:00
/** @type Record<string,Function> */
2021-09-28 22:13:22 +00:00
#actions = {};
2021-09-29 14:21:17 +00:00
#selectors = {};
2021-10-07 16:04:15 +00:00
#mutations = {};
2021-10-07 19:08:21 +00:00
#effects = [];
2021-10-08 23:56:55 +00:00
#subscriptions = [];
2021-09-28 22:13:22 +00:00
constructor(config) {
this.#initial = config.initial ?? {};
this.#subduxes = config.subduxes ?? {};
this.#actions = config.actions ?? {};
2021-09-29 14:21:17 +00:00
this.#selectors = config.selectors ?? {};
2021-10-07 16:04:15 +00:00
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`);
});
2021-10-07 19:08:21 +00:00
if (config.effects) {
this.#effects = Object.entries(config.effects);
}
2021-10-08 23:56:55 +00:00
this.#subscriptions = config.subscriptions ?? [];
2021-09-28 22:13:22 +00:00
}
2021-10-07 16:04:15 +00:00
#memoInitial = moize(buildInitial);
2021-09-28 22:13:22 +00:00
#memoActions = moize(buildActions);
2021-09-29 14:21:17 +00:00
#memoSelectors = moize(buildSelectors);
2021-10-07 16:04:15 +00:00
#memoUpreducer = moize(buildUpreducer);
2021-10-07 19:08:21 +00:00
#memoMiddleware = moize(buildMiddleware);
2021-10-08 23:56:55 +00:00
get subscriptions() {
const subscriptions = [...this.#subscriptions].map((s) =>
memoizeSubscription(s)
);
return [
...subscriptions,
...map(this.#subduxes, (v, k) =>
v.subscriptions.map(sliceSubscriber(k, v))
).flat(),
];
}
2021-10-07 19:08:21 +00:00
get middleware() {
return this.#memoMiddleware(
this.#effects,
this.actions,
this.selectors,
this.#subduxes
);
}
2021-09-28 22:13:22 +00:00
get initial() {
2021-10-07 16:04:15 +00:00
return this.#memoInitial(this.#initial, this.#subduxes);
2021-09-28 22:13:22 +00:00
}
2021-10-07 16:04:15 +00:00
/**
* @return {Record<string,Function>}
*/
2021-09-28 22:13:22 +00:00
get actions() {
return this.#memoActions(this.#actions, this.#subduxes);
}
2021-09-29 13:40:02 +00:00
get selectors() {
2021-10-07 16:04:15 +00:00
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);
2021-09-29 13:40:02 +00:00
}
2021-10-08 23:56:55 +00:00
addSubscription(subscription) {
this.#subscriptions = [...this.#subscriptions, subscription];
}
2021-09-28 22:13:22 +00:00
addAction(type, payloadFunc) {
2021-10-07 16:04:15 +00:00
const theAction = action(type, payloadFunc);
2021-09-28 22:13:22 +00:00
2021-10-07 17:27:14 +00:00
this.#actions = { ...this.#actions, [type]: theAction };
2021-09-28 22:13:22 +00:00
return theAction;
}
2021-09-29 13:40:02 +00:00
addSelector(name, func) {
2021-10-07 17:27:14 +00:00
this.#selectors = {
...this.#selectors,
[name]: func,
};
2021-09-29 14:21:17 +00:00
return func;
2021-09-29 13:40:02 +00:00
}
2021-10-07 16:04:15 +00:00
addMutation(name, mutation) {
if (typeof name === 'function') name = name.type;
this.#mutations = {
...this.#mutations,
[name]: mutation,
};
return this;
}
2021-10-07 17:27:14 +00:00
2021-10-08 16:42:08 +00:00
addEffect(action, effect) {
this.#effects = [...this.#effects, [action, effect]];
}
2021-10-07 19:08:21 +00:00
2021-10-08 16:42:08 +00:00
createStore() {
2021-10-07 19:08:21 +00:00
const store = reduxCreateStore(
this.reducer,
this.initial,
applyMiddleware(this.middleware)
);
2021-10-07 17:27:14 +00:00
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));
};
}
2021-10-08 23:56:55 +00:00
_subscribeToStore(store, this.subscriptions);
2021-10-07 17:27:14 +00:00
return store;
}
2021-09-28 22:13:22 +00:00
}