UNPKG

clean-redux

Version:

Utilities for implementing clean architecture using Redux

199 lines (185 loc) 8.01 kB
import type { NonPostableEvt } from "evt"; import type { Middleware, ActionCreator, ActionCreatorWithPayload, ActionCreatorWithoutPayload, } from "@reduxjs/toolkit"; import { Evt } from "evt"; import { exclude } from "tsafe/exclude"; import { typeGuard } from "tsafe/typeGuard"; import { symToStr } from "tsafe/symToStr"; /* type RecordToUnion<O> = O[keyof O]; type CaseReducerToEvent<T extends Record<string, ActionCreator<any>>> = RecordToUnion<{ [K in keyof T]: { actionName: K } & (T[K] extends ActionCreatorWithoutPayload<any> ? {} : { payload: T[K] extends ActionCreatorWithPayload<infer U> ? U : never; }); }>; type UsecaseToEvent< Usecase extends { name: string } & ( | { actions: Record<string, ActionCreator<any>> } | { reducer: null } ), > = RecordToUnion<{ [Key in Extract<Usecase, { actions: unknown }>["name"]]: Usecase extends Extract< Usecase, { name: Key; actions: unknown } > ? { sliceName: Key } & CaseReducerToEvent<Usecase["actions"]> : never; }>; */ export function createMiddlewareEvtActionFactory< Usecase extends { name: string } & ( | { actions: Record<string, ActionCreator<any>> } | { reducer: null } ), >(usecases: readonly Usecase[]) { function createMiddlewareEvtAction(): { /** NOTE: We use an expended version of the type so that, * when the cursor hovers evtAction we get an explicit type instead of * a composition of union: * This: https://user-images.githubusercontent.com/6702424/147380322-bf2fa468-0a1c-4961-a7d8-16eaa14b6c4e.png * Instead of this: https://user-images.githubusercontent.com/6702424/147380353-9956a98a-9f9c-4811-a8b4-16cd1e4e76ca.png * Don't try, however, to work edit the expanded version. Start from the factorized * form by uncommenting the following line and the helper types it leverages. */ //evtAction: NonPostableEvt<UsecaseToEvent<Usecase>>; evtAction: NonPostableEvt< { [Key in Extract<Usecase, { actions: unknown }>["name"]]: Usecase extends Extract< Usecase, { name: Key; actions: unknown } > ? { [K in keyof Usecase["actions"]]: Usecase["actions"][K] extends ActionCreatorWithoutPayload<any> ? { sliceName: Key; actionName: K; } : { sliceName: Key; actionName: K; payload: Usecase["actions"][K] extends ActionCreatorWithPayload< infer U > ? U : never; }; }[keyof { [K in keyof Usecase["actions"]]: Usecase["actions"][K] extends ActionCreatorWithoutPayload<any> ? { sliceName: Key; actionName: K; } : { sliceName: Key; actionName: K; payload: Usecase["actions"][K] extends ActionCreatorWithPayload< infer U > ? U : never; }; }] : never; }[keyof { [Key in Extract<Usecase, { actions: unknown }>["name"]]: Usecase extends Extract< Usecase, { name: Key; actions: unknown } > ? { [K in keyof Usecase["actions"]]: Usecase["actions"][K] extends ActionCreatorWithoutPayload<any> ? { sliceName: Key; actionName: K; } : { sliceName: Key; actionName: K; payload: Usecase["actions"][K] extends ActionCreatorWithPayload< infer U > ? U : never; }; }[keyof { [K in keyof Usecase["actions"]]: Usecase["actions"][K] extends ActionCreatorWithoutPayload<any> ? { sliceName: Key; actionName: K; } : { sliceName: Key; actionName: K; payload: Usecase["actions"][K] extends ActionCreatorWithPayload< infer U > ? U : never; }; }] : never; }] >; middlewareEvtAction: Middleware; } { const actionTypes = new Set( usecases .map(usecase => typeGuard<Extract<typeof usecase, { actions: unknown }>>( usecase, "reducer" in usecase && usecase.reducer !== null, ) ? usecase : undefined, ) .filter(exclude(undefined)) .map(({ name, actions }) => Object.keys(actions).map(actionName => `${name}/${actionName}`), ) .reduce((prev, curr) => [...prev, ...curr], []), ); const evtAction = Evt.create<{ sliceName: string; actionName: string; lifecycleStage?: string; payload: any; }>(); const middlewareEvtAction: Middleware = () => next => (action: { type: string; payload: any }) => { if ( !actionTypes.has(action.type) && !["pending", "rejected", "fulfilled"].find(lifecycleStage => action.type.endsWith(`/${lifecycleStage}`), ) ) { console.warn( [ `Unknown action type ${action.type}.`, `${symToStr({ middlewareEvtAction })} is misconfigured`, ].join(" "), ); return next(action); } const [sliceName, actionName, ...lifecycleStage] = action.type.split("/"); const out = next(action); evtAction.post({ sliceName, actionName, ...(lifecycleStage.length === 0 ? {} : { "lifecycleStage": lifecycleStage.join("/"), }), "payload": action.payload, }); return out; }; return { "evtAction": evtAction as any, middlewareEvtAction }; } return { createMiddlewareEvtAction }; }