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