/* eslint-disable @typescript-eslint/no-explicit-any */ import { type NonEmptyArray, type NonEmptyReadonlyArray } from "effect/Array" import * as Effect from "effect/Effect" import * as Layer from "effect/Layer" import * as S from "effect/Schema" import type * as Scope from "effect/Scope" import { type Simplify } from "effect/Types" import { Rpc, type RpcGroup, type RpcSchema } from "effect/unstable/rpc" import { type HandlersFrom } from "effect/unstable/rpc/RpcGroup" import * as Context from "../Context.js" import { PreludeLogger } from "../logger.js" import { type TypeTestId } from "../TypeTest.js" import { type GetContextConfig, type RequestContextMapTagAny, type RpcContextMap } from "./RpcContextMap.js" import { type AddMiddleware, type AnyDynamic, type RpcDynamic, type RpcMiddlewareV4, type TagClassAny } from "./RpcMiddleware.js" import * as RpcMiddlewareX from "./RpcMiddleware.js" // adapter for effect/rpc v3 middleware provides. (in effect-smol (v4), it's just a Service Identifier, no tags.) // hm? type MakeTags = A export interface MiddlewareMaker< Self, Id extends string, RequestContextMap extends Record, MiddlewareProviders extends ReadonlyArray > extends RpcMiddlewareX.TagClass< Self, Id, Simplify< & (Exclude< MiddlewareMaker.ManyRequired, MiddlewareMaker.ManyProvided > extends never ? {} : { readonly requires: MakeTags< Exclude< MiddlewareMaker.ManyRequired, MiddlewareMaker.ManyProvided > > }) & (MiddlewareMaker.ManyErrors extends never ? {} : { readonly error: S.Codec> }) & (MiddlewareMaker.ManyProvided extends never ? {} : { readonly provides: MakeTags> }) >, { provides: MiddlewareMaker.ManyProvided extends never ? never : MakeTags> requires: Exclude< MiddlewareMaker.ManyRequired, MiddlewareMaker.ManyProvided > extends never ? never : MakeTags< Exclude< MiddlewareMaker.ManyRequired, MiddlewareMaker.ManyProvided > > } > { readonly layer: Layer.Layer> readonly requestContext: RequestContextTag readonly requestContextMap: RequestContextMap } export interface RequestContextTag> extends Context.Service<"RequestContextConfig", GetContextConfig> {} export namespace MiddlewareMaker { export type Any = TagClassAny export type ApplyServices = Exclude> | Required export type ApplyManyServices, R> = | Exclude }[number]> | { [K in keyof A]: Required }[number] export type ManyProvided> = A extends NonEmptyReadonlyArray ? { [K in keyof A]: Provided }[number] : Provided export type ManyRequired> = A extends NonEmptyReadonlyArray ? { [K in keyof A]: Required }[number] : Required export type ManyErrors> = A extends NonEmptyReadonlyArray ? { [K in keyof A]: Errors }[number] : Errors export type Provided = T extends TagClassAny ? T extends { provides: infer _P } ? _P : never : never export type Errors = T extends TagClassAny ? T extends { error: S.Top } ? S.Schema.Type : never : never export type Required = T extends TagClassAny ? T extends { requires: infer _R } ? _R : never : never } // the following implements sort of builder pattern // we support both sideways and upwards elimination of dependencies // it's for dynamic middlewares type GetDependsOnKeys = MW extends { dependsOn: NonEmptyReadonlyArray } ? { [K in keyof MW["dependsOn"]]: MW["dependsOn"][K] extends AnyDynamic ? MW["dependsOn"][K]["dynamic"]["key"] : never }[keyof MW["dependsOn"]] : never type FilterInDynamicMiddlewares< MWs extends ReadonlyArray, RequestContextMap extends Record > = { [K in keyof MWs]: MWs[K] extends { dynamic: RpcDynamic } ? MWs[K] : never } type RecursiveHandleMWsSideways< MWs, R extends { self: any id: string rcm: Record provided: keyof R["rcm"] // that's fine middlewares: ReadonlyArray dmp: any middlewareR: any } > = MWs extends [] ? R : MWs extends [infer F, ...infer Rest extends ReadonlyArray] ? F extends MiddlewareMaker.Any ? RecursiveHandleMWsSideways[number]["dynamic"]["key"], // F is fine here because only dynamic middlewares will have 'dependsOn' prop GetDependsOnKeys > middlewares: [...R["middlewares"], F] dmp: [FilterInDynamicMiddlewares<[F], R["rcm"]>[number]] extends [never] ? R["dmp"] : & R["dmp"] & { [U in FilterInDynamicMiddlewares<[F], R["rcm"]>[number] as U["dynamic"]["key"]]: U } middlewareR: MiddlewareMaker.ApplyManyServices<[F], R["middlewareR"]> }> // TypeScript inference fails when checking F extends MiddlewareMaker.Any during F's inference // if F is a class with static properties - deferring the check avoids this limitation : `Absurd: F must extend MiddlewareMaker.Any` : never export interface BuildingMiddleware< Self, Id extends string, RequestContextMap extends Record, Provided extends keyof RequestContextMap, Middlewares extends ReadonlyArray, DynamicMiddlewareProviders, out MiddlewareR extends { _tag: string } = never > { rpc: < const Tag extends string, Payload extends S.Top | S.Struct.Fields = typeof S.Void, Success extends S.Top = typeof S.Void, Error extends S.Top = typeof S.Never, const Stream extends boolean = false, Config extends GetContextConfig = {} >(tag: Tag, options?: { readonly payload?: Payload readonly success?: Success readonly error?: Error readonly stream?: Stream readonly config?: Config readonly primaryKey?: [Payload] extends [S.Struct.Fields] ? (( payload: Payload extends S.Struct.Fields ? Simplify["Type"]> : Payload["Type"] ) => string) : never }) => & Rpc.Rpc< Tag, Payload extends S.Struct.Fields ? S.Struct : Payload, Stream extends true ? RpcSchema.Stream : Success, Stream extends true ? typeof S.Never : Error > & { readonly config: Config } middleware>( ...mw: MWs ): RecursiveHandleMWsSideways extends infer Res extends { self: any id: string rcm: RequestContextMap provided: keyof RequestContextMap middlewares: ReadonlyArray dmp: any middlewareR: any } ? MiddlewaresBuilder< Res["self"], Res["id"], Res["rcm"], Res["provided"], Res["middlewares"], Res["dmp"], Res["middlewareR"] > : never // helps debugging what are the missing requirements (type only) readonly [TypeTestId]: { missingDynamicMiddlewares: Exclude missingContext: MiddlewareR } } export type MiddlewaresBuilder< Self, Id extends string, RequestContextMap extends Record, Provided extends keyof RequestContextMap = never, Middlewares extends ReadonlyArray = [], DynamicMiddlewareProviders = unknown, MiddlewareR extends { _tag: string } = never > = & BuildingMiddleware< Self, Id, RequestContextMap, Provided, Middlewares, DynamicMiddlewareProviders, MiddlewareR > & // keyof Omit extends never is true when all the dynamic middlewares are provided // MiddlewareR is never when all the required services from generic & dynamic middlewares are provided (keyof Omit extends never ? [MiddlewareR] extends [never] ? MiddlewareMaker< Self, Id, RequestContextMap, Middlewares > : { new(_: never): {} } : { new(_: never): {} }) const middlewareMaker = Effect.fnUntraced(function*< MiddlewareProviders extends ReadonlyArray >(middlewares: MiddlewareProviders) { type Middleware = RpcMiddlewareV4< MiddlewareMaker.ManyProvided, MiddlewareMaker.ManyErrors, Exclude< MiddlewareMaker.ManyRequired, MiddlewareMaker.ManyProvided > extends never ? never : Exclude, MiddlewareMaker.ManyProvided> > type Next = Parameters[0] type Options = Parameters[1] // we want to run them in reverse order because latter middlewares will provide context to former ones const reversed = middlewares.toReversed() const context = yield* Effect.context() // returns a Effect/RpcMiddlewareV4 with Scope.Scope in requirements // v4: wrap middleware takes (effect, options) as two params instead of a single options bag return (next: Next, options: Options) => { // we start with the actual handler let handler = next // inspired from Effect/RpcMiddleware for (const tag of reversed) { // use the tag to get the middleware from context const middleware = Context.getUnsafe(context, tag) // wrap the current handler, allowing the middleware to run before and after it handler = PreludeLogger.logDebug("Applying middleware wrap " + tag.key).pipe( Effect.andThen(middleware(handler, options)) ) as any } return handler } }) const makeMiddlewareBasic = () => // by setting RequestContextMap beforehand, execute contextual typing does not fuck up itself to anys < const Id extends string, RequestContextMap extends Record, MiddlewareProviders extends ReadonlyArray >( id: Id, rcm: RequestContextMap, ...make: MiddlewareProviders ) => { // reverse middlewares and wrap one after the other const middleware = middlewareMaker(make) // Per-middleware error: union of the static `error` on the tag (if any) AND // the rcm config entry pointed at by the middleware's `dynamic.key` (if any). // Reason: middlewares declared with `dynamic: RequestContextMap.get("foo")` // don't set a static `error` field — at runtime their `.error` defaults to // `S.Never`. Without pulling from rcm, the composite middleware's // `.error` collapses to `Never`, and `Rpc.exitSchema` (which walks // `rpc.middlewares[*].error` to build the wire failure union) can't decode // the actual middleware-thrown error type. Critical for stream rpcs whose // top-level `errorSchema` is force-set to `Never` by effect-rpc. const isMeaningfulError = (e: S.Top | undefined): e is S.Top => e !== undefined && e !== null && e !== S.Never const rcmRecord = rcm as Record const failures: Array = make.flatMap((_) => { const out: Array = [] if (isMeaningfulError(_.error)) out.push(_.error) const key = _.dynamic?.key as string | undefined if (key && rcmRecord[key] && isMeaningfulError(rcmRecord[key].error)) { out.push(rcmRecord[key].error) } return out }) const provides = make.flatMap((_) => !_.provides ? [] : Array.isArray(_.provides) ? _.provides : [_.provides]) const requires = make .flatMap((_) => !_.requires ? [] : Array.isArray(_.requires) ? _.requires : [_.requires]) .filter((_) => !provides.includes(_)) const [firstFailure, ...restFailures] = failures const MiddlewareMaker = RpcMiddlewareX.Tag()(id, { error: (firstFailure ? S.Union([firstFailure, ...restFailures]) : S.Never) as unknown as MiddlewareMaker.ManyErrors extends never ? never : S.Codec>, requires: (requires.length > 0 ? requires : undefined) as unknown as Exclude< MiddlewareMaker.ManyRequired, MiddlewareMaker.ManyProvided > extends never ? never : [ MakeTags< Exclude< MiddlewareMaker.ManyRequired, MiddlewareMaker.ManyProvided > > ], provides: (provides.length > 0 ? provides : undefined) as unknown as MiddlewareMaker.ManyProvided extends never ? never : MakeTags> }) const layer = Layer .effect( MiddlewareMaker, middleware as Effect.Effect< any > // todo; they dont change the type.. // Effect.Error, // Effect.Services ) // add to the tag a default implementation return Object.assign(MiddlewareMaker, { layer, // tag to be used to retrieve the RequestContextConfig from Rpc annotations requestContext: Context.Service<"RequestContextConfig", GetContextConfig>( "RequestContextConfig" ), requestContextMap: rcm }) } export const Tag = () => < const Id extends string, RequestContextMap extends RequestContextMapTagAny >(id: Id, rcm: RequestContextMap): MiddlewaresBuilder => { const allMiddleware: MiddlewareMaker.Any[] = [] const requestContext = Context.Service<"RequestContextConfig", GetContextConfig>( "RequestContextConfig" ) const it = { id, // rpc with config rpc: < const Tag extends string, Payload extends S.Top | S.Struct.Fields = typeof S.Void, Success extends S.Top = typeof S.Void, Error extends S.Top = typeof S.Never, const Stream extends boolean = false, Config extends GetContextConfig = {} >(tag: Tag, options?: { readonly payload?: Payload readonly success?: Success readonly error?: Error readonly stream?: Stream readonly config?: Config readonly primaryKey?: [Payload] extends [S.Struct.Fields] ? (( payload: Payload extends S.Struct.Fields ? Simplify["Type"]> : Payload["Type"] ) => string) : never }): & Rpc.Rpc< Tag, Payload extends S.Struct.Fields ? S.Struct : Payload, // TODO: enhance `Error`. type based on middleware config. Stream extends true ? RpcSchema.Stream : Success, Stream extends true ? typeof S.Never : Error > & { config: Config } => { const config = options?.config ?? {} as Config // The rpc's `error` schema carries ONLY the request's own declared errors. // Middleware errors (rcm-derived) reach the wire via the middleware tag // attached to the rpc group later (`RpcGroup.middleware(...)` at the // routing/client level), and are unioned into the failure schema by // `Rpc.exitSchema`'s `rpc.middlewares[*].error` walk. // @ts-expect-error — TypeScript can't prove Simplify ≡ { [K in keyof T]: T[K] } for unresolved generics (primaryKey) const rpc = Rpc.make(tag, { ...options?.payload !== undefined ? { payload: options.payload } : {}, ...options?.success !== undefined ? { success: options.success } : {}, ...options?.error !== undefined ? { error: options.error } : {}, ...options?.stream !== undefined ? { stream: options.stream } : {}, ...options?.primaryKey !== undefined ? { primaryKey: options.primaryKey } : {} }) as any return Object.assign(rpc.annotate(requestContext, config), { config }) }, middleware: (...middlewares: any[]) => { for (const mw of middlewares) { // recall that we run middlewares in reverse order allMiddleware.unshift(mw) } return allMiddleware.filter((m) => !!m.dynamic).length !== Object.keys(rcm.config).length // for sure, until all the dynamic middlewares are provided it's non sensical to call makeMiddlewareBasic ? it // actually, we don't know yet if MiddlewareR is never, but we can't easily check it at runtime : Object.assign(makeMiddlewareBasic()(id, rcm.config, ...allMiddleware), it) } } return it as any } // alternatively consider group.serverMiddleware? hmmm export const middlewareGroup = < RequestContextMap extends Record, Middleware extends RpcMiddlewareX.TagClassAny & { readonly requestContext: RequestContextTag readonly requestContextMap: RequestContextMap } >( middleware: Middleware ) => (group: RpcGroup.RpcGroup) => { type RN = AddMiddleware const middlewaredGroup = group.middleware(middleware) as unknown as RpcGroup.RpcGroup const toLayerOriginal = middlewaredGroup.toLayer.bind(middlewaredGroup) return Object.assign(middlewaredGroup, { toLayerDynamic: < Handlers extends HandlersFrom, EX = never, RX = never >( build: | Handlers | Effect.Effect ): Layer.Layer< Rpc.ToHandler, EX, | Exclude | RpcMiddlewareX.HandlersContext > => { return toLayerOriginal(build as any) as any // ?? } }) }