splat
This commit is contained in:
parent
912ea85edc
commit
d14eb08bf0
@ -64,7 +64,7 @@
|
|||||||
"url": "https://github.com/yanick/updux/issues"
|
"url": "https://github.com/yanick/updux/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/yanick/updux#readme",
|
"homepage": "https://github.com/yanick/updux#readme",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./types/index.d.ts",
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"tabWidth": 4,
|
"tabWidth": 4,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
|
200
src/Updux.js
200
src/Updux.js
@ -1,7 +1,8 @@
|
|||||||
|
/* TODO change * for leftovers to +, change subscriptions to reactions */
|
||||||
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 { map, mapValues } from 'lodash-es';
|
import { get, map, mapValues, merge, difference } 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';
|
||||||
@ -13,56 +14,6 @@ import {
|
|||||||
augmentMiddlewareApi,
|
augmentMiddlewareApi,
|
||||||
} from './buildMiddleware/index.js';
|
} from './buildMiddleware/index.js';
|
||||||
|
|
||||||
function _subscribeToStore(store, subscriptions) {
|
|
||||||
for (const sub of subscriptions) {
|
|
||||||
const subscriber = sub({
|
|
||||||
...store,
|
|
||||||
subscribe(subscriber) {
|
|
||||||
let previous;
|
|
||||||
const unsub = store.subscribe(() => {
|
|
||||||
const state = store.getState();
|
|
||||||
if (state === previous) return;
|
|
||||||
let p = previous;
|
|
||||||
previous = state;
|
|
||||||
subscriber(state, p, unsub);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
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, previous, unsub) =>
|
|
||||||
subscription(localStore)(
|
|
||||||
state[slice],
|
|
||||||
previous && previous[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
|
||||||
* `Updux` is a way to minimize and simplify the boilerplate associated with the
|
* `Updux` is a way to minimize and simplify the boilerplate associated with the
|
||||||
@ -71,6 +22,7 @@ const memoizeSubscription = (subscription) => (store) => {
|
|||||||
* In true `Redux`-like fashion, upduxes can be made of sub-upduxes (`subduxes` for short) for different slices of the root state.
|
* In true `Redux`-like fashion, upduxes can be made of sub-upduxes (`subduxes` for short) for different slices of the root state.
|
||||||
*/
|
*/
|
||||||
export class Updux {
|
export class Updux {
|
||||||
|
/** @type { unknown } */
|
||||||
#initial = {};
|
#initial = {};
|
||||||
#subduxes = {};
|
#subduxes = {};
|
||||||
|
|
||||||
@ -80,10 +32,19 @@ export class Updux {
|
|||||||
#mutations = {};
|
#mutations = {};
|
||||||
#effects = [];
|
#effects = [];
|
||||||
#subscriptions = [];
|
#subscriptions = [];
|
||||||
|
#splatSelector = undefined;
|
||||||
|
#splatReaction = undefined;
|
||||||
|
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
this.#initial = config.initial ?? {};
|
this.#initial = config.initial ?? {};
|
||||||
this.#subduxes = config.subduxes ?? {};
|
this.#subduxes = config.subduxes ?? {};
|
||||||
|
|
||||||
|
if (config.subduxes) {
|
||||||
|
this.#subduxes = mapValues(config.subduxes, (sub) =>
|
||||||
|
sub instanceof Updux ? sub : new Updux(sub)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (config.actions) {
|
if (config.actions) {
|
||||||
for (const [type, actionArg] of Object.entries(config.actions)) {
|
for (const [type, actionArg] of Object.entries(config.actions)) {
|
||||||
if (typeof actionArg === 'function' && actionArg.type) {
|
if (typeof actionArg === 'function' && actionArg.type) {
|
||||||
@ -98,6 +59,8 @@ export class Updux {
|
|||||||
|
|
||||||
this.#mutations = config.mutations ?? {};
|
this.#mutations = config.mutations ?? {};
|
||||||
|
|
||||||
|
this.#splatSelector = config.splatSelector;
|
||||||
|
|
||||||
Object.keys(this.#mutations)
|
Object.keys(this.#mutations)
|
||||||
.filter((action) => action !== '*')
|
.filter((action) => action !== '*')
|
||||||
.filter((action) => !this.actions.hasOwnProperty(action))
|
.filter((action) => !this.actions.hasOwnProperty(action))
|
||||||
@ -110,6 +73,8 @@ export class Updux {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.#subscriptions = config.subscriptions ?? [];
|
this.#subscriptions = config.subscriptions ?? [];
|
||||||
|
|
||||||
|
this.#splatReaction = config.splatReaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
#memoInitial = moize(buildInitial);
|
#memoInitial = moize(buildInitial);
|
||||||
@ -122,6 +87,10 @@ export class Updux {
|
|||||||
return this.#subscriptions;
|
return this.#subscriptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSplatSelector(name, f) {
|
||||||
|
this.#splatSelector = [name, f];
|
||||||
|
}
|
||||||
|
|
||||||
get middleware() {
|
get middleware() {
|
||||||
return this.#memoMiddleware(
|
return this.#memoMiddleware(
|
||||||
this.#effects,
|
this.#effects,
|
||||||
@ -131,6 +100,7 @@ export class Updux {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return { import('./Updux').What } */
|
||||||
get initial() {
|
get initial() {
|
||||||
return this.#memoInitial(this.#initial, this.#subduxes);
|
return this.#memoInitial(this.#initial, this.#subduxes);
|
||||||
}
|
}
|
||||||
@ -143,7 +113,11 @@ export class Updux {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get selectors() {
|
get selectors() {
|
||||||
return this.#memoSelectors(this.#selectors, this.#subduxes);
|
return this.#memoSelectors(
|
||||||
|
this.#selectors,
|
||||||
|
this.#splatSelector,
|
||||||
|
this.#subduxes
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get upreducer() {
|
get upreducer() {
|
||||||
@ -193,7 +167,49 @@ export class Updux {
|
|||||||
this.#effects = [...this.#effects, [action, effect]];
|
this.#effects = [...this.#effects, [action, effect]];
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribeTo(store, subscription) {
|
splatSubscriber(store, inner, splatReaction) {
|
||||||
|
const cache = {};
|
||||||
|
|
||||||
|
return () => (state, previous, unsub) => {
|
||||||
|
const cacheKeys = Object.keys(cache);
|
||||||
|
|
||||||
|
const newKeys = difference(Object.keys(state), cacheKeys);
|
||||||
|
|
||||||
|
for (const slice of newKeys) {
|
||||||
|
let localStore = {
|
||||||
|
...store,
|
||||||
|
getState: () => store.getState()[slice],
|
||||||
|
};
|
||||||
|
|
||||||
|
cache[slice] = [];
|
||||||
|
|
||||||
|
if (typeof splatReaction === 'function') {
|
||||||
|
localStore = {
|
||||||
|
...localStore,
|
||||||
|
...splatReaction(localStore, slice),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { unsub, subscriber, subscriberRaw } =
|
||||||
|
inner.subscribeAll(localStore);
|
||||||
|
|
||||||
|
cache[slice].push({ unsub, subscriber, subscriberRaw });
|
||||||
|
subscriber();
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletedKeys = difference(cacheKeys, Object.keys(state));
|
||||||
|
|
||||||
|
for (const deleted of deletedKeys) {
|
||||||
|
for (const inner of cache[deleted]) {
|
||||||
|
inner.subscriber();
|
||||||
|
inner.unsub();
|
||||||
|
}
|
||||||
|
delete cache[deleted];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeTo(store, subscription, setupArgs = []) {
|
||||||
const localStore = augmentMiddlewareApi(
|
const localStore = augmentMiddlewareApi(
|
||||||
{
|
{
|
||||||
...store,
|
...store,
|
||||||
@ -204,29 +220,77 @@ export class Updux {
|
|||||||
this.selectors
|
this.selectors
|
||||||
);
|
);
|
||||||
|
|
||||||
const subscriber = subscription(localStore);
|
const subscriber = subscription(localStore, ...setupArgs);
|
||||||
|
|
||||||
let previous;
|
let previous;
|
||||||
|
|
||||||
const unsub = store.subscribe(() => {
|
const memoSub = () => {
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
if (state === previous) return;
|
if (state === previous) return;
|
||||||
let p = previous;
|
let p = previous;
|
||||||
previous = state;
|
previous = state;
|
||||||
subscriber(state, p, unsub);
|
subscriber(state, p, unsub);
|
||||||
});
|
};
|
||||||
|
|
||||||
|
let ret = store.subscribe(memoSub);
|
||||||
|
const unsub = typeof ret === 'function' ? ret : ret.unsub;
|
||||||
|
return {
|
||||||
|
unsub,
|
||||||
|
subscriber: memoSub,
|
||||||
|
subscriberRaw: subscriber,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
createStore() {
|
subscribeAll(store) {
|
||||||
|
let results = this.#subscriptions.map((sub) =>
|
||||||
|
this.subscribeTo(store, sub)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const subdux in this.#subduxes) {
|
||||||
|
if (subdux !== '*') {
|
||||||
|
const localStore = {
|
||||||
|
...store,
|
||||||
|
getState: () => get(store.getState(), subdux),
|
||||||
|
};
|
||||||
|
results.push(this.#subduxes[subdux].subscribeAll(localStore));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.#splatReaction) {
|
||||||
|
results.push(
|
||||||
|
this.subscribeTo(
|
||||||
|
store,
|
||||||
|
this.splatSubscriber(
|
||||||
|
store,
|
||||||
|
this.#subduxes['*'],
|
||||||
|
this.#splatReaction
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
unsub: () => results.forEach(({ unsub }) => unsub()),
|
||||||
|
subscriber: () => results.forEach(({ subscriber }) => subscriber()),
|
||||||
|
subscriberRaw: (...args) =>
|
||||||
|
results.forEach(({ subscriberRaw }) => subscriberRaw(...args)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
createStore(initial) {
|
||||||
const store = reduxCreateStore(
|
const store = reduxCreateStore(
|
||||||
this.reducer,
|
this.reducer,
|
||||||
this.initial,
|
initial ?? this.initial,
|
||||||
applyMiddleware(this.middleware)
|
applyMiddleware(this.middleware)
|
||||||
);
|
);
|
||||||
|
|
||||||
store.actions = this.actions;
|
store.actions = this.actions;
|
||||||
|
|
||||||
store.selectors = mapValues(this.selectors, (selector) => {
|
store.selectors = this.selectors;
|
||||||
|
|
||||||
|
merge(
|
||||||
|
store.getState,
|
||||||
|
mapValues(this.selectors, (selector) => {
|
||||||
return (...args) => {
|
return (...args) => {
|
||||||
let result = selector(store.getState());
|
let result = selector(store.getState());
|
||||||
|
|
||||||
@ -234,7 +298,8 @@ export class Updux {
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
for (const action in this.actions) {
|
for (const action in this.actions) {
|
||||||
store.dispatch[action] = (...args) => {
|
store.dispatch[action] = (...args) => {
|
||||||
@ -242,18 +307,11 @@ export class Updux {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#subscriptions.forEach((sub) => this.subscribeTo(store, sub));
|
this.subscribeAll(store);
|
||||||
|
|
||||||
for (const subdux in this.#subduxes) {
|
|
||||||
const localStore = {
|
|
||||||
...store,
|
|
||||||
getState: () => store.getState()[subdux],
|
|
||||||
};
|
|
||||||
for (const subscription of this.#subduxes[subdux].subscriptions) {
|
|
||||||
this.#subduxes[subdux].subscribeTo(localStore, subscription);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const x = new Updux();
|
||||||
|
x.selectors;
|
||||||
|
@ -74,7 +74,7 @@ test('basic selectors', async (t) => {
|
|||||||
getBar: ({ bar }) => bar,
|
getBar: ({ bar }) => bar,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
dux.addSelector('getFoo', (state) => state.foo);
|
dux.addSelector('getFoo', ({ foo }) => foo);
|
||||||
dux.addSelector(
|
dux.addSelector(
|
||||||
'getAdd',
|
'getAdd',
|
||||||
({ foo }) =>
|
({ foo }) =>
|
||||||
@ -91,9 +91,9 @@ test('basic selectors', async (t) => {
|
|||||||
|
|
||||||
const store = dux.createStore();
|
const store = dux.createStore();
|
||||||
|
|
||||||
t.equal(store.selectors.getFoo(), 1);
|
t.equal(store.getState.getFoo(), 1);
|
||||||
t.equal(store.selectors.getQuux(), 3);
|
t.equal(store.getState.getQuux(), 3);
|
||||||
t.equal(store.selectors.getAdd(7), 8);
|
t.equal(store.getState.getAdd(7), 8);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('mutations', async (t) => {
|
test('mutations', async (t) => {
|
||||||
|
@ -7,6 +7,8 @@ export function buildInitial(initial, subduxes = {}) {
|
|||||||
"can't have subduxes on a dux which state is not an object"
|
"can't have subduxes on a dux which state is not an object"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (Object.keys(subduxes).length === 1 && subduxes['*']) return initial;
|
||||||
|
|
||||||
const subInitial = mapValues(subduxes, ({ initial }, key) =>
|
const subInitial = mapValues(subduxes, ({ initial }, key) =>
|
||||||
key === '*' ? [] : initial
|
key === '*' ? [] : initial
|
||||||
);
|
);
|
||||||
|
@ -69,7 +69,7 @@ export function buildMiddleware(
|
|||||||
sub = {}
|
sub = {}
|
||||||
) {
|
) {
|
||||||
let inner = map(sub, ({ middleware }, slice) =>
|
let inner = map(sub, ({ middleware }, slice) =>
|
||||||
middleware ? sliceMw(slice, middleware) : undefined
|
slice !== '*' && middleware ? sliceMw(slice, middleware) : undefined
|
||||||
).filter((x) => x);
|
).filter((x) => x);
|
||||||
|
|
||||||
const local = effects.map((effect) =>
|
const local = effects.map((effect) =>
|
||||||
|
@ -1,11 +1,30 @@
|
|||||||
import { map, mapValues, merge } from 'lodash-es';
|
import { map, mapValues, merge } from 'lodash-es';
|
||||||
|
|
||||||
export function buildSelectors(localSelectors, subduxes) {
|
export function buildSelectors(localSelectors, splatSelector, subduxes) {
|
||||||
const subSelectors = map(subduxes, ({ selectors }, slice) => {
|
const subSelectors = map(subduxes, ({ selectors }, slice) => {
|
||||||
if (!selectors) return {};
|
if (!selectors) return {};
|
||||||
|
if (slice === '*') return {};
|
||||||
|
|
||||||
return mapValues(selectors, (func) => (state) => func(state[slice]));
|
return mapValues(selectors, (func) => (state) => func(state[slice]));
|
||||||
});
|
});
|
||||||
|
|
||||||
return merge({}, ...subSelectors, localSelectors);
|
let splat = {};
|
||||||
|
if (splatSelector) {
|
||||||
|
splat[splatSelector[0]] =
|
||||||
|
(state) =>
|
||||||
|
(...args) => {
|
||||||
|
const value = splatSelector[1](state)(...args);
|
||||||
|
|
||||||
|
const res = () => value;
|
||||||
|
return merge(
|
||||||
|
res,
|
||||||
|
mapValues(
|
||||||
|
subduxes['*'].selectors,
|
||||||
|
(selector) => () => selector(value)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return merge({}, ...subSelectors, localSelectors, splat);
|
||||||
}
|
}
|
||||||
|
@ -11,12 +11,20 @@ export function buildUpreducer(initial, mutations, subduxes = {}) {
|
|||||||
let newState = state ?? initial;
|
let newState = state ?? initial;
|
||||||
|
|
||||||
if (subReducers) {
|
if (subReducers) {
|
||||||
|
if (subduxes['*']) {
|
||||||
|
newState = u.updateIn(
|
||||||
|
'*',
|
||||||
|
subduxes['*'].upreducer(action),
|
||||||
|
newState
|
||||||
|
);
|
||||||
|
} else {
|
||||||
const update = mapValues(subReducers, (upReducer) =>
|
const update = mapValues(subReducers, (upReducer) =>
|
||||||
upReducer(action)
|
upReducer(action)
|
||||||
);
|
);
|
||||||
|
|
||||||
newState = u(update, newState);
|
newState = u(update, newState);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const a = mutations[action.type] || mutations['*'];
|
const a = mutations[action.type] || mutations['*'];
|
||||||
|
|
||||||
|
314
src/splat.test.js
Normal file
314
src/splat.test.js
Normal file
@ -0,0 +1,314 @@
|
|||||||
|
import { test } from 'tap';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
import { difference, omit } from 'lodash-es';
|
||||||
|
|
||||||
|
import { Updux } from './Updux.js';
|
||||||
|
|
||||||
|
test('initial', async (t) => {
|
||||||
|
const dux = new Updux({
|
||||||
|
initial: {},
|
||||||
|
subduxes: {
|
||||||
|
'*': {
|
||||||
|
initial: { a: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
t.same(dux.initial, {});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('actions', async (t) => {
|
||||||
|
const dux = new Updux({
|
||||||
|
initial: {},
|
||||||
|
subduxes: {
|
||||||
|
'*': {
|
||||||
|
initial: { a: 1 },
|
||||||
|
actions: { foo: null },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
t.type(dux.actions.foo, 'function');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('selectors', async (t) => {
|
||||||
|
const dux = new Updux({
|
||||||
|
initial: {},
|
||||||
|
subduxes: {
|
||||||
|
'*': {
|
||||||
|
initial: { a: 1 },
|
||||||
|
selectors: {
|
||||||
|
getA: ({ a }) => a,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
t.same(dux.selectors, {});
|
||||||
|
|
||||||
|
const getInner = (state) => (index) => state[index];
|
||||||
|
dux.setSplatSelector('getInner', getInner);
|
||||||
|
|
||||||
|
t.type(dux.selectors.getInner, 'function');
|
||||||
|
|
||||||
|
t.same(
|
||||||
|
dux.selectors.getInner({
|
||||||
|
one: { a: 1 },
|
||||||
|
two: { a: 2 },
|
||||||
|
})('one')(),
|
||||||
|
{ a: 1 }
|
||||||
|
);
|
||||||
|
|
||||||
|
t.same(
|
||||||
|
dux.selectors
|
||||||
|
.getInner({
|
||||||
|
one: { a: 1 },
|
||||||
|
two: { a: 2 },
|
||||||
|
})('one')
|
||||||
|
.getA(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
// and now with the store
|
||||||
|
const store = dux.createStore({
|
||||||
|
one: { a: 1 },
|
||||||
|
two: { a: 2 },
|
||||||
|
});
|
||||||
|
|
||||||
|
t.same(store.getState.getInner('two')(), { a: 2 });
|
||||||
|
t.same(store.getState.getInner('two').getA(), 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('splat middleware', async (t) => {
|
||||||
|
const snitch = sinon.fake(() => true);
|
||||||
|
|
||||||
|
const dux = new Updux({
|
||||||
|
initial: {},
|
||||||
|
subduxes: {
|
||||||
|
'*': {
|
||||||
|
initial: { a: 1 },
|
||||||
|
actions: { foo: null },
|
||||||
|
effects: {
|
||||||
|
foo: (api) => (next) => (action) => {
|
||||||
|
snitch();
|
||||||
|
next(action);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = dux.createStore({ one: { a: 1 }, two: { a: 2 } });
|
||||||
|
store.dispatch.foo();
|
||||||
|
|
||||||
|
t.notOk(snitch.called);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('splat subscriptions', async (t) => {
|
||||||
|
const snitch = sinon.fake(() => true);
|
||||||
|
|
||||||
|
const dux = new Updux({
|
||||||
|
initial: {},
|
||||||
|
subduxes: {
|
||||||
|
'*': {
|
||||||
|
initial: { a: 1 },
|
||||||
|
actions: { foo: null },
|
||||||
|
mutations: {
|
||||||
|
foo: (id) => (state) =>
|
||||||
|
state.a === i ? { ...state, b: 1 } : state,
|
||||||
|
},
|
||||||
|
subscriptions: [
|
||||||
|
(store) => (state, previous, unsub) => {
|
||||||
|
snitch(state);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = dux.createStore({ one: { a: 1 }, two: { a: 2 } });
|
||||||
|
store.dispatch({ type: 'noop' });
|
||||||
|
|
||||||
|
t.notOk(snitch.called);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('splat subscriptions, more', async (t) => {
|
||||||
|
const snitch = sinon.fake(() => true);
|
||||||
|
|
||||||
|
const inner = new Updux({
|
||||||
|
initial: { a: 1 },
|
||||||
|
actions: { foo: null, incAll: null },
|
||||||
|
mutations: {
|
||||||
|
foo: (id) => (state) => state.a === id ? { ...state, b: 1 } : state,
|
||||||
|
incAll: () => (state) => ({ ...state, a: state.a + 1 }),
|
||||||
|
},
|
||||||
|
subscriptions: [() => snitch],
|
||||||
|
});
|
||||||
|
|
||||||
|
const dux = new Updux({
|
||||||
|
initial: {},
|
||||||
|
actions: {
|
||||||
|
delete: null,
|
||||||
|
newEntry: null,
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
delete: (id) => (state) => omit(state, id),
|
||||||
|
newEntry: (name) => (state) => ({ ...state, [name]: { a: 10 } }),
|
||||||
|
},
|
||||||
|
splatReaction: 'whatev',
|
||||||
|
subduxes: {
|
||||||
|
'*': inner,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = dux.createStore({ one: { a: 1 }, two: { a: 2 } });
|
||||||
|
store.dispatch({ type: 'noop' });
|
||||||
|
|
||||||
|
t.equal(snitch.callCount, 2);
|
||||||
|
|
||||||
|
t.same(snitch.firstCall.args[0], { a: 1 });
|
||||||
|
t.same(snitch.secondCall.args[0], { a: 2 });
|
||||||
|
|
||||||
|
snitch.resetHistory();
|
||||||
|
|
||||||
|
store.dispatch.foo(2);
|
||||||
|
|
||||||
|
t.equal(snitch.callCount, 1);
|
||||||
|
|
||||||
|
t.same(snitch.firstCall.args[0], { a: 2, b: 1 });
|
||||||
|
|
||||||
|
snitch.resetHistory();
|
||||||
|
store.dispatch.delete('one');
|
||||||
|
|
||||||
|
t.same(store.getState(), {
|
||||||
|
two: { a: 2, b: 1 },
|
||||||
|
});
|
||||||
|
|
||||||
|
t.equal(snitch.callCount, 1);
|
||||||
|
|
||||||
|
t.ok(
|
||||||
|
snitch.calledWithMatch(
|
||||||
|
undefined,
|
||||||
|
{ a: 1 },
|
||||||
|
sinon.match.typeOf('function')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await t.test('only one subscriber left', async (t) => {
|
||||||
|
snitch.resetHistory();
|
||||||
|
store.dispatch.incAll();
|
||||||
|
|
||||||
|
t.equal(snitch.callCount, 1);
|
||||||
|
|
||||||
|
t.ok(
|
||||||
|
snitch.calledWithMatch(
|
||||||
|
{ a: 3, b: 1 },
|
||||||
|
{ a: 2, b: 1 },
|
||||||
|
sinon.match.typeOf('function')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test('new entry gets subscribed', async (t) => {
|
||||||
|
snitch.resetHistory();
|
||||||
|
store.dispatch.newEntry('newbie');
|
||||||
|
|
||||||
|
t.equal(snitch.callCount, 1);
|
||||||
|
|
||||||
|
t.ok(
|
||||||
|
snitch.calledWithMatch(
|
||||||
|
{ a: 10 },
|
||||||
|
undefined,
|
||||||
|
sinon.match.typeOf('function')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('many levels down', { only: false }, async (t) => {
|
||||||
|
const snitch = sinon.fake(() => true);
|
||||||
|
|
||||||
|
const dux = new Updux({
|
||||||
|
splatReaction: 'potato',
|
||||||
|
actions: { remove: null },
|
||||||
|
mutations: {
|
||||||
|
remove: () => (state) => ({}),
|
||||||
|
},
|
||||||
|
subduxes: {
|
||||||
|
'*': {
|
||||||
|
subduxes: {
|
||||||
|
a: {
|
||||||
|
initial: 1,
|
||||||
|
actions: {
|
||||||
|
add: null,
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
add: () => (x) => x + 1,
|
||||||
|
},
|
||||||
|
subscriptions: [
|
||||||
|
(store) => (state) => snitch(state, store),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = dux.createStore({ one: { a: 1 } });
|
||||||
|
store.dispatch({ type: 'foo' });
|
||||||
|
|
||||||
|
t.ok(snitch.calledOnce);
|
||||||
|
t.same(snitch.firstCall.firstArg, 1);
|
||||||
|
|
||||||
|
snitch.resetHistory();
|
||||||
|
|
||||||
|
store.dispatch.remove();
|
||||||
|
|
||||||
|
t.ok(snitch.calledOnce);
|
||||||
|
t.same(snitch.firstCall.firstArg, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('inherit info via the store', async (t) => {
|
||||||
|
const snitch = sinon.fake(() => true);
|
||||||
|
const splatSnitch = sinon.fake(() => true);
|
||||||
|
|
||||||
|
const dux = new Updux({
|
||||||
|
splatReaction: (store, key) => {
|
||||||
|
store.itemId = key;
|
||||||
|
|
||||||
|
splatSnitch();
|
||||||
|
return { itemId: key };
|
||||||
|
},
|
||||||
|
subduxes: {
|
||||||
|
'*': {
|
||||||
|
subduxes: {
|
||||||
|
a: {
|
||||||
|
initial: 1,
|
||||||
|
actions: {
|
||||||
|
add: null,
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
add: () => (x) => x + 1,
|
||||||
|
},
|
||||||
|
subscriptions: [
|
||||||
|
(store) => (state) => snitch(state, store.itemId),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = dux.createStore({
|
||||||
|
one: { a: 1 },
|
||||||
|
two: { a: 2 },
|
||||||
|
});
|
||||||
|
|
||||||
|
store.dispatch({ type: 'noop' });
|
||||||
|
|
||||||
|
t.ok(splatSnitch.calledTwice);
|
||||||
|
|
||||||
|
t.ok(snitch.calledTwice);
|
||||||
|
t.ok(snitch.calledWithMatch(1, 'one'));
|
||||||
|
t.ok(snitch.calledWithMatch(2, 'two'));
|
||||||
|
});
|
@ -119,7 +119,6 @@ tap.test('subscription within subduxes', { todo: false }, async (t) => {
|
|||||||
},
|
},
|
||||||
subscriptions: [
|
subscriptions: [
|
||||||
(store) => (state, previous, unsub) => {
|
(store) => (state, previous, unsub) => {
|
||||||
console.log(state, previous);
|
|
||||||
if (!previous) return;
|
if (!previous) return;
|
||||||
store.subscribe(innerState);
|
store.subscribe(innerState);
|
||||||
unsub();
|
unsub();
|
||||||
|
Loading…
Reference in New Issue
Block a user