add subscriptions

This commit is contained in:
Yanick Champoux 2021-10-08 19:56:55 -04:00
parent 5247aa2255
commit d61c9478a2
3 changed files with 118 additions and 64 deletions

View File

@ -1,14 +1,50 @@
import moize from 'moize'; import moize from 'moize';
import u from '@yanick/updeep'; import u from '@yanick/updeep';
import { createStore as reduxCreateStore, applyMiddleware } from 'redux'; 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 { buildInitial } from './buildInitial/index.js';
import { buildActions } from './buildActions/index.js'; import { buildActions } from './buildActions/index.js';
import { buildSelectors } from './buildSelectors/index.js'; import { buildSelectors } from './buildSelectors/index.js';
import { action } from './actions.js'; import { action } from './actions.js';
import { buildUpreducer } from './buildUpreducer.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 * @public
@ -26,6 +62,7 @@ export class Updux {
#selectors = {}; #selectors = {};
#mutations = {}; #mutations = {};
#effects = []; #effects = [];
#subscriptions = [];
constructor(config) { constructor(config) {
this.#initial = config.initial ?? {}; this.#initial = config.initial ?? {};
@ -45,6 +82,8 @@ export class Updux {
if (config.effects) { if (config.effects) {
this.#effects = Object.entries(config.effects); this.#effects = Object.entries(config.effects);
} }
this.#subscriptions = config.subscriptions ?? [];
} }
#memoInitial = moize(buildInitial); #memoInitial = moize(buildInitial);
@ -53,6 +92,19 @@ export class Updux {
#memoUpreducer = moize(buildUpreducer); #memoUpreducer = moize(buildUpreducer);
#memoMiddleware = moize(buildMiddleware); #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() { get middleware() {
return this.#memoMiddleware( return this.#memoMiddleware(
this.#effects, this.#effects,
@ -89,6 +141,10 @@ export class Updux {
return (state, action) => this.upreducer(action)(state); return (state, action) => this.upreducer(action)(state);
} }
addSubscription(subscription) {
this.#subscriptions = [...this.#subscriptions, subscription];
}
addAction(type, payloadFunc) { addAction(type, payloadFunc) {
const theAction = action(type, payloadFunc); const theAction = action(type, payloadFunc);
@ -145,6 +201,8 @@ export class Updux {
}; };
} }
_subscribeToStore(store, this.subscriptions);
return store; return store;
} }
} }

View File

@ -13,7 +13,7 @@ const sliceMw = (slice, mw) => (api) => {
return mw({ ...api, getState: getSliceState }); return mw({ ...api, getState: getSliceState });
}; };
function augmentMiddlewareApi(api, actions, selectors) { export function augmentMiddlewareApi(api, actions, selectors) {
const getState = () => api.getState(); const getState = () => api.getState();
const dispatch = (action) => api.dispatch(action); const dispatch = (action) => api.dispatch(action);

View File

@ -1,61 +1,60 @@
import tap from 'tap'; import tap from 'tap';
import Updux from '.';
import u from '@yanick/updeep'; import u from '@yanick/updeep';
tap.test( 'subscriptions', async() => { import { Updux } from './Updux.js';
import { action } from './actions.js';
const inc = action('inc'); tap.test('subscriptions', async () => {
const set_copy = action('set_copy'); const inc = action('inc');
const set_copy = action('set_copy');
const dux = new Updux({ const dux = new Updux({
initial: { initial: {
x: 0, x: 0,
copy: 0, copy: 0,
}, },
actions: { actions: {
inc, inc,
}, set_copy,
mutations: { },
inc: payload => u({ x: x => x + 1 }), mutations: {
set_copy: copy => u({ copy }), 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) => { tap.test('subduxes subscriptions', async (t) => {
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 => {
const inc_top = action('inc_top'); const inc_top = action('inc_top');
const inc_bar = action('inc_bar'); const inc_bar = action('inc_bar');
const transform_bar = action('transform_bar'); const transform_bar = action('transform_bar');
const bar = new Updux({ const bar = new Updux({
initial: 'a', initial: 'a',
mutations: [ actions: { inc_bar, transform_bar },
[inc_bar, () => state => state + 'a'], mutations: {
[transform_bar, outcome => () => outcome], inc_bar: () => (state) => state + 'a',
], transform_bar: (outcome) => () => outcome,
},
subscriptions: [ subscriptions: [
store => (state, unsubscribe) => { (store) => (state, previous, unsubscribe) => {
console.log({ state });
if (state.length <= 2) return; if (state.length <= 2) return;
unsubscribe(); unsubscribe();
store.dispatch(transform_bar('look at ' + state)); store.dispatch(transform_bar('look at ' + state));
@ -67,27 +66,24 @@ tap.test('subduxes subscriptions', async t => {
initial: { initial: {
count: 0, count: 0,
}, },
subduxes: { subduxes: { bar },
bar: bar.asDux, 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: [ subscriptions: [
store => { (store) => {
let previous; return ({ count }, { count: previous } = {}) => {
return ({ count }) => {
if (count !== previous) { if (count !== previous) {
previous = count; previous = count;
store.dispatch(inc_bar()); store.dispatch.inc_bar();
} }
}; };
}, },