add splatReaction support
This commit is contained in:
parent
f206026087
commit
82f5d53df2
110
src/Updux.js
110
src/Updux.js
@ -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
127
src/splatReactions.test.js
Normal 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 },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user