diff --git a/src/Updux.js b/src/Updux.js index 1a84745..18bdd7d 100644 --- a/src/Updux.js +++ b/src/Updux.js @@ -4,7 +4,7 @@ import { createStore as reduxCreateStore, applyMiddleware } from 'redux'; import { buildSelectors } from './selectors.js'; import { buildUpreducer } from './upreducer.js'; -import { buildMiddleware } from './middleware.js'; +import { buildMiddleware, augmentMiddlewareApi } from './middleware.js'; import { action, isActionGen } from './actions.js'; /** @@ -21,6 +21,7 @@ export class Updux { #config = {}; #selectors = {}; #effects = []; + #localReactions = []; constructor(config = {}) { this.#config = config; @@ -49,6 +50,8 @@ export class Updux { } else if (R.isObject(config.effects)) { this.#effects = Object.entries(config.effects); } + + this.#localReactions = config.reactions ?? []; } #addSubduxActions(_slice, subdux) { @@ -171,7 +174,7 @@ export class Updux { }; } - //this.subscribeAll(store); + this.subscribeAll(store); return store; } @@ -179,6 +182,67 @@ export class Updux { 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); diff --git a/src/reactions.test.js b/src/reactions.test.js new file mode 100644 index 0000000..225b2c7 --- /dev/null +++ b/src/reactions.test.js @@ -0,0 +1,50 @@ +import { test, expect, vi } from 'vitest'; + +import { Updux } from './Updux.js'; + +test('basic reactions', async () => { + const spyA = vi.fn(); + const spyB = vi.fn(); + const foo = new Updux({ + initial: { i: 0 }, + reactions: [() => spyA], + actions: { inc: null }, + mutations: { + inc: () => (state) => ({ ...state, i: state.i + 1 }), + }, + }); + + foo.addReaction((api) => spyB); + + const store = foo.createStore(); + store.dispatch.inc(); + + expect(spyA).toHaveBeenCalledOnce(); + expect(spyB).toHaveBeenCalledOnce(); +}); + +test('subduxes reactions', async () => { + const spyA = vi.fn(); + const spyB = vi.fn(); + const foo = new Updux({ + subduxes: { + a: new Updux({ + initial: 1, + reactions: [() => state => spyA(state)], + actions: { inc: null }, + mutations: { + inc: () => (state) => state + 1, + }, + }), + b: new Updux({ initial: 10, reactions: [() => spyB] }), + }, + }); + + const store = foo.createStore(); + store.dispatch.inc(); + store.dispatch.inc(); + + expect(spyA).toHaveBeenCalledTimes(2); + expect(spyA).toHaveBeenCalledWith(3) + expect(spyB).toHaveBeenCalledOnce(); // the original inc initialized the state +});