add splatReaction support

This commit is contained in:
Yanick Champoux 2022-09-06 12:54:23 -04:00
parent f206026087
commit 82f5d53df2
2 changed files with 208 additions and 29 deletions

View File

@ -14,6 +14,7 @@ import { action, isActionGen } from './actions.js';
*/ */
export class Updux { export class Updux {
#name = 'unknown';
#localInitial = {}; #localInitial = {};
#subduxes = {}; #subduxes = {};
#actions; #actions;
@ -23,10 +24,15 @@ export class Updux {
#effects = []; #effects = [];
#localReactions = []; #localReactions = [];
#middlewareWrapper; #middlewareWrapper;
#splatReactionMapper;
constructor(config = {}) { constructor(config = {}) {
this.#config = config; this.#config = config;
this.#name = config.name || 'unknown';
this.#splatReactionMapper = config.splatReactionMapper;
this.#middlewareWrapper = config.middlewareWrapper; this.#middlewareWrapper = config.middlewareWrapper;
this.#localInitial = config.initial; this.#localInitial = config.initial;
@ -58,6 +64,10 @@ export class Updux {
this.#localReactions = config.reactions ?? []; this.#localReactions = config.reactions ?? [];
} }
get name() {
return this.#name;
}
#addSubduxActions(_slice, subdux) { #addSubduxActions(_slice, subdux) {
if (!subdux.actions) return; if (!subdux.actions) return;
// TODO action 'blah' defined multiple times: <where> // TODO action 'blah' defined multiple times: <where>
@ -206,16 +216,15 @@ export class Updux {
} }
subscribeTo(store, subscription) { subscribeTo(store, subscription) {
const localStore = this.augmentMiddlewareApi( const localStore = this.augmentMiddlewareApi({
{
...store, ...store,
subscribe: (subscriber) => this.subscribeTo(store, subscriber), // TODO not sure subscribe: (subscriber) => this.subscribeTo(store, subscriber), // TODO not sure
} });
);
const subscriber = subscription(localStore); const subscriber = subscription(localStore);
let previous; let previous;
let unsub;
const memoSub = () => { const memoSub = () => {
const state = store.getState(); const state = store.getState();
@ -225,38 +234,81 @@ export class Updux {
subscriber(state, p, unsub); subscriber(state, p, unsub);
}; };
let ret = store.subscribe(memoSub); return store.subscribe(memoSub);
const unsub = typeof ret === 'function' ? ret : ret.unsub; }
return {
unsub, createSplatReaction() {
subscriberMemoized: memoSub, const subdux = this.#subduxes['*'];
subscriber, const mapper = this.#splatReactionMapper;
return (api) => {
const cache = {};
return (state, previousState, unsubscribe) => {
const gone = { ...cache };
// TODO assuming object here
for (const key in state) {
if (cache[key]) {
delete gone[key];
} else {
const dux = new Updux({
initial: null,
actions: { update: null },
mutations: {
update: (payload) => () => payload,
},
});
const store = dux.createStore();
// TODO need to change the store to have the
// subscribe pointing to the right slice?
const context = {
...(api.context ?? {}),
[subdux.name]: key,
}; };
const unsub = subdux.subscribeAll({
...store,
context,
});
cache[key] = { store, unsub };
}
cache[key].store.dispatch.update(state[key]);
}
for (const key in gone) {
cache[key].store.dispatch.update(null);
cache[key].unsub();
delete cache[key];
}
};
};
}
subscribeSplatReaction(store) {
return this.subscribeTo(store, this.createSplatReaction());
} }
subscribeAll(store) { subscribeAll(store) {
let results = this.#localReactions.map((sub) => let unsubs = this.#localReactions.map((sub) =>
this.subscribeTo(store, sub), this.subscribeTo(store, sub),
); );
for (const subdux in this.#subduxes) { if (this.#splatReactionMapper) {
if (subdux !== '*') { unsubs.push(this.subscribeSplatReaction(store));
const localStore = {
...store,
getState: () => store.getState()[subdux],
};
results.push(this.#subduxes[subdux].subscribeAll(localStore));
}
} }
return { unsubs.push(
unsub: () => results.forEach(({ unsub }) => unsub()), ...Object.entries(this.#subduxes)
subscriberMemoized: () => .filter(([slice]) => slice !== '*')
results.forEach(({ subscriberMemoized }) => .map(([slice, subdux]) => {
subscriberMemoized(), subdux.subscribeAll({
), ...store,
subscriber: () => results.forEach(({ subscriber }) => subscriber()), getState: () => store.getState()[slice],
}; });
}),
);
return () => unsubs.forEach((u) => u());
} }
} }

127
src/splatReactions.test.js Normal file
View File

@ -0,0 +1,127 @@
import { test, expect, vi, describe } from 'vitest';
import u from 'updeep';
import { Updux } from './Updux.js';
import { matches } from './utils';
const reactionSnitch = vi.fn();
const thingReactionSnitch = vi.fn();
const subThing = new Updux({
name: 'subThing',
initial: 0,
});
subThing.addReaction((api) => (state, previousState, unsubscribe) => {
reactionSnitch({ ...api, state, previousState });
});
const thing = new Updux({
name: 'thing',
initial: {},
subduxes: {
'*': subThing,
},
actions: {
setSubThing: (id, value, thingId) => ({ thingId, id, value }),
deleteSubThing: (id) => id,
},
mutations: {
setSubThing: ({ id, value }) => u.updateIn(id, value),
deleteSubThing: (id) => u.updateIn(id, u.omitted),
},
splatReactionMapper: ({ id }) => id,
});
thing.addReaction((api) => (state, previousState, unsubscribe) => {
thingReactionSnitch({ ...api, state, previousState });
});
const things = new Updux({
subduxes: {
'*': thing,
},
initial: {},
actions: { newThing: (id) => id },
splatReactionMapper: ({ id }) => id,
mutations: {
newThing: (id) => (state) => ({ ...state, [id]: thing.initial }),
},
});
things.setMutation(
'setSubThing',
({ thingId }, action) => u.updateIn(thingId, thing.upreducer(action)),
true,
);
describe('just one level', () => {
const store = thing.createStore();
test('set', async () => {
store.dispatch.setSubThing('a', 13);
expect(reactionSnitch).toHaveBeenCalledWith(
expect.objectContaining({ state: 13 }),
);
});
test('other key', async () => {
reactionSnitch.mockReset();
store.dispatch.setSubThing('b', 23);
expect(reactionSnitch).not.toHaveBeenCalledWith(
expect.objectContaining({ state: 13 }),
);
expect(reactionSnitch).toHaveBeenCalledWith(
expect.objectContaining({ state: 23 }),
);
});
test('delete', async () => {
reactionSnitch.mockReset();
store.dispatch.deleteSubThing('a');
expect(reactionSnitch).toHaveBeenCalledOnce();
expect(reactionSnitch).toHaveBeenCalledWith(
expect.objectContaining({ state: null }),
);
});
test('context', async () => {
expect(reactionSnitch).toHaveBeenCalledWith(
expect.objectContaining({
context: { subThing: 'a' },
}),
);
});
});
test('two levels', async () => {
const store = things.createStore();
reactionSnitch.mockReset();
thingReactionSnitch.mockReset();
store.dispatch.newThing('alpha');
store.dispatch.newThing('beta');
store.dispatch.setSubThing('a', 13, 'alpha');
expect(reactionSnitch).toHaveBeenCalledWith(
expect.objectContaining({
context: { thing: 'alpha', subThing: 'a' },
state: 13,
}),
);
expect(thingReactionSnitch).toHaveBeenCalledWith(
expect.objectContaining({
context: {
thing: 'alpha',
},
state: { a: 13 },
}),
);
});