diff --git a/src/Updux.js b/src/Updux.js index ad7a9c7..0a74852 100644 --- a/src/Updux.js +++ b/src/Updux.js @@ -1,14 +1,50 @@ import moize from 'moize'; import u from '@yanick/updeep'; import { createStore as reduxCreateStore, applyMiddleware } from 'redux'; -import { mapValues } from 'lodash-es'; +import { map, mapValues } 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 } from './buildMiddleware/index.js'; +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); + }; +}; /** * @public @@ -26,6 +62,7 @@ export class Updux { #selectors = {}; #mutations = {}; #effects = []; + #subscriptions = []; constructor(config) { this.#initial = config.initial ?? {}; @@ -45,6 +82,8 @@ export class Updux { if (config.effects) { this.#effects = Object.entries(config.effects); } + + this.#subscriptions = config.subscriptions ?? []; } #memoInitial = moize(buildInitial); @@ -53,6 +92,19 @@ export class Updux { #memoUpreducer = moize(buildUpreducer); #memoMiddleware = moize(buildMiddleware); + get subscriptions() { + const subscriptions = [...this.#subscriptions].map((s) => + memoizeSubscription(s) + ); + + return [ + ...subscriptions, + ...map(this.#subduxes, (v, k) => + v.subscriptions.map(sliceSubscriber(k, v)) + ).flat(), + ]; + } + get middleware() { return this.#memoMiddleware( this.#effects, @@ -89,6 +141,10 @@ export class Updux { return (state, action) => this.upreducer(action)(state); } + addSubscription(subscription) { + this.#subscriptions = [...this.#subscriptions, subscription]; + } + addAction(type, payloadFunc) { const theAction = action(type, payloadFunc); @@ -145,6 +201,8 @@ export class Updux { }; } + _subscribeToStore(store, this.subscriptions); + return store; } } diff --git a/src/buildMiddleware/index.js b/src/buildMiddleware/index.js index 3808ed8..af4697f 100644 --- a/src/buildMiddleware/index.js +++ b/src/buildMiddleware/index.js @@ -13,7 +13,7 @@ const sliceMw = (slice, mw) => (api) => { return mw({ ...api, getState: getSliceState }); }; -function augmentMiddlewareApi(api, actions, selectors) { +export function augmentMiddlewareApi(api, actions, selectors) { const getState = () => api.getState(); const dispatch = (action) => api.dispatch(action); diff --git a/src/subscriptions.test.js b/src/subscriptions.test.js index eb1d779..848b887 100644 --- a/src/subscriptions.test.js +++ b/src/subscriptions.test.js @@ -1,61 +1,60 @@ import tap from 'tap'; -import Updux from '.'; import u from '@yanick/updeep'; -tap.test( 'subscriptions', async() => { +import { Updux } from './Updux.js'; +import { action } from './actions.js'; -const inc = action('inc'); -const set_copy = action('set_copy'); +tap.test('subscriptions', async () => { + const inc = action('inc'); + const set_copy = action('set_copy'); -const dux = new Updux({ - initial: { - x: 0, - copy: 0, - }, - actions: { - inc, - }, - mutations: { - inc: payload => u({ x: x => x + 1 }), - set_copy: copy => u({ copy }), - }, + const dux = new Updux({ + initial: { + x: 0, + copy: 0, + }, + actions: { + inc, + set_copy, + }, + mutations: { + inc: (payload) => u({ x: (x) => x + 1 }), + set_copy: (copy) => u({ copy }), + }, + }); + + dux.addSubscription((store) => (state, previous, unsubscribe) => { + if (state.x > 2) return unsubscribe(); + + store.dispatch(set_copy(state.x)); + }); + + const store = dux.createStore(); + + store.dispatch(inc()); + + tap.same(store.getState(), { x: 1, copy: 1 }); + + store.dispatch(inc()); + store.dispatch(inc()); + + tap.same(store.getState(), { x: 3, copy: 2 }, 'we unsubscribed'); }); -dux.addSubscription(store => (state, unsubscribe) => { - if (state.x > 2) return unsubscribe(); - - store.dispatch(set_copy(state.x)); -}); - -const store = dux.createStore(); - -store.dispatch(inc()); - -tap.same(store.getState(), { x: 1, copy: 1 }); - -store.dispatch(inc()); -store.dispatch(inc()); - -tap.same(store.getState(), { x: 3, copy: 2 }, 'we unsubscribed'); - - -} ); - -tap.test('subduxes subscriptions', async t => { +tap.test('subduxes subscriptions', async (t) => { const inc_top = action('inc_top'); const inc_bar = action('inc_bar'); const transform_bar = action('transform_bar'); const bar = new Updux({ initial: 'a', - mutations: [ - [inc_bar, () => state => state + 'a'], - [transform_bar, outcome => () => outcome], - ], + actions: { inc_bar, transform_bar }, + mutations: { + inc_bar: () => (state) => state + 'a', + transform_bar: (outcome) => () => outcome, + }, subscriptions: [ - store => (state, unsubscribe) => { - console.log({ state }); - + (store) => (state, previous, unsubscribe) => { if (state.length <= 2) return; unsubscribe(); store.dispatch(transform_bar('look at ' + state)); @@ -67,27 +66,24 @@ tap.test('subduxes subscriptions', async t => { initial: { count: 0, }, - subduxes: { - bar: bar.asDux, + subduxes: { bar }, + actions: { + inc_top, + }, + mutations: { + inc_top: () => u({ count: (count) => count + 1 }), + }, + effects: { + '*': () => (next) => (action) => { + next(action); + }, }, - mutations: [[inc_top, () => u({ count: count => count + 1 })]], - effects: [ - [ - '*', - () => next => action => { - console.log('before ', action.type); - next(action); - console.log({ action }); - }, - ], - ], subscriptions: [ - store => { - let previous; - return ({ count }) => { + (store) => { + return ({ count }, { count: previous } = {}) => { if (count !== previous) { previous = count; - store.dispatch(inc_bar()); + store.dispatch.inc_bar(); } }; },