From e9778f98e8088b3987e746fbd01da148e238468b Mon Sep 17 00:00:00 2001 From: Yanick Champoux Date: Tue, 7 Mar 2023 10:52:19 -0500 Subject: [PATCH 1/4] initial state and subduxes --- src/Updux.ts | 7 ++++- src/buildInitial.test.ts | 21 +++++++++++++ src/buildInitial.ts | 12 +++++++ src/initial.test.todo | 46 --------------------------- src/initial.test.ts | 67 ++++++++++++++++++++++++++++++++++++++++ src/tutorial.test.ts | 2 +- 6 files changed, 107 insertions(+), 48 deletions(-) create mode 100644 src/buildInitial.test.ts create mode 100644 src/buildInitial.ts delete mode 100644 src/initial.test.todo diff --git a/src/Updux.ts b/src/Updux.ts index 20221a2..3a70c08 100644 --- a/src/Updux.ts +++ b/src/Updux.ts @@ -18,6 +18,7 @@ import { import { withPayload } from './actions.js'; import { AggregateActions, Dux, UnionToIntersection } from './types.js'; import { buildActions } from './buildActions.js'; +import { buildInitial } from './buildInitial.js'; type AggregateState = L; @@ -73,6 +74,8 @@ export default class Updux< #actions: AggregateActions, SUBDUXES>; + #initial: any; + constructor( config: Partial<{ initial: T_LocalState; @@ -86,6 +89,8 @@ export default class Updux< this.#subduxes = config.subduxes ?? ({} as SUBDUXES); this.#actions = buildActions(this.#localActions, this.#subduxes); + + this.#initial = buildInitial(this.#localInitial, this.#subduxes); } get actions() { @@ -94,7 +99,7 @@ export default class Updux< // TODO memoize? get initial() { - return this.#localInitial; + return this.#initial; } createStore( diff --git a/src/buildInitial.test.ts b/src/buildInitial.test.ts new file mode 100644 index 0000000..7e8767e --- /dev/null +++ b/src/buildInitial.test.ts @@ -0,0 +1,21 @@ +import { test, expect } from 'vitest'; +import { buildInitial } from './buildInitial.js'; + +test('basic', () => { + expect( + buildInitial( + { a: 1 }, + { b: { initial: { c: 2 } }, d: { initial: 'e' } }, + ), + ).toEqual({ + a: 1, + b: { c: 2 }, + d: 'e', + }); +}); + +test('throw if subduxes and initial is not an object', () => { + expect(() => { + buildInitial(3, { bar: 'foo' }); + }).toThrow(); +}); diff --git a/src/buildInitial.ts b/src/buildInitial.ts new file mode 100644 index 0000000..7e2330e --- /dev/null +++ b/src/buildInitial.ts @@ -0,0 +1,12 @@ +import u from '@yanick/updeep-remeda'; +import * as R from 'remeda'; + +export function buildInitial(localInitial, subduxes) { + if (Object.keys(subduxes).length > 0 && typeof localInitial !== 'object') { + throw new Error( + "can't have subduxes when the initial value is not an object", + ); + } + + return u(localInitial, R.mapValues(subduxes, R.pathOr(['initial'], {}))); +} diff --git a/src/initial.test.todo b/src/initial.test.todo deleted file mode 100644 index 0aa2a85..0000000 --- a/src/initial.test.todo +++ /dev/null @@ -1,46 +0,0 @@ -import { test, expect } from 'vitest'; - -import { Updux } from './Updux.js'; - -const bar = new Updux({ initial: 123 }); - -const foo = new Updux({ - initial: { root: 'abc' }, - subduxes: { - bar, - }, -}); - -test('single dux', () => { - const foo = new Updux({ - initial: { a: 1 }, - }); - - expect(foo.initial).toEqual({ a: 1 }); -}); - -test('initial value', () => { - expect(foo.initial).toEqual({ - root: 'abc', - bar: 123, - }); -}); - -test('splat initial', async () => { - const bar = new Updux({ - initial: { id: 0 }, - }); - - const foo = new Updux({ - subduxes: { '*': bar }, - }); - - expect(foo.initial).toEqual([]); - - expect( - new Updux({ - initial: 'overriden', - subduxes: { '*': bar }, - }).initial, - ).toEqual('overriden'); -}); diff --git a/src/initial.test.ts b/src/initial.test.ts index 32194e6..74ef278 100644 --- a/src/initial.test.ts +++ b/src/initial.test.ts @@ -1,5 +1,15 @@ +import { expectType } from './tutorial.test.js'; import Updux from './Updux.js'; +const bar = new Updux({ initial: 123 }); + +const foo = new Updux({ + initial: { root: 'abc' }, + subduxes: { + bar, + }, +}); + test('default', () => { const { initial } = new Updux({}); @@ -29,3 +39,60 @@ test('initial to createStore', () => { b: 4, }); }); + +test('single dux', () => { + const foo = new Updux({ + initial: { a: 1 }, + }); + + expect(foo.initial).toEqual({ a: 1 }); +}); + +// TODO add 'check for no todo eslint rule' +test.only('initial value', () => { + expect(foo.initial).toEqual({ + root: 'abc', + bar: 123, + }); + + expectType<{ + root: string; + bar: number; + }>(foo.initial); +}); + +test('no initial', () => { + const dux = new Updux({}); + expectType<{}>(dux.initial); + expect(dux.initial).toEqual({}); +}); + +test('no initial for subdux', () => { + const dux = new Updux({ + subduxes: { + bar: new Updux({}), + baz: new Updux({ initial: 'potato' }), + }, + }); + expectType<{ bar: {}; baz: string }>(dux.initial); + expect(dux.initial).toEqual({ bar: {}, baz: 'potato' }); +}); + +test('splat initial', async () => { + const bar = new Updux({ + initial: { id: 0 }, + }); + + const foo = new Updux({ + subduxes: { '*': bar }, + }); + + expect(foo.initial).toEqual([]); + + expect( + new Updux({ + initial: 'overriden', + subduxes: { '*': bar }, + }).initial, + ).toEqual('overriden'); +}); diff --git a/src/tutorial.test.ts b/src/tutorial.test.ts index 753e746..e763eb4 100644 --- a/src/tutorial.test.ts +++ b/src/tutorial.test.ts @@ -1,7 +1,7 @@ import Updux, { createAction, withPayload } from './index.js'; import u from '@yanick/updeep-remeda'; -const expectType = (value: T) => value; +export const expectType = (value: T) => value; test('initial state', () => { const initial = { From 042f5b8f13925265c66ce1b395abf6f51a3e5c1f Mon Sep 17 00:00:00 2001 From: Yanick Champoux Date: Tue, 7 Mar 2023 11:53:44 -0500 Subject: [PATCH 2/4] initial state and subduxes tests are passing --- src/Updux.ts | 17 +++++++++++++---- src/initial.test.ts | 4 ++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/Updux.ts b/src/Updux.ts index 3a70c08..2352b17 100644 --- a/src/Updux.ts +++ b/src/Updux.ts @@ -20,7 +20,16 @@ import { AggregateActions, Dux, UnionToIntersection } from './types.js'; import { buildActions } from './buildActions.js'; import { buildInitial } from './buildInitial.js'; -type AggregateState = L; +type SubduxState = 'initial' extends keyof S ? S['initial'] : {}; + +type AggregateState> = LOCAL & + (keyof SUBDUXES extends never + ? {} + : { + [Slice in keyof SUBDUXES]: Slice extends string + ? SubduxState + : never; + }); type MyActionCreator = { type: string } & ((...args: any) => any); @@ -66,7 +75,7 @@ export default class Updux< #localActions: T_LocalActions; #localMutations: Record< string, - Mutation, AggregateState> + Mutation, AggregateState> > = {}; #subduxes: SUBDUXES; @@ -74,7 +83,7 @@ export default class Updux< #actions: AggregateActions, SUBDUXES>; - #initial: any; + #initial: AggregateState; constructor( config: Partial<{ @@ -119,7 +128,7 @@ export default class Updux< // TODO force the actionCreator to be one of the actions? mutation>( actionCreator: A, - mutation: Mutation>, + mutation: Mutation>, ) { this.#localMutations[(actionCreator as any).type] = mutation; } diff --git a/src/initial.test.ts b/src/initial.test.ts index 74ef278..245632a 100644 --- a/src/initial.test.ts +++ b/src/initial.test.ts @@ -49,7 +49,7 @@ test('single dux', () => { }); // TODO add 'check for no todo eslint rule' -test.only('initial value', () => { +test('initial value', () => { expect(foo.initial).toEqual({ root: 'abc', bar: 123, @@ -78,7 +78,7 @@ test('no initial for subdux', () => { expect(dux.initial).toEqual({ bar: {}, baz: 'potato' }); }); -test('splat initial', async () => { +test.todo('splat initial', async () => { const bar = new Updux({ initial: { id: 0 }, }); From 736377effd7bad27b67cd2e33ce1cc50afc80c75 Mon Sep 17 00:00:00 2001 From: Yanick Champoux Date: Tue, 7 Mar 2023 11:57:41 -0500 Subject: [PATCH 3/4] rename SUBDUXES to T_Subduxes --- src/Updux.ts | 31 +++++++++-------------------- src/buildInitial.test.ts | 2 +- src/{buildInitial.ts => initial.ts} | 11 ++++++++++ 3 files changed, 21 insertions(+), 23 deletions(-) rename src/{buildInitial.ts => initial.ts} (52%) diff --git a/src/Updux.ts b/src/Updux.ts index 2352b17..1bc0d37 100644 --- a/src/Updux.ts +++ b/src/Updux.ts @@ -18,23 +18,10 @@ import { import { withPayload } from './actions.js'; import { AggregateActions, Dux, UnionToIntersection } from './types.js'; import { buildActions } from './buildActions.js'; -import { buildInitial } from './buildInitial.js'; - -type SubduxState = 'initial' extends keyof S ? S['initial'] : {}; - -type AggregateState> = LOCAL & - (keyof SUBDUXES extends never - ? {} - : { - [Slice in keyof SUBDUXES]: Slice extends string - ? SubduxState - : never; - }); +import { buildInitial, AggregateState } from './initial.js'; type MyActionCreator = { type: string } & ((...args: any) => any); -type F = null extends any ? 'u' : 'n'; - type ResolveAction< ActionType extends string, ActionArg extends any, @@ -69,33 +56,33 @@ export default class Updux< T_LocalActions extends { [actionType: string]: any; } = {}, - SUBDUXES extends Record = {}, + T_Subduxes extends Record = {}, > { #localInitial: T_LocalState; #localActions: T_LocalActions; #localMutations: Record< string, - Mutation, AggregateState> + Mutation, AggregateState> > = {}; - #subduxes: SUBDUXES; + #subduxes: T_Subduxes; #name: string; - #actions: AggregateActions, SUBDUXES>; + #actions: AggregateActions, T_Subduxes>; - #initial: AggregateState; + #initial: AggregateState; constructor( config: Partial<{ initial: T_LocalState; actions: T_LocalActions; - subduxes: SUBDUXES; + subduxes: T_Subduxes; }>, ) { // TODO check that we can't alter the initial after the fact this.#localInitial = config.initial ?? ({} as T_LocalState); this.#localActions = config.actions ?? ({} as T_LocalActions); - this.#subduxes = config.subduxes ?? ({} as SUBDUXES); + this.#subduxes = config.subduxes ?? ({} as T_Subduxes); this.#actions = buildActions(this.#localActions, this.#subduxes); @@ -128,7 +115,7 @@ export default class Updux< // TODO force the actionCreator to be one of the actions? mutation>( actionCreator: A, - mutation: Mutation>, + mutation: Mutation>, ) { this.#localMutations[(actionCreator as any).type] = mutation; } diff --git a/src/buildInitial.test.ts b/src/buildInitial.test.ts index 7e8767e..63743e8 100644 --- a/src/buildInitial.test.ts +++ b/src/buildInitial.test.ts @@ -1,5 +1,5 @@ import { test, expect } from 'vitest'; -import { buildInitial } from './buildInitial.js'; +import { buildInitial } from './initial.js'; test('basic', () => { expect( diff --git a/src/buildInitial.ts b/src/initial.ts similarity index 52% rename from src/buildInitial.ts rename to src/initial.ts index 7e2330e..36aaf92 100644 --- a/src/buildInitial.ts +++ b/src/initial.ts @@ -1,6 +1,17 @@ import u from '@yanick/updeep-remeda'; import * as R from 'remeda'; +type SubduxState = 'initial' extends keyof S ? S['initial'] : {}; + +export type AggregateState> = LOCAL & + (keyof SUBDUXES extends never + ? {} + : { + [Slice in keyof SUBDUXES]: Slice extends string + ? SubduxState + : never; + }); + export function buildInitial(localInitial, subduxes) { if (Object.keys(subduxes).length > 0 && typeof localInitial !== 'object') { throw new Error( From b3088334a475033d2f4664e17865c6ebd9a4785e Mon Sep 17 00:00:00 2001 From: Yanick Champoux Date: Tue, 7 Mar 2023 19:07:17 -0500 Subject: [PATCH 4/4] remove unused imports --- src/Updux.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Updux.ts b/src/Updux.ts index 1bc0d37..d275700 100644 --- a/src/Updux.ts +++ b/src/Updux.ts @@ -8,15 +8,11 @@ import { import { configureStore, Reducer, - createAction, ActionCreator, - PrepareAction, - ActionCreatorWithPayload, ActionCreatorWithoutPayload, ActionCreatorWithPreparedPayload, } from '@reduxjs/toolkit'; -import { withPayload } from './actions.js'; -import { AggregateActions, Dux, UnionToIntersection } from './types.js'; +import { AggregateActions, Dux } from './types.js'; import { buildActions } from './buildActions.js'; import { buildInitial, AggregateState } from './initial.js';