249 lines
7.0 KiB
JavaScript
249 lines
7.0 KiB
JavaScript
import R from 'remeda';
|
|
import u from 'updeep';
|
|
import { createStore as reduxCreateStore, applyMiddleware } from 'redux';
|
|
|
|
import { buildSelectors } from './selectors.js';
|
|
import { buildUpreducer } from './upreducer.js';
|
|
import { buildMiddleware, augmentMiddlewareApi } from './middleware.js';
|
|
import { action, isActionGen } from './actions.js';
|
|
|
|
/**
|
|
* 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 = {};
|
|
#effects = [];
|
|
#localReactions = [];
|
|
|
|
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),
|
|
);
|
|
|
|
Object.entries(config.mutations ?? {}).forEach((args) =>
|
|
this.setMutation(...args),
|
|
);
|
|
|
|
this.#selectors = buildSelectors(
|
|
config.selectors,
|
|
config.findSelectors,
|
|
this.#subduxes,
|
|
);
|
|
|
|
if (Array.isArray(config.effects)) {
|
|
this.#effects = config.effects;
|
|
} else if (R.isObject(config.effects)) {
|
|
this.#effects = Object.entries(config.effects);
|
|
}
|
|
|
|
this.#localReactions = config.reactions ?? [];
|
|
}
|
|
|
|
#addSubduxActions(_slice, subdux) {
|
|
if (!subdux.actions) return;
|
|
// TODO action 'blah' defined multiple times: <where>
|
|
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 ?? {};
|
|
|
|
if( this.#subduxes['*'] ) {
|
|
if( this.#localInitial ) return this.#localInitial;
|
|
return [];
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
get middleware() {
|
|
return buildMiddleware(
|
|
this.#effects,
|
|
this.actions,
|
|
this.selectors,
|
|
this.subduxes,
|
|
);
|
|
}
|
|
|
|
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;
|
|
|
|
Object.entries(this.selectors).forEach(([selector, fn]) => {
|
|
store.getState[selector] = (...args) => {
|
|
let result = fn(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;
|
|
}
|
|
|
|
addEffect(action, effect) {
|
|
this.#effects = [...this.#effects, [action, effect]];
|
|
}
|
|
|
|
addReaction( reaction ) {
|
|
this.#localReactions = [ ...this.#localReactions, reaction ]
|
|
}
|
|
|
|
subscribeTo(store, subscription) {
|
|
const localStore = augmentMiddlewareApi(
|
|
{
|
|
...store,
|
|
subscribe: (subscriber) =>
|
|
this.subscribeTo(store, subscriber), // TODO not sure
|
|
},
|
|
this.actions,
|
|
this.selectors
|
|
);
|
|
|
|
const subscriber = subscription(localStore);
|
|
|
|
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,
|
|
subscriberMemoized: memoSub,
|
|
subscriber,
|
|
};
|
|
}
|
|
|
|
subscribeAll(store) {
|
|
let results = this.#localReactions.map((sub) =>
|
|
this.subscribeTo(store, sub)
|
|
);
|
|
|
|
for (const subdux in this.#subduxes) {
|
|
if (subdux !== '*') {
|
|
const localStore = {
|
|
...store,
|
|
getState: () => store.getState()[subdux],
|
|
};
|
|
results.push(this.#subduxes[subdux].subscribeAll(localStore));
|
|
}
|
|
}
|
|
|
|
return {
|
|
unsub: () => results.forEach(({ unsub }) => unsub()),
|
|
subscriberMemoized: () =>
|
|
results.forEach(({ subscriberMemoized}) => subscriberMemoized()),
|
|
subscriber: () =>
|
|
results.forEach(({ subscriber }) => subscriber()
|
|
),
|
|
};
|
|
}
|
|
}
|
|
|
|
export const dux = (config) => new Updux(config);
|