Merge branch 'reducer-ts' into typescript
This commit is contained in:
commit
e55bf7a771
69
src/Updux.ts
69
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, MutationCase } from './reducer.js';
|
||||||
|
|
||||||
type MyActionCreator = { type: string } & ((...args: any) => any);
|
type MyActionCreator = { type: string } & ((...args: any) => any);
|
||||||
|
|
||||||
@ -41,11 +42,14 @@ type ResolveActions<
|
|||||||
: never;
|
: never;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Mutation<A extends ActionCreator<any> = ActionCreator<any>, S = any> = (
|
export type Mutation<A extends Action<any> = Action<any>, S = any> = (
|
||||||
state: S,
|
payload: A extends {
|
||||||
payload: ReturnType<A>['payload'],
|
payload: infer P;
|
||||||
action: ReturnType<A>,
|
}
|
||||||
) => S | void;
|
? P
|
||||||
|
: undefined,
|
||||||
|
action: A,
|
||||||
|
) => (state: S) => S | void;
|
||||||
|
|
||||||
export default class Updux<
|
export default class Updux<
|
||||||
T_LocalState = Record<any, any>,
|
T_LocalState = Record<any, any>,
|
||||||
@ -56,10 +60,8 @@ export default class Updux<
|
|||||||
> {
|
> {
|
||||||
#localInitial: T_LocalState;
|
#localInitial: T_LocalState;
|
||||||
#localActions: T_LocalActions;
|
#localActions: T_LocalActions;
|
||||||
#localMutations: Record<
|
#localMutations: MutationCase[] = [];
|
||||||
string,
|
#defaultMutation: Omit<MutationCase, 'matcher'>;
|
||||||
Mutation<ActionCreator<any>, AggregateState<T_LocalState, T_Subduxes>>
|
|
||||||
> = {};
|
|
||||||
#subduxes: T_Subduxes;
|
#subduxes: T_Subduxes;
|
||||||
|
|
||||||
#name: string;
|
#name: string;
|
||||||
@ -108,11 +110,52 @@ 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,
|
||||||
|
this.#defaultMutation,
|
||||||
|
) as any as (
|
||||||
|
state: undefined | typeof this.initial,
|
||||||
|
action: Action,
|
||||||
|
) => typeof this.initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO be smarter with the guard?
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addDefaultMutation(
|
||||||
|
mutation: Mutation<
|
||||||
|
Action<any>,
|
||||||
|
AggregateState<T_LocalState, T_Subduxes>
|
||||||
|
>,
|
||||||
|
terminal = false,
|
||||||
) {
|
) {
|
||||||
this.#localMutations[(actionCreator as any).type] = mutation;
|
this.#defaultMutation = { mutation, terminal };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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]);
|
|
||||||
});
|
|
||||||
|
55
src/mutations.test.ts
Normal file
55
src/mutations.test.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('default mutation', () => {
|
||||||
|
const dux = new Updux({
|
||||||
|
initial: '',
|
||||||
|
actions: {
|
||||||
|
foo: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
dux.addMutation(dux.actions.foo, () => () => 'got it');
|
||||||
|
|
||||||
|
dux.addDefaultMutation((_payload, action) => () => action.type);
|
||||||
|
|
||||||
|
expect(dux.reducer(undefined, { type: 'foo' })).toEqual('got it');
|
||||||
|
expect(dux.reducer(undefined, { type: 'bar' })).toEqual('bar');
|
||||||
|
});
|
@ -1,19 +0,0 @@
|
|||||||
import { test, expect } from 'vitest';
|
|
||||||
|
|
||||||
import { Updux } from './Updux.js';
|
|
||||||
|
|
||||||
test('basic reducer', () => {
|
|
||||||
const dux = new Updux({ initial: { a: 3 } });
|
|
||||||
|
|
||||||
expect(dux.reducer).toBeTypeOf('function');
|
|
||||||
|
|
||||||
expect(dux.reducer({ a: 1 }, { type: 'foo' })).toMatchObject({ a: 1 }); // noop
|
|
||||||
});
|
|
||||||
|
|
||||||
test('basic upreducer', () => {
|
|
||||||
const dux = new Updux({ initial: { a: 3 } });
|
|
||||||
|
|
||||||
expect(dux.upreducer).toBeTypeOf('function');
|
|
||||||
|
|
||||||
expect(dux.upreducer({ type: 'foo' })({ a: 1 })).toMatchObject({ a: 1 }); // noop
|
|
||||||
});
|
|
31
src/reducer.test.ts
Normal file
31
src/reducer.test.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { test, expect } from 'vitest';
|
||||||
|
|
||||||
|
import { buildReducer } from './reducer.js';
|
||||||
|
import Updux from './Updux.js';
|
||||||
|
|
||||||
|
test('buildReducer, initial state', () => {
|
||||||
|
const reducer = buildReducer({ a: 1 });
|
||||||
|
|
||||||
|
expect(reducer(undefined, { type: 'foo' })).toEqual({ a: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildReducer, mutation', () => {
|
||||||
|
const reducer = buildReducer(1, [
|
||||||
|
{
|
||||||
|
matcher: ({ type }) => type === 'inc',
|
||||||
|
mutation: () => (state) => state + 1,
|
||||||
|
terminal: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(reducer(undefined, { type: 'foo' })).toEqual(1);
|
||||||
|
expect(reducer(undefined, { type: 'inc' })).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.todo('basic reducer', () => {
|
||||||
|
const dux = new Updux({ initial: { a: 3 } });
|
||||||
|
|
||||||
|
expect(dux.reducer).toBeTypeOf('function');
|
||||||
|
|
||||||
|
expect(dux.reducer({ a: 1 }, { type: 'foo' })).toMatchObject({ a: 1 }); // noop
|
||||||
|
});
|
82
src/reducer.ts
Normal file
82
src/reducer.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import { Action, ActionCreator, createAction } from '@reduxjs/toolkit';
|
||||||
|
import { BaseActionCreator } from '@reduxjs/toolkit/dist/createAction.js';
|
||||||
|
import * as R from 'remeda';
|
||||||
|
import { Dux } from './types.js';
|
||||||
|
import { Mutation } from './Updux.js';
|
||||||
|
|
||||||
|
export type MutationCase = {
|
||||||
|
matcher: (action: Action) => boolean;
|
||||||
|
mutation: Mutation;
|
||||||
|
terminal: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildReducer(
|
||||||
|
initialState: any,
|
||||||
|
mutations: MutationCase[] = [],
|
||||||
|
defaultMutation?: Omit<MutationCase, 'matcher'>,
|
||||||
|
subduxes: Record<string, Dux> = {},
|
||||||
|
) {
|
||||||
|
// const subReducers =
|
||||||
|
// ? R.mapValues(subduxes, R.prop('reducer'));
|
||||||
|
|
||||||
|
// TODO matcherMutation
|
||||||
|
// TODO defaultMutation
|
||||||
|
//
|
||||||
|
const reducer = (state = initialState, action: Action) => {
|
||||||
|
if (!action?.type)
|
||||||
|
throw new Error('upreducer called with a bad action');
|
||||||
|
|
||||||
|
let terminal = false;
|
||||||
|
let didSomething = false;
|
||||||
|
|
||||||
|
mutations
|
||||||
|
.filter(({ matcher }) => matcher(action))
|
||||||
|
.forEach(({ mutation, terminal: t }) => {
|
||||||
|
if (t) terminal = true;
|
||||||
|
didSomething = true;
|
||||||
|
//
|
||||||
|
// TODO wrap mutations in immer
|
||||||
|
state = mutation((action as any).payload, action)(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!didSomething && defaultMutation) {
|
||||||
|
if (defaultMutation.terminal) terminal = true;
|
||||||
|
|
||||||
|
state = defaultMutation.mutation(
|
||||||
|
(action as any).payload,
|
||||||
|
action,
|
||||||
|
)(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
return reducer;
|
||||||
|
|
||||||
|
/*
|
||||||
|
if (subReducers) {
|
||||||
|
if (subduxes['*']) {
|
||||||
|
newState = u.updateIn(
|
||||||
|
'*',
|
||||||
|
subduxes['*'].upreducer(action),
|
||||||
|
newState,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const update = mapValues(subReducers, (upReducer) =>
|
||||||
|
upReducer(action),
|
||||||
|
);
|
||||||
|
|
||||||
|
newState = u(update, newState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const a = mutations[action.type] || mutations['+'];
|
||||||
|
|
||||||
|
if (!a) return newState;
|
||||||
|
|
||||||
|
return a(action.payload, action)(newState);
|
||||||
|
};
|
||||||
|
|
||||||
|
return wrapper ? wrapper(upreducer) : upreducer;
|
||||||
|
*/
|
||||||
|
}
|
@ -54,7 +54,7 @@ test('mutation', () => {
|
|||||||
initial: { nextId: 0, todos: [] as Todo[] },
|
initial: { nextId: 0, todos: [] as Todo[] },
|
||||||
});
|
});
|
||||||
|
|
||||||
dux.mutation(addTodo, (state, description) => {
|
dux.addMutation(addTodo, (description) => (state) => {
|
||||||
state.todos.unshift({ description, id: state.nextId, done: false });
|
state.todos.unshift({ description, id: state.nextId, done: false });
|
||||||
state.nextId++;
|
state.nextId++;
|
||||||
});
|
});
|
||||||
|
@ -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