From 4e4fa13d902038fabadc87ae1ff2fe7669b5f9aa Mon Sep 17 00:00:00 2001 From: Yanick Champoux Date: Mon, 18 Oct 2021 09:02:20 -0400 Subject: [PATCH] generic actions --- src/Updux.d.ts | 16 +++++++-------- src/Updux.ts | 40 ++++++++++++++++++------------------- src/actions.test.js | 8 ++++---- src/actions.ts | 19 +++++++++++------- src/buildInitial/test.ts | 4 +--- src/buildSelectors/index.ts | 15 ++++++++++---- src/buildUpreducer.js | 3 ++- src/documentation.test.ts | 2 +- src/mutations.test.js | 33 +++++++++++++++--------------- src/reducer.test.js | 6 +++--- src/splat.test.js | 25 +++++------------------ src/subscriptions.test.js | 3 +-- src/types.ts | 25 ++++++++++++++++++----- 13 files changed, 103 insertions(+), 96 deletions(-) diff --git a/src/Updux.d.ts b/src/Updux.d.ts index 8e0af6f..949ff65 100644 --- a/src/Updux.d.ts +++ b/src/Updux.d.ts @@ -1,19 +1,17 @@ type UpduxConfig = Partial<{ initial: TState; - subduxes: Record; + subduxes: Record; actions: Record; selectors: Record; mutations: Record; - mappedSelectors: Record; - effects: Record; - reactions: Record; + mappedSelectors: Record; + effects: Record; + reactions: Record; mappedReaction: Function; -}> +}>; - -export class Updux { - - constructor( config: UpduxConfig ); +export class Updux { + constructor(config: UpduxConfig); get initial(): TState; get selectors(): unknown; diff --git a/src/Updux.ts b/src/Updux.ts index 9ba43f3..fbed23d 100644 --- a/src/Updux.ts +++ b/src/Updux.ts @@ -9,12 +9,9 @@ import { buildActions } from './buildActions'; import { buildSelectors } from './buildSelectors'; import { action } from './actions'; import { buildUpreducer } from './buildUpreducer'; -import { - buildMiddleware, - augmentMiddlewareApi, -} from './buildMiddleware'; +import { buildMiddleware, augmentMiddlewareApi } from './buildMiddleware'; -import { AggregateDuxState, Dict } from './types'; +import { AggregateDuxActions, AggregateDuxState, Dict } from './types'; /** * Configuration object typically passed to the constructor of the class Updux. @@ -23,7 +20,7 @@ export interface UpduxConfig< TState = any, TActions = {}, TSelectors = {}, - TSubduxes = {}, + TSubduxes = {} > { /** * Local initial state. @@ -39,7 +36,7 @@ export interface UpduxConfig< /** * Local actions. */ - actions?: Record; + actions?: TActions; /** * Local selectors. @@ -77,9 +74,9 @@ export interface UpduxConfig< export class Updux< TState extends any = {}, - TActions = {}, + TActions extends object = {}, TSelectors = {}, - TSubduxes extends object = {}, + TSubduxes extends object = {} > { /** @type { unknown } */ #initial = {}; @@ -94,7 +91,7 @@ export class Updux< #mappedSelectors = undefined; #mappedReaction = undefined; - constructor(config: UpduxConfig) { + constructor(config: UpduxConfig) { this.#initial = config.initial ?? {}; this.#subduxes = config.subduxes ?? {}; @@ -145,7 +142,7 @@ export class Updux< this.#mappedSelectors = { ...this.#mappedSelectors, [name]: f, - } + }; } get middleware() { @@ -158,12 +155,12 @@ export class Updux< } /** @member { unknown } */ - get initial() : AggregateDuxState { + get initial(): AggregateDuxState { return this.#memoInitial(this.#initial, this.#subduxes); } - get actions(): Record { - return this.#memoActions(this.#actions, this.#subduxes); + get actions(): AggregateDuxActions { + return this.#memoActions(this.#actions, this.#subduxes) as any; } get selectors() { @@ -333,14 +330,15 @@ export class Updux< } createStore(initial?: unknown, enhancerGenerator?: Function) { + const enhancer = (enhancerGenerator ?? applyMiddleware)( + this.middleware + ); - const enhancer = (enhancerGenerator ?? applyMiddleware)(this.middleware); - - const store : { - getState: Function & Record, - dispatch: Function & Record, - selectors: Record, - actions: Record, + const store: { + getState: Function & Record; + dispatch: Function & Record; + selectors: Record; + actions: AggregateDuxActions; } = reduxCreateStore( this.reducer, initial ?? this.initial, diff --git a/src/actions.test.js b/src/actions.test.js index 0351891..a5accb6 100644 --- a/src/actions.test.js +++ b/src/actions.test.js @@ -3,11 +3,11 @@ import { action } from './actions'; test('action generators', () => { const foo = action('foo'); - expect(foo.type).toEqual( 'foo'); - expect(foo()).toMatchObject( { type: 'foo' }); + expect(foo.type).toEqual('foo'); + expect(foo()).toMatchObject({ type: 'foo' }); const bar = action('bar'); - expect(bar.type).toEqual( 'bar'); - expect(bar()).toMatchObject( { type: 'bar' }); + expect(bar.type).toEqual('bar'); + expect(bar()).toMatchObject({ type: 'bar' }); }); diff --git a/src/actions.ts b/src/actions.ts index c265700..03d1328 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -1,11 +1,16 @@ -export type Action = { - type: T; meta?: Record; } & ( - { payload?: TPayload } -) +export type Action = { + type: T; + meta?: Record; +} & { + payload?: TPayload; +}; -export type ActionGenerator = { +export type ActionGenerator< + TType extends string = string, + TPayloadGen = undefined +> = { type: TType; -} & (TPayloadGen extends (...args:any) => any +} & (TPayloadGen extends (...args: any) => any ? (...args: Parameters) => { type: TType; payload: ReturnType; @@ -22,7 +27,7 @@ export type ActionGenerator { - expect( - buildInitial({ a: 1 }, { b: { initial: { c: 2 } } }) - ).toMatchObject({ + expect(buildInitial({ a: 1 }, { b: { initial: { c: 2 } } })).toMatchObject({ a: 1, b: { c: 2 }, }); diff --git a/src/buildSelectors/index.ts b/src/buildSelectors/index.ts index 82c0904..36cbb35 100644 --- a/src/buildSelectors/index.ts +++ b/src/buildSelectors/index.ts @@ -1,16 +1,23 @@ import { map, mapValues, merge } from 'lodash'; -export function buildSelectors(localSelectors, splatSelector = {}, subduxes ={}) { +export function buildSelectors( + localSelectors, + splatSelector = {}, + subduxes = {} +) { const subSelectors = map(subduxes, ({ selectors }, slice) => { if (!selectors) return {}; if (slice === '*') return {}; - return mapValues(selectors, (func: Function) => (state) => func(state[slice])); + return mapValues( + selectors, + (func: Function) => (state) => func(state[slice]) + ); }); let splat = {}; - for ( const name in splatSelector ) { + for (const name in splatSelector) { splat[name] = (state) => (...args) => { @@ -25,7 +32,7 @@ export function buildSelectors(localSelectors, splatSelector = {}, subduxes ={}) ) ); }; - } + } return merge({}, ...subSelectors, localSelectors, splat); } diff --git a/src/buildUpreducer.js b/src/buildUpreducer.js index 6d6bb54..e544a74 100644 --- a/src/buildUpreducer.js +++ b/src/buildUpreducer.js @@ -8,7 +8,8 @@ export function buildUpreducer(initial, mutations, subduxes = {}) { : null; 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; diff --git a/src/documentation.test.ts b/src/documentation.test.ts index b0669d9..319440f 100644 --- a/src/documentation.test.ts +++ b/src/documentation.test.ts @@ -32,7 +32,7 @@ test('README.md', () => { }); test('tutorial', () => { - const todosDux = new Updux({ + const todosDux = new Updux({ initial: { next_id: 1, todos: [], diff --git a/src/mutations.test.js b/src/mutations.test.js index 8a09503..fde726f 100644 --- a/src/mutations.test.js +++ b/src/mutations.test.js @@ -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', () => { @@ -48,8 +50,7 @@ test('override', () => { undefined ); - expect(state).toMatchObject( - { + expect(state).toMatchObject({ alpha: ['foo', 'bar'], subbie: 1, }); @@ -77,8 +78,9 @@ test('order of processing', () => { }, }); - expect(dux.reducer(undefined, foo())) - .toMatchObject({ x: ['subdux', 'main'] }); + expect(dux.reducer(undefined, foo())).toMatchObject({ + x: ['subdux', 'main'], + }); }); test('setMutation', () => { @@ -89,21 +91,20 @@ test('setMutation', () => { }); // noop - expect(dux.reducer(undefined, foo())).toEqual( ''); + expect(dux.reducer(undefined, foo())).toEqual(''); dux.setMutation('foo', () => () => 'foo'); - expect(dux.reducer(undefined, foo())).toEqual( 'foo'); - + expect(dux.reducer(undefined, foo())).toEqual('foo'); }); test('setMutation, name as function', () => { - const bar = action('bar'); + const bar = action('bar'); - const dux = new Updux({ - initial: '', - }); - dux.setMutation(bar, () => () => 'bar'); - - expect(dux.reducer(undefined, bar())).toEqual( 'bar'); + const dux = new Updux({ + initial: '', }); + dux.setMutation(bar, () => () => 'bar'); + + expect(dux.reducer(undefined, bar())).toEqual('bar'); +}); diff --git a/src/reducer.test.js b/src/reducer.test.js index 62cd0e7..f475cc8 100644 --- a/src/reducer.test.js +++ b/src/reducer.test.js @@ -7,7 +7,7 @@ test('basic reducer', () => { 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', () => { @@ -15,7 +15,7 @@ test('basic upreducer', () => { 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', () => { @@ -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 }); }); diff --git a/src/splat.test.js b/src/splat.test.js index 5220460..bb2ab45 100644 --- a/src/splat.test.js +++ b/src/splat.test.js @@ -135,8 +135,7 @@ test('splat subscriptions, more', () => { initial: { a: 1 }, actions: { foo: null, incAll: null }, mutations: { - foo: (id) => (state) => - state.a === id ? { ...state, b: 1 } : state, + foo: (id) => (state) => state.a === id ? { ...state, b: 1 } : state, incAll: () => (state) => ({ ...state, a: state.a + 1 }), }, reactions: [() => snitch], @@ -163,16 +162,8 @@ test('splat subscriptions, more', () => { expect(snitch).toHaveBeenCalledTimes(2); - expect(snitch).toHaveBeenCalledWith( - { a: 1 }, - undefined, - expect.anything() - ); - expect(snitch).toHaveBeenCalledWith( - { a: 2 }, - undefined, - expect.anything() - ); + expect(snitch).toHaveBeenCalledWith({ a: 1 }, undefined, expect.anything()); + expect(snitch).toHaveBeenCalledWith({ a: 2 }, undefined, expect.anything()); snitch.mockReset(); @@ -194,11 +185,7 @@ test('splat subscriptions, more', () => { expect(snitch).toHaveBeenCalledTimes(1); - expect(snitch).toHaveBeenCalledWith( - undefined, - { a: 1 }, - expect.anything() - ); + expect(snitch).toHaveBeenCalledWith(undefined, { a: 1 }, expect.anything()); // only one subscriber left snitch.mockReset(); @@ -246,9 +233,7 @@ test('many levels down', () => { mutations: { add: () => (x) => x + 1, }, - reactions: [ - (store) => (state) => snitch(state, store), - ], + reactions: [(store) => (state) => snitch(state, store)], }, }, }, diff --git a/src/subscriptions.test.js b/src/subscriptions.test.js index 870d4f1..ef89012 100644 --- a/src/subscriptions.test.js +++ b/src/subscriptions.test.js @@ -109,8 +109,7 @@ test('subscription within subduxes', () => { let innerState = jest.fn(() => null); let outerState = jest.fn(() => null); - const resetMocks = () => - [innerState, outerState].map((f) => f.mockReset()); + const resetMocks = () => [innerState, outerState].map((f) => f.mockReset()); const inner = new Updux({ initial: 1, diff --git a/src/types.ts b/src/types.ts index 8ca01f7..8d8cf48 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,20 +1,35 @@ - export type Dict = Record; +export type UnionToIntersection = ( + U extends any ? (k: U) => void : never +) extends (k: infer I) => void + ? I + : never; + type Subdux = { - initial: TState + initial: TState; }; type StateOf = D extends { initial: infer I } ? I : unknown; +type ActionsOf = C extends { actions: infer A } ? { + [K in keyof A]: Function +} : {}; -type Subduxes = Record; +type Subduxes = Record; export type DuxStateSubduxes = C extends { '*': infer I } ? { [key: string]: StateOf; [index: number]: StateOf; } - : { [K in keyof C]: StateOf }; + : { [K in keyof C]: StateOf }; -export type AggregateDuxState = TState & DuxStateSubduxes; +export type AggregateDuxState = TState & + DuxStateSubduxes; +type DuxActionsSubduxes = C extends object ? ActionsOf : unknown; + +type ItemsOf = C extends object ? C[keyof C] : unknown; + +export type AggregateDuxActions = TActions & + UnionToIntersection>>;