updux/src/Updux.js

308 lines
8.3 KiB
JavaScript
Raw Normal View History

2021-10-12 13:42:30 +00:00
/* TODO change * for leftovers to +, change subscriptions to reactions */
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-12 13:42:30 +00:00
import { get, map, mapValues, merge, difference } 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';
2021-09-28 22:13:22 +00:00
export class Updux {
2021-10-12 13:42:30 +00:00
/** @type { unknown } */
2021-09-28 22:13:22 +00:00
#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-10-12 13:42:30 +00:00
#splatSelector = undefined;
#splatReaction = undefined;
2021-09-28 22:13:22 +00:00
constructor(config) {
this.#initial = config.initial ?? {};
this.#subduxes = config.subduxes ?? {};
2021-10-12 13:42:30 +00:00
if (config.subduxes) {
this.#subduxes = mapValues(config.subduxes, (sub) =>
sub instanceof Updux ? sub : new Updux(sub)
);
}
2021-10-09 17:15:35 +00:00
if (config.actions) {
for (const [type, actionArg] of Object.entries(config.actions)) {
if (typeof actionArg === 'function' && actionArg.type) {
2021-10-09 16:13:50 +00:00
this.#actions[type] = actionArg;
2021-10-09 17:15:35 +00:00
} else {
this.#actions[type] = action(type, actionArg);
2021-10-09 16:13:50 +00:00
}
}
}
2021-09-29 14:21:17 +00:00
this.#selectors = config.selectors ?? {};
2021-10-12 17:11:32 +00:00
this.#splatSelector = config.splatSelector;
2021-10-07 16:04:15 +00:00
this.#mutations = config.mutations ?? {};
Object.keys(this.#mutations)
2021-10-13 17:54:17 +00:00
.filter((action) => action !== '+')
2021-10-07 16:04:15 +00:00
.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-10-12 13:42:30 +00:00
this.#splatReaction = config.splatReaction;
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() {
2021-10-09 17:15:35 +00:00
return this.#subscriptions;
2021-10-08 23:56:55 +00:00
}
2021-10-12 13:42:30 +00:00
setSplatSelector(name, f) {
this.#splatSelector = [name, f];
}
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
2021-10-12 17:11:32 +00:00
/** @member { unknown } */
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-12 13:42:30 +00:00
return this.#memoSelectors(
this.#selectors,
this.#splatSelector,
this.#subduxes
);
2021-10-07 16:04:15 +00:00
}
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-10-12 22:13:59 +00:00
setAction(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
2021-10-13 23:31:37 +00:00
setSelector(name, func) {
// TODO selector already exists? Complain!
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
2021-10-12 22:13:59 +00:00
setMutation(name, mutation) {
2021-10-07 16:04:15 +00:00
if (typeof name === 'function') name = name.type;
this.#mutations = {
...this.#mutations,
[name]: mutation,
};
2021-10-12 22:13:59 +00:00
return mutation;
2021-10-07 16:04:15 +00:00
}
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-12 13:42:30 +00:00
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 = []) {
2021-10-09 17:15:35 +00:00
const localStore = augmentMiddlewareApi(
{
...store,
subscribe: (subscriber) =>
this.subscribeTo(store, () => subscriber),
},
this.actions,
this.selectors
);
2021-10-12 13:42:30 +00:00
const subscriber = subscription(localStore, ...setupArgs);
2021-10-09 17:15:35 +00:00
let previous;
2021-10-12 13:42:30 +00:00
const memoSub = () => {
2021-10-09 17:15:35 +00:00
const state = store.getState();
if (state === previous) return;
let p = previous;
previous = state;
subscriber(state, p, unsub);
2021-10-12 13:42:30 +00:00
};
let ret = store.subscribe(memoSub);
const unsub = typeof ret === 'function' ? ret : ret.unsub;
return {
unsub,
subscriber: memoSub,
subscriberRaw: subscriber,
};
2021-10-09 17:15:35 +00:00
}
2021-10-12 13:42:30 +00:00
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) {
2021-10-07 19:08:21 +00:00
const store = reduxCreateStore(
this.reducer,
2021-10-12 13:42:30 +00:00
initial ?? this.initial,
2021-10-07 19:08:21 +00:00
applyMiddleware(this.middleware)
);
2021-10-07 17:27:14 +00:00
store.actions = this.actions;
2021-10-12 13:42:30 +00:00
store.selectors = this.selectors;
2021-10-07 17:27:14 +00:00
2021-10-12 13:42:30 +00:00
merge(
store.getState,
mapValues(this.selectors, (selector) => {
return (...args) => {
let result = selector(store.getState());
2021-10-07 17:27:14 +00:00
2021-10-12 13:42:30 +00:00
if (typeof result === 'function') return result(...args);
return result;
};
})
);
2021-10-07 17:27:14 +00:00
for (const action in this.actions) {
store.dispatch[action] = (...args) => {
return store.dispatch(this.actions[action](...args));
};
}
2021-10-12 13:42:30 +00:00
this.subscribeAll(store);
2021-10-08 23:56:55 +00:00
2021-10-07 17:27:14 +00:00
return store;
}
2021-09-28 22:13:22 +00:00
}