/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-empty-object-type */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Config, Effect, Layer, type NonEmptyReadonlyArray, Predicate, S, type Scope } from "effect-app" import { getMeta } from "effect-app/client" import { type HttpHeaders } from "effect-app/http" import { type GetEffectContext, type GetEffectError, type RpcContextMap } from "effect-app/rpc/RpcContextMap" import { type TypeTestId } from "effect-app/TypeTest" import { typedKeysOf, typedValuesOf } from "effect-app/utils" import { type Yieldable } from "effect/Effect" import { Rpc, RpcGroup, type RpcSerialization, RpcServer } from "effect/unstable/rpc" import { type LayerUtils } from "./layerUtils.js" import { RequestType as RequestTypeAnnotation, type RouterMiddleware } from "./routing/middleware.js" export * from "./routing/middleware.js" export const applyRequestTypeInterruptibility = ( requestType: "command" | "query", effect: Effect.Effect ) => requestType === "command" ? Rpc.uninterruptible(effect) : effect // it's the result of extending S.Req setting success, config // it's a schema plus some metadata export type AnyRequestModule = S.Top & { _tag: string // unique identifier for the request module type: "command" | "query" config: any // ? success: S.Top // validates the success response error: S.Top // validates the failure response } // builder pattern for adding actions to a router until all actions are added export interface AddAction = {}> { accum: Accum add>( a: A ): A extends Handler ? Exclude extends never ? & Accum & { [K in M["_tag"]]: A } : & AddAction< Exclude, & Accum & { [K in M["_tag"]]: A } > & Accum & { [K in M["_tag"]]: A } : never } // note: // "d" stands for decoded i.e. the Type // "raw" stands for encoded i.e. the Encoded namespace RequestTypes { export const DECODED = "d" as const export type DECODED = typeof DECODED export const RAW = "raw" as const export type RAW = typeof RAW } type RequestType = typeof RequestTypes[keyof typeof RequestTypes] type GetSuccess = T extends { success: S.Top } ? T["success"] : typeof S.Void type GetFailure = T["error"] extends never ? typeof S.Never : T["error"] type GetSuccessShape = { d: S.Schema.Type> raw: S.Codec.Encoded> }[RT] interface HandlerBase { new(): {} _tag: RT stack: string handler: (req: S.Schema.Type, headers: HttpHeaders.Headers) => Effect.Effect } export interface Handler extends HandlerBase< Action, RT, GetSuccessShape, S.Schema.Type> | S.SchemaError, R > {} type AnyHandler = Handler< Action, RequestType, any // R > // a Resource is typically the whole module with all the exported sh*t // this helper retrieves only the entities (classes) which are built by extending S.Req type FilterRequestModules = { [K in keyof T as T[K] extends AnyRequestModule ? K : never]: T[K] } type RpcRouteR< T extends [any, (req: any, headers: HttpHeaders.Headers) => Effect.Effect] > = T extends [ any, (...args: any[]) => Effect.Effect ] ? R : never type Match< Resource extends Record, RequestContextMap extends Record, RT extends RequestType, Key extends keyof Resource > = { // note: the defaults of = never prevent the whole router to error (??) , R2 = never, E = never>( f: Effect.Effect ): Handler< Resource[Key], RT, Exclude< Exclude>, Scope.Scope > > , R2 = never, E = never>( f: (req: S.Schema.Type) => Effect.Effect ): Handler< Resource[Key], RT, Exclude< Exclude>, Scope.Scope > > } export type RouteMatcher< RequestContextMap extends Record, Resource extends Record > = { // use Resource as Key over using Keys, so that the Go To on X.Action remain in tact in Controllers files /** * Requires the Type shape */ [Key in keyof FilterRequestModules]: & Match & { success: Resource[Key]["success"] successRaw: S.Codec> error: Resource[Key]["error"] /** * Requires the Encoded shape (e.g directly undecoded from DB, so that we don't do multiple Decode/Encode) */ raw: Match } } export const skipOnProd = Effect .gen(function*() { const env = yield* Config.string("env") return env !== "prod" }) .pipe(Effect.orDie) export const makeRouter = < Self, RequestContextMap extends Record, MakeMiddlewareE, MakeMiddlewareR, ContextProviderA, ContextProviderE, ContextProviderR, RequestContextId >( middleware: RouterMiddleware< Self, RequestContextMap, MakeMiddlewareE, MakeMiddlewareR, ContextProviderA, ContextProviderE, ContextProviderR, RequestContextId > ) => { /** * Create a Router for specified resource * if `check` is provided, the router will only be created if the effect succeeds with true */ function matchFor< const Resource extends Record >( rsc: Resource, options?: { check?: Effect.Effect } ) { type HandlerWithInputGen< Action extends AnyRequestModule, RT extends RequestType > = ( req: S.Schema.Type ) => Generator< Yieldable< any, any, S.Schema.Type> | S.SchemaError, // the actual implementation of the handler may just require the dynamic context provided by the middleware // and the per request context provided by the context provider GetEffectContext | ContextProviderA >, GetSuccessShape, never > type HandlerWithInputEff< Action extends AnyRequestModule, RT extends RequestType > = ( req: S.Schema.Type ) => Effect.Effect< GetSuccessShape, S.Schema.Type> | S.SchemaError, // the actual implementation of the handler may just require the dynamic context provided by the middleware // and the per request context provided by the context provider GetEffectContext | ContextProviderA > type HandlerEff< Action extends AnyRequestModule, RT extends RequestType > = Effect.Effect< GetSuccessShape, S.Schema.Type> | S.SchemaError, // the actual implementation of the handler may just require the dynamic context provided by the middleware // and the per request context provided by the context provider GetEffectContext | ContextProviderA > type Handlers = | HandlerWithInputGen | HandlerWithInputEff | HandlerEff type HandlersDecoded = Handlers type HandlersRaw = | { raw: HandlerWithInputGen } | { raw: HandlerWithInputEff } | { raw: HandlerEff } type AnyHandlers = HandlersRaw | HandlersDecoded const meta = getMeta(rsc) type RequestModules = FilterRequestModules const requestModules = typedKeysOf(rsc).reduce((acc, cur) => { if (Predicate.isObjectKeyword(rsc[cur]) && rsc[cur]["success"]) { acc[cur as keyof RequestModules] = rsc[cur] } return acc }, {} as RequestModules) const routeMatcher = typedKeysOf(requestModules).reduce( (prev, cur) => { ;(prev as any)[cur] = Object.assign((handlerImpl: any) => { // handlerImpl is the actual handler implementation if (handlerImpl[Symbol.toStringTag] === "GeneratorFunction") handlerImpl = Effect.fnUntraced(handlerImpl) const stack = new Error().stack?.split("\n").slice(2).join("\n") return Effect.isEffect(handlerImpl) // oxlint-disable-next-line typescript/no-extraneous-class ? class { static request = rsc[cur] static stack = stack static _tag = RequestTypes.DECODED static handler = () => handlerImpl } // oxlint-disable-next-line typescript/no-extraneous-class : class { static request = rsc[cur] static stack = stack static _tag = RequestTypes.DECODED static handler = handlerImpl } }, { success: rsc[cur].success, successRaw: S.toEncoded(rsc[cur].success), error: rsc[cur].error, raw: // "Raw" variations are for when you don't want to decode just to encode it again on the response // e.g for direct projection from DB // but more importantly, to skip Effectful decoders, like to resolve relationships from the database or remote client. (handlerImpl: any) => { if (handlerImpl[Symbol.toStringTag] === "GeneratorFunction") handlerImpl = Effect.fnUntraced(handlerImpl) const stack = new Error().stack?.split("\n").slice(2).join("\n") return Effect.isEffect(handlerImpl) // oxlint-disable-next-line typescript/no-extraneous-class ? class { static request = rsc[cur] static stack = stack static _tag = RequestTypes.RAW static handler = () => handlerImpl } // oxlint-disable-next-line typescript/no-extraneous-class : class { static request = rsc[cur] static stack = stack static _tag = RequestTypes.RAW static handler = handlerImpl } } }) return prev }, {} as RouteMatcher ) const router3: < const Impl extends { [K in keyof FilterRequestModules]: AnyHandlers } >( impl: Impl ) => { [K in keyof Impl & keyof FilterRequestModules]: Handler< FilterRequestModules[K], Impl[K] extends { raw: any } ? RequestTypes.RAW : RequestTypes.DECODED, Exclude< Exclude< // retrieves context R from the actual implementation of the handler Impl[K] extends { raw: any } ? Impl[K]["raw"] extends (...args: any[]) => Effect.Effect ? R : Impl[K]["raw"] extends Effect.Effect ? R : Impl[K]["raw"] extends (...args: any[]) => Generator< Yieldable, any, any > ? R : never : Impl[K] extends (...args: any[]) => Effect.Effect ? R : Impl[K] extends Effect.Effect ? R : Impl[K] extends (...args: any[]) => Generator< Yieldable, any, any > ? R : never, | GetEffectContext | ContextProviderA >, Scope.Scope > > } = (impl: Record) => typedKeysOf(impl).reduce((acc, cur) => { acc[cur] = "raw" in impl[cur] ? routeMatcher[cur].raw(impl[cur].raw) : routeMatcher[cur](impl[cur]) return acc }, {} as any) const makeRoutes = < MakeE, MakeR, THandlers extends { // important to keep them separate via | for type checking!! [K in keyof RequestModules]: AnyHandler }, MakeDependencies extends NonEmptyReadonlyArray | never[] >( dependencies: MakeDependencies, make: ( match: any ) => | Effect.Effect | Generator, THandlers, any> ) => { const dependenciesL = (dependencies ? Layer.mergeAll(...dependencies as any) : Layer.empty) as Layer.Layer< LayerUtils.GetLayersSuccess, LayerUtils.GetLayersError, LayerUtils.GetLayersContext > const layer = Effect .gen(function*() { const finalMake = ((make as any)[Symbol.toStringTag] === "GeneratorFunction" ? Effect.fnUntraced(make as any)(router3) as any : make(router3) as any) as Effect.Effect const controllers = yield* finalMake // return make.pipe(Effect.map((c) => controllers(c, dependencies))) const mapped = typedKeysOf(requestModules).reduce((acc, cur) => { const handler = controllers[cur as keyof typeof controllers] const resource = rsc[cur] acc[cur] = [ handler._tag === RequestTypes.RAW ? class extends (resource as any) { static success = S.toEncoded(resource.success) } as any : resource, (payload: any, headers: any) => { const effect = (handler.handler(payload, headers) as Effect.Effect).pipe( Effect.withSpan(`Request.${meta.moduleName}.${resource._tag}`, {}, { captureStackTrace: () => handler.stack // capturing the handler stack is the main reason why we are doing the span here }) ) return applyRequestTypeInterruptibility(resource.type, effect) } ] as const return acc }, {} as any) as { [K in keyof RequestModules]: [ Resource[K], ( req: any, headers: HttpHeaders.Headers ) => Effect.Effect< Effect.Success>, | Effect.Error> | GetEffectError, Exclude< Effect.Services>, ContextProviderA | GetEffectContext > > ] } const rpcs = RpcGroup .make( ...typedValuesOf(mapped).map(([resource]) => { return Rpc .make(resource._tag, { payload: resource, success: resource.success, error: resource.error }) .annotate(middleware.requestContext, resource.config ?? {}) .annotate(RequestTypeAnnotation, resource.type) }) ) .prefix(`${meta.moduleName}.`) .middleware(middleware as any) const rpc = rpcs .toLayer(Effect.gen(function*() { return typedValuesOf(mapped).reduce((acc, [resource, handler]) => { acc[`${meta.moduleName}.${resource._tag}`] = handler return acc }, {} as Record) as any // TODO })) as unknown as Layer.Layer< { [K in keyof RequestModules]: Rpc.Handler }, MakeE, RpcRouteR > return RpcServer .layerHttp({ spanPrefix: "RpcServer." + meta.moduleName, group: rpcs, path: ("/rpc/" + meta.moduleName) as `/${typeof meta.moduleName}`, protocol: "http" }) .pipe(Layer.provide(rpc)) }) .pipe(Layer.unwrap) const routes = layer.pipe( Layer.provide([ dependenciesL, middleware.Default ]) ) const check = options?.check return check ? Effect .gen(function*() { if (!(yield* check)) { yield* Effect.logWarning(`Skipping router for module ${meta.moduleName}`) return Layer.empty } return routes }) .pipe(Layer.unwrap) : routes } const effect: { // Multiple times duplicated the "good" overload, so that errors will only mention the last overload when failing < const Make extends { dependencies?: ReadonlyArray effect: (match: typeof router3) => Generator< Yieldable< any, any, any, any >, { [K in keyof FilterRequestModules]: AnyHandler }, any > /** @deprecated */ readonly ಠ_ಠ: never } >( make: Make ): & Layer.Layer< never, | MakeErrors | MakeDepsE | Layer.Error, | MakeDepsIn | Layer.Services | Exclude< MakeContext, MakeDepsOut > | RpcSerialization.RpcSerialization > & { // just for type testing purposes [TypeTestId]: Make } < const Make extends { dependencies?: ReadonlyArray // v4: generators yield Yieldable with asEffect() effect: (match: typeof router3) => Generator< Yieldable, { [K in keyof FilterRequestModules]: AnyHandler }, any > } >( make: Make ): & Layer.Layer< never, | MakeErrors | MakeDepsE | Layer.Error, | MakeDepsIn | Layer.Services | Exclude< MakeContext, MakeDepsOut > | RpcSerialization.RpcSerialization > & { // just for type testing purposes readonly [TypeTestId]: Make } } = ((make: { dependencies: any; effect: any }) => Object.assign(makeRoutes(make.dependencies, make.effect), { make })) as any return effect } function matchAll< T extends { [key: string]: Layer.Layer } >( handlers: T ) { const routers = typedValuesOf(handlers) return Layer.mergeAll(...routers as [any]) as unknown as Layer.Layer< never, Layer.Error, Layer.Services > } return { matchAll, Router: matchFor } } export type MakeDeps = Make extends { readonly dependencies: ReadonlyArray } ? Make["dependencies"][number] : never export type MakeErrors = /*Make extends { readonly effect: (_: any) => Effect.Effect } ? E : Make extends { readonly effect: (_: any) => Effect.Effect } ? never : */ // v4: generators yield Yieldable with asEffect() Make extends { readonly effect: (_: any) => Generator, any, any> } ? never : Make extends { readonly effect: (_: any) => Generator, any, any> } ? E : never export type MakeContext = /*Make extends { readonly effect: (_: any) => Effect.Effect } ? R : Make extends { readonly effect: (_: any) => Effect.Effect } ? never : */ // v4: generators yield Yieldable with asEffect() Make extends { readonly effect: (_: any) => Generator, any, any> } ? never : Make extends { readonly effect: (_: any) => Generator, any, any> } ? R : never export type MakeHandlers> = /*Make extends { readonly effect: (_: any) => Effect.Effect<{ [K in keyof Handlers]: AnyHandler }, any, any> } ? Effect.Success> : */ Make extends { readonly effect: (_: any) => Generator } ? S : never export type MakeDepsE = Layer.Error> export type MakeDepsIn = Layer.Services> export type MakeDepsOut = Layer.Success>