Compare commits
6 Commits
90e90779e1
...
760263b555
Author | SHA1 | Date | |
---|---|---|---|
760263b555 | |||
9b23a6995b | |||
39aaf231ac | |||
5fe858ef16 | |||
91e6bdd4f0 | |||
2f687cfda3 |
@ -7,6 +7,21 @@ vars:
|
|||||||
PARENT_BRANCH: main
|
PARENT_BRANCH: main
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
|
release:gitea:
|
||||||
|
cmds:
|
||||||
|
- tea releases create -asset releases/updux-{{.VERSION}}.tgz -p --title {{.VERSION}} --tag {{.VERSION}}
|
||||||
|
vars:
|
||||||
|
VERSION: { sh: 'npm version --json | jq -r .updux' }
|
||||||
|
prerelease:
|
||||||
|
desc: deploy a prerelease
|
||||||
|
deps:
|
||||||
|
- build
|
||||||
|
- checks
|
||||||
|
cmds:
|
||||||
|
- npm version prerelease
|
||||||
|
- npm pack --pack-destination releases
|
||||||
|
- { task: 'release:gitea' }
|
||||||
|
|
||||||
pack:
|
pack:
|
||||||
desc: bundle the distro
|
desc: bundle the distro
|
||||||
deps: [build, checks]
|
deps: [build, checks]
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "4.0.0-alpha.4",
|
"version": "4.0.0-alpha.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mobily/ts-belt": "4.0.0-rc.5",
|
"@mobily/ts-belt": "4.0.0-rc.5",
|
||||||
|
45
src/Updux.ts
45
src/Updux.ts
@ -11,6 +11,7 @@ import {
|
|||||||
DuxSelectors,
|
DuxSelectors,
|
||||||
DuxState,
|
DuxState,
|
||||||
Mutation,
|
Mutation,
|
||||||
|
MutationEntry,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import baseMoize from 'moize/mjs/index.mjs';
|
import baseMoize from 'moize/mjs/index.mjs';
|
||||||
import { buildInitialState } from './initialState.js';
|
import { buildInitialState } from './initialState.js';
|
||||||
@ -36,6 +37,7 @@ interface ActionCreator<T extends string, P, A extends Array<any>> {
|
|||||||
|
|
||||||
const moize = (func) => baseMoize(func, { maxSize: 1 });
|
const moize = (func) => baseMoize(func, { maxSize: 1 });
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description main Updux class
|
* @description main Updux class
|
||||||
*/
|
*/
|
||||||
@ -68,7 +70,7 @@ export default class Updux<D extends DuxConfig> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
#effects = [];
|
#effects: EffectMiddleware<D>[] = [];
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
#reactions: DuxReaction<D>[] = [];
|
#reactions: DuxReaction<D>[] = [];
|
||||||
@ -76,12 +78,21 @@ export default class Updux<D extends DuxConfig> {
|
|||||||
/** @internal */
|
/** @internal */
|
||||||
#mutations: any[] = [];
|
#mutations: any[] = [];
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
#inheritedReducer: (state: DuxState<D> | undefined, action: AnyAction) => DuxState<D>;
|
||||||
|
|
||||||
constructor(private readonly duxConfig: D) {
|
constructor(private readonly duxConfig: D) {
|
||||||
if (duxConfig.subduxes)
|
if (duxConfig.subduxes)
|
||||||
this.#subduxes = D.map(duxConfig.subduxes, (s) =>
|
this.#subduxes = D.map(duxConfig.subduxes, (s) =>
|
||||||
s instanceof Updux ? s : new Updux(s),
|
s instanceof Updux ? s : new Updux(s),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.#inheritedReducer = duxConfig.reducer;
|
||||||
|
|
||||||
|
this.#effects = duxConfig.effects ?? [];
|
||||||
|
|
||||||
|
this.#reactions = (duxConfig.reactions as any) ?? [];
|
||||||
|
|
||||||
this.#actions = buildActions(duxConfig.actions, this.#subduxes) as any;
|
this.#actions = buildActions(duxConfig.actions, this.#subduxes) as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,7 +221,7 @@ export default class Updux<D extends DuxConfig> {
|
|||||||
setDefaultMutation(
|
setDefaultMutation(
|
||||||
mutation: Mutation<any, DuxState<D>>,
|
mutation: Mutation<any, DuxState<D>>,
|
||||||
terminal?: boolean,
|
terminal?: boolean,
|
||||||
);
|
): Updux<D>;
|
||||||
setDefaultMutation(mutation, terminal = false) {
|
setDefaultMutation(mutation, terminal = false) {
|
||||||
this.#defaultMutation = { terminal, mutation };
|
this.#defaultMutation = { terminal, mutation };
|
||||||
return this;
|
return this;
|
||||||
@ -223,6 +234,7 @@ export default class Updux<D extends DuxConfig> {
|
|||||||
this.#mutations,
|
this.#mutations,
|
||||||
this.#defaultMutation,
|
this.#defaultMutation,
|
||||||
this.#subduxes,
|
this.#subduxes,
|
||||||
|
this.#inheritedReducer,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -282,11 +294,11 @@ export default class Updux<D extends DuxConfig> {
|
|||||||
return this as any;
|
return this as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
get effects(): any {
|
get effects(): any[] {
|
||||||
return this.#memoBuildEffects(this.#effects, this.#subduxes);
|
return this.#memoBuildEffects(this.#effects, this.#subduxes);
|
||||||
}
|
}
|
||||||
|
|
||||||
get upreducer() {
|
get upreducer(): (action: AnyAction) => (state?: DuxState<D>) => DuxState<D> {
|
||||||
return (action: AnyAction) => (state?: DuxState<D>) => this.reducer(state, action);
|
return (action: AnyAction) => (state?: DuxState<D>) => this.reducer(state, action);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -318,4 +330,29 @@ export default class Updux<D extends DuxConfig> {
|
|||||||
return this.#memoBuildReactions(this.#reactions, this.#subduxes);
|
return this.#memoBuildReactions(this.#reactions, this.#subduxes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get defaultMutation() {
|
||||||
|
return this.#defaultMutation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Returns an object holding the Updux reducer and all its
|
||||||
|
* paraphernalia.
|
||||||
|
*/
|
||||||
|
get asDux(): {
|
||||||
|
initialState: DuxState<D>,
|
||||||
|
actions: DuxActions<D>,
|
||||||
|
reducer: (state: DuxState<D>, action: AnyAction) => DuxState<D>,
|
||||||
|
effects: EffectMiddleware<D>[],
|
||||||
|
reactions: DuxReaction<D>[],
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
initialState: this.initialState,
|
||||||
|
actions: this.actions,
|
||||||
|
effects: this.effects,
|
||||||
|
reactions: this.reactions,
|
||||||
|
reducer: this.reducer,
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
37
src/asDux.test.ts
Normal file
37
src/asDux.test.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import Updux, { createAction } from './index.js';
|
||||||
|
|
||||||
|
test('asDux', () => {
|
||||||
|
const actionA = createAction('actionA');
|
||||||
|
const defaultMutation = () => (state: number) => state + 1;
|
||||||
|
const dux = new Updux({
|
||||||
|
initialState: 13,
|
||||||
|
actions: { actionA },
|
||||||
|
})
|
||||||
|
.setDefaultMutation(defaultMutation)
|
||||||
|
.addReaction((api) => (state) => { })
|
||||||
|
.addEffect((api) => (next) => (action) => next(action));
|
||||||
|
|
||||||
|
const asDux = dux.asDux;
|
||||||
|
|
||||||
|
expect(asDux.initialState).toEqual(13);
|
||||||
|
|
||||||
|
expect(asDux.reducer).toBeTypeOf('function');
|
||||||
|
|
||||||
|
expect(asDux.actions.actionA()).toMatchObject({ type: 'actionA' });
|
||||||
|
|
||||||
|
expect(asDux.effects).toHaveLength(1);
|
||||||
|
|
||||||
|
expect(asDux.reactions).toHaveLength(1);
|
||||||
|
|
||||||
|
const newDux = new Updux(asDux);
|
||||||
|
|
||||||
|
expect(newDux.initialState).toEqual(13);
|
||||||
|
|
||||||
|
expect(newDux.reducer).toBeTypeOf('function');
|
||||||
|
|
||||||
|
expect(newDux.actions.actionA()).toMatchObject({ type: 'actionA' });
|
||||||
|
|
||||||
|
expect(newDux.effects).toHaveLength(1);
|
||||||
|
|
||||||
|
expect(newDux.reactions).toHaveLength(1);
|
||||||
|
});
|
@ -98,3 +98,15 @@ test('actionType as string', () => {
|
|||||||
dux.addMutation('unknown', () => (x) => x);
|
dux.addMutation('unknown', () => (x) => x);
|
||||||
}).toThrow();
|
}).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('setDefaultMutation return value', () => {
|
||||||
|
const dux = new Updux({
|
||||||
|
initialState: 13,
|
||||||
|
});
|
||||||
|
|
||||||
|
let withDM = dux.setDefaultMutation(() => (state) => state);
|
||||||
|
|
||||||
|
expect(withDM).toEqual(dux);
|
||||||
|
|
||||||
|
expectTypeOf(withDM.initialState).toBeNumber();
|
||||||
|
});
|
||||||
|
@ -4,6 +4,7 @@ import * as rtk from '@reduxjs/toolkit';
|
|||||||
import { DuxConfig, Mutation } from './types.js';
|
import { DuxConfig, Mutation } from './types.js';
|
||||||
import { D } from '@mobily/ts-belt';
|
import { D } from '@mobily/ts-belt';
|
||||||
import Updux from './Updux.js';
|
import Updux from './Updux.js';
|
||||||
|
import { AnyAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
export type MutationCase = {
|
export type MutationCase = {
|
||||||
matcher: (action: rtk.AnyAction) => boolean;
|
matcher: (action: rtk.AnyAction) => boolean;
|
||||||
@ -16,12 +17,10 @@ export function buildReducer(
|
|||||||
mutations: MutationCase[] = [],
|
mutations: MutationCase[] = [],
|
||||||
defaultMutation?: Omit<MutationCase, 'matcher'>,
|
defaultMutation?: Omit<MutationCase, 'matcher'>,
|
||||||
subduxes: Record<string, Updux<any>> = {},
|
subduxes: Record<string, Updux<any>> = {},
|
||||||
|
inheritedReducer?: (state: any, action: AnyAction) => any,
|
||||||
) {
|
) {
|
||||||
const subReducers = D.map(subduxes, D.getUnsafe('reducer'));
|
const subReducers = D.map(subduxes, D.getUnsafe('reducer'));
|
||||||
|
|
||||||
// TODO matcherMutation
|
|
||||||
// TODO defaultMutation
|
|
||||||
//
|
|
||||||
const reducer = (state = initialStateState, action: rtk.AnyAction) => {
|
const reducer = (state = initialStateState, action: rtk.AnyAction) => {
|
||||||
if (!action?.type) throw new Error('reducer called with a bad action');
|
if (!action?.type) throw new Error('reducer called with a bad action');
|
||||||
|
|
||||||
@ -30,6 +29,13 @@ export function buildReducer(
|
|||||||
if (active.length === 0 && defaultMutation)
|
if (active.length === 0 && defaultMutation)
|
||||||
active.push(defaultMutation as any);
|
active.push(defaultMutation as any);
|
||||||
|
|
||||||
|
if (!active.some(R.prop<any, any>('terminal')) && inheritedReducer) {
|
||||||
|
active.push({
|
||||||
|
mutation: (_payload, action) => (state) => {
|
||||||
|
return u(state, inheritedReducer(state, action));
|
||||||
|
},
|
||||||
|
} as any);
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
!active.some(R.prop<any, any>('terminal')) &&
|
!active.some(R.prop<any, any>('terminal')) &&
|
||||||
D.values(subReducers).length > 0
|
D.values(subReducers).length > 0
|
||||||
@ -49,10 +55,6 @@ export function buildReducer(
|
|||||||
} as any);
|
} as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
// frozen objects don't play well with immer
|
|
||||||
// if (Object.isFrozen(state)) {
|
|
||||||
// state = { ...(state as any) };
|
|
||||||
// }
|
|
||||||
return active.reduce(
|
return active.reduce(
|
||||||
(state, { mutation }) =>
|
(state, { mutation }) =>
|
||||||
mutation((action as any).payload, action)(state),
|
mutation((action as any).payload, action)(state),
|
||||||
|
27
src/types.ts
27
src/types.ts
@ -5,6 +5,7 @@ import {
|
|||||||
Dispatch,
|
Dispatch,
|
||||||
MiddlewareAPI,
|
MiddlewareAPI,
|
||||||
} from '@reduxjs/toolkit';
|
} from '@reduxjs/toolkit';
|
||||||
|
import { EffectMiddleware } from './effects.js';
|
||||||
import Updux from './Updux.js';
|
import Updux from './Updux.js';
|
||||||
|
|
||||||
export type DuxConfig = Partial<{
|
export type DuxConfig = Partial<{
|
||||||
@ -12,6 +13,9 @@ export type DuxConfig = Partial<{
|
|||||||
subduxes: Record<string, DuxConfig>;
|
subduxes: Record<string, DuxConfig>;
|
||||||
actions: Record<string, ActionCreator<string> | Function>;
|
actions: Record<string, ActionCreator<string> | Function>;
|
||||||
selectors: Record<string, Function>;
|
selectors: Record<string, Function>;
|
||||||
|
effects: EffectMiddleware<any>[];
|
||||||
|
reactions: DuxReaction<any>[];
|
||||||
|
reducer: (state: any, action: AnyAction) => any;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
type UpduxConfig<D> = D extends Updux<infer T> ? T : D;
|
type UpduxConfig<D> = D extends Updux<infer T> ? T : D;
|
||||||
@ -62,6 +66,12 @@ export type DuxActions<D> = ResolveActions<
|
|||||||
|
|
||||||
export type Subduxes = Record<string, Updux<any> | DuxConfig>;
|
export type Subduxes = Record<string, Updux<any> | DuxConfig>;
|
||||||
|
|
||||||
|
export type MutationEntry<S = any> = {
|
||||||
|
terminal: boolean;
|
||||||
|
matcher?: (action: AnyAction) => boolean;
|
||||||
|
mutation: Mutation<AnyAction, S>;
|
||||||
|
};
|
||||||
|
|
||||||
export type Mutation<A = AnyAction, S = any> = (
|
export type Mutation<A = AnyAction, S = any> = (
|
||||||
payload: A extends {
|
payload: A extends {
|
||||||
payload: infer P;
|
payload: infer P;
|
||||||
@ -89,14 +99,16 @@ export type AugmentedMiddlewareAPI<D> = MiddlewareAPI<
|
|||||||
selectors: DuxSelectors<D>;
|
selectors: DuxSelectors<D>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DuxSelectors<D> = ForceResolveObject<(D extends { selectors: infer S } ? S : {}) &
|
export type DuxSelectors<D> = ForceResolveObject<
|
||||||
|
(D extends { selectors: infer S } ? S : {}) &
|
||||||
(D extends { subduxes: infer SUB }
|
(D extends { subduxes: infer SUB }
|
||||||
? UnionToIntersection<
|
? UnionToIntersection<
|
||||||
Values<{
|
Values<{
|
||||||
[key in keyof SUB]: RebaseSelectors<key, SUB[key]>;
|
[key in keyof SUB]: RebaseSelectors<key, SUB[key]>;
|
||||||
}>
|
}>
|
||||||
>
|
>
|
||||||
: {})>;
|
: {})
|
||||||
|
>;
|
||||||
|
|
||||||
export type UnionToIntersection<U> = (
|
export type UnionToIntersection<U> = (
|
||||||
U extends any ? (k: U) => void : never
|
U extends any ? (k: U) => void : never
|
||||||
@ -117,9 +129,10 @@ type RebaseSelector<SLICE, S> = SLICE extends string
|
|||||||
: never;
|
: never;
|
||||||
type Values<X> = X[keyof X];
|
type Values<X> = X[keyof X];
|
||||||
|
|
||||||
|
export type DuxReaction<D extends DuxConfig> = (
|
||||||
export type DuxReaction<D extends DuxConfig> =
|
api: AugmentedMiddlewareAPI<D>,
|
||||||
(api: AugmentedMiddlewareAPI<D>) => (state: DuxState<D>,
|
) => (
|
||||||
|
state: DuxState<D>,
|
||||||
previousState: DuxState<D> | undefined,
|
previousState: DuxState<D> | undefined,
|
||||||
unsubscribe: () => void) => void;
|
unsubscribe: () => void,
|
||||||
|
) => void;
|
||||||
|
Loading…
Reference in New Issue
Block a user