mutation generic matcher
This commit is contained in:
parent
4c28a9ad05
commit
2e03a51e15
55
src/Updux.ts
55
src/Updux.ts
@ -15,6 +15,7 @@ import {
|
|||||||
import { AggregateActions, Dux } from './types.js';
|
import { AggregateActions, Dux } from './types.js';
|
||||||
import { buildActions } from './buildActions.js';
|
import { buildActions } from './buildActions.js';
|
||||||
import { buildInitial, AggregateState } from './initial.js';
|
import { buildInitial, AggregateState } from './initial.js';
|
||||||
|
import { buildReducer } from './reducer.js';
|
||||||
|
|
||||||
type MyActionCreator = { type: string } & ((...args: any) => any);
|
type MyActionCreator = { type: string } & ((...args: any) => any);
|
||||||
|
|
||||||
@ -41,12 +42,13 @@ type ResolveActions<
|
|||||||
: never;
|
: never;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Mutation<
|
export type Mutation<A extends Action<any> = Action<any>, S = any> = (
|
||||||
A extends ActionCreator<any> = ActionCreator<any>,
|
payload: A extends {
|
||||||
S = any,
|
payload: infer P;
|
||||||
> = (
|
}
|
||||||
payload: ReturnType<A>['payload'],
|
? P
|
||||||
action: ReturnType<A>,
|
: undefined,
|
||||||
|
action: A,
|
||||||
) => (state: S) => S | void;
|
) => (state: S) => S | void;
|
||||||
|
|
||||||
export default class Updux<
|
export default class Updux<
|
||||||
@ -58,10 +60,7 @@ export default class Updux<
|
|||||||
> {
|
> {
|
||||||
#localInitial: T_LocalState;
|
#localInitial: T_LocalState;
|
||||||
#localActions: T_LocalActions;
|
#localActions: T_LocalActions;
|
||||||
#localMutations: Record<
|
#localMutations: MutationCase[] = [];
|
||||||
string,
|
|
||||||
Mutation<ActionCreator<any>, AggregateState<T_LocalState, T_Subduxes>>
|
|
||||||
> = {};
|
|
||||||
#subduxes: T_Subduxes;
|
#subduxes: T_Subduxes;
|
||||||
|
|
||||||
#name: string;
|
#name: string;
|
||||||
@ -110,11 +109,37 @@ export default class Updux<
|
|||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO force the actionCreator to be one of the actions?
|
// TODO memoize this sucker
|
||||||
mutation<A extends ActionCreator<any>>(
|
get reducer() {
|
||||||
actionCreator: A,
|
return buildReducer(this.initial, this.#localMutations) as any as (
|
||||||
|
state: undefined | typeof this.initial,
|
||||||
|
action: Action,
|
||||||
|
) => typeof this.initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
addMutation<A extends Action<any>>(
|
||||||
|
matcher: (action: A) => boolean,
|
||||||
mutation: Mutation<A, AggregateState<T_LocalState, T_Subduxes>>,
|
mutation: Mutation<A, AggregateState<T_LocalState, T_Subduxes>>,
|
||||||
) {
|
terminal?: boolean,
|
||||||
this.#localMutations[(actionCreator as any).type] = mutation;
|
);
|
||||||
|
addMutation<A extends ActionCreator<any>>(
|
||||||
|
actionCreator: A,
|
||||||
|
mutation: Mutation<
|
||||||
|
ReturnType<A>,
|
||||||
|
AggregateState<T_LocalState, T_Subduxes>
|
||||||
|
>,
|
||||||
|
terminal?: boolean,
|
||||||
|
);
|
||||||
|
addMutation(matcher, mutation, terminal = false) {
|
||||||
|
if (typeof matcher === 'function' && matcher.match) {
|
||||||
|
// matcher, matcher man...
|
||||||
|
matcher = matcher.match;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#localMutations.push({
|
||||||
|
terminal,
|
||||||
|
matcher,
|
||||||
|
mutation,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { test, expect } from 'vitest';
|
|
||||||
import schema from 'json-schema-shorthand';
|
import schema from 'json-schema-shorthand';
|
||||||
import u from 'updeep';
|
import u from 'updeep';
|
||||||
|
|
||||||
@ -6,31 +5,6 @@ import { action } from './actions.js';
|
|||||||
|
|
||||||
import { Updux, dux } from './Updux.js';
|
import { Updux, dux } from './Updux.js';
|
||||||
|
|
||||||
test('set a mutation', () => {
|
|
||||||
const dux = new Updux({
|
|
||||||
initial: {
|
|
||||||
x: 'potato',
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
foo: action('foo', (x) => ({ x })),
|
|
||||||
bar: action('bar'),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
dux.setMutation(dux.actions.foo, (payload, action) => {
|
|
||||||
expect(payload).toEqual({ x: 'hello ' });
|
|
||||||
expect(action).toEqual(dux.actions.foo('hello '));
|
|
||||||
return u({
|
|
||||||
x: payload.x + action.type,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = dux.reducer(undefined, dux.actions.foo('hello '));
|
|
||||||
expect(result).toEqual({
|
|
||||||
x: 'hello foo',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('mutation of a subdux', async () => {
|
test('mutation of a subdux', async () => {
|
||||||
const bar = dux({
|
const bar = dux({
|
||||||
actions: {
|
actions: {
|
||||||
@ -72,19 +46,3 @@ test('strings and generators', async () => {
|
|||||||
expect(foo.actions.d).toBeTypeOf('function');
|
expect(foo.actions.d).toBeTypeOf('function');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('splat mutation', () => {
|
|
||||||
const myDux = new Updux({
|
|
||||||
initial: [],
|
|
||||||
actions: { one: null, two: null },
|
|
||||||
mutations: {
|
|
||||||
'*': (payload) => (state) => payload ? [...state, payload] : state,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const store = myDux.createStore();
|
|
||||||
expect(store.getState()).toEqual([]);
|
|
||||||
|
|
||||||
store.dispatch.one(11);
|
|
||||||
store.dispatch.two(22);
|
|
||||||
|
|
||||||
expect(store.getState()).toEqual([11, 22]);
|
|
||||||
});
|
|
||||||
|
39
src/mutations.test.ts
Normal file
39
src/mutations.test.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { test, expect } from 'vitest';
|
||||||
|
|
||||||
|
import Updux from './index.js';
|
||||||
|
|
||||||
|
test('set a mutation', () => {
|
||||||
|
const dux = new Updux({
|
||||||
|
initial: 'potato',
|
||||||
|
actions: {
|
||||||
|
foo: (x) => ({ x }),
|
||||||
|
bar: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let didIt = false;
|
||||||
|
|
||||||
|
dux.addMutation(dux.actions.foo, (payload, action) => () => {
|
||||||
|
didIt = true;
|
||||||
|
expect(payload).toEqual({ x: 'hello ' });
|
||||||
|
expect(action).toEqual(dux.actions.foo('hello '));
|
||||||
|
return payload.x + action.type;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = dux.reducer(undefined, dux.actions.foo('hello '));
|
||||||
|
expect(didIt).toBeTruthy();
|
||||||
|
expect(result).toEqual('hello foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('catch-all mutation', () => {
|
||||||
|
const dux = new Updux({
|
||||||
|
initial: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
dux.addMutation(
|
||||||
|
() => true,
|
||||||
|
(payload, action) => () => 'got it',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(dux.reducer(undefined, { type: 'foo' })).toEqual('got it');
|
||||||
|
});
|
@ -4,7 +4,7 @@ import * as R from 'remeda';
|
|||||||
import { Dux } from './types.js';
|
import { Dux } from './types.js';
|
||||||
import { Mutation } from './Updux.js';
|
import { Mutation } from './Updux.js';
|
||||||
|
|
||||||
type MutationCase = {
|
export type MutationCase = {
|
||||||
matcher: (action: Action) => boolean;
|
matcher: (action: Action) => boolean;
|
||||||
mutation: Mutation;
|
mutation: Mutation;
|
||||||
terminal: boolean;
|
terminal: boolean;
|
||||||
@ -20,7 +20,7 @@ export function buildReducer(
|
|||||||
|
|
||||||
// TODO matcherMutation
|
// TODO matcherMutation
|
||||||
// TODO defaultMutation
|
// TODO defaultMutation
|
||||||
|
//
|
||||||
const reducer = (state = initialState, action: Action) => {
|
const reducer = (state = initialState, action: Action) => {
|
||||||
if (!action?.type)
|
if (!action?.type)
|
||||||
throw new Error('upreducer called with a bad action');
|
throw new Error('upreducer called with a bad action');
|
||||||
@ -28,20 +28,14 @@ export function buildReducer(
|
|||||||
let terminal = false;
|
let terminal = false;
|
||||||
let didSomething = false;
|
let didSomething = false;
|
||||||
|
|
||||||
const foo = createAction('foo');
|
mutations
|
||||||
|
.filter(({ matcher }) => matcher(action))
|
||||||
const localMutation = mutations.find(({ matcher }) => matcher(action));
|
.forEach(({ mutation, terminal: t }) => {
|
||||||
|
if (t) terminal = true;
|
||||||
if (localMutation) {
|
//
|
||||||
didSomething = true;
|
|
||||||
if (localMutation.terminal) terminal = true;
|
|
||||||
|
|
||||||
// TODO wrap mutations in immer
|
// TODO wrap mutations in immer
|
||||||
state = localMutation.mutation(
|
state = mutation((action as any).payload, action)(state);
|
||||||
(action as any).payload,
|
});
|
||||||
action,
|
|
||||||
)(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO defaultMutation
|
// TODO defaultMutation
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Action, ActionCreator } from 'redux';
|
import { Action, ActionCreator, Reducer } from 'redux';
|
||||||
|
|
||||||
export type Dux<
|
export type Dux<
|
||||||
STATE = any,
|
STATE = any,
|
||||||
@ -6,6 +6,10 @@ export type Dux<
|
|||||||
> = Partial<{
|
> = Partial<{
|
||||||
initial: STATE;
|
initial: STATE;
|
||||||
actions: ACTIONS;
|
actions: ACTIONS;
|
||||||
|
reducer: (
|
||||||
|
state: STATE,
|
||||||
|
action: ReturnType<ACTIONS[keyof ACTIONS]>,
|
||||||
|
) => STATE;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
type ActionsOf<DUX> = DUX extends { actions: infer A } ? A : {};
|
type ActionsOf<DUX> = DUX extends { actions: infer A } ? A : {};
|
||||||
|
Loading…
Reference in New Issue
Block a user