Merge branch 'actions-subdux' into typescript
This commit is contained in:
commit
f5cdc4207a
@ -3,6 +3,11 @@
|
|||||||
version: '3'
|
version: '3'
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
|
build: tsc
|
||||||
|
|
||||||
|
checks:
|
||||||
|
deps: [test, build]
|
||||||
|
|
||||||
test: vitest run src
|
test: vitest run src
|
||||||
test:dev: vitest src
|
test:dev: vitest src
|
||||||
|
|
||||||
|
6
foo.ts
6
foo.ts
@ -1,6 +0,0 @@
|
|||||||
const foo = <A, B>(x: Partial<{ a: A; b: B }>) => x as B;
|
|
||||||
|
|
||||||
const y = foo({
|
|
||||||
a: 'potato',
|
|
||||||
b: 2,
|
|
||||||
});
|
|
21
src/Updux.ts
21
src/Updux.ts
@ -1,3 +1,4 @@
|
|||||||
|
import * as R from 'remeda';
|
||||||
import {
|
import {
|
||||||
createStore as reduxCreateStore,
|
createStore as reduxCreateStore,
|
||||||
applyMiddleware,
|
applyMiddleware,
|
||||||
@ -5,7 +6,8 @@ import {
|
|||||||
Action,
|
Action,
|
||||||
} from 'redux';
|
} from 'redux';
|
||||||
import { configureStore, Reducer, createAction } from '@reduxjs/toolkit';
|
import { configureStore, Reducer, createAction } from '@reduxjs/toolkit';
|
||||||
import { withPayload } from './actions';
|
import { withPayload } from './actions.js';
|
||||||
|
import { AggregateActions, Dux, UnionToIntersection } from './types.js';
|
||||||
|
|
||||||
type ActionCreator = ReturnType<typeof createAction>;
|
type ActionCreator = ReturnType<typeof createAction>;
|
||||||
|
|
||||||
@ -20,6 +22,7 @@ type Mutation<A extends ActionCreator = ActionCreator, S = any> = (
|
|||||||
export default class Updux<
|
export default class Updux<
|
||||||
T_LocalState = Record<any, any>,
|
T_LocalState = Record<any, any>,
|
||||||
T_LocalActions = {},
|
T_LocalActions = {},
|
||||||
|
SUBDUXES extends Record<string, Dux> = {},
|
||||||
> {
|
> {
|
||||||
#localInitial: T_LocalState;
|
#localInitial: T_LocalState;
|
||||||
#localActions: T_LocalActions;
|
#localActions: T_LocalActions;
|
||||||
@ -27,16 +30,28 @@ export default class Updux<
|
|||||||
string,
|
string,
|
||||||
Mutation<ActionCreator, AggregateState<T_LocalState>>
|
Mutation<ActionCreator, AggregateState<T_LocalState>>
|
||||||
> = {};
|
> = {};
|
||||||
|
#subduxes: SUBDUXES;
|
||||||
|
|
||||||
|
#actions: Record<string, ActionCreator>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
config: Partial<{
|
config: Partial<{
|
||||||
initial: T_LocalState;
|
initial: T_LocalState;
|
||||||
actions: T_LocalActions;
|
actions: T_LocalActions;
|
||||||
|
subduxes: SUBDUXES;
|
||||||
}>,
|
}>,
|
||||||
) {
|
) {
|
||||||
// TODO check that we can't alter the initial after the fact
|
// TODO check that we can't alter the initial after the fact
|
||||||
this.#localInitial = config.initial ?? ({} as T_LocalState);
|
this.#localInitial = config.initial ?? ({} as T_LocalState);
|
||||||
this.#localActions = config.actions ?? ({} as T_LocalActions);
|
this.#localActions = config.actions ?? ({} as T_LocalActions);
|
||||||
|
this.#subduxes = config.subduxes ?? ({} as SUBDUXES);
|
||||||
|
}
|
||||||
|
|
||||||
|
get actions(): AggregateActions<T_LocalActions, SUBDUXES> {
|
||||||
|
return R.mergeAll([
|
||||||
|
this.#localActions,
|
||||||
|
...Object.values(this.#subduxes).map(R.pathOr(['actions'], {})),
|
||||||
|
]) as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO memoize?
|
// TODO memoize?
|
||||||
@ -44,10 +59,6 @@ export default class Updux<
|
|||||||
return this.#localInitial;
|
return this.#localInitial;
|
||||||
}
|
}
|
||||||
|
|
||||||
get actions() {
|
|
||||||
return this.#localActions;
|
|
||||||
}
|
|
||||||
|
|
||||||
createStore(
|
createStore(
|
||||||
options: Partial<{
|
options: Partial<{
|
||||||
initial: T_LocalState;
|
initial: T_LocalState;
|
||||||
|
@ -34,23 +34,6 @@ test('Updux config accepts actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('subduxes actions', () => {
|
|
||||||
const foo = new Updux({
|
|
||||||
actions: {
|
|
||||||
foo: null,
|
|
||||||
},
|
|
||||||
subduxes: {
|
|
||||||
beta: {
|
|
||||||
actions: {
|
|
||||||
bar: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(foo.actions).toHaveProperty('foo');
|
|
||||||
expect(foo.actions).toHaveProperty('bar');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('throw if double action', () => {
|
test('throw if double action', () => {
|
||||||
expect(
|
expect(
|
||||||
|
27
src/actions.test.ts
Normal file
27
src/actions.test.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import Updux, { createAction } from './index.js';
|
||||||
|
|
||||||
|
test('subduxes actions', () => {
|
||||||
|
const bar = createAction<number>('bar');
|
||||||
|
const baz = createAction('baz');
|
||||||
|
|
||||||
|
const foo = new Updux({
|
||||||
|
actions: {
|
||||||
|
bar,
|
||||||
|
},
|
||||||
|
subduxes: {
|
||||||
|
beta: {
|
||||||
|
actions: {
|
||||||
|
baz,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// to check if we can deal with empty actions
|
||||||
|
gamma: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(foo.actions).toHaveProperty('bar');
|
||||||
|
expect(foo.actions).toHaveProperty('baz');
|
||||||
|
|
||||||
|
expect(foo.actions.bar(2)).toHaveProperty('type', 'bar');
|
||||||
|
expect(foo.actions.baz()).toHaveProperty('type', 'baz');
|
||||||
|
});
|
@ -1,5 +1,7 @@
|
|||||||
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
export { createAction } from '@reduxjs/toolkit';
|
export { createAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
export const withPayload =
|
export const withPayload =
|
||||||
<P>() =>
|
<P>() =>
|
||||||
(payload: P) => ({ payload });
|
(payload: P) => ({ payload });
|
||||||
|
48
src/buildMiddleware/index.js
Normal file
48
src/buildMiddleware/index.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { mapValues, map, get } from 'lodash-es';
|
||||||
|
const middlewareFor = (type, middleware) => (api) => (next) => (action) => {
|
||||||
|
if (type !== '*' && action.type !== type)
|
||||||
|
return next(action);
|
||||||
|
return middleware(api)(next)(action);
|
||||||
|
};
|
||||||
|
const sliceMw = (slice, mw) => (api) => {
|
||||||
|
const getSliceState = () => get(api.getState(), slice);
|
||||||
|
return mw(Object.assign(Object.assign({}, api), { getState: getSliceState }));
|
||||||
|
};
|
||||||
|
export function augmentMiddlewareApi(api, actions, selectors) {
|
||||||
|
const getState = () => api.getState();
|
||||||
|
const dispatch = (action) => api.dispatch(action);
|
||||||
|
Object.assign(getState, mapValues(selectors, (selector) => {
|
||||||
|
return (...args) => {
|
||||||
|
let result = selector(api.getState());
|
||||||
|
if (typeof result === 'function')
|
||||||
|
return result(...args);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
Object.assign(dispatch, mapValues(actions, (action) => {
|
||||||
|
return (...args) => api.dispatch(action(...args));
|
||||||
|
}));
|
||||||
|
return Object.assign(Object.assign({}, api), { getState,
|
||||||
|
dispatch,
|
||||||
|
actions,
|
||||||
|
selectors });
|
||||||
|
}
|
||||||
|
export const effectToMiddleware = (effect, actions, selectors) => {
|
||||||
|
let mw = effect;
|
||||||
|
let action = '*';
|
||||||
|
if (Array.isArray(effect)) {
|
||||||
|
action = effect[0];
|
||||||
|
mw = effect[1];
|
||||||
|
mw = middlewareFor(action, mw);
|
||||||
|
}
|
||||||
|
return (api) => mw(augmentMiddlewareApi(api, actions, selectors));
|
||||||
|
};
|
||||||
|
const composeMw = (mws) => (api) => (original_next) => mws.reduceRight((next, mw) => mw(api)(next), original_next);
|
||||||
|
export function buildMiddleware(effects = [], actions = {}, selectors = {}, sub = {}, wrapper = undefined, dux = undefined) {
|
||||||
|
let inner = map(sub, ({ middleware }, slice) => slice !== '*' && middleware ? sliceMw(slice, middleware) : undefined).filter((x) => x);
|
||||||
|
const local = effects.map((effect) => effectToMiddleware(effect, actions, selectors));
|
||||||
|
let mws = [...local, ...inner];
|
||||||
|
if (wrapper)
|
||||||
|
mws = wrapper(mws, dux);
|
||||||
|
return composeMw(mws);
|
||||||
|
}
|
20
src/buildSelectors/index.js
Normal file
20
src/buildSelectors/index.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { map, mapValues, merge } from 'lodash-es';
|
||||||
|
export function buildSelectors(localSelectors, splatSelector = {}, subduxes = {}) {
|
||||||
|
const subSelectors = map(subduxes, ({ selectors }, slice) => {
|
||||||
|
if (!selectors)
|
||||||
|
return {};
|
||||||
|
if (slice === '*')
|
||||||
|
return {};
|
||||||
|
return mapValues(selectors, (func) => (state) => func(state[slice]));
|
||||||
|
});
|
||||||
|
let splat = {};
|
||||||
|
for (const name in splatSelector) {
|
||||||
|
splat[name] =
|
||||||
|
(state) => (...args) => {
|
||||||
|
const value = splatSelector[name](state)(...args);
|
||||||
|
const res = () => value;
|
||||||
|
return merge(res, mapValues(subduxes['*'].selectors, (selector) => () => selector(value)));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return merge({}, ...subSelectors, localSelectors, splat);
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import Updux from './Updux';
|
import Updux from './Updux.js';
|
||||||
|
|
||||||
export { withPayload, createAction } from './actions';
|
export { withPayload, createAction } from './actions.js';
|
||||||
|
|
||||||
export default Updux;
|
export default Updux;
|
||||||
|
21
src/types.ts
Normal file
21
src/types.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { Action, ActionCreator } from 'redux';
|
||||||
|
|
||||||
|
export type Dux<
|
||||||
|
STATE = any,
|
||||||
|
ACTIONS extends Record<string, ActionCreator<string>> = {},
|
||||||
|
> = Partial<{
|
||||||
|
initial: STATE;
|
||||||
|
actions: ACTIONS;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
type ActionsOf<DUX> = DUX extends { actions: infer A } ? A : {};
|
||||||
|
|
||||||
|
export type AggregateActions<A, S> = UnionToIntersection<
|
||||||
|
ActionsOf<S[keyof S]> | A
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type UnionToIntersection<U> = (
|
||||||
|
U extends any ? (k: U) => void : never
|
||||||
|
) extends (k: infer I) => void
|
||||||
|
? I
|
||||||
|
: never;
|
@ -25,9 +25,9 @@
|
|||||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||||
|
|
||||||
/* Modules */
|
/* Modules */
|
||||||
"module": "ES2020" /* Specify what module code is generated. */,
|
"module": "nodenext" /* Specify what module code is generated. */,
|
||||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
"rootDir": "./src" /* Specify the root folder within your source files. */,
|
||||||
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
|
"moduleResolution": "nodenext" /* Specify how TypeScript looks up a file from a given module specifier. */,
|
||||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||||
@ -51,7 +51,7 @@
|
|||||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||||
// "outDir": "./", /* Specify an output folder for all emitted files. */
|
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
|
||||||
// "removeComments": true, /* Disable emitting comments. */
|
// "removeComments": true, /* Disable emitting comments. */
|
||||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||||
@ -78,7 +78,7 @@
|
|||||||
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
||||||
|
|
||||||
/* Type Checking */
|
/* Type Checking */
|
||||||
"strict": true /* Enable all strict type-checking options. */,
|
"strict": false /* Enable all strict type-checking options. */,
|
||||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||||
|
Loading…
Reference in New Issue
Block a user