feat: add support for subscriptions

This commit is contained in:
Yanick Champoux 2020-06-19 19:29:12 -04:00
parent 86dd272603
commit 9c45ee7efc
6 changed files with 964 additions and 752 deletions

View File

@ -1,4 +1,3 @@
# What's Updux? # What's Updux?
So, I'm a fan of [Redux](https://redux.js.org). Two days ago I discovered So, I'm a fan of [Redux](https://redux.js.org). Two days ago I discovered
@ -16,17 +15,16 @@ is the fun in that? And I'm also having a strong love for
All that to say, say hello to `Updux`. Heavily inspired by `rematch`, but twisted All that to say, say hello to `Updux`. Heavily inspired by `rematch`, but twisted
to work with `updeep` and to fit my peculiar needs. It offers features such as to work with `updeep` and to fit my peculiar needs. It offers features such as
* Mimic the way VueX has mutations (reducer reactions to specific actions) and - Mimic the way VueX has mutations (reducer reactions to specific actions) and
effects (middleware reacting to actions that can be asynchronous and/or effects (middleware reacting to actions that can be asynchronous and/or
have side-effects), so everything pertaining to a store are all defined have side-effects), so everything pertaining to a store are all defined
in the space place. in the space place.
* Automatically gather all actions used by the updux's effects and mutations, - Automatically gather all actions used by the updux's effects and mutations,
and makes then accessible as attributes to the `dispatch` object of the and makes then accessible as attributes to the `dispatch` object of the
store. store.
* Mutations have a signature that is friendly to Updux and Immer. - Mutations have a signature that is friendly to Updux and Immer.
* Also, the mutation signature auto-unwrap the payload of the actions for you. - Also, the mutation signature auto-unwrap the payload of the actions for you.
* TypeScript types. - TypeScript types.
Fair warning: this package is still very new, probably very buggy, Fair warning: this package is still very new, probably very buggy,
definitively very badly documented, and very subject to changes. Caveat definitively very badly documented, and very subject to changes. Caveat
@ -78,6 +76,8 @@ store.dispatch.inc(3);
# Description # Description
Full documentation can be [found here](https://yanick.github.io/updux/). Full documentation can be [found here](https://yanick.github.io/updux/).
Right now the best way to understand the whole thing is to go
through the [tutorial](https://yanick.github.io/updux/#/tutorial)
## Exporting upduxes ## Exporting upduxes
@ -94,7 +94,6 @@ const updux = new Updux({ ... });
export default updux; export default updux;
``` ```
Then you can use them as subduxes like this: Then you can use them as subduxes like this:
``` ```
@ -196,7 +195,6 @@ const updux = new Updux({
Converting it to Immer would look like: Converting it to Immer would look like:
``` ```
import Updux from 'updux'; import Updux from 'updux';
import { produce } from 'Immer'; import { produce } from 'Immer';
@ -213,7 +211,6 @@ const updux = new Updux({
But since typing `produce` over and over is no fun, `groomMutations` But since typing `produce` over and over is no fun, `groomMutations`
can be used to wrap all mutations with it: can be used to wrap all mutations with it:
``` ```
import Updux from 'updux'; import Updux from 'updux';
import { produce } from 'Immer'; import { produce } from 'Immer';
@ -227,6 +224,3 @@ const updux = new Updux({
}); });
``` ```

View File

@ -101,7 +101,6 @@ For TypeScript projects I recommend declaring the actions as part of the
configuration passed to the constructors, as it makes them accessible to the class configuration passed to the constructors, as it makes them accessible to the class
at compile-time, and allows Updux to auto-add them to its aggregated `actions` type. at compile-time, and allows Updux to auto-add them to its aggregated `actions` type.
``` ```
const todosUpdux = new Updux({ const todosUpdux = new Updux({
actions: { actions: {
@ -210,7 +209,6 @@ todosUpdux.addMutation( '*', (payload,action) => state => {
}); });
``` ```
## Effects ## Effects
In addition to mutations, Updux also provides action-specific middleware, here In addition to mutations, Updux also provides action-specific middleware, here
@ -498,7 +496,7 @@ todosUpdux.addMutation( add_todo_with_id, payload =>
export default updux.asDux; export default updux.asDux;
``` ```
Note the special '*' subdux key used here. This Note the special '\*' subdux key used here. This
allows the updux to map every item present in its allows the updux to map every item present in its
state to a `todo` updux. See [this recipe](/recipes?id=mapping-a-mutation-to-all-values-of-a-state) for details. state to a `todo` updux. See [this recipe](/recipes?id=mapping-a-mutation-to-all-values-of-a-state) for details.
We could also have written the updux as: We could also have written the updux as:
@ -519,11 +517,9 @@ const updux = new Updux({
``` ```
Note how we are using the `upreducer` accessor in the first case (which yields Note how we are using the `upreducer` accessor in the first case (which yields
a reducer for the dux using the signature `(payload,action) => state => a reducer for the dux using the signature `(payload,action) => state => new_state`) and `reducer` in the second case (which yield an equivalent
new_state`) and `reducer` in the second case (which yield an equivalent
reducer using the classic signature `(state,action) => new_state`). reducer using the classic signature `(state,action) => new_state`).
### Main store ### Main store
``` ```
@ -573,6 +569,50 @@ at the main level is actually defined as:
const getNextId = state => next_id.selectors.getNextId(state.next_id); const getNextId = state => next_id.selectors.getNextId(state.next_id);
``` ```
## Subscriptions
Subscriptions can be added by default to a updux store via the initial config
or the method `addSubscription`. The signature of a subscription is:
```
(store) => (state,unsubscribe) => {
...
}
```
Subscriptions registered for an updux and its subduxes are automatically
subscribed to the store when calling `createStore`.
The `state` passed to the subscriptions of the subduxes is the local state.
Also, all subscriptions are wrapped such that they are called only if the
local `state` changed since their last invocation.
Example:
```
const set_nbr_todos = action('set_nbr_todos', payload() );
const todos = dux({
initial: [],
subscriptions: [
({dispatch}) => todos => dispatch(set_nbr_todos(todos.length))
],
});
const myDux = dux({
initial: {
nbr_todos: 0
},
subduxes: {
todos,
},
mutations: [
[ set_nbr_todos, nbr_todos => u({nbr_todos}) ]
]
})
```
## Exporting upduxes ## Exporting upduxes
As a general rule, don't directly export your upduxes, but rather use the accessor `asDux`. As a general rule, don't directly export your upduxes, but rather use the accessor `asDux`.

View File

@ -43,7 +43,9 @@
"scripts": { "scripts": {
"docsify:serve": "docsify serve docs", "docsify:serve": "docsify serve docs",
"build": "tsc", "build": "tsc",
"test": "tap src/**test.ts" "test": "tap src/**test.ts",
"lint": "prettier -c --",
"lint:fix": "prettier --write --"
}, },
"version": "2.0.0", "version": "2.0.0",
"repository": { "repository": {

107
src/subscriptions.test.ts Normal file
View File

@ -0,0 +1,107 @@
import tap from 'tap';
import Updux from '.';
import { action, payload } from 'ts-action';
import u from 'updeep';
const inc = action('inc');
const set_double = action('set_double', payload<number>());
const dux = new Updux({
initial: {
x: 0,
double: 0,
},
actions: {
inc,
},
mutations: [
[inc, payload => u({ x: x => x + 1 })],
[set_double, double => u({ double })],
],
});
dux.addSubscription(store => (state, unsubscribe) => {
if (state.x > 2) return unsubscribe();
store.dispatch(set_double(state.x * 2));
});
const store = dux.createStore();
store.dispatch(inc());
tap.same(store.getState(), { x: 1, double: 2 });
store.dispatch(inc());
store.dispatch(inc());
tap.same(store.getState(), { x: 3, double: 4 }, 'we unsubscribed');
tap.test('subduxes subscriptions', async t => {
const inc_top = action('inc_top');
const inc_bar = action('inc_bar');
const transform_bar = action('transform_bar', payload());
const bar = new Updux({
initial: 'a',
mutations: [
[inc_bar, () => state => state + 'a'],
[transform_bar, outcome => () => outcome],
],
subscriptions: [
store => (state, unsubscribe) => {
console.log({ state });
if (state.length <= 2) return;
unsubscribe();
store.dispatch(transform_bar('look at ' + state));
},
],
});
const dux = new Updux({
initial: {
count: 0,
},
subduxes: {
bar: bar.asDux,
},
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: any;
return ({ count }) => {
if (count !== previous) {
previous = count;
store.dispatch(inc_bar());
}
};
},
],
});
const store = dux.createStore();
store.dispatch(inc_top());
store.dispatch(inc_top());
t.same(store.getState(), {
count: 2,
bar: 'look at look at aaa',
});
store.dispatch(inc_top());
t.same(store.getState(), {
count: 3,
bar: 'look at look at aaaa',
});
});

View File

@ -37,324 +37,339 @@ export type UnionToIntersection<U> = (U extends any
export type StateOf<D> = D extends { initial: infer I } ? I : unknown; export type StateOf<D> = D extends { initial: infer I } ? I : unknown;
export type DuxStateCoduxes<C> = C extends Array<infer U> ? UnionToIntersection<StateOf<U>>: unknown export type DuxStateCoduxes<C> = C extends Array<infer U>
export type DuxStateSubduxes<C> = ? UnionToIntersection<StateOf<U>>
C extends { '*': infer I } ? { : unknown;
[ key: string ]: StateOf<I>, export type DuxStateSubduxes<C> = C extends { '*': infer I }
[ index: number ]: StateOf<I>, ? {
} : [key: string]: StateOf<I>;
C extends object ? { [ K in keyof C]: StateOf<C[K]>}: unknown; [index: number]: StateOf<I>;
}
: C extends object
? { [K in keyof C]: StateOf<C[K]> }
: unknown;
type DuxStateGlobSub<S> = S extends { '*': infer I } ? StateOf<I> : unknown; type DuxStateGlobSub<S> = S extends { '*': infer I } ? StateOf<I> : unknown;
type LocalDuxState<S> = S extends never[] ? unknown[] : S; type LocalDuxState<S> = S extends never[] ? unknown[] : S;
/** @ignore */ /** @ignore */
type AggDuxState2<L,S,C> = ( type AggDuxState2<L, S, C> = (L extends never[]
L extends never[] ? Array<DuxStateGlobSub<S>> : L & DuxStateSubduxes<S> ) & DuxStateCoduxes<C>; ? Array<DuxStateGlobSub<S>>
: L & DuxStateSubduxes<S>) &
DuxStateCoduxes<C>;
/** @ignore */ /** @ignore */
export type AggDuxState<O,S extends UpduxConfig> = unknown extends O ? export type AggDuxState<O, S extends UpduxConfig> = unknown extends O
AggDuxState2<S['initial'],S['subduxes'],S['coduxes']> : O ? AggDuxState2<S['initial'], S['subduxes'], S['coduxes']>
: O;
type SelectorsOf<C> = C extends { selectors: infer S } ? S : unknown; type SelectorsOf<C> = C extends { selectors: infer S } ? S : unknown;
/** @ignore */ /** @ignore */
export type DuxSelectorsSubduxes<C> = C extends object ? UnionToIntersection<SelectorsOf<C[keyof C]>> : unknown; export type DuxSelectorsSubduxes<C> = C extends object
? UnionToIntersection<SelectorsOf<C[keyof C]>>
: unknown;
/** @ignore */ /** @ignore */
export type DuxSelectorsCoduxes<C> = C extends Array<infer U> ? UnionToIntersection<SelectorsOf<U>> : unknown; export type DuxSelectorsCoduxes<C> = C extends Array<infer U>
? UnionToIntersection<SelectorsOf<U>>
: unknown;
type MaybeReturnType<X> = X extends (...args: any) => any ? ReturnType<X> : unknown; type MaybeReturnType<X> = X extends (...args: any) => any
? ReturnType<X>
: unknown;
type RebaseSelector<S,X> = { type RebaseSelector<S, X> = {
[ K in keyof X]: (state: S) => MaybeReturnType< X[K] > [K in keyof X]: (state: S) => MaybeReturnType<X[K]>;
} };
type ActionsOf<C> = C extends { actions: infer A } ? A : {}; type ActionsOf<C> = C extends { actions: infer A } ? A : {};
type DuxActionsSubduxes<C> = C extends object ? ActionsOf<C[keyof C]> : unknown; type DuxActionsSubduxes<C> = C extends object ? ActionsOf<C[keyof C]> : unknown;
export type DuxActionsCoduxes<C> = C extends Array<infer I> ? UnionToIntersection<ActionsOf<I>> : {}; export type DuxActionsCoduxes<C> = C extends Array<infer I>
? UnionToIntersection<ActionsOf<I>>
: {};
type ItemsOf<C> = C extends object? C[keyof C] : unknown type ItemsOf<C> = C extends object ? C[keyof C] : unknown;
export type DuxActions<A,C extends UpduxConfig> = A extends object ? A: ( export type DuxActions<A, C extends UpduxConfig> = A extends object
UnionToIntersection<ActionsOf<C|ItemsOf<C['subduxes']>|ItemsOf<C['coduxes']>>> ? A
); : UnionToIntersection<
ActionsOf<C | ItemsOf<C['subduxes']> | ItemsOf<C['coduxes']>>
>;
export type DuxSelectors<S,X,C extends UpduxConfig> = unknown extends X ? ( export type DuxSelectors<S, X, C extends UpduxConfig> = unknown extends X
RebaseSelector<S, ? RebaseSelector<
C['selectors'] & DuxSelectorsCoduxes<C['coduxes']> & S,
DuxSelectorsSubduxes<C['subduxes']> > C['selectors'] &
): X DuxSelectorsCoduxes<C['coduxes']> &
DuxSelectorsSubduxes<C['subduxes']>
>
: X;
export type Dux< export type Dux<S = unknown, A = unknown, X = unknown, C = unknown> = {
S = unknown, subduxes: Dictionary<Dux>;
A = unknown, coduxes: Dux[];
X = unknown, initial: AggDuxState<S, C>;
C = unknown, actions: A;
> = { subscriptions: Function[];
subduxes: Dictionary<Dux>, };
coduxes: Dux[],
initial: AggDuxState<S,C>,
actions: A,
}
/** /**
* Configuration object given to Updux's constructor. * Configuration object given to Updux's constructor.
* *
* #### arguments * #### arguments
* *
* ##### initial * ##### initial
* *
* Default initial state of the reducer. If applicable, is merged with * Default initial state of the reducer. If applicable, is merged with
* the subduxes initial states, with the parent having precedence. * the subduxes initial states, with the parent having precedence.
* *
* If not provided, defaults to an empty object. * If not provided, defaults to an empty object.
* *
* ##### actions * ##### actions
* *
* [Actions](/concepts/Actions) used by the updux. * [Actions](/concepts/Actions) used by the updux.
* *
* ```js * ```js
* import { dux } from 'updux'; * import { dux } from 'updux';
* import { action, payload } from 'ts-action'; * import { action, payload } from 'ts-action';
* *
* const bar = action('BAR', payload<int>()); * const bar = action('BAR', payload<int>());
* const foo = action('FOO'); * const foo = action('FOO');
* *
* const myDux = dux({ * const myDux = dux({
* actions: { * actions: {
* bar * bar
* }, * },
* mutations: [ * mutations: [
* [ foo, () => state => state ] * [ foo, () => state => state ]
* ] * ]
* }); * });
* *
* myDux.actions.foo({ x: 1, y: 2 }); // => { type: foo, x:1, y:2 } * myDux.actions.foo({ x: 1, y: 2 }); // => { type: foo, x:1, y:2 }
* myDux.actions.bar(2); // => { type: bar, payload: 2 } * myDux.actions.bar(2); // => { type: bar, payload: 2 }
* ``` * ```
* *
* New actions used directly in mutations and effects will be added to the * New actions used directly in mutations and effects will be added to the
* dux actions -- that is, they will be accessible via `dux.actions` -- but will * dux actions -- that is, they will be accessible via `dux.actions` -- but will
* not appear as part of its Typescript type. * not appear as part of its Typescript type.
* *
* ##### selectors * ##### selectors
* *
* Dictionary of selectors for the current updux. The updux also * Dictionary of selectors for the current updux. The updux also
* inherit its subduxes' selectors. * inherit its subduxes' selectors.
* *
* The selectors are available via the class' getter. * The selectors are available via the class' getter.
* *
* ##### mutations * ##### mutations
* *
* mutations: [ * mutations: [
* [ action, mutation, isSink ], * [ action, mutation, isSink ],
* ... * ...
* ] * ]
* *
* or * or
* *
* mutations: { * mutations: {
* action: mutation, * action: mutation,
* ... * ...
* } * }
* *
* List of mutations for assign to the dux. If you want Typescript goodness, you * List of mutations for assign to the dux. If you want Typescript goodness, you
* probably want to use `addMutation()` instead. * probably want to use `addMutation()` instead.
* *
* In its generic array-of-array form, * In its generic array-of-array form,
* each mutation tuple contains: the action, the mutation, * each mutation tuple contains: the action, the mutation,
* and boolean indicating if this is a sink mutation. * and boolean indicating if this is a sink mutation.
* *
* The action can be an action creator function or a string. If it's a string, it's considered to be the * The action can be an action creator function or a string. If it's a string, it's considered to be the
* action type and a generic `action( actionName, payload() )` creator will be * action type and a generic `action( actionName, payload() )` creator will be
* generated for it. If an action is not already defined in the `actions` * generated for it. If an action is not already defined in the `actions`
* parameter, it'll be automatically added. * parameter, it'll be automatically added.
* *
* The pseudo-action type `*` can be used to match any action not explicitly matched by other mutations. * The pseudo-action type `*` can be used to match any action not explicitly matched by other mutations.
* *
* ```js * ```js
* const todosUpdux = updux({ * const todosUpdux = updux({
* mutations: { * mutations: {
* add: todo => state => [ ...state, todo ], * add: todo => state => [ ...state, todo ],
* done: done_id => u.map( u.if( ({id} => id === done_id), {done: true} ) ), * done: done_id => u.map( u.if( ({id} => id === done_id), {done: true} ) ),
* '*' (payload,action) => state => { * '*' (payload,action) => state => {
* console.warn( "unexpected action ", action.type ); * console.warn( "unexpected action ", action.type );
* return state; * return state;
* }, * },
* } * }
* }); * });
* ``` * ```
* *
* The signature of the mutations is `(payload,action) => state => newState`. * The signature of the mutations is `(payload,action) => state => newState`.
* It is designed to play well with `Updeep` (and [Immer](https://immerjs.github.io/immer/docs/introduction)). This way, instead of doing * It is designed to play well with `Updeep` (and [Immer](https://immerjs.github.io/immer/docs/introduction)). This way, instead of doing
* *
* ```js * ```js
* mutation: { * mutation: {
* renameTodo: newName => state => { ...state, name: newName } * renameTodo: newName => state => { ...state, name: newName }
* } * }
* ``` * ```
* *
* we can do * we can do
* *
* ```js * ```js
* mutation: { * mutation: {
* renameTodo: newName => u({ name: newName }) * renameTodo: newName => u({ name: newName })
* } * }
* ``` * ```
* *
* The final argument is the optional boolean `isSink`. If it is true, it'll * The final argument is the optional boolean `isSink`. If it is true, it'll
* prevent subduxes' mutations on the same action. It defaults to `false`. * prevent subduxes' mutations on the same action. It defaults to `false`.
* *
* The object version of the argument can be used as a shortcut when all actions * The object version of the argument can be used as a shortcut when all actions
* are strings. In that case, `isSink` is `false` for all mutations. * are strings. In that case, `isSink` is `false` for all mutations.
* *
* ##### groomMutations * ##### groomMutations
* *
* Function that can be provided to alter all local mutations of the updux * Function that can be provided to alter all local mutations of the updux
* (the mutations of subduxes are left untouched). * (the mutations of subduxes are left untouched).
* *
* Can be used, for example, for Immer integration: * Can be used, for example, for Immer integration:
* *
* ```js * ```js
* import Updux from 'updux'; * import Updux from 'updux';
* import { produce } from 'Immer'; * import { produce } from 'Immer';
* *
* const updux = new Updux({ * const updux = new Updux({
* initial: { counter: 0 }, * initial: { counter: 0 },
* groomMutations: mutation => (...args) => produce( mutation(...args) ), * groomMutations: mutation => (...args) => produce( mutation(...args) ),
* mutations: { * mutations: {
* add: (inc=1) => draft => draft.counter += inc * add: (inc=1) => draft => draft.counter += inc
* } * }
* }); * });
* ``` * ```
* *
* Or perhaps for debugging: * Or perhaps for debugging:
* *
* ```js * ```js
* import Updux from 'updux'; * import Updux from 'updux';
* *
* const updux = new Updux({ * const updux = new Updux({
* initial: { counter: 0 }, * initial: { counter: 0 },
* groomMutations: mutation => (...args) => state => { * groomMutations: mutation => (...args) => state => {
* console.log( "got action ", args[1] ); * console.log( "got action ", args[1] );
* return mutation(...args)(state); * return mutation(...args)(state);
* } * }
* }); * });
* ``` * ```
* ##### subduxes * ##### subduxes
* *
* Object mapping slices of the state to sub-upduxes. In addition to creating * Object mapping slices of the state to sub-upduxes. In addition to creating
* sub-reducers for those slices, it'll make the parend updux inherit all the * sub-reducers for those slices, it'll make the parend updux inherit all the
* actions and middleware from its subduxes. * actions and middleware from its subduxes.
* *
* For example, if in plain Redux you would do * For example, if in plain Redux you would do
* *
* ```js * ```js
* import { combineReducers } from 'redux'; * import { combineReducers } from 'redux';
* import todosReducer from './todos'; * import todosReducer from './todos';
* import statisticsReducer from './statistics'; * import statisticsReducer from './statistics';
* *
* const rootReducer = combineReducers({ * const rootReducer = combineReducers({
* todos: todosReducer, * todos: todosReducer,
* stats: statisticsReducer, * stats: statisticsReducer,
* }); * });
* ``` * ```
* *
* then with Updux you'd do * then with Updux you'd do
* *
* ```js * ```js
* import { updux } from 'updux'; * import { updux } from 'updux';
* import todos from './todos'; * import todos from './todos';
* import statistics from './statistics'; * import statistics from './statistics';
* *
* const rootUpdux = updux({ * const rootUpdux = updux({
* subduxes: { * subduxes: {
* todos, * todos,
* statistics * statistics
* } * }
* }); * });
* ``` * ```
* *
* ##### effects * ##### effects
* *
* Array of arrays or plain object defining asynchronous actions and side-effects triggered by actions. * Array of arrays or plain object defining asynchronous actions and side-effects triggered by actions.
* The effects themselves are Redux middleware, with the `dispatch` * The effects themselves are Redux middleware, with the `dispatch`
* property of the first argument augmented with all the available actions. * property of the first argument augmented with all the available actions.
* *
* ``` * ```
* updux({ * updux({
* effects: { * effects: {
* fetch: ({dispatch}) => next => async (action) => { * fetch: ({dispatch}) => next => async (action) => {
* next(action); * next(action);
* *
* let result = await fetch(action.payload.url).then( result => result.json() ); * let result = await fetch(action.payload.url).then( result => result.json() );
* dispatch.fetchSuccess(result); * dispatch.fetchSuccess(result);
* } * }
* } * }
* }); * });
* ``` * ```
* *
* @example * @example
* *
* ``` * ```
* import Updux from 'updux'; * import Updux from 'updux';
* import { actions, payload } from 'ts-action'; * import { actions, payload } from 'ts-action';
* import u from 'updeep'; * import u from 'updeep';
* *
* const todoUpdux = new Updux({ * const todoUpdux = new Updux({
* initial: { * initial: {
* done: false, * done: false,
* note: "", * note: "",
* }, * },
* actions: { * actions: {
* finish: action('FINISH', payload()), * finish: action('FINISH', payload()),
* edit: action('EDIT', payload()), * edit: action('EDIT', payload()),
* }, * },
* mutations: [ * mutations: [
* [ edit, note => u({note}) ] * [ edit, note => u({note}) ]
* ], * ],
* selectors: { * selectors: {
* getNote: state => state.note * getNote: state => state.note
* }, * },
* groomMutations: mutation => transform(mutation), * groomMutations: mutation => transform(mutation),
* subduxes: { * subduxes: {
* foo * foo
* }, * },
* effects: { * effects: {
* finish: () => next => action => { * finish: () => next => action => {
* console.log( "Woo! one more bites the dust" ); * console.log( "Woo! one more bites the dust" );
* } * }
* } * }
* }) * })
* ``` * ```
*/ */
export type UpduxConfig = Partial<{ export type UpduxConfig = Partial<{
initial: unknown, /** foo */ initial: unknown /** foo */;
subduxes: Dictionary<Dux>, subduxes: Dictionary<Dux>;
coduxes: Dux[], coduxes: Dux[];
actions: Dictionary<ActionCreator>, actions: Dictionary<ActionCreator>;
selectors: Dictionary<Selector>, selectors: Dictionary<Selector>;
mutations: any, mutations: any;
groomMutations: (m: Mutation) => Mutation, groomMutations: (m: Mutation) => Mutation;
effects: any, effects: any;
subscriptions: Function[];
}>; }>;
export type Upreducer<S = any> = (action: Action) => (state: S) => S; export type Upreducer<S = any> = (action: Action) => (state: S) => S;
/** @ignore */ /** @ignore */
export interface UpduxMiddlewareAPI<S=any,X = Dictionary<Selector>> { export interface UpduxMiddlewareAPI<S = any, X = Dictionary<Selector>> {
dispatch: Function; dispatch: Function;
getState(): S; getState(): S;
selectors: X; selectors: X;
actions: Dictionary<ActionCreator>; actions: Dictionary<ActionCreator>;
} }
export type UpduxMiddleware<S=any,X = Dictionary<Selector>,A = Action> = ( export type UpduxMiddleware<S = any, X = Dictionary<Selector>, A = Action> = (
api: UpduxMiddlewareAPI<S,X> api: UpduxMiddlewareAPI<S, X>
) => (next: Function) => (action: A) => any; ) => (next: Function) => (action: A) => any;
export type Selector<S = any> = (state: S) => unknown; export type Selector<S = any> = (state: S) => unknown;

View File

@ -1,6 +1,6 @@
import fp from 'lodash/fp'; import fp from 'lodash/fp';
import { action, payload, ActionCreator, ActionType } from 'ts-action'; import { action, payload, ActionCreator, ActionType } from 'ts-action';
import {AnyAction} from 'redux'; import { AnyAction } from 'redux';
import buildInitial from './buildInitial'; import buildInitial from './buildInitial';
import buildMutations from './buildMutations'; import buildMutations from './buildMutations';
@ -51,6 +51,39 @@ type StoreWithDispatchActions<
dispatch: { [type in keyof Actions]: (...args: any) => void }; dispatch: { [type in keyof Actions]: (...args: any) => void };
}; };
function wrap_subscription(sub) {
return store => {
const sub_curried = sub(store);
let previous: unknown;
return (state, unsubscribe) => {
if (state === previous) return;
previous = state;
return sub_curried(state, unsubscribe);
};
};
}
function _subscribeToStore(store: any, subscriptions: Function[] = []) {
subscriptions.forEach(sub => {
const subscriber = sub(store);
let unsub = store.subscribe(() => {
const state = store.getState();
return subscriber(state, unsub);
});
});
}
function sliced_subscription(slice, sub) {
return store => {
const sub_curried = sub(store);
return (state, unsubscribe) =>
sub_curried(fp.get(slice, state), unsubscribe);
};
}
/** /**
* @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
@ -63,7 +96,7 @@ export class Updux<
A = null, A = null,
X = unknown, X = unknown,
C extends UpduxConfig = {} C extends UpduxConfig = {}
> { > {
subduxes: Dictionary<Dux>; subduxes: Dictionary<Dux>;
coduxes: Dux[]; coduxes: Dux[];
@ -81,6 +114,8 @@ export class Updux<
Mutation<S> | [Mutation<S>, boolean | undefined] Mutation<S> | [Mutation<S>, boolean | undefined]
> = {}; > = {};
private localSubscriptions: Function[] = [];
get initial(): AggDuxState<S, C> { get initial(): AggDuxState<S, C> {
return buildInitial( return buildInitial(
this.localInitial, this.localInitial,
@ -99,30 +134,33 @@ export class Updux<
this.coduxes = config.coduxes ?? []; this.coduxes = config.coduxes ?? [];
this.subduxes = config.subduxes ?? {}; this.subduxes = config.subduxes ?? {};
Object.entries(config.actions ?? {}).forEach((args) => Object.entries(config.actions ?? {}).forEach(args =>
(this.addAction as any)(...args) (this.addAction as any)(...args)
); );
this.coduxes.forEach((c: any) => this.coduxes.forEach((c: any) =>
Object.entries(c.actions).forEach((args) => Object.entries(c.actions).forEach(args =>
(this.addAction as any)(...args) (this.addAction as any)(...args)
) )
); );
Object.values(this.subduxes).forEach((c: any) => { Object.values(this.subduxes).forEach((c: any) => {
Object.entries(c.actions).forEach((args) => { Object.entries(c.actions).forEach(args => {
(this.addAction as any)(...args); (this.addAction as any)(...args);
}); });
}); });
this.groomMutations = if (config.subscriptions) {
config.groomMutations ?? ((x: Mutation<S>) => x); config.subscriptions.forEach(sub => this.addSubscription(sub));
}
this.groomMutations = config.groomMutations ?? ((x: Mutation<S>) => x);
let effects = config.effects ?? []; let effects = config.effects ?? [];
if (!Array.isArray(effects)) { if (!Array.isArray(effects)) {
effects = (Object.entries(effects) as unknown) as Effect[]; effects = (Object.entries(effects) as unknown) as Effect[];
} }
effects.forEach((effect) => (this.addEffect as any)(...effect)); effects.forEach(effect => (this.addEffect as any)(...effect));
let mutations = config.mutations ?? []; let mutations = config.mutations ?? [];
@ -130,7 +168,7 @@ export class Updux<
mutations = fp.toPairs(mutations); mutations = fp.toPairs(mutations);
} }
mutations.forEach((args) => (this.addMutation as any)(...args)); mutations.forEach(args => (this.addMutation as any)(...args));
/* /*
@ -172,7 +210,7 @@ export class Updux<
const selectors = this.selectors; const selectors = this.selectors;
const actions = this.actions; const actions = this.actions;
return buildMiddleware( return buildMiddleware(
this.localEffects.map((effect) => this.localEffects.map(effect =>
effectToMw(effect, actions as any, selectors as any) effectToMw(effect, actions as any, selectors as any)
), ),
(this.coduxes as any).map(fp.get('middleware')) as any, (this.coduxes as any).map(fp.get('middleware')) as any,
@ -204,10 +242,7 @@ export class Updux<
} }
get upreducer(): Upreducer<S> { get upreducer(): Upreducer<S> {
return buildUpreducer( return buildUpreducer(this.initial, this.mutations as any) as any;
this.initial,
this.mutations as any
) as any;
} }
/** /**
@ -254,10 +289,7 @@ export class Updux<
* *
*/ */
get subduxUpreducer() { get subduxUpreducer() {
return buildUpreducer( return buildUpreducer(this.initial, buildMutations({}, this.subduxes));
this.initial,
buildMutations({}, this.subduxes)
);
} }
/** /**
@ -278,12 +310,35 @@ export class Updux<
* *
* *
*/ */
get createStore() { createStore(...args: any) {
return buildCreateStore<AggDuxState<S, C>, DuxActions<A, C>>( const store = buildCreateStore<AggDuxState<S, C>, DuxActions<A, C>>(
this.reducer as any, this.reducer as any,
this.middleware as any, this.middleware as any,
this.actions this.actions
)(...args);
_subscribeToStore(store, this.subscriptions);
return store;
}
/**
* Returns an array of all subscription functions registered for the dux.
* Subdux subscriptions are wrapped such that they are getting their
* local state. Also all subscriptions are further wrapped such that
* they are only called when the local state changed
*/
get subscriptions() {
let subscriptions = ([
this.localSubscriptions,
Object.entries(this.subduxes).map(([slice, subdux]) => {
return subdux.subscriptions.map(sub =>
sliced_subscription(slice, sub)
); );
}),
] as any).flat(Infinity);
return subscriptions.map(sub => wrap_subscription(sub));
} }
/** /**
@ -305,6 +360,7 @@ export class Updux<
* mutations, * mutations,
* initial, * initial,
* selectors, * selectors,
* subscriptions,
* } = myUpdux.asDux; * } = myUpdux.asDux;
* ``` * ```
* *
@ -324,6 +380,7 @@ export class Updux<
mutations: this.mutations, mutations: this.mutations,
initial: this.initial, initial: this.initial,
selectors: this.selectors, selectors: this.selectors,
subscriptions: this.subscriptions,
}; };
} }
@ -358,11 +415,7 @@ export class Updux<
mutation: Mutation<S, any>, mutation: Mutation<S, any>,
isSink?: boolean isSink?: boolean
); );
addMutation<A extends ActionCreator = any>( addMutation<A extends ActionCreator = any>(creator, mutation, isSink) {
creator,
mutation,
isSink
) {
const c = this.addAction(creator); const c = this.addAction(creator);
this.localMutations[c.type] = [ this.localMutations[c.type] = [
@ -420,10 +473,7 @@ export class Updux<
* baz(); // => { type: 'baz', payload: undefined } * baz(); // => { type: 'baz', payload: undefined }
* ``` * ```
*/ */
addAction( addAction(theaction: string, transform?: any): ActionCreator<string, any>;
theaction: string,
transform?: any
): ActionCreator<string, any>;
addAction( addAction(
theaction: string | ActionCreator<any>, theaction: string | ActionCreator<any>,
transform?: never transform?: never
@ -442,8 +492,7 @@ export class Updux<
payload: transform(...args), payload: transform(...args),
})); }));
} else { } else {
creator = creator = this.localActions[name] ?? action(name, payload());
this.localActions[name] ?? action(name, payload());
} }
} else { } else {
name = actionIn.type; name = actionIn.type;
@ -466,9 +515,7 @@ export class Updux<
const groupByOrder = (mws: any) => const groupByOrder = (mws: any) =>
fp.groupBy( fp.groupBy(
([, , actionType]: any) => ([, , actionType]: any) =>
['^', '$'].includes(actionType) ['^', '$'].includes(actionType) ? actionType : 'middle',
? actionType
: 'middle',
mws mws
); );
@ -486,7 +533,7 @@ export class Updux<
])(this.subduxes); ])(this.subduxes);
const local = groupByOrder( const local = groupByOrder(
this.localEffects.map((x) => [this, [], ...x]) this.localEffects.map(x => [this, [], ...x])
); );
return fp.flatten( return fp.flatten(
@ -497,7 +544,7 @@ export class Updux<
subs.middle, subs.middle,
subs['$'], subs['$'],
local['$'], local['$'],
].filter((x) => x) ].filter(x => x)
); );
} }
@ -517,6 +564,13 @@ sub-state already taken care of you).
fp.mapValues('selectors', this.subduxes) fp.mapValues('selectors', this.subduxes)
) as any; ) as any;
} }
/**
* Add a subscription to the dux.
*/
addSubscription(subscription: Function) {
this.localSubscriptions = [...this.localSubscriptions, subscription];
} }
}
export default Updux; export default Updux;