/* 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>