diff --git a/.gitignore b/.gitignore index ca82d9c..3ae7eb0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ node_modules/ tsconfig.tsbuildinfo **/*.orig -dist package-lock.json yarn.lock .nyc_output/ diff --git a/dist/Updux.d.ts b/dist/Updux.d.ts new file mode 100644 index 0000000..c590735 --- /dev/null +++ b/dist/Updux.d.ts @@ -0,0 +1,82 @@ +import * as rtk from '@reduxjs/toolkit'; +import { Action, AnyAction, EnhancedStore, PayloadAction } from '@reduxjs/toolkit'; +import { AugmentedMiddlewareAPI, DuxActions, DuxConfig, DuxReaction, DuxSelectors, DuxState, Mutation } from './types.js'; +import { EffectMiddleware } from './effects.js'; +type CreateStoreOptions = Partial<{ + preloadedState: DuxState; +}>; +interface ActionCreator> { + type: T; + match: (action: Action) => action is PayloadAction; + (...args: A): PayloadAction; +} +/** + * @description main Updux class + */ +export default class Updux { + #private; + private readonly duxConfig; + constructor(duxConfig: D); + /** + * @description Initial state of the dux. + */ + get initialState(): DuxState; + get subduxes(): {}; + get actions(): DuxActions; + createStore(options?: CreateStoreOptions): EnhancedStore> & AugmentedMiddlewareAPI; + addAction(action: ActionCreator): void; + addSelector(name: N, selector: (state: DuxState) => R): Updux) => R>; + }>; + addMutation>(actionType: A, mutation: Mutation[A] extends (...args: any[]) => infer R ? R : AnyAction, DuxState>, terminal?: boolean): Updux; + addMutation>(actionCreator: AC, mutation: Mutation, DuxState>, terminal?: boolean): Updux; + }>; + addMutation>(actionCreator: ActionCreator, mutation: Mutation>, DuxState>, terminal?: boolean): Updux>; + }>; + addMutation(matcher: (action: AnyAction) => boolean, mutation: Mutation>, terminal?: boolean): Updux; + setDefaultMutation(mutation: Mutation>, terminal?: boolean): Updux; + get reducer(): any; + get selectors(): DuxSelectors; + addEffect>(actionType: A, effect: EffectMiddleware[A] extends (...args: any[]) => infer R ? R : AnyAction>): Updux; + addEffect>(actionCreator: AC, effect: EffectMiddleware>): Updux; + }>; + addEffect(guardFunc: (action: AnyAction) => boolean, effect: EffectMiddleware): Updux; + addEffect(effect: EffectMiddleware): Updux; + get effects(): any[]; + get upreducer(): (action: AnyAction) => (state?: DuxState) => DuxState; + addReaction(reaction: DuxReaction): this; + get reactions(): any; + get defaultMutation(): { + terminal: boolean; + mutation: Mutation : unknown) extends infer T ? T extends (D extends { + initialState: any; + } ? D["initialState"] : {}) & (D extends { + subduxes: any; + } ? import("./types.js").SubduxesState : unknown) ? T extends { + [key: string]: any; + } ? { [key in keyof T]: T[key]; } : T : never : never>; + }; + /** + * @description Returns an object holding the Updux reducer and all its + * paraphernalia. + */ + get asDux(): { + initialState: DuxState; + actions: DuxActions; + reducer: (state: DuxState, action: AnyAction) => DuxState; + effects: EffectMiddleware[]; + reactions: DuxReaction[]; + }; +} +export {}; diff --git a/dist/Updux.js b/dist/Updux.js new file mode 100644 index 0000000..f1b966a --- /dev/null +++ b/dist/Updux.js @@ -0,0 +1,210 @@ +var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { + if (kind === "m") throw new TypeError("Private method is not writable"); + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); + return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; +}; +var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); + return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); +}; +var _Updux_subduxes, _Updux_memoInitialState, _Updux_memoBuildReducer, _Updux_memoBuildSelectors, _Updux_memoBuildEffects, _Updux_memoBuildReactions, _Updux_actions, _Updux_defaultMutation, _Updux_effects, _Updux_reactions, _Updux_mutations, _Updux_inheritedReducer, _Updux_selectors; +import * as rtk from '@reduxjs/toolkit'; +const { configureStore, } = rtk; +import baseMoize from 'moize/mjs/index.mjs'; +import { buildInitialState } from './initialState.js'; +import { buildActions } from './actions.js'; +import { D } from '@mobily/ts-belt'; +import { buildReducer } from './reducer.js'; +import { buildSelectors } from './selectors.js'; +import { buildEffectsMiddleware, buildEffects, augmentMiddlewareApi } from './effects.js'; +import { augmentGetState, augmentDispatch } from './createStore.js'; +import { buildReactions } from './reactions.js'; +const moize = (func) => baseMoize(func, { maxSize: 1 }); +/** + * @description main Updux class + */ +export default class Updux { + constructor(duxConfig) { + var _a, _b, _c; + this.duxConfig = duxConfig; + /** @internal */ + _Updux_subduxes.set(this, void 0); + /** @internal */ + _Updux_memoInitialState.set(this, moize(buildInitialState)); + /** @internal */ + _Updux_memoBuildReducer.set(this, moize(buildReducer)); + /** @internal */ + _Updux_memoBuildSelectors.set(this, moize(buildSelectors)); + /** @internal */ + _Updux_memoBuildEffects.set(this, moize(buildEffects)); + /** @internal */ + _Updux_memoBuildReactions.set(this, moize(buildReactions)); + /** @internal */ + _Updux_actions.set(this, {}); + /** @internal */ + _Updux_defaultMutation.set(this, void 0); + /** @internal */ + _Updux_effects.set(this, []); + /** @internal */ + _Updux_reactions.set(this, []); + /** @internal */ + _Updux_mutations.set(this, []); + /** @internal */ + _Updux_inheritedReducer.set(this, void 0); + /** @internal */ + _Updux_selectors.set(this, {}); + if (duxConfig.subduxes) + __classPrivateFieldSet(this, _Updux_subduxes, D.map(duxConfig.subduxes, (s) => s instanceof Updux ? s : new Updux(s)), "f"); + __classPrivateFieldSet(this, _Updux_inheritedReducer, duxConfig.reducer, "f"); + __classPrivateFieldSet(this, _Updux_effects, (_a = duxConfig.effects) !== null && _a !== void 0 ? _a : [], "f"); + __classPrivateFieldSet(this, _Updux_reactions, (_b = duxConfig.reactions) !== null && _b !== void 0 ? _b : [], "f"); + __classPrivateFieldSet(this, _Updux_actions, buildActions(duxConfig.actions, __classPrivateFieldGet(this, _Updux_subduxes, "f")), "f"); + __classPrivateFieldSet(this, _Updux_selectors, (_c = duxConfig.selectors) !== null && _c !== void 0 ? _c : {}, "f"); + } + /** + * @description Initial state of the dux. + */ + get initialState() { + return __classPrivateFieldGet(this, _Updux_memoInitialState, "f").call(this, this.duxConfig.initialState, __classPrivateFieldGet(this, _Updux_subduxes, "f")); + } + get subduxes() { + return __classPrivateFieldGet(this, _Updux_subduxes, "f"); + } + get actions() { + return __classPrivateFieldGet(this, _Updux_actions, "f"); + } + createStore(options = {}) { + const preloadedState = options.preloadedState; + const effects = buildEffectsMiddleware(this.effects, this.actions, this.selectors); + let middleware = (gdm) => gdm().concat(effects); + const store = configureStore({ + preloadedState, + reducer: this.reducer, + middleware, + }); + store.dispatch = augmentDispatch(store.dispatch, this.actions); + store.getState = augmentGetState(store.getState, this.selectors); + store.actions = this.actions; + store.selectors = this.selectors; + for (const reaction of this.reactions) { + let unsub; + const r = reaction(store); + unsub = store.subscribe(() => r(unsub)); + } + return store; + } + addAction(action) { + const { type } = action; + if (!__classPrivateFieldGet(this, _Updux_actions, "f")[type]) { + __classPrivateFieldSet(this, _Updux_actions, Object.assign(Object.assign({}, __classPrivateFieldGet(this, _Updux_actions, "f")), { [type]: action }), "f"); + return; + } + if (__classPrivateFieldGet(this, _Updux_actions, "f")[type] !== action) + throw new Error(`redefining action ${type}`); + } + addSelector(name, selector) { + __classPrivateFieldSet(this, _Updux_selectors, Object.assign(Object.assign({}, __classPrivateFieldGet(this, _Updux_selectors, "f")), { [name]: selector }), "f"); + return this; + } + addMutation(actionCreator, mutation, terminal = false) { + var _a; + if (typeof actionCreator === 'string') { + if (!this.actions[actionCreator]) + throw new Error(`action ${actionCreator} not found`); + actionCreator = this.actions[actionCreator]; + } + else { + this.addAction(actionCreator); + } + __classPrivateFieldSet(this, _Updux_mutations, __classPrivateFieldGet(this, _Updux_mutations, "f").concat({ + terminal, + matcher: (_a = actionCreator.match) !== null && _a !== void 0 ? _a : actionCreator, + mutation, + }), "f"); + return this; + } + setDefaultMutation(mutation, terminal = false) { + __classPrivateFieldSet(this, _Updux_defaultMutation, { terminal, mutation }, "f"); + return this; + } + get reducer() { + return __classPrivateFieldGet(this, _Updux_memoBuildReducer, "f").call(this, this.initialState, __classPrivateFieldGet(this, _Updux_mutations, "f"), __classPrivateFieldGet(this, _Updux_defaultMutation, "f"), __classPrivateFieldGet(this, _Updux_subduxes, "f"), __classPrivateFieldGet(this, _Updux_inheritedReducer, "f")); + } + get selectors() { + return __classPrivateFieldGet(this, _Updux_memoBuildSelectors, "f").call(this, __classPrivateFieldGet(this, _Updux_selectors, "f"), __classPrivateFieldGet(this, _Updux_subduxes, "f")); + } + addEffect(...args) { + let effect; + if (args.length === 1) { + effect = args[0]; + } + else { + let [actionCreator, originalEffect] = args; + if (typeof actionCreator === 'string') { + if (this.actions[actionCreator]) { + actionCreator = this.actions[actionCreator]; + } + else { + throw new Error(`action '${actionCreator}' is unknown`); + } + } + if (!this.actions[actionCreator.type]) + this.addAction(actionCreator); + const test = actionCreator.hasOwnProperty('match') + ? actionCreator.match + : actionCreator; + effect = (api) => (next) => { + const e = originalEffect(api)(next); + return (action) => { + const func = test(action) ? e : next; + return func(action); + }; + }; + } + __classPrivateFieldSet(this, _Updux_effects, __classPrivateFieldGet(this, _Updux_effects, "f").concat(effect), "f"); + return this; + } + get effects() { + return __classPrivateFieldGet(this, _Updux_memoBuildEffects, "f").call(this, __classPrivateFieldGet(this, _Updux_effects, "f"), __classPrivateFieldGet(this, _Updux_subduxes, "f")); + } + get upreducer() { + return (action) => (state) => this.reducer(state, action); + } + addReaction(reaction) { + let previous; + const memoized = (api) => { + api = augmentMiddlewareApi(api, this.actions, this.selectors); + const r = reaction(api); + const rMemoized = (localState, unsub) => { + let p = previous; + previous = localState; + r(localState, p, unsub); + }; + return (unsub) => rMemoized(api.getState(), unsub); + }; + __classPrivateFieldSet(this, _Updux_reactions, __classPrivateFieldGet(this, _Updux_reactions, "f").concat(memoized), "f"); + return this; + } + get reactions() { + return __classPrivateFieldGet(this, _Updux_memoBuildReactions, "f").call(this, __classPrivateFieldGet(this, _Updux_reactions, "f"), __classPrivateFieldGet(this, _Updux_subduxes, "f")); + } + get defaultMutation() { + return __classPrivateFieldGet(this, _Updux_defaultMutation, "f"); + } + /** + * @description Returns an object holding the Updux reducer and all its + * paraphernalia. + */ + get asDux() { + return { + initialState: this.initialState, + actions: this.actions, + effects: this.effects, + reactions: this.reactions, + reducer: this.reducer, + }; + } +} +_Updux_subduxes = new WeakMap(), _Updux_memoInitialState = new WeakMap(), _Updux_memoBuildReducer = new WeakMap(), _Updux_memoBuildSelectors = new WeakMap(), _Updux_memoBuildEffects = new WeakMap(), _Updux_memoBuildReactions = new WeakMap(), _Updux_actions = new WeakMap(), _Updux_defaultMutation = new WeakMap(), _Updux_effects = new WeakMap(), _Updux_reactions = new WeakMap(), _Updux_mutations = new WeakMap(), _Updux_inheritedReducer = new WeakMap(), _Updux_selectors = new WeakMap(); diff --git a/dist/Updux.test.js b/dist/Updux.test.js new file mode 100644 index 0000000..ca52453 --- /dev/null +++ b/dist/Updux.test.js @@ -0,0 +1,11 @@ +import { test, expect } from 'vitest'; +import Updux from './Updux.js'; +test('subdux idempotency', () => { + const foo = new Updux({ + subduxes: { + a: new Updux({ initialState: 2 }), + }, + }); + let fooState = foo.reducer(undefined, { type: 'noop' }); + expect(foo.reducer(fooState, { type: 'noop' })).toBe(fooState); +}); diff --git a/dist/actions.d.ts b/dist/actions.d.ts new file mode 100644 index 0000000..aeeff8d --- /dev/null +++ b/dist/actions.d.ts @@ -0,0 +1,16 @@ +import { DuxActions, DuxConfig, Subduxes } from './types.js'; +export { createAction } from '@reduxjs/toolkit'; +export declare function withPayload

(): (input: P) => { + payload: P; +}; +export declare function withPayload(prepare: (...args: A) => P): (...input: A) => { + payload: P; +}; +export declare function buildActions(localActions: L): DuxActions<{ + actions: L; +}>; +export declare function buildActions, S extends Subduxes>(localActions: L, subduxes: S): DuxActions<{ + actions: L; + subduxes: S; +}>; +export declare function expandAction(prepare: any, actionType?: string): any; diff --git a/dist/actions.js b/dist/actions.js new file mode 100644 index 0000000..e96abf8 --- /dev/null +++ b/dist/actions.js @@ -0,0 +1,40 @@ +import { createAction } from '@reduxjs/toolkit'; +import { D } from '@mobily/ts-belt'; +export { createAction } from '@reduxjs/toolkit'; +export function withPayload(prepare = (input) => input) { + return (...input) => ({ + payload: prepare.apply(null, input), + }); +} +export function buildActions(localActions = {}, subduxes = {}) { + localActions = D.mapWithKey(localActions, (key, value) => expandAction(value, String(key))); + let actions = {}; + for (const slice in subduxes) { + const subdux = subduxes[slice].actions; + if (!subdux) + continue; + for (const a in subdux) { + if (actions[a] && subduxes[actions[a]].actions[a] !== subdux[a]) { + throw new Error(`action '${a}' defined both in subduxes '${actions[a]}' and '${slice}'`); + } + actions[a] = slice; + } + for (const a in localActions) { + if (actions[a]) { + throw new Error(`action '${a}' defined both locally and in subdux '${actions[a]}'`); + } + } + } + return [ + localActions, + ...D.values(subduxes).map((s) => { var _a; return (_a = s.actions) !== null && _a !== void 0 ? _a : {}; }), + ].reduce(D.merge); +} +export function expandAction(prepare, actionType) { + if (typeof prepare === 'function' && prepare.type) + return prepare; + if (typeof prepare === 'function') + return createAction(actionType, withPayload(prepare)); + if (actionType) + return createAction(actionType); +} diff --git a/dist/actions.test.d.ts b/dist/actions.test.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/actions.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/actions.test.js b/dist/actions.test.js new file mode 100644 index 0000000..93eb130 --- /dev/null +++ b/dist/actions.test.js @@ -0,0 +1,74 @@ +import { buildActions, createAction, withPayload } from './actions.js'; +import Updux from './index.js'; +test('basic action', () => { + const foo = createAction('foo', withPayload((thing) => ({ thing }))); + expect(foo('bar')).toEqual({ + type: 'foo', + payload: { + thing: 'bar', + }, + }); + expectTypeOf(foo).parameters.toMatchTypeOf(); + expectTypeOf(foo).returns.toMatchTypeOf(); +}); +test('withPayload, just the type', () => { + const foo = createAction('foo', withPayload()); + expect(foo('bar')).toEqual({ + type: 'foo', + payload: 'bar', + }); + expectTypeOf(foo).parameters.toMatchTypeOf(); + expectTypeOf(foo).returns.toMatchTypeOf(); +}); +test('buildActions', () => { + const actions = buildActions({ + one: createAction('one'), + two: (x) => x, + withoutValue: null, + }, { a: { actions: buildActions({ three: () => 3 }) } }); + expect(actions.one()).toEqual({ + type: 'one', + }); + expectTypeOf(actions.one()).toMatchTypeOf(); + expect(actions.two('potato')).toEqual({ type: 'two', payload: 'potato' }); + expectTypeOf(actions.two('potato')).toMatchTypeOf(); + expect(actions.three()).toEqual({ type: 'three', payload: 3 }); + expectTypeOf(actions.three()).toMatchTypeOf(); + expect(actions.withoutValue()).toEqual({ type: 'withoutValue' }); + expectTypeOf(actions.withoutValue()).toMatchTypeOf(); +}); +describe('Updux interactions', () => { + var _a; + const dux = new Updux({ + initialState: { a: 3 }, + actions: { + add: (x) => x, + withNull: null, + }, + subduxes: { + subdux1: new Updux({ + actions: { + fromSubdux: (x) => x, + }, + }), + subdux2: { + actions: { + ohmy: () => { }, + }, + }, + }, + }); + test('actions getter', () => { + expect(dux.actions.add).toBeTruthy(); + }); + expectTypeOf(dux.actions.withNull).toMatchTypeOf(); + expect(dux.actions.withNull()).toMatchObject({ + type: 'withNull', + }); + expectTypeOf(dux.actions).toMatchTypeOf(); + expectTypeOf((_a = dux.actions) === null || _a === void 0 ? void 0 : _a.add).not.toEqualTypeOf(); + expect(dux.actions.fromSubdux('payload')).toEqual({ + type: 'fromSubdux', + payload: 'payload', + }); +}); diff --git a/dist/asDux.test.d.ts b/dist/asDux.test.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/asDux.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/asDux.test.js b/dist/asDux.test.js new file mode 100644 index 0000000..ffdd1d5 --- /dev/null +++ b/dist/asDux.test.js @@ -0,0 +1,24 @@ +import Updux, { createAction } from './index.js'; +test('asDux', () => { + const actionA = createAction('actionA'); + const defaultMutation = () => (state) => state + 1; + const dux = new Updux({ + initialState: 13, + actions: { actionA }, + }) + .setDefaultMutation(defaultMutation) + .addReaction((api) => (state) => { }) + .addEffect((api) => (next) => (action) => next(action)); + const asDux = dux.asDux; + expect(asDux.initialState).toEqual(13); + expect(asDux.reducer).toBeTypeOf('function'); + expect(asDux.actions.actionA()).toMatchObject({ type: 'actionA' }); + expect(asDux.effects).toHaveLength(1); + expect(asDux.reactions).toHaveLength(1); + const newDux = new Updux(asDux); + expect(newDux.initialState).toEqual(13); + expect(newDux.reducer).toBeTypeOf('function'); + expect(newDux.actions.actionA()).toMatchObject({ type: 'actionA' }); + expect(newDux.effects).toHaveLength(1); + expect(newDux.reactions).toHaveLength(1); +}); diff --git a/dist/buildActions.js b/dist/buildActions.js new file mode 100644 index 0000000..af7e9b0 --- /dev/null +++ b/dist/buildActions.js @@ -0,0 +1,34 @@ +import { createAction } from '@reduxjs/toolkit'; +import * as R from 'remeda'; +import { withPayload } from './actions.js'; +function resolveActions(configActions) { + return R.mapValues(configActions, (prepare, type) => { + if (typeof prepare === 'function' && prepare.type) + return prepare; + return createAction(type, withPayload(prepare)); + }); +} +export function buildActions(localActions, subduxes) { + localActions = resolveActions(localActions); + let actions = {}; + for (const slice in subduxes) { + const subdux = subduxes[slice].actions; + if (!subdux) + continue; + for (const a in subdux) { + if (actions[a] && subduxes[actions[a]].actions[a] !== subdux[a]) { + throw new Error(`action '${a}' defined both in subduxes '${actions[a]}' and '${slice}'`); + } + actions[a] = slice; + } + } + for (const a in localActions) { + if (actions[a]) { + throw new Error(`action '${a}' defined both locally and in subdux '${actions[a]}'`); + } + } + return R.mergeAll([ + localActions, + ...Object.values(subduxes).map(R.pathOr(['actions'], {})), + ]); +} diff --git a/dist/buildInitial.js b/dist/buildInitial.js new file mode 100644 index 0000000..3908ac8 --- /dev/null +++ b/dist/buildInitial.js @@ -0,0 +1,8 @@ +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/dist/buildInitial.test.js b/dist/buildInitial.test.js new file mode 100644 index 0000000..7f80d0d --- /dev/null +++ b/dist/buildInitial.test.js @@ -0,0 +1,14 @@ +import { test, expect } from 'vitest'; +import { buildInitial } from './initial.js'; +test('basic', () => { + expect(buildInitial({ a: 1 }, { b: { initialState: { c: 2 } }, d: { initialState: 'e' } })).toEqual({ + a: 1, + b: { c: 2 }, + d: 'e', + }); +}); +test('throw if subduxes and initialState is not an object', () => { + expect(() => { + buildInitial(3, { bar: 'foo' }); + }).toThrow(); +}); diff --git a/dist/buildInitialState.d.ts b/dist/buildInitialState.d.ts new file mode 100644 index 0000000..4332bcd --- /dev/null +++ b/dist/buildInitialState.d.ts @@ -0,0 +1 @@ +export declare function buildInitialState(localInitialState: any, subduxes: any): any; diff --git a/dist/buildInitialState.js b/dist/buildInitialState.js new file mode 100644 index 0000000..9785050 --- /dev/null +++ b/dist/buildInitialState.js @@ -0,0 +1,12 @@ +import u from '@yanick/updeep-remeda'; +import { D } from '@mobily/ts-belt'; +export function buildInitialState(localInitialState, subduxes) { + let state = localInitialState !== null && localInitialState !== void 0 ? localInitialState : {}; + if (subduxes) { + if (typeof state !== 'object') { + throw new Error('root initial state is not an object'); + } + state = u(state, D.map(subduxes, D.prop('initialState'))); + } + return state; +} diff --git a/dist/buildMiddleware/index.js b/dist/buildMiddleware/index.js new file mode 100644 index 0000000..2085412 --- /dev/null +++ b/dist/buildMiddleware/index.js @@ -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); +} diff --git a/dist/buildSelectors/index.js b/dist/buildSelectors/index.js new file mode 100644 index 0000000..dfd7550 --- /dev/null +++ b/dist/buildSelectors/index.js @@ -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); +} diff --git a/dist/createStore.d.ts b/dist/createStore.d.ts new file mode 100644 index 0000000..d7908e8 --- /dev/null +++ b/dist/createStore.d.ts @@ -0,0 +1,2 @@ +export declare function augmentGetState(originalGetState: any, selectors: any): () => any; +export declare function augmentDispatch(dispatch: any, actions: any): any; diff --git a/dist/createStore.js b/dist/createStore.js new file mode 100644 index 0000000..283da9b --- /dev/null +++ b/dist/createStore.js @@ -0,0 +1,22 @@ +export function augmentGetState(originalGetState, selectors) { + const getState = () => originalGetState(); + for (const s in selectors) { + getState[s] = (...args) => { + let result = selectors[s](originalGetState()); + if (typeof result === 'function') + return result(...args); + return result; + }; + } + return getState; +} +export function augmentDispatch(dispatch, actions) { + for (const a in actions) { + dispatch[a] = (...args) => { + const action = actions[a](...args); + dispatch(action); + return action; + }; + } + return dispatch; +} diff --git a/dist/createStore.test.d.ts b/dist/createStore.test.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/createStore.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/createStore.test.js b/dist/createStore.test.js new file mode 100644 index 0000000..3369a93 --- /dev/null +++ b/dist/createStore.test.js @@ -0,0 +1,25 @@ +import { test, expect } from 'vitest'; +import Updux, { createAction, withPayload } from './index.js'; +const incr = createAction('incr', withPayload()); +const dux = new Updux({ + initialState: 1, + // selectors: { + // double: (x: string) => x + x, + // }, +}).addMutation(incr, (i, action) => state => { + expectTypeOf(i).toEqualTypeOf(); + expectTypeOf(state).toEqualTypeOf(); + expectTypeOf(action).toEqualTypeOf(); + return state + i; +}); +suite.only('store dispatch actions', () => { + const store = dux.createStore(); + test('dispatch actions', () => { + expect(store.dispatch.incr).toBeTypeOf('function'); + expectTypeOf(store.dispatch.incr).toMatchTypeOf(); + }); + test('reducer does something', () => { + store.dispatch.incr(7); + expect(store.getState()).toEqual(8); + }); +}); diff --git a/dist/effe.ts.2023-08-18.js b/dist/effe.ts.2023-08-18.js new file mode 100644 index 0000000..62328cd --- /dev/null +++ b/dist/effe.ts.2023-08-18.js @@ -0,0 +1,21 @@ +const composeMw = (mws) => (api) => (originalNext) => mws.reduceRight((next, mw) => mw(api)(next), originalNext); +const augmentDispatch = (originalDispatch, actions) => { + const dispatch = (action) => originalDispatch(action); + for (const a in actions) { + dispatch[a] = (...args) => dispatch(actions[a](...args)); + } + return dispatch; +}; +export const augmentMiddlewareApi = (api, actions, selectors) => { + return Object.assign(Object.assign({}, api), { getState: augmentGetState(api.getState, selectors), dispatch: augmentDispatch(api.dispatch, actions), actions, + selectors }); +}; +export function buildEffectsMiddleware(effects = [], actions = {}, selectors = {}) { + return (api) => { + const newApi = augmentMiddlewareApi(api, actions, selectors); + let mws = effects.map((e) => e(newApi)); + return (originalNext) => { + return mws.reduceRight((next, mw) => mw(next), originalNext); + }; + }; +} diff --git a/dist/effe.ts.2023-08-18.test.js b/dist/effe.ts.2023-08-18.test.js new file mode 100644 index 0000000..a0beb3e --- /dev/null +++ b/dist/effe.ts.2023-08-18.test.js @@ -0,0 +1,119 @@ +import { buildEffectsMiddleware } from './effects.js'; +import Updux, { createAction } from './index.js'; +test('buildEffectsMiddleware', () => { + let seen = 0; + const mw = buildEffectsMiddleware([ + (api) => (next) => (action) => { + seen++; + expect(api).toHaveProperty('getState'); + expect(api.getState).toBeTypeOf('function'); + expect(api.getState()).toEqual('the state'); + expect(action).toHaveProperty('type'); + expect(next).toBeTypeOf('function'); + expect(api).toHaveProperty('actions'); + expect(api.actions.action1()).toHaveProperty('type', 'action1'); + api.dispatch.action1(); + expect(api.selectors.getFoo(2)).toBe(2); + expect(api.getState.getFoo()).toBe('the state'); + expect(api.getState.getBar(2)).toBe('the state2'); + next(action); + }, + ], { + action1: createAction('action1'), + }, { + getFoo: (state) => state, + getBar: (state) => (i) => state + i, + }); + expect(seen).toEqual(0); + const dispatch = vi.fn(); + mw({ getState: () => 'the state', dispatch })(() => { })({ + type: 'noop', + }); + expect(seen).toEqual(1); + expect(dispatch).toHaveBeenCalledWith({ type: 'action1' }); +}); +test('basic', () => { + const dux = new Updux({ + initialState: { + loaded: true, + }, + actions: { + foo: 0, + }, + }); + let seen = 0; + dux.addEffect((api) => (next) => (action) => { + seen++; + expect(api).toHaveProperty('getState'); + expect(api.getState()).toHaveProperty('loaded'); + expect(action).toHaveProperty('type'); + expect(next).toBeTypeOf('function'); + next(action); + }); + const store = dux.createStore(); + expect(seen).toEqual(0); + store.dispatch.foo(); + expect(seen).toEqual(1); +}); +test('subdux', () => { + const bar = new Updux({ + initialState: 'bar state', + actions: { foo: 0 }, + }); + let seen = 0; + bar.addEffect((api) => (next) => (action) => { + seen++; + expect(api.getState()).toBe('bar state'); + next(action); + }); + const dux = new Updux({ + initialState: { + loaded: true, + }, + subduxes: { + bar, + }, + }); + const store = dux.createStore(); + expect(seen).toEqual(0); + store.dispatch.foo(); + expect(seen).toEqual(1); +}); +test('addEffect with actionCreator', () => { + const dux = new Updux({ + actions: { + foo: null, + bar: null, + }, + }); + const next = vi.fn(); + const spy = vi.fn(); + const mw = dux.addEffect(dux.actions.foo, (api) => (next) => (action) => next(spy(action))); + mw({})(next)(dux.actions.bar()); + expect(next).toHaveBeenCalled(); + expect(spy).not.toHaveBeenCalled(); + next.mockReset(); + mw({})(next)(dux.actions.foo()); + expect(next).toHaveBeenCalled(); + expect(spy).toHaveBeenCalled(); +}); +test('addEffect with function', () => { + const dux = new Updux({ + actions: { + foo: () => { }, + bar: () => { }, + }, + }); + const next = vi.fn(); + const spy = vi.fn(); + const mw = dux.addEffect((action) => action.type[0] === 'f', (api) => (next) => (action) => next(spy(action))); + mw({})(next)(dux.actions.bar()); + expect(next).toHaveBeenCalled(); + expect(spy).not.toHaveBeenCalled(); + next.mockReset(); + mw({})(next)(dux.actions.foo()); + expect(next).toHaveBeenCalled(); + expect(spy).toHaveBeenCalled(); +}); +// TODO subdux effects +// TODO allow to subscribe / unsubscribe effects? diff --git a/dist/effects.d.ts b/dist/effects.d.ts new file mode 100644 index 0000000..6ea4460 --- /dev/null +++ b/dist/effects.d.ts @@ -0,0 +1,8 @@ +import * as rtk from '@reduxjs/toolkit'; +import { AugmentedMiddlewareAPI } from './types.js'; +export interface EffectMiddleware { + (api: AugmentedMiddlewareAPI): (next: rtk.Dispatch) => (action: A) => any; +} +export declare function buildEffects(localEffects: any, subduxes?: {}): any[]; +export declare function buildEffectsMiddleware(effects?: any[], actions?: {}, selectors?: {}): (api: any) => (originalNext: any) => any; +export declare const augmentMiddlewareApi: (api: any, actions: any, selectors: any) => any; diff --git a/dist/effects.js b/dist/effects.js new file mode 100644 index 0000000..6f24191 --- /dev/null +++ b/dist/effects.js @@ -0,0 +1,31 @@ +import { augmentGetState } from './createStore.js'; +export function buildEffects(localEffects, subduxes = {}) { + return [ + ...localEffects, + ...Object.entries(subduxes).flatMap(([slice, { effects, actions, selectors }]) => { + if (!effects) + return []; + return effects.map((effect) => (api) => effect(augmentMiddlewareApi(Object.assign(Object.assign({}, api), { getState: () => api.getState()[slice] }), actions, selectors))); + }), + ]; +} +export function buildEffectsMiddleware(effects = [], actions = {}, selectors = {}) { + return (api) => { + const newApi = augmentMiddlewareApi(api, actions, selectors); + let mws = effects.map((e) => e(newApi)); + return (originalNext) => { + return mws.reduceRight((next, mw) => mw(next), originalNext); + }; + }; +} +export const augmentMiddlewareApi = (api, actions, selectors) => { + return Object.assign(Object.assign({}, api), { getState: augmentGetState(api.getState, selectors), dispatch: augmentDispatch(api.dispatch, actions), actions, + selectors }); +}; +const augmentDispatch = (originalDispatch, actions) => { + const dispatch = (action) => originalDispatch(action); + for (const a in actions) { + dispatch[a] = (...args) => dispatch(actions[a](...args)); + } + return dispatch; +}; diff --git a/dist/effects.test.d.ts b/dist/effects.test.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/effects.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/effects.test.js b/dist/effects.test.js new file mode 100644 index 0000000..a11d080 --- /dev/null +++ b/dist/effects.test.js @@ -0,0 +1,195 @@ +import { test, expect } from 'vitest'; +import Updux from './Updux.js'; +import { buildEffectsMiddleware } from './effects.js'; +import { createAction, withPayload } from './index.js'; +test('addEffect signatures', () => { + const someAction = createAction('someAction', withPayload()); + const dux = new Updux({ + actions: { + someAction, + } + }); + dux.addEffect((api) => (next) => (action) => { + expectTypeOf(action).toMatchTypeOf(); + expectTypeOf(next).toMatchTypeOf(); + expectTypeOf(api).toMatchTypeOf(); + }); + dux.addEffect('someAction', (api) => (next) => (action) => { + expectTypeOf(action).toMatchTypeOf(); + expectTypeOf(next).toMatchTypeOf(); + expectTypeOf(api).toMatchTypeOf(); + }); + dux.addEffect(someAction, (api) => (next) => (action) => { + expectTypeOf(action).toMatchTypeOf(); + expectTypeOf(next).toMatchTypeOf(); + expectTypeOf(api).toMatchTypeOf(); + }); + dux.addEffect((action) => (action === null || action === void 0 ? void 0 : action.payload) === 3, (api) => (next) => (action) => { + expectTypeOf(action).toMatchTypeOf(); + expectTypeOf(next).toMatchTypeOf(); + expectTypeOf(api).toMatchTypeOf(); + }); +}); +test('buildEffectsMiddleware', () => { + let seen = 0; + const mw = buildEffectsMiddleware([ + (api) => (next) => (action) => { + seen++; + expect(api).toHaveProperty('getState'); + expect(api.getState).toBeTypeOf('function'); + expect(api.getState()).toEqual('the state'); + expect(action).toHaveProperty('type'); + expect(next).toBeTypeOf('function'); + expect(api).toHaveProperty('actions'); + expect(api.actions.action1()).toHaveProperty('type', 'action1'); + api.dispatch.action1(); + expect(api.selectors.getFoo(2)).toBe(2); + expect(api.getState.getFoo()).toBe('the state'); + expect(api.getState.getBar(2)).toBe('the state2'); + next(action); + }, + ], { + action1: createAction('action1'), + }, { + getFoo: (state) => state, + getBar: (state) => (i) => state + i, + }); + expect(seen).toEqual(0); + const dispatch = vi.fn(); + mw({ getState: () => 'the state', dispatch })(() => { })({ + type: 'noop', + }); + expect(seen).toEqual(1); + expect(dispatch).toHaveBeenCalledWith({ type: 'action1' }); +}); +test('basic', () => { + const dux = new Updux({ + initialState: { + loaded: true, + }, + actions: { + foo: null, + }, + }); + let seen = 0; + dux.addEffect((api) => (next) => (action) => { + seen++; + expect(api).toHaveProperty('getState'); + expect(api.getState()).toHaveProperty('loaded'); + expect(action).toHaveProperty('type'); + expect(next).toBeTypeOf('function'); + next(action); + }); + const store = dux.createStore(); + expect(seen).toEqual(0); + store.dispatch.foo(); + expect(seen).toEqual(1); +}); +test('subdux', () => { + const bar = new Updux({ + initialState: 'bar state', + actions: { foo: null }, + }); + let seen = 0; + bar.addEffect((api) => (next) => (action) => { + seen++; + expect(api.getState()).toBe('bar state'); + next(action); + }); + const dux = new Updux({ + initialState: { + loaded: true, + }, + subduxes: { + bar, + }, + }); + const store = dux.createStore(); + expect(seen).toEqual(0); + store.dispatch.foo(); + expect(seen).toEqual(1); +}); +test('addEffect with actionCreator', () => { + const dux = new Updux({ + actions: { + foo: null, + bar: null, + }, + }); + const next = vi.fn(); + const spy = vi.fn(); + const [mw] = dux.addEffect(dux.actions.foo, (api) => (next) => (action) => next(spy(action))).effects; + mw({})(next)(dux.actions.bar()); + expect(next).toHaveBeenCalled(); + expect(spy).not.toHaveBeenCalled(); + next.mockReset(); + mw({})(next)(dux.actions.foo()); + expect(next).toHaveBeenCalled(); + expect(spy).toHaveBeenCalled(); +}); +test('addEffect with function', () => { + const dux = new Updux({ + actions: { + foo: () => { }, + bar: () => { }, + }, + }); + const next = vi.fn(); + const spy = vi.fn(); + const [mw] = dux.addEffect((action) => action.type[0] === 'f', (api) => (next) => (action) => next(spy(action))).effects; + mw({})(next)(dux.actions.bar()); + expect(next).toHaveBeenCalled(); + expect(spy).not.toHaveBeenCalled(); + next.mockReset(); + mw({})(next)(dux.actions.foo()); + expect(next).toHaveBeenCalled(); + expect(spy).toHaveBeenCalled(); +}); +test('catchall addEffect', () => { + const dux = new Updux({ + initialState: { + a: 1, + }, + }); + const spy = vi.fn(); + dux.addEffect((api) => (next) => (action) => { + expectTypeOf(api.getState()).toMatchTypeOf(); + spy(); + next(action); + }); + const store = dux.createStore(); + expect(spy).not.toHaveBeenCalled(); + store.dispatch({ type: 'noop' }); + expect(spy).toHaveBeenCalled(); +}); +test('addEffect with unknown actionCreator adds it', () => { + const foo = createAction('foo'); + const dux = new Updux({}).addEffect(foo, () => () => () => { }); + expectTypeOf(dux.actions.foo).toMatchTypeOf(); + expect(dux.actions.foo()).toMatchObject({ type: 'foo' }); +}); +test('effects of subduxes', () => { + const foo = new Updux({ + initialState: 12, + actions: { + bar: null, + }, + selectors: { + addHundred: (x) => x + 100 + } + }) + .addMutation(createAction('setFoo', withPayload()), (state) => () => state) + .addEffect(({ type: t }) => t === 'doit', (api) => next => action => { + api.dispatch.setFoo(api.getState.addHundred()); + }); + const dux = new Updux({ + subduxes: { + foo + } + }); + const store = dux.createStore(); + store.dispatch({ type: "doit" }); + expect(store.getState()).toMatchObject({ foo: 112 }); +}); +// TODO subdux effects +// TODO allow to subscribe / unsubscribe effects? diff --git a/dist/foo.js b/dist/foo.js new file mode 100644 index 0000000..88d60d6 --- /dev/null +++ b/dist/foo.js @@ -0,0 +1,17 @@ +class Foo { + constuctor(t) { + this.t = t; + } + something(a) { + return 3; + } +} +class Bar extends Foo { + constructor() { + super(...arguments); + this.something = (a) => { }; + } +} +const x = new Bar(); +x.something; +export {}; diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..13bd23d --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,3 @@ +import Updux from './Updux.js'; +export { withPayload, createAction } from './actions.js'; +export default Updux; diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..13bd23d --- /dev/null +++ b/dist/index.js @@ -0,0 +1,3 @@ +import Updux from './Updux.js'; +export { withPayload, createAction } from './actions.js'; +export default Updux; diff --git a/dist/initial.js b/dist/initial.js new file mode 100644 index 0000000..7dbc57f --- /dev/null +++ b/dist/initial.js @@ -0,0 +1,8 @@ +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 initialState value is not an object"); + } + return u(localInitial, R.mapValues(subduxes, R.pathOr(['initialState'], {}))); +} diff --git a/dist/initial.test.js b/dist/initial.test.js new file mode 100644 index 0000000..621f97b --- /dev/null +++ b/dist/initial.test.js @@ -0,0 +1,74 @@ +import { expectType } from './tutorial.test.js'; +import Updux from './Updux.js'; +const bar = new Updux({ initialState: 123 }); +const foo = new Updux({ + initialState: { root: 'abc' }, + subduxes: { + bar, + }, +}); +test('default', () => { + const { initialState } = new Updux({}); + expect(initialState).toBeTypeOf('object'); + expect(initialState).toEqual({}); +}); +test('number', () => { + const { initialState } = new Updux({ initialState: 3 }); + expect(initialState).toBeTypeOf('number'); + expect(initialState).toEqual(3); +}); +test('initialState to createStore', () => { + const initialState = { + a: 1, + b: 2, + }; + const dux = new Updux({ + initialState, + }); + expect(dux.createStore({ preloadedState: { a: 3, b: 4 } }).getState()).toEqual({ + a: 3, + b: 4, + }); +}); +test('single dux', () => { + const foo = new Updux({ + initialState: { a: 1 }, + }); + expect(foo.initialState).toEqual({ a: 1 }); +}); +// TODO add 'check for no todo eslint rule' +test('initialState value', () => { + expect(foo.initialState).toEqual({ + root: 'abc', + bar: 123, + }); + expectType(foo.initialState); +}); +test('no initialState', () => { + const dux = new Updux({}); + expectType(dux.initialState); + expect(dux.initialState).toEqual({}); +}); +test('no initialState for subdux', () => { + const dux = new Updux({ + subduxes: { + bar: new Updux({}), + baz: new Updux({ initialState: 'potato' }), + }, + }); + expectType(dux.initialState); + expect(dux.initialState).toEqual({ bar: {}, baz: 'potato' }); +}); +test.todo('splat initialState', () => { + const bar = new Updux({ + initialState: { id: 0 }, + }); + const foo = new Updux({ + subduxes: { '*': bar }, + }); + expect(foo.initialState).toEqual([]); + expect(new Updux({ + initialState: 'overriden', + subduxes: { '*': bar }, + }).initialState).toEqual('overriden'); +}); diff --git a/dist/initialState.d.ts b/dist/initialState.d.ts new file mode 100644 index 0000000..4332bcd --- /dev/null +++ b/dist/initialState.d.ts @@ -0,0 +1 @@ +export declare function buildInitialState(localInitialState: any, subduxes: any): any; diff --git a/dist/initialState.js b/dist/initialState.js new file mode 100644 index 0000000..9785050 --- /dev/null +++ b/dist/initialState.js @@ -0,0 +1,12 @@ +import u from '@yanick/updeep-remeda'; +import { D } from '@mobily/ts-belt'; +export function buildInitialState(localInitialState, subduxes) { + let state = localInitialState !== null && localInitialState !== void 0 ? localInitialState : {}; + if (subduxes) { + if (typeof state !== 'object') { + throw new Error('root initial state is not an object'); + } + state = u(state, D.map(subduxes, D.prop('initialState'))); + } + return state; +} diff --git a/dist/initialState.test.d.ts b/dist/initialState.test.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/initialState.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/initialState.test.js b/dist/initialState.test.js new file mode 100644 index 0000000..fd7ad67 --- /dev/null +++ b/dist/initialState.test.js @@ -0,0 +1,86 @@ +import { buildInitialState } from './initialState.js'; +import Updux from './Updux.js'; +test('default', () => { + const dux = new Updux({}); + expect(dux.initialState).toBeTypeOf('object'); + expect(dux.initialState).toEqual({}); + expectTypeOf(dux.initialState).toEqualTypeOf(); +}); +test('number', () => { + const dux = new Updux({ initialState: 3 }); + expect(dux.initialState).toEqual(3); + expectTypeOf(dux.initialState).toEqualTypeOf(); + const f = { initialState: dux.initialState }; +}); +test('single dux', () => { + const foo = new Updux({ + initialState: { a: 1 }, + }); + expect(foo.initialState).toEqual({ a: 1 }); + expectTypeOf(foo.initialState).toEqualTypeOf(); +}); +test('no initialState for subdux', () => { + const subduxes = { + bar: new Updux({}), + baz: new Updux({ initialState: 'potato' }), + }; + const dux = new Updux({ + subduxes, + }); + expectTypeOf(dux.initialState).toEqualTypeOf(); + expect(dux.initialState).toEqual({ bar: {}, baz: 'potato' }); +}); +test('basic', () => { + expect(buildInitialState({ a: 1 }, { b: { initialState: { c: 2 } }, d: { initialState: 'e' } })).toEqual({ + a: 1, + b: { c: 2 }, + d: 'e', + }); +}); +test('throw if subduxes and initialState is not an object', () => { + expect(() => { + buildInitialState(3, { bar: 'foo' }); + }).toThrow(); +}); +const bar = new Updux({ initialState: 123 }); +const foo = new Updux({ + initialState: { root: 'abc' }, + subduxes: { + bar + }, +}); +test('initialState to createStore', () => { + const initialState = { + a: 1, + b: 2, + }; + const dux = new Updux({ + initialState, + }); + expect(dux.createStore({ preloadedState: { a: 3, b: 4 } }).getState()).toEqual({ + a: 3, + b: 4, + }); +}); +// TODO add 'check for no todo eslint rule' +test('initialState value', () => { + expect(foo.initialState).toEqual({ + root: 'abc', + bar: 123, + }); + expectTypeOf(foo.initialState.bar).toMatchTypeOf(); + expectTypeOf(foo.initialState).toMatchTypeOf(); +}); +test.todo('splat initialState', async () => { + const bar = new Updux({ + initialState: { id: 0 }, + }); + const foo = new Updux({ + subduxes: { '*': bar }, + }); + expect(foo.initialState).toEqual([]); + expect(new Updux({ + initialState: 'overriden', + subduxes: { '*': bar }, + }).initialState).toEqual('overriden'); +}); diff --git a/dist/mutations.test.d.ts b/dist/mutations.test.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/mutations.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/mutations.test.js b/dist/mutations.test.js new file mode 100644 index 0000000..bf77c1c --- /dev/null +++ b/dist/mutations.test.js @@ -0,0 +1,96 @@ +import { createAction } from '@reduxjs/toolkit'; +import { test, expect } from 'vitest'; +import { withPayload } from './actions.js'; +import Updux from './Updux.js'; +test('set a mutation', () => { + const dux = new Updux({ + initialState: 'potato', + actions: { + foo: (x) => ({ x }), + bar: null, + }, + }); + let didIt = false; + dux.addMutation(dux.actions.foo, (payload, action) => () => { + expectTypeOf(payload).toMatchTypeOf(); + 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({ + initialState: '', + }); + dux.addMutation(() => true, (payload, action) => () => 'got it'); + expect(dux.reducer(undefined, { type: 'foo' })).toEqual('got it'); +}); +test('default mutation', () => { + const dux = new Updux({ + initialState: '', + actions: { + foo: null, + }, + }); + dux.addMutation(dux.actions.foo, () => () => 'got it'); + dux.setDefaultMutation((_payload, action) => () => action.type); + expect(dux.reducer(undefined, { type: 'foo' })).toEqual('got it'); + expect(dux.reducer(undefined, { type: 'bar' })).toEqual('bar'); +}); +test('mutation of a subdux', () => { + const baz = createAction('baz'); + const noop = createAction('noop'); + const stopit = createAction('stopit'); + const bar = new Updux({ + initialState: 0, + actions: { + baz, + stopit, + }, + }); + bar.addMutation(baz, () => () => 1); + bar.addMutation(stopit, () => () => 2); + const foo = new Updux({ + subduxes: { bar }, + }); + foo.addMutation(stopit, () => (state) => state, true); + expect(foo.reducer(undefined, noop())).toHaveProperty('bar', 0); + expect(foo.reducer(undefined, baz())).toHaveProperty('bar', 1); + expect(foo.reducer(undefined, stopit())).toHaveProperty('bar', 0); +}); +test('actionType as string', () => { + const dux = new Updux({ + actions: { + doIt: (id) => id, + }, + }); + dux.addMutation('doIt', (payload) => (state) => { + expectTypeOf(payload).toMatchTypeOf(); + return state; + }); + expect(() => { + // @ts-ignore + dux.addMutation('unknown', () => (x) => x); + }).toThrow(); +}); +test('setDefaultMutation return value', () => { + const dux = new Updux({ + initialState: 13, + }); + let withDM = dux.setDefaultMutation(() => (state) => state); + expect(withDM).toEqual(dux); + expectTypeOf(withDM.initialState).toBeNumber(); +}); +test('addMutation with createAction', () => { + const setName = createAction('setName', withPayload()); + const dux = new Updux({ + initialState: { + name: '', + round: 1, + }, + }).addMutation(setName, (name) => (state) => (Object.assign(Object.assign({}, state), { name }))); +}); diff --git a/dist/new-types.js b/dist/new-types.js new file mode 100644 index 0000000..ea4f9d3 --- /dev/null +++ b/dist/new-types.js @@ -0,0 +1,2 @@ +(DuxState) = D['initialState']; +export {}; diff --git a/dist/reactions.d.ts b/dist/reactions.d.ts new file mode 100644 index 0000000..c934c1c --- /dev/null +++ b/dist/reactions.d.ts @@ -0,0 +1 @@ +export declare function buildReactions(reactions: any, subduxes: any): any[]; diff --git a/dist/reactions.js b/dist/reactions.js new file mode 100644 index 0000000..acfcf3f --- /dev/null +++ b/dist/reactions.js @@ -0,0 +1,6 @@ +export function buildReactions(reactions, subduxes) { + return [ + ...reactions, + ...Object.entries(subduxes !== null && subduxes !== void 0 ? subduxes : {}).flatMap(([slice, { reactions }]) => reactions.map((r) => (api, unsub) => r(Object.assign(Object.assign({}, api), { getState: () => api.getState()[slice] }), unsub))), + ]; +} diff --git a/dist/reactions.test.d.ts b/dist/reactions.test.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/reactions.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/reactions.test.js b/dist/reactions.test.js new file mode 100644 index 0000000..15f2fc6 --- /dev/null +++ b/dist/reactions.test.js @@ -0,0 +1,72 @@ +import { test, expect } from 'vitest'; +import Updux from './index.js'; +test('basic reactions', () => { + const foo = new Updux({ + initialState: 0, + actions: { inc: null, reset: null }, + }); + foo.addMutation(foo.actions.inc, () => (state) => state + 1); + foo.addMutation(foo.actions.reset, () => (state) => 0); + foo.addReaction((api) => (state, _previous, unsubscribe) => { + if (state < 3) + return; + unsubscribe(); + api.dispatch.reset(); + }); + // TODO + //reaction: (api) => (state,previous,unsubscribe) + const store = foo.createStore(); + store.dispatch.inc(); + expect(store.getState()).toEqual(1); + store.dispatch.inc(); + store.dispatch.inc(); + expect(store.getState()).toEqual(0); // we've been reset + store.dispatch.inc(); + store.dispatch.inc(); + store.dispatch.inc(); + store.dispatch.inc(); + expect(store.getState()).toEqual(4); // we've unsubscribed +}); +test('subdux reactions', () => { + const bar = new Updux({ + initialState: 0, + actions: { inc: null, reset: null }, + selectors: { + getIt: (x) => x, + }, + }); + bar.addMutation(bar.actions.inc, () => (state) => { + return state + 1; + }); + bar.addMutation(bar.actions.reset, () => (state) => 0); + let seen = 0; + bar.addReaction((api) => (state, _previous, unsubscribe) => { + seen++; + expect(api.actions).not.toHaveProperty('notInBar'); + expect(state).toBeTypeOf('number'); + if (state < 3) + return; + unsubscribe(); + api.dispatch.reset(); + }); + const foo = new Updux({ + actions: { notInBar: null }, + subduxes: { bar }, + }); + const store = foo.createStore(); + store.dispatch.inc(); + expect(seen).toEqual(1); + expect(store.getState()).toEqual({ bar: 1 }); + expect(store.getState.getIt()).toEqual(1); + store.dispatch.inc(); + expect(seen).toEqual(2); + store.dispatch.inc(); + expect(seen).toEqual(3); + expect(store.getState.getIt()).toEqual(0); // we've been reset + store.dispatch.inc(); + store.dispatch.inc(); + store.dispatch.inc(); + store.dispatch.inc(); + expect(seen).toEqual(3); + expect(store.getState.getIt()).toEqual(4); // we've unsubscribed +}); diff --git a/dist/reducer.d.ts b/dist/reducer.d.ts new file mode 100644 index 0000000..1cdda4e --- /dev/null +++ b/dist/reducer.d.ts @@ -0,0 +1,10 @@ +import * as rtk from '@reduxjs/toolkit'; +import { Mutation } from './types.js'; +import Updux from './Updux.js'; +import { AnyAction } from '@reduxjs/toolkit'; +export type MutationCase = { + matcher: (action: rtk.AnyAction) => boolean; + mutation: Mutation; + terminal: boolean; +}; +export declare function buildReducer(initialStateState: unknown, mutations?: MutationCase[], defaultMutation?: Omit, subduxes?: Record>, inheritedReducer?: (state: any, action: AnyAction) => any): (state: unknown, action: rtk.AnyAction) => unknown; diff --git a/dist/reducer.js b/dist/reducer.js new file mode 100644 index 0000000..c07555e --- /dev/null +++ b/dist/reducer.js @@ -0,0 +1,32 @@ +import * as R from 'remeda'; +import u from '@yanick/updeep-remeda'; +import { D } from '@mobily/ts-belt'; +export function buildReducer(initialStateState, mutations = [], defaultMutation, subduxes = {}, inheritedReducer) { + const subReducers = D.map(subduxes, D.getUnsafe('reducer')); + const reducer = (state = initialStateState, action) => { + if (!(action === null || action === void 0 ? void 0 : action.type)) + throw new Error('reducer called with a bad action'); + let active = mutations.filter(({ matcher }) => matcher(action)); + if (active.length === 0 && defaultMutation) + active.push(defaultMutation); + if (!active.some(R.prop('terminal')) && inheritedReducer) { + active.push({ + mutation: (_payload, action) => (state) => { + return u(state, inheritedReducer(state, action)); + }, + }); + } + if (!active.some(R.prop('terminal')) && + D.values(subReducers).length > 0) { + active.push({ + mutation: (payload, action) => (state) => { + return u(state, R.mapValues(subReducers, (reducer, slice) => (state) => { + return reducer(state, action); + })); + }, + }); + } + return active.reduce((state, { mutation }) => mutation(action.payload, action)(state), state); + }; + return reducer; +} diff --git a/dist/reducer.test.d.ts b/dist/reducer.test.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/reducer.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/reducer.test.js b/dist/reducer.test.js new file mode 100644 index 0000000..eafffa8 --- /dev/null +++ b/dist/reducer.test.js @@ -0,0 +1,79 @@ +import { createAction } from '@reduxjs/toolkit'; +import { test, expect } from 'vitest'; +import { withPayload } from './actions.js'; +import { buildReducer } from './reducer.js'; +import Updux from './Updux.js'; +test('buildReducer, initialState 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('basic reducer', () => { + const add = createAction('add', withPayload()); + const dux = new Updux({ + initialState: 12, + }).addMutation(add, (incr, action) => (state) => { + expectTypeOf(incr).toMatchTypeOf(); + expectTypeOf(state).toMatchTypeOf(); + expectTypeOf(action).toMatchTypeOf(); + return state + incr; + }); + expect(dux.reducer).toBeTypeOf('function'); + expect(dux.reducer(1, { type: 'noop' })).toEqual(1); // noop + expect(dux.reducer(1, dux.actions.add(2))).toEqual(3); +}); +test('defaultMutation', () => { + const dux = new Updux({ + initialState: { a: 0, b: 0 }, + actions: { + add: (x) => x, + }, + }); + dux.addMutation(dux.actions.add, (incr) => (state) => (Object.assign(Object.assign({}, state), { a: state.a + incr }))); + dux.setDefaultMutation((payload) => (state) => (Object.assign(Object.assign({}, state), { b: state.b + 1 }))); + expect(dux.reducer({ a: 0, b: 0 }, { type: 'noop' })).toMatchObject({ + a: 0, + b: 1, + }); // noop catches the default mutation + expect(dux.reducer({ a: 1, b: 0 }, dux.actions.add(2))).toMatchObject({ + a: 3, + b: 0, + }); +}); +test('subduxes mutations', () => { + const sub1 = new Updux({ + initialState: 0, + actions: { + sub1action: null, + }, + }); + sub1.addMutation(sub1.actions.sub1action, () => (state) => state + 1); + const dux = new Updux({ + subduxes: { + sub1, + }, + }); + expect(dux.reducer(undefined, dux.actions.sub1action())).toMatchObject({ + sub1: 1, + }); +}); +test('upreducer', () => { + const dux = new Updux({ + initialState: 0, + actions: { + incr: (x) => x, + }, + }).addMutation('incr', (i) => state => state + i); + expect(dux.reducer(undefined, dux.actions.incr(5))).toEqual(5); + expect(dux.upreducer(dux.actions.incr(7))()).toEqual(7); +}); diff --git a/dist/schema.d.ts b/dist/schema.d.ts new file mode 100644 index 0000000..580f2e3 --- /dev/null +++ b/dist/schema.d.ts @@ -0,0 +1 @@ +export default function buildSchema(schema?: any, initialState?: any, subduxes?: {}): any; diff --git a/dist/schema.js b/dist/schema.js new file mode 100644 index 0000000..13e0a62 --- /dev/null +++ b/dist/schema.js @@ -0,0 +1,27 @@ +import u from '@yanick/updeep-remeda'; +import * as R from 'remeda'; +export default function buildSchema(schema = {}, initialState = undefined, subduxes = {}) { + if (typeof initialState !== 'undefined') + schema = u(schema, { default: u.constant(initialState) }); + if (!schema.type) { + schema = u(schema, { + type: Array.isArray(schema.default) + ? 'array' + : typeof schema.default, + }); + } + if (schema.type === 'object') { + // TODO will break with objects and arrays + schema = u(schema, { + properties: R.mapValues(schema.default, (v) => ({ + type: typeof v, + })), + }); + } + if (Object.keys(subduxes).length > 0) { + schema = u(schema, { + properties: R.mapValues(subduxes, R.prop('schema')), + }); + } + return schema; +} diff --git a/dist/schema.test.d.ts b/dist/schema.test.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/schema.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/schema.test.js b/dist/schema.test.js new file mode 100644 index 0000000..927fa2b --- /dev/null +++ b/dist/schema.test.js @@ -0,0 +1,34 @@ +import { test, expect } from 'vitest'; +import Updux from './Updux.js'; +test('default schema', () => { + const dux = new Updux({}); + expect(dux.schema).toMatchObject({ + type: 'object', + }); +}); +test('basic schema', () => { + const dux = new Updux({ + schema: { type: 'number' }, + }); + expect(dux.schema).toMatchObject({ + type: 'number', + }); +}); +test('schema default inherits from initial state', () => { + const dux = new Updux({ + schema: { type: 'number' }, + initialState: 8, + }); + expect(dux.schema.default).toEqual(8); +}); +test('validate', () => { + const dux = new Updux({ + initialState: 12, + actions: { + doItWrong: null, + }, + }); + dux.addMutation('doItWrong', () => () => 'potato'); + const store = dux.createStore({ validate: true }); + expect(() => store.dispatch.doItWrong()).toThrow(); +}); diff --git a/dist/schemas.js b/dist/schemas.js new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/schemas.js @@ -0,0 +1 @@ +export {}; diff --git a/dist/schemas.test.js b/dist/schemas.test.js new file mode 100644 index 0000000..59feb67 --- /dev/null +++ b/dist/schemas.test.js @@ -0,0 +1,15 @@ +import { test } from 'vitest'; +import { expectTypeOf } from 'expect-type'; +test('from simple schema', () => { + const x = { + schema: { + type: 'object', + required: ['x'], + properties: { + x: { type: 'number' }, + }, + }, + }; + let y; + expectTypeOf(y).toMatchTypeOf(); +}); diff --git a/dist/selectors.d.ts b/dist/selectors.d.ts new file mode 100644 index 0000000..6a8628b --- /dev/null +++ b/dist/selectors.d.ts @@ -0,0 +1 @@ +export declare function buildSelectors(localSelectors?: {}, subduxes?: {}): any; diff --git a/dist/selectors.js b/dist/selectors.js new file mode 100644 index 0000000..2b76a7c --- /dev/null +++ b/dist/selectors.js @@ -0,0 +1,5 @@ +import { D } from '@mobily/ts-belt'; +export function buildSelectors(localSelectors = {}, subduxes = {}) { + const subSelectors = Object.entries(subduxes).map(([slice, { selectors }]) => D.map(selectors, (subSelect) => (state) => subSelect(state[slice]))); + return [localSelectors, ...subSelectors].reduce(D.merge); +} diff --git a/dist/selectors.test.d.ts b/dist/selectors.test.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/selectors.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/selectors.test.js b/dist/selectors.test.js new file mode 100644 index 0000000..a649346 --- /dev/null +++ b/dist/selectors.test.js @@ -0,0 +1,47 @@ +import { test, expect } from 'vitest'; +import Updux from './index.js'; +describe('basic selectors', () => { + const config = { + initialState: { + x: 1, + }, + selectors: { + getX: ({ x }) => x, + }, + subduxes: { + bar: new Updux({ + initialState: { y: 2 }, + selectors: { + getY: ({ y }) => y, + getYPlus: ({ y }) => (incr) => (y + incr), + }, + }), + // cause the type to fail + baz: new Updux({ + selectors: { + getFromBaz: () => 'potato', + }, + }), + }, + }; + const foo = new Updux(config); + const sample = { + x: 4, + bar: { y: 3 }, + }; + test('updux selectors', () => { + expect(foo.selectors.getX(sample)).toBe(4); + expect(foo.selectors.getY(sample)).toBe(3); + expect(foo.selectors.getYPlus(sample)(3)).toBe(6); + }); + test('store selectors', () => { + const store = foo.createStore(); + expect(store.getState.getY()).toBe(2); + expect(store.getState.getYPlus(3)).toBe(5); + }); + test('addSelector', () => { + const dux = new Updux({ initialState: 13 }).addSelector('plusHundred', (state) => state + 100); + expectTypeOf(dux.selectors.plusHundred).toMatchTypeOf(); + expect(dux.selectors.plusHundred(7)).toBe(107); + }); +}); diff --git a/dist/subduxes.test.d.ts b/dist/subduxes.test.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/subduxes.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/subduxes.test.js b/dist/subduxes.test.js new file mode 100644 index 0000000..2f1caac --- /dev/null +++ b/dist/subduxes.test.js @@ -0,0 +1,21 @@ +import { test, expect } from 'vitest'; +import { expectTypeOf } from 'expect-type'; +import Updux from './Updux.js'; +const subA = new Updux({ + initialState: true, + actions: { + action1: (x) => x, + }, + selectors: { + getAntiSub: (s) => !s, + }, +}); +const mainDux = new Updux({ + subduxes: { subA }, +}); +test('subduxes resolves as objects', () => { + expectTypeOf(mainDux.initialState).toMatchTypeOf(); + expectTypeOf(mainDux.actions.action1('foo')).toMatchTypeOf(); + expectTypeOf(mainDux.selectors.getAntiSub({ subA: true })).toMatchTypeOf(); + expect(mainDux.selectors.getAntiSub({ subA: true })).toEqual(false); +}); diff --git a/dist/tutorial.test.d.ts b/dist/tutorial.test.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/tutorial.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/tutorial.test.js b/dist/tutorial.test.js new file mode 100644 index 0000000..e47e619 --- /dev/null +++ b/dist/tutorial.test.js @@ -0,0 +1,39 @@ +import Updux, { createAction, withPayload } from './index.js'; +import { expectTypeOf } from 'expect-type'; +test('initialState state', () => { + const initialState = { + next_id: 1, + todos: [], + }; + const dux = new Updux({ + initialState, + }); + expectTypeOf(dux.initialState).toMatchTypeOf(); + expect(dux.initialState).toEqual(initialState); + const store = dux.createStore(); + expect(store.getState()).toEqual(initialState); +}); +test('actions', () => { + const addTodo = createAction('addTodo', withPayload()); + const todoDone = createAction('todoDone'); + const todosDux = new Updux({ + actions: { + addTodo, + todoDone, + }, + }); + expect(todosDux.actions.addTodo('write tutorial')).toEqual({ + type: 'addTodo', + payload: 'write tutorial', + }); +}); +test('mutation', () => { + const addTodo = createAction('addTodo', withPayload()); + const dux = new Updux({ + initialState: { nextId: 0, todos: [] }, + }); + dux.addMutation(addTodo, (description) => (state) => { + state.todos.unshift({ description, id: state.nextId, done: false }); + state.nextId++; + }); +}); diff --git a/dist/tutorial/actions.test.d.ts b/dist/tutorial/actions.test.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/tutorial/actions.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/tutorial/actions.test.js b/dist/tutorial/actions.test.js new file mode 100644 index 0000000..c026804 --- /dev/null +++ b/dist/tutorial/actions.test.js @@ -0,0 +1,70 @@ +import { test, expect } from 'vitest'; +/// --8<-- [start:actions1] +import Updux, { createAction, withPayload } from 'updux'; +const addTodo = createAction('addTodo', withPayload()); +const todoDone = createAction('todoDone', withPayload()); +/// --8<-- [end:actions1] +test('createAction', () => { + expect(addTodo).toBeTypeOf('function'); + expect(todoDone).toBeTypeOf('function'); +}); +const todosDux = new Updux({ + initialState: { + nextId: 1, + todos: [], + }, + actions: { + addTodo, + todoDone, + }, +}); +/// --8<-- [start:actions2] +todosDux.actions.addTodo('write tutorial'); +// { type: 'addTodo', payload: 'write tutorial' } +/// --8<-- [end:actions2] +test('basic', () => { + expect(todosDux.actions.addTodo('write tutorial')).toEqual({ + type: 'addTodo', + payload: 'write tutorial', + }); +}); +/// --8<-- [start:addMutation-1] +todosDux.addMutation(addTodo, (description) => ({ todos, nextId }) => ({ + nextId: 1 + nextId, + todos: todos.concat({ description, id: nextId, done: false }), +})); +todosDux.addMutation(todoDone, (id) => ({ todos, nextId }) => ({ + nextId: 1 + nextId, + todos: todos.map((todo) => { + if (todo.id !== id) + return todo; + return Object.assign(Object.assign({}, todo), { done: true }); + }), +})); +/// --8<-- [end:addMutation-1] +const store = todosDux.createStore(); +store.dispatch.addTodo('write tutorial'); +const state = store.getState(); +// { +// nextId: 2, +// todos: [ +// { +// description: 'write tutorial', +// done: false, +// id: 1, +// } +// ] +// } +/// --8<-- [end:addMutation] +test('addMutation', () => { + expect(state).toEqual({ + nextId: 2, + todos: [ + { + description: 'write tutorial', + done: false, + id: 1, + }, + ], + }); +}); diff --git a/dist/tutorial/effects.test.d.ts b/dist/tutorial/effects.test.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/tutorial/effects.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/tutorial/effects.test.js b/dist/tutorial/effects.test.js new file mode 100644 index 0000000..32d1935 --- /dev/null +++ b/dist/tutorial/effects.test.js @@ -0,0 +1,34 @@ +import { test, expect } from 'vitest'; +/// --8<-- [start:effects-1] +import u from '@yanick/updeep-remeda'; +import * as R from 'remeda'; +import Updux, { createAction, withPayload } from 'updux'; +const addTodoWithId = createAction('addTodoWithId', withPayload()); +const incNextId = createAction('incNextId'); +const addTodo = createAction('addTodo', withPayload()); +const todosDux = new Updux({ + initialState: { nextId: 1, todos: [] }, + actions: { addTodo, incNextId, addTodoWithId }, + selectors: { + nextId: ({ nextId }) => nextId, + }, +}); +todosDux.addMutation(addTodoWithId, (todo) => u({ + todos: R.concat([u(todo, { done: false })]), +})); +todosDux.addMutation(incNextId, () => u({ nextId: (id) => id + 1 })); +todosDux.addEffect('addTodo', ({ getState, dispatch }) => (next) => (action) => { + const id = getState.nextId(); + dispatch.incNextId(); + next(action); + dispatch.addTodoWithId({ id, description: action.payload }); +}); +const store = todosDux.createStore(); +store.dispatch.addTodo('write tutorial'); +/// --8<-- [end:effects-1] +test('basic', () => { + expect(store.getState()).toMatchObject({ + nextId: 2, + todos: [{ id: 1, description: 'write tutorial', done: false }], + }); +}); diff --git a/dist/tutorial/final.test.d.ts b/dist/tutorial/final.test.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/tutorial/final.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/tutorial/final.test.js b/dist/tutorial/final.test.js new file mode 100644 index 0000000..7de2916 --- /dev/null +++ b/dist/tutorial/final.test.js @@ -0,0 +1,30 @@ +import { test, expect } from 'vitest'; +import todoListDux from './todoList.js'; +test('basic', () => { + const store = todoListDux.createStore(); + store.dispatch.addTodo('write tutorial'); + store.dispatch.addTodo('test code snippets'); + store.dispatch.todoDone(2); + const s = store.getState(); + expectTypeOf(s).toMatchTypeOf(); + expect(store.getState()).toMatchObject({ + todos: [ + { id: 1, done: false }, + { id: 2, done: true }, + ], + }); + // expect(todoListDux.schema).toMatchObject({ + // type: 'object', + // properties: { + // nextId: { type: 'number', default: 1 }, + // todos: { + // default: [], + // type: 'array', + // } + // }, + // default: { + // nextId: 1, + // todos: [], + // }, + // }); +}); diff --git a/dist/tutorial/initialState.test.d.ts b/dist/tutorial/initialState.test.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/tutorial/initialState.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/tutorial/initialState.test.js b/dist/tutorial/initialState.test.js new file mode 100644 index 0000000..946ca9e --- /dev/null +++ b/dist/tutorial/initialState.test.js @@ -0,0 +1,22 @@ +import { test, expect, expectTypeOf } from 'vitest'; +/// --8<-- [start:tut1] +import Updux from 'updux'; +const todosDux = new Updux({ + initialState: { + nextId: 1, + todos: [], + }, +}); +/// ---8<-- [end:tut1] +test('basic', () => { + /// ---8<-- [start:tut2] + const store = todosDux.createStore(); + const expected = { + nextId: 1, + todos: [], + }; + expect(todosDux.initialState).toEqual(expected); + expect(store.getState()).toEqual(expected); + expectTypeOf(todosDux.initialState).toMatchTypeOf(); + /// ---8<-- [end:tut2] +}); diff --git a/dist/tutorial/monolith.test.d.ts b/dist/tutorial/monolith.test.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/tutorial/monolith.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/tutorial/monolith.test.js b/dist/tutorial/monolith.test.js new file mode 100644 index 0000000..09068b7 --- /dev/null +++ b/dist/tutorial/monolith.test.js @@ -0,0 +1,54 @@ +import { test, expect } from 'vitest'; +/// --8<-- [start:mono] +import Updux from '../index.js'; +import u from '@yanick/updeep-remeda'; +const todosDux = new Updux({ + initialState: { + nextId: 1, + todos: [], + }, + actions: { + addTodo: (description) => description, + addTodoWithId: (description, id) => ({ + description, + id, + done: false, + }), + todoDone: (id) => id, + incNextId: () => { }, + }, + selectors: { + getTodoById: ({ todos }) => (id) => todos.find(u.matches({ id })), + getNextId: ({ nextId }) => nextId, + }, +}) + .addMutation('addTodoWithId', (todo) => u.updateIn('todos', (todos) => [...todos, todo])) + .addMutation('incNextId', () => u({ nextId: (x) => x + 1 })) + .addMutation('todoDone', (id) => u.updateIn('todos', u.map(u.if(u.matches({ id }), { done: true })))) + .addEffect('addTodo', ({ getState, dispatch }) => (next) => (action) => { + const id = getState.getNextId(); + dispatch.incNextId(); + next(action); + dispatch.addTodoWithId(action.payload, id); +}); +/// --8<-- [end:mono] +test('basic', () => { + const store = todosDux.createStore(); + store.dispatch.addTodo('write tutorial'); + store.dispatch.addTodo('have fun'); + expect(store.getState()).toMatchObject({ + nextId: 3, + todos: [ + { id: 1, description: 'write tutorial', done: false }, + { id: 2, description: 'have fun', done: false }, + ], + }); + store.dispatch.todoDone(1); + expect(store.getState()).toMatchObject({ + nextId: 3, + todos: [ + { id: 1, description: 'write tutorial', done: true }, + { id: 2, description: 'have fun', done: false }, + ], + }); +}); diff --git a/dist/tutorial/nextId.d.ts b/dist/tutorial/nextId.d.ts new file mode 100644 index 0000000..fee6f2c --- /dev/null +++ b/dist/tutorial/nextId.d.ts @@ -0,0 +1,11 @@ +import Updux from '../index.js'; +declare const _default: Updux<{ + initialState: number; + actions: { + incNextId: any; + }; + selectors: { + getNextId: (state: any) => any; + }; +}>; +export default _default; diff --git a/dist/tutorial/nextId.js b/dist/tutorial/nextId.js new file mode 100644 index 0000000..3a919dc --- /dev/null +++ b/dist/tutorial/nextId.js @@ -0,0 +1,13 @@ +/// --8<-- [start:dux] +import Updux from '../index.js'; +export default new Updux({ + initialState: 1, + actions: { + incNextId: null, + }, + selectors: { + getNextId: (state) => state, + }, +}) + .addMutation('incNextId', () => (id) => id + 1); +/// --8<-- [end:dux] diff --git a/dist/tutorial/recipes.test.d.ts b/dist/tutorial/recipes.test.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/tutorial/recipes.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/tutorial/recipes.test.js b/dist/tutorial/recipes.test.js new file mode 100644 index 0000000..3d9c998 --- /dev/null +++ b/dist/tutorial/recipes.test.js @@ -0,0 +1,33 @@ +import { test, expect } from 'vitest'; +import u from '@yanick/updeep-remeda'; +import * as R from 'remeda'; +import Updux from '../index.js'; +const done = (text) => text; +const todo = new Updux({ + initial: { text: '', done: false }, + actions: { done, doneAll: null }, // doneAll is a synonym for done for this dux +}); +todo.addMutation('done', () => u({ done: true })); +todo.addMutation('doneAll', () => u({ done: true })); +const todos = new Updux({ + initialState: [], + actions: Object.assign({ addTodo: null }, todo.actions), +}) + .addMutation('addTodo', (text) => R.concat([{ text }])) + .addMutation('done', (text, action) => u.mapIf({ text }, todo.upreducer(action))); +todos.setDefaultMutation((_payload, action) => R.map(todo.upreducer(action))); +test('tutorial', async () => { + const store = todos.createStore(); + store.dispatch.addTodo('one'); + store.dispatch.addTodo('two'); + store.dispatch.addTodo('three'); + store.dispatch.done('two'); + expect(store.getState()[1].done).toBeTruthy(); + expect(store.getState()[2].done).toBeFalsy(); + store.dispatch.doneAll(); + expect(store.getState().map(({ done }) => done)).toEqual([ + true, + true, + true, + ]); +}); diff --git a/dist/tutorial/selectors.test.d.ts b/dist/tutorial/selectors.test.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/tutorial/selectors.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/tutorial/selectors.test.js b/dist/tutorial/selectors.test.js new file mode 100644 index 0000000..2c41adc --- /dev/null +++ b/dist/tutorial/selectors.test.js @@ -0,0 +1,33 @@ +import { test, expect } from 'vitest'; +/// --8<-- [start:sel1] +import Updux from 'updux'; +const dux = new Updux({ + initialState: [], + selectors: { + getDone: (state) => state.filter(({ done }) => done), + getById: (state) => (id) => state.find((todo) => todo.id === id), + }, +}); +const state = [ + { id: 1, done: true }, + { id: 2, done: false }, +]; +dux.selectors.getDone(state); // = [ { id: 1, done: true } ] +dux.selectors.getById(state)(2); // = { id: 2, done: false } +const store = dux.createStore({ preloadedState: state }); +store.selectors.getDone(state); // = [ { id: 1, done: true } ] +store.selectors.getById(state)(2); // = { id: 2, done: false } +store.getState.getDone(); // = [ { id: 1, done: true } ] +store.getState.getById(2); // = { id: 2, done: false } +/// --8<-- [end:sel1] +test('selectors', () => { + expect(dux.selectors.getDone(state)).toMatchObject([{ id: 1, done: true }]); + expect(dux.selectors.getById(state)(2)).toMatchObject({ id: 2 }); + expect(store.selectors.getDone(state)).toMatchObject([ + { id: 1, done: true }, + ]); + expect(store.selectors.getById(state)(2)).toMatchObject({ id: 2 }); + expect(store.getState()).toMatchObject([{ id: 1 }, { id: 2 }]); + expect(store.getState.getDone()).toMatchObject([{ id: 1, done: true }]); + expect(store.getState.getById(2)).toMatchObject({ id: 2 }); +}); diff --git a/dist/tutorial/todo.d.ts b/dist/tutorial/todo.d.ts new file mode 100644 index 0000000..ef19c4f --- /dev/null +++ b/dist/tutorial/todo.d.ts @@ -0,0 +1,17 @@ +import Updux from '../index.js'; +declare const _default: Updux<{ + initialState: { + id: number; + description: string; + done: boolean; + }; + actions: { + todoDone: any; + }; + selectors: { + desc: ({ description }: { + description: any; + }) => any; + }; +}>; +export default _default; diff --git a/dist/tutorial/todo.js b/dist/tutorial/todo.js new file mode 100644 index 0000000..3873058 --- /dev/null +++ b/dist/tutorial/todo.js @@ -0,0 +1,16 @@ +import Updux from '../index.js'; +import u from '@yanick/updeep-remeda'; +export default new Updux({ + initialState: { + id: 0, + description: '', + done: false, + }, + actions: { + todoDone: null, + }, + selectors: { + desc: ({ description }) => description, + }, +}) + .addMutation('todoDone', () => u({ done: true })); diff --git a/dist/tutorial/todoList.d.ts b/dist/tutorial/todoList.d.ts new file mode 100644 index 0000000..bed7848 --- /dev/null +++ b/dist/tutorial/todoList.d.ts @@ -0,0 +1,35 @@ +import Updux from '../index.js'; +declare const _default: Updux<{ + subduxes: { + todos: Updux<{ + initialState: { + id: number; + description: string; + done: boolean; + }[]; + actions: { + addTodoWithId: (description: any, id: any) => { + description: any; + id: any; + }; + todoDone: (id: number) => number; + }; + findSelectors: { + getTodoById: (state: any) => (id: any) => any; + }; + }>; + nextId: Updux<{ + initialState: number; + actions: { + incNextId: any; + }; + selectors: { + getNextId: (state: any) => any; + }; + }>; + }; + actions: { + addTodo: (description: string) => string; + }; +}>; +export default _default; diff --git a/dist/tutorial/todoList.js b/dist/tutorial/todoList.js new file mode 100644 index 0000000..5467873 --- /dev/null +++ b/dist/tutorial/todoList.js @@ -0,0 +1,31 @@ +import Updux from '../index.js'; +import nextIdDux from './nextId.js'; +import todosDux from './todos.js'; +export default new Updux({ + subduxes: { + todos: todosDux, + nextId: nextIdDux, + }, + actions: { + addTodo: (description) => description, + }, +}).addEffect('addTodo', ({ getState, dispatch }) => (next) => (action) => { + const id = getState.getNextId(); + dispatch.incNextId(); + next(action); + dispatch.addTodoWithId(action.payload, id); +}); +const x = new Updux({ + subduxes: { + todos: todosDux, + nextId: nextIdDux, + }, + actions: { + addTodo: (description) => description, + }, +}).addEffect('addTodo', ({ getState, dispatch }) => (next) => (action) => { + const id = getState.getNextId(); + dispatch.incNextId(); + next(action); + dispatch.addTodoWithId(action.payload, id); +}); diff --git a/dist/tutorial/todos.d.ts b/dist/tutorial/todos.d.ts new file mode 100644 index 0000000..b347b9e --- /dev/null +++ b/dist/tutorial/todos.d.ts @@ -0,0 +1,19 @@ +import Updux from '../index.js'; +declare const _default: Updux<{ + initialState: { + id: number; + description: string; + done: boolean; + }[]; + actions: { + addTodoWithId: (description: any, id: any) => { + description: any; + id: any; + }; + todoDone: (id: number) => number; + }; + findSelectors: { + getTodoById: (state: any) => (id: any) => any; + }; +}>; +export default _default; diff --git a/dist/tutorial/todos.js b/dist/tutorial/todos.js new file mode 100644 index 0000000..a40a442 --- /dev/null +++ b/dist/tutorial/todos.js @@ -0,0 +1,27 @@ +import Updux from '../index.js'; +import u from '@yanick/updeep-remeda'; +import todoDux from './todo.js'; +export default new Updux({ + initialState: [], + actions: { + addTodoWithId: (description, id) => ({ description, id }), + todoDone: (id) => id, + }, + findSelectors: { + getTodoById: (state) => (id) => state.find(u.matches({ id })), + }, +}) + .addMutation('addTodoWithId', (todo) => (todos) => todos.concat(Object.assign(Object.assign({}, todo), { done: false }))) + .addMutation('todoDone', (id, action) => u.map(u.if(u.matches({ id }), todoDux.upreducer(action)))); +const x = new Updux({ + initialState: [], + actions: { + addTodoWithId: (description, id) => ({ description, id }), + todoDone: (id) => id, + }, + findSelectors: { + getTodoById: (state) => (id) => state.find(u.matches({ id })), + }, +}) + .addMutation('addTodoWithId', (todo) => (todos) => todos.concat(Object.assign(Object.assign({}, todo), { done: false }))) + .addMutation('todoDone', (id, action) => u.map(u.if(u.matches({ id }), todoDux.upreducer(action)))); diff --git a/dist/types.d.ts b/dist/types.d.ts new file mode 100644 index 0000000..4600a87 --- /dev/null +++ b/dist/types.d.ts @@ -0,0 +1,83 @@ +import { ActionCreator, ActionCreatorWithPreparedPayload, AnyAction, Dispatch, MiddlewareAPI } from '@reduxjs/toolkit'; +import { EffectMiddleware } from './effects.js'; +import Updux from './Updux.js'; +export type DuxConfig = Partial<{ + initialState: any; + subduxes: Record; + actions: Record | Function | null>; + selectors: Record; + effects: EffectMiddleware[]; + reactions: DuxReaction[]; + reducer: (state: any, action: AnyAction) => any; +}>; +type UpduxConfig = D extends Updux ? T : D; +export type SubduxesState = D extends { + subduxes: infer S; +} ? { + [key in keyof S]: DuxState>; +} : unknown; +type ForceResolveObject = O extends { + [key: string]: any; +} ? { + [key in keyof O]: O[key]; +} : O; +export type DuxState = ForceResolveObject<(D extends { + initialState: any; +} ? D['initialState'] : {}) & (D extends { + subduxes: any; +} ? SubduxesState : unknown)>; +type SubduxesActions = D extends { + subduxes: infer S; +} ? UnionToIntersection>> : unknown; +type IfAny = 0 extends 1 & T ? Y : N; +type IsAny = IfAny; +type ResolveAction = true extends IsAny ? ActionCreator : A extends Function & { + type: string; +} ? A : A extends (...args: infer PARAMS) => infer R ? ActionCreatorWithPreparedPayload : ActionCreator; +type ResolveActions = A extends { + [key: string]: any; +} ? { + [key in keyof A]: key extends string ? ResolveAction : never; +} : A; +export type DuxActions = ResolveActions<(D extends { + actions: any; +} ? D['actions'] : {}) & (D extends { + subduxes: any; +} ? SubduxesActions : unknown)>; +export type Subduxes = Record | DuxConfig>; +export type MutationEntry = { + terminal: boolean; + matcher?: (action: AnyAction) => boolean; + mutation: Mutation; +}; +export type Mutation = (payload: A extends { + payload: infer P; +} ? P : undefined, action: A) => (state: S) => S | void; +type CurriedSelectors = { + [key in keyof S]: CurriedSelector; +}; +type XSel = R extends Function ? R : () => R; +type CurriedSelector = S extends (...args: any) => infer R ? XSel : never; +export type AugmentedMiddlewareAPI = MiddlewareAPI, DuxState> & { + dispatch: DuxActions; + getState: CurriedSelectors>; + actions: DuxActions; + selectors: DuxSelectors; +}; +export type DuxSelectors = ForceResolveObject<(D extends { + selectors: infer S; +} ? S : {}) & (D extends { + subduxes: infer SUB; +} ? UnionToIntersection; +}>> : {})>; +export type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; +type RebaseSelectors = DUX extends { + selectors: infer S; +} ? { + [key in keyof S]: RebaseSelector; +} : never; +type RebaseSelector = SLICE extends string ? S extends (state: infer STATE) => infer R ? (state: Record) => R : never : never; +type Values = X[keyof X]; +export type DuxReaction = (api: AugmentedMiddlewareAPI) => (state: DuxState, previousState: DuxState | undefined, unsubscribe: () => void) => void; +export {}; diff --git a/dist/types.js b/dist/types.js new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/types.js @@ -0,0 +1 @@ +export {}; diff --git a/dist/types.test.d.ts b/dist/types.test.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/types.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/types.test.js b/dist/types.test.js new file mode 100644 index 0000000..f9fc548 --- /dev/null +++ b/dist/types.test.js @@ -0,0 +1,8 @@ +import { test } from 'vitest'; +import { expectTypeOf } from 'expect-type'; +test('duxstate basic', () => { + const x = { + initialState: 'potato', + }; + expectTypeOf(true).toEqualTypeOf(); +});