Merge branch 'generics' into return-to-ts

This commit is contained in:
Yanick Champoux 2021-10-18 10:18:09 -04:00
commit 8314ff94ca
24 changed files with 222 additions and 168 deletions

View File

@ -1,2 +1,7 @@
dist
out
pnpm-lock.yaml
types
docs
Changes Changes
.prettierignore .prettierignore

View File

@ -6,54 +6,58 @@ All notable changes to this project will be documented in this file. See [standa
### ⚠ BREAKING CHANGES ### ⚠ BREAKING CHANGES
* Upgrade to Typescript 4. - Upgrade to Typescript 4.
* Switch from 'updeep' to '@yanick/updeep'. - Switch from 'updeep' to '@yanick/updeep'.
## [2.1.0](https://github.com/yanick/updux/compare/v2.0.0...v2.1.0) (2020-06-19) ## [2.1.0](https://github.com/yanick/updux/compare/v2.0.0...v2.1.0) (2020-06-19)
### Features ### Features
* add support for subscriptions ([9c45ee7](https://github.com/yanick/updux/commit/9c45ee7efcb623defb9da5d01165fbad0e4424f9)) - add support for subscriptions ([9c45ee7](https://github.com/yanick/updux/commit/9c45ee7efcb623defb9da5d01165fbad0e4424f9))
## [2.0.0](https://github.com/yanick/updux/compare/v1.2.0...v2.0.0) (2020-06-13) ## [2.0.0](https://github.com/yanick/updux/compare/v1.2.0...v2.0.0) (2020-06-13)
### ⚠ BREAKING CHANGES ### ⚠ BREAKING CHANGES
* use ts-action for action creation - use ts-action for action creation
* middleware support refined - middleware support refined
### Features ### Features
* allow adding actionCreators via addAction() ([27ae46d](https://github.com/yanick/updux/commit/27ae46dbab289b27ea99aca149aaa3b7c90ee7d0)) - allow adding actionCreators via addAction() ([27ae46d](https://github.com/yanick/updux/commit/27ae46dbab289b27ea99aca149aaa3b7c90ee7d0))
* middleware support refined ([d90d721](https://github.com/yanick/updux/commit/d90d72148c2d4ba186a19650d961c64df5791c55)) - middleware support refined ([d90d721](https://github.com/yanick/updux/commit/d90d72148c2d4ba186a19650d961c64df5791c55))
* moving documentation to docsify ([fa55762](https://github.com/yanick/updux/commit/fa55762efcbd4db356150f6022fd62750adc27a9)) - moving documentation to docsify ([fa55762](https://github.com/yanick/updux/commit/fa55762efcbd4db356150f6022fd62750adc27a9))
* use ts-action for action creation ([6349d72](https://github.com/yanick/updux/commit/6349d720b8aba4b443a7225d6a377c5c929a3021)) - use ts-action for action creation ([6349d72](https://github.com/yanick/updux/commit/6349d720b8aba4b443a7225d6a377c5c929a3021))
### Bug Fixes ### Bug Fixes
* state is a PreloadedState<S> ([93bebc5](https://github.com/yanick/updux/commit/93bebc5acf193752aa6b4857507f05d52b1b7665)) - state is a PreloadedState<S> ([93bebc5](https://github.com/yanick/updux/commit/93bebc5acf193752aa6b4857507f05d52b1b7665))
## 1.2.0 2019-11-06 ## 1.2.0 2019-11-06
- The middleware's 'getState' returns the local state of its updux, - The middleware's 'getState' returns the local state of its updux,
instead of the root state. Plus we add `getRootState` to get instead of the root state. Plus we add `getRootState` to get
the root state. the root state.
## 1.1.0 2019-11-05 ## 1.1.0 2019-11-05
- Document mapping behavior of the '*' subdux. - Document mapping behavior of the '*' subdux.
- add subduxUpreducer. - add subduxUpreducer.
- add sink mutations. - add sink mutations.
## 1.0.0 2019-11-04 ## 1.0.0 2019-11-04
- Pretty big rework. - Pretty big rework.
- Better documentation. - Better documentation.
## 0.2.0 2019-10-24 ## 0.2.0 2019-10-24
- Converted everything to Typescript. - Converted everything to Typescript.
## 0.1.0 2019-10-22 ## 0.1.0 2019-10-22
- Add 'actions' in the config. - Add 'actions' in the config.
## 0.0.1 2019-10-22 ## 0.0.1 2019-10-22
- Initial release. - Initial release.

View File

@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
Examples of behavior that contributes to a positive environment for our Examples of behavior that contributes to a positive environment for our
community include: community include:
* Demonstrating empathy and kindness toward other people - Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences - Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback - Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes, - Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience and learning from the experience
* Focusing on what is best not just for us as individuals, but for the - Focusing on what is best not just for us as individuals, but for the
overall community overall community
Examples of unacceptable behavior include: Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or - The use of sexualized language or imagery, and sexual attention or
advances of any kind advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks - Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment - Public or private harassment
* Publishing others' private information, such as a physical or email - Publishing others' private information, such as a physical or email
address, without their explicit permission address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a - Other conduct which could reasonably be considered inappropriate in a
professional setting professional setting
## Enforcement Responsibilities ## Enforcement Responsibilities
@ -126,4 +126,3 @@ enforcement ladder](https://github.com/mozilla/diversity).
For answers to common questions about this code of conduct, see the FAQ at For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations. https://www.contributor-covenant.org/translations.

View File

@ -6,6 +6,20 @@ vars:
GREETING: Hello, World! GREETING: Hello, World!
tasks: tasks:
tsc:
cmds:
- tsc
- echo 🙌 typescript compilation successful
test: jest
'check:prettier': prettier --check .
'check:prettier:fix': prettier --write .
check:
deps: [tsc, test, 'check:prettier']
'test:types': tsd 'test:types': tsd
docs: docs:
cmds: cmds:

View File

@ -1,6 +1,6 @@
module.exports = { module.exports = {
presets: [ presets: [
['@babel/preset-env', {targets: {node: 'current'}}], ['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript', '@babel/preset-typescript',
], ],
}; };

View File

@ -1,3 +1,3 @@
export default { export default {
roots: [ './src' ] roots: ['./src'],
} };

View File

@ -8,7 +8,7 @@
"sourceType": "module", "sourceType": "module",
"tags": { "tags": {
"allowUnknownTags": true, "allowUnknownTags": true,
"dictionaries": ["jsdoc","closure"] "dictionaries": ["jsdoc", "closure"]
}, },
"templates": { "templates": {
"cleverLinks": false, "cleverLinks": false,
@ -19,4 +19,3 @@
"tutorials": "./tutorials" "tutorials": "./tutorials"
} }
} }

16
src/Updux.d.ts vendored
View File

@ -1,19 +1,17 @@
type UpduxConfig<TState> = Partial<{ type UpduxConfig<TState> = Partial<{
initial: TState; initial: TState;
subduxes: Record<string,any>; subduxes: Record<string, any>;
actions: Record<string, any>; actions: Record<string, any>;
selectors: Record<string, Function>; selectors: Record<string, Function>;
mutations: Record<string, Function>; mutations: Record<string, Function>;
mappedSelectors: Record<string,Function>; mappedSelectors: Record<string, Function>;
effects: Record<string,Function>; effects: Record<string, Function>;
reactions: Record<string,Function>; reactions: Record<string, Function>;
mappedReaction: Function; mappedReaction: Function;
}> }>;
export class Updux<TState = unknown> {
export class Updux<TState=unknown> { constructor(config: UpduxConfig<TState>);
constructor( config: UpduxConfig<TState> );
get initial(): TState; get initial(): TState;
get selectors(): unknown; get selectors(): unknown;

View File

@ -9,17 +9,19 @@ import { buildActions } from './buildActions';
import { buildSelectors } from './buildSelectors'; import { buildSelectors } from './buildSelectors';
import { action } from './actions'; import { action } from './actions';
import { buildUpreducer } from './buildUpreducer'; import { buildUpreducer } from './buildUpreducer';
import { import { buildMiddleware, augmentMiddlewareApi } from './buildMiddleware';
buildMiddleware,
augmentMiddlewareApi,
} from './buildMiddleware';
import { Dict } from './types'; import { AggregateDuxActions, AggregateDuxState, Dict } from './types';
/** /**
* Configuration object typically passed to the constructor of the class Updux. * Configuration object typically passed to the constructor of the class Updux.
*/ */
export interface UpduxConfig<TState = unknown> { export interface UpduxConfig<
TState = any,
TActions = {},
TSelectors = {},
TSubduxes = {}
> {
/** /**
* Local initial state. * Local initial state.
* @default {} * @default {}
@ -29,12 +31,12 @@ export interface UpduxConfig<TState = unknown> {
/** /**
* Subduxes to be merged to this dux. * Subduxes to be merged to this dux.
*/ */
subduxes?: Dict<Updux | UpduxConfig>; subduxes?: TSubduxes;
/** /**
* Local actions. * Local actions.
*/ */
actions?: Record<string, any>; actions?: TActions;
/** /**
* Local selectors. * Local selectors.
@ -70,7 +72,12 @@ export interface UpduxConfig<TState = unknown> {
mappedReaction?: Function | boolean; mappedReaction?: Function | boolean;
} }
export class Updux { export class Updux<
TState extends any = {},
TActions extends object = {},
TSelectors = {},
TSubduxes extends object = {}
> {
/** @type { unknown } */ /** @type { unknown } */
#initial = {}; #initial = {};
#subduxes = {}; #subduxes = {};
@ -84,7 +91,7 @@ export class Updux {
#mappedSelectors = undefined; #mappedSelectors = undefined;
#mappedReaction = undefined; #mappedReaction = undefined;
constructor(config: UpduxConfig) { constructor(config: UpduxConfig<TState, TActions, TSelectors, TSubduxes>) {
this.#initial = config.initial ?? {}; this.#initial = config.initial ?? {};
this.#subduxes = config.subduxes ?? {}; this.#subduxes = config.subduxes ?? {};
@ -135,7 +142,7 @@ export class Updux {
this.#mappedSelectors = { this.#mappedSelectors = {
...this.#mappedSelectors, ...this.#mappedSelectors,
[name]: f, [name]: f,
} };
} }
get middleware() { get middleware() {
@ -148,12 +155,12 @@ export class Updux {
} }
/** @member { unknown } */ /** @member { unknown } */
get initial() { get initial(): AggregateDuxState<TState, TSubduxes> {
return this.#memoInitial(this.#initial, this.#subduxes); return this.#memoInitial(this.#initial, this.#subduxes);
} }
get actions(): Record<string, Function> { get actions(): AggregateDuxActions<TActions, TSubduxes> {
return this.#memoActions(this.#actions, this.#subduxes); return this.#memoActions(this.#actions, this.#subduxes) as any;
} }
get selectors() { get selectors() {
@ -323,14 +330,15 @@ export class Updux {
} }
createStore(initial?: unknown, enhancerGenerator?: Function) { createStore(initial?: unknown, enhancerGenerator?: Function) {
const enhancer = (enhancerGenerator ?? applyMiddleware)(
this.middleware
);
const enhancer = (enhancerGenerator ?? applyMiddleware)(this.middleware); const store: {
getState: Function & Record<string, Function>;
const store : { dispatch: Function & Record<string, Function>;
getState: Function & Record<string,Function>, selectors: Record<string, Function>;
dispatch: Function & Record<string,Function>, actions: AggregateDuxActions<TActions, TSubduxes>;
selectors: Record<string,Function>,
actions: Record<string,Function>,
} = reduxCreateStore( } = reduxCreateStore(
this.reducer, this.reducer,
initial ?? this.initial, initial ?? this.initial,

View File

@ -1,13 +1,13 @@
import { action } from './actions.js'; import { action } from './actions';
test('action generators', () => { test('action generators', () => {
const foo = action('foo'); const foo = action('foo');
expect(foo.type).toEqual( 'foo'); expect(foo.type).toEqual('foo');
expect(foo()).toMatchObject( { type: 'foo' }); expect(foo()).toMatchObject({ type: 'foo' });
const bar = action('bar'); const bar = action('bar');
expect(bar.type).toEqual( 'bar'); expect(bar.type).toEqual('bar');
expect(bar()).toMatchObject( { type: 'bar' }); expect(bar()).toMatchObject({ type: 'bar' });
}); });

View File

@ -1,11 +1,16 @@
export type Action<T extends string=string,TPayload=unknown> = { export type Action<T extends string = string, TPayload = unknown> = {
type: T; meta?: Record<string,unknown>; } & ( type: T;
{ payload?: TPayload } meta?: Record<string, unknown>;
) } & {
payload?: TPayload;
};
export type ActionGenerator<TType extends string = string, TPayloadGen = undefined> = { export type ActionGenerator<
TType extends string = string,
TPayloadGen = undefined
> = {
type: TType; type: TType;
} & (TPayloadGen extends (...args:any) => any } & (TPayloadGen extends (...args: any) => any
? (...args: Parameters<TPayloadGen>) => { ? (...args: Parameters<TPayloadGen>) => {
type: TType; type: TType;
payload: ReturnType<TPayloadGen>; payload: ReturnType<TPayloadGen>;
@ -22,7 +27,7 @@ export type ActionGenerator<TType extends string = string, TPayloadGen = undefin
export function action(type, payloadFunction = null) { export function action(type, payloadFunction = null) {
const generator = function (...payloadArg) { const generator = function (...payloadArg) {
const result :Action = { type }; const result: Action = { type };
if (payloadFunction) { if (payloadFunction) {
result.payload = payloadFunction(...payloadArg); result.payload = payloadFunction(...payloadArg);

View File

@ -1,9 +1,7 @@
import { buildInitial } from '.'; import { buildInitial } from '.';
test('basic', () => { test('basic', () => {
expect( expect(buildInitial({ a: 1 }, { b: { initial: { c: 2 } } })).toMatchObject({
buildInitial({ a: 1 }, { b: { initial: { c: 2 } } })
).toMatchObject({
a: 1, a: 1,
b: { c: 2 }, b: { c: 2 },
}); });

View File

@ -1,16 +1,23 @@
import { map, mapValues, merge } from 'lodash'; import { map, mapValues, merge } from 'lodash';
export function buildSelectors(localSelectors, splatSelector = {}, subduxes ={}) { export function buildSelectors(
localSelectors,
splatSelector = {},
subduxes = {}
) {
const subSelectors = map(subduxes, ({ selectors }, slice) => { const subSelectors = map(subduxes, ({ selectors }, slice) => {
if (!selectors) return {}; if (!selectors) return {};
if (slice === '*') return {}; if (slice === '*') return {};
return mapValues(selectors, (func: Function) => (state) => func(state[slice])); return mapValues(
selectors,
(func: Function) => (state) => func(state[slice])
);
}); });
let splat = {}; let splat = {};
for ( const name in splatSelector ) { for (const name in splatSelector) {
splat[name] = splat[name] =
(state) => (state) =>
(...args) => { (...args) => {

View File

@ -8,7 +8,8 @@ export function buildUpreducer(initial, mutations, subduxes = {}) {
: null; : null;
return (action) => (state) => { return (action) => (state) => {
if( !action?.type ) throw new Error("upreducer called with a bad action"); if (!action?.type)
throw new Error('upreducer called with a bad action');
let newState = state ?? initial; let newState = state ?? initial;

View File

@ -32,7 +32,7 @@ test('README.md', () => {
}); });
test('tutorial', () => { test('tutorial', () => {
const todosDux = new Updux({ const todosDux = new Updux<any, any>({
initial: { initial: {
next_id: 1, next_id: 1,
todos: [], todos: [],

View File

@ -14,9 +14,11 @@ test('basic', () => {
}, },
}); });
expect(dux.reducer(undefined, dux.actions.doIt())).toEqual( 'bingo'); expect(dux.reducer(undefined, dux.actions.doIt())).toEqual('bingo');
expect(dux.reducer(undefined, dux.actions.thisToo())).toEqual( 'straight type'); expect(dux.reducer(undefined, dux.actions.thisToo())).toEqual(
'straight type'
);
}); });
test('override', () => { test('override', () => {
@ -48,8 +50,7 @@ test('override', () => {
undefined undefined
); );
expect(state).toMatchObject( expect(state).toMatchObject({
{
alpha: ['foo', 'bar'], alpha: ['foo', 'bar'],
subbie: 1, subbie: 1,
}); });
@ -77,8 +78,9 @@ test('order of processing', () => {
}, },
}); });
expect(dux.reducer(undefined, foo())) expect(dux.reducer(undefined, foo())).toMatchObject({
.toMatchObject({ x: ['subdux', 'main'] }); x: ['subdux', 'main'],
});
}); });
test('setMutation', () => { test('setMutation', () => {
@ -89,12 +91,11 @@ test('setMutation', () => {
}); });
// noop // noop
expect(dux.reducer(undefined, foo())).toEqual( ''); expect(dux.reducer(undefined, foo())).toEqual('');
dux.setMutation('foo', () => () => 'foo'); dux.setMutation('foo', () => () => 'foo');
expect(dux.reducer(undefined, foo())).toEqual( 'foo'); expect(dux.reducer(undefined, foo())).toEqual('foo');
}); });
test('setMutation, name as function', () => { test('setMutation, name as function', () => {
@ -105,5 +106,5 @@ test('setMutation, name as function', () => {
}); });
dux.setMutation(bar, () => () => 'bar'); dux.setMutation(bar, () => () => 'bar');
expect(dux.reducer(undefined, bar())).toEqual( 'bar'); expect(dux.reducer(undefined, bar())).toEqual('bar');
}); });

View File

@ -7,7 +7,7 @@ test('basic reducer', () => {
expect(typeof dux.reducer).toBe('function'); expect(typeof dux.reducer).toBe('function');
expect(dux.reducer({ a: 1 }, { type: 'foo' })).toMatchObject({a:1}); // noop expect(dux.reducer({ a: 1 }, { type: 'foo' })).toMatchObject({ a: 1 }); // noop
}); });
test('basic upreducer', () => { test('basic upreducer', () => {
@ -15,7 +15,7 @@ test('basic upreducer', () => {
expect(typeof dux.upreducer).toBe('function'); expect(typeof dux.upreducer).toBe('function');
expect(dux.upreducer({type:'foo'})({ a: 1 })).toMatchObject({a:1}); // noop expect(dux.upreducer({ type: 'foo' })({ a: 1 })).toMatchObject({ a: 1 }); // noop
}); });
test('reducer with action', () => { test('reducer with action', () => {
@ -28,5 +28,5 @@ test('reducer with action', () => {
}, },
}); });
expect(dux.reducer({ a: 1 }, { type: 'inc' })).toMatchObject({a:2}); expect(dux.reducer({ a: 1 }, { type: 'inc' })).toMatchObject({ a: 2 });
}); });

View File

@ -135,8 +135,7 @@ test('splat subscriptions, more', () => {
initial: { a: 1 }, initial: { a: 1 },
actions: { foo: null, incAll: null }, actions: { foo: null, incAll: null },
mutations: { mutations: {
foo: (id) => (state) => foo: (id) => (state) => state.a === id ? { ...state, b: 1 } : state,
state.a === id ? { ...state, b: 1 } : state,
incAll: () => (state) => ({ ...state, a: state.a + 1 }), incAll: () => (state) => ({ ...state, a: state.a + 1 }),
}, },
reactions: [() => snitch], reactions: [() => snitch],
@ -163,16 +162,8 @@ test('splat subscriptions, more', () => {
expect(snitch).toHaveBeenCalledTimes(2); expect(snitch).toHaveBeenCalledTimes(2);
expect(snitch).toHaveBeenCalledWith( expect(snitch).toHaveBeenCalledWith({ a: 1 }, undefined, expect.anything());
{ a: 1 }, expect(snitch).toHaveBeenCalledWith({ a: 2 }, undefined, expect.anything());
undefined,
expect.anything()
);
expect(snitch).toHaveBeenCalledWith(
{ a: 2 },
undefined,
expect.anything()
);
snitch.mockReset(); snitch.mockReset();
@ -194,11 +185,7 @@ test('splat subscriptions, more', () => {
expect(snitch).toHaveBeenCalledTimes(1); expect(snitch).toHaveBeenCalledTimes(1);
expect(snitch).toHaveBeenCalledWith( expect(snitch).toHaveBeenCalledWith(undefined, { a: 1 }, expect.anything());
undefined,
{ a: 1 },
expect.anything()
);
// only one subscriber left // only one subscriber left
snitch.mockReset(); snitch.mockReset();
@ -246,9 +233,7 @@ test('many levels down', () => {
mutations: { mutations: {
add: () => (x) => x + 1, add: () => (x) => x + 1,
}, },
reactions: [ reactions: [(store) => (state) => snitch(state, store)],
(store) => (state) => snitch(state, store),
],
}, },
}, },
}, },

View File

@ -109,8 +109,7 @@ test('subscription within subduxes', () => {
let innerState = jest.fn(() => null); let innerState = jest.fn(() => null);
let outerState = jest.fn(() => null); let outerState = jest.fn(() => null);
const resetMocks = () => const resetMocks = () => [innerState, outerState].map((f) => f.mockReset());
[innerState, outerState].map((f) => f.mockReset());
const inner = new Updux({ const inner = new Updux({
initial: 1, initial: 1,

View File

@ -1,2 +1,37 @@
export type Dict<T> = Record<string, T>; export type Dict<T> = Record<string, T>;
export type UnionToIntersection<U> = (
U extends any ? (k: U) => void : never
) extends (k: infer I) => void
? I
: never;
type Subdux<TState = any> = {
initial: TState;
};
type StateOf<D> = D extends { initial: infer I } ? I : unknown;
type ActionsOf<C> = C extends { actions: infer A }
? {
[K in keyof A]: Function;
}
: {};
type Subduxes = Record<string, Subdux>;
export type DuxStateSubduxes<C> = C extends { '*': infer I }
? {
[key: string]: StateOf<I>;
[index: number]: StateOf<I>;
}
: { [K in keyof C]: StateOf<C[K]> };
export type AggregateDuxState<TState, TSubduxes> = TState &
DuxStateSubduxes<TSubduxes>;
type DuxActionsSubduxes<C> = C extends object ? ActionsOf<C[keyof C]> : unknown;
type ItemsOf<C> = C extends object ? C[keyof C] : unknown;
export type AggregateDuxActions<TActions, TSubduxes> = TActions &
UnionToIntersection<ActionsOf<ItemsOf<TSubduxes>>>;

View File

@ -1,13 +1,11 @@
{ {
"include": [ "./src" ], "include": ["./src"],
"exclude": [ "./docs", "./dist" ], "exclude": ["./docs", "./dist"],
"compilerOptions": { "compilerOptions": {
"rootDir": "src", "rootDir": "src",
"outDir": "dist", "outDir": "dist",
"target": "es2020", "target": "es2020",
"lib": [ "lib": ["es2020"],
"es2020"
],
"module": "ES2020", "module": "ES2020",
"moduleResolution": "Node", "moduleResolution": "Node",
"strict": false, "strict": false,

View File

@ -1,8 +1,6 @@
{ {
"name": "Updux", "name": "Updux",
"entryPoints": [ "entryPoints": ["./types/index.d.ts"],
"./types/index.d.ts"
],
"out": "docs/API", "out": "docs/API",
"excludeExternals": true, "excludeExternals": true,
"excludePrivate": true, "excludePrivate": true,