import type { AnyContractRouter, AnySchema, InferSchemaOutput } from "@orpc/contract"; import { ORPCError } from "@orpc/contract"; import type { Implementer, Router } from "@orpc/server"; import { implement, onError } from "@orpc/server"; import { Context, Effect, type Context as EffectContext, type Layer, type Scope } from "effect"; import { extractFromFiberFailure, formatORPCError } from "./runtime/errors"; type ContextOutput = T extends AnySchema ? InferSchemaOutput : Record; export type PluginConfigFor< V extends AnySchema, S extends AnySchema, TRequestContext extends AnySchema | undefined, > = { variables: V; secrets: S; context: TRequestContext; }; type PluginInitializeInput = { variables: InferSchemaOutput; secrets: InferSchemaOutput; }; /** * Tools provided to plugin initialize/services for building long-lived scoped resources. */ type ServiceOf = T extends EffectContext.Tag ? S : never; export type PluginServicesTools = { buildService: (tag: T, layer: Layer.Layer) => Effect.Effect>; }; export class PluginIdTag extends Context.Tag("PluginId")() {} type PluginDefinition< V extends AnySchema, S extends AnySchema, TContract extends AnyContractRouter, TRequestContext extends AnySchema | undefined, TDeps extends Record, P extends Record, > = { variables: V; secrets: S; contract: TContract; context?: TRequestContext; /** * Initialize the plugin and build dependencies. * A third argument `tools` is provided for building long-lived scoped resources * (e.g. database pools) via `tools.buildService(tag, layer)`. */ initialize?: ( config: PluginInitializeInput, plugins: P, tools: PluginServicesTools, ) => Effect.Effect; createRouter: ( deps: TDeps, builder: Implementer, ContextOutput>, ) => Router; shutdown?: (deps: TDeps) => Effect.Effect; }; /** * Loaded plugin with static binding property */ export interface LoadedPluginWithBinding< TContract extends AnyContractRouter, TVariables extends AnySchema, TSecrets extends AnySchema, TRequestContext extends AnySchema | undefined, TDeps extends Record = Record, > { new (): Plugin; binding: { contract: TContract; variables: TVariables; secrets: TSecrets; context: TRequestContext; }; } /** * Plugin interface */ export interface Plugin< TContract extends AnyContractRouter, TVariables extends AnySchema, TSecrets extends AnySchema, TRequestContext extends AnySchema | undefined, TDeps extends Record = Record, > { readonly id: string; readonly contract: TContract; readonly configSchema: PluginConfigFor; initialize( config: PluginInitializeInput, plugins: Record, tools: PluginServicesTools, ): Effect.Effect; shutdown(): Effect.Effect; /** * Creates the strongly-typed oRPC router for this plugin. * The router's procedure types are inferred directly from the contract. * @param deps The initialized plugin dependencies * @returns A router with procedures matching the plugin's contract */ createRouter(deps: TDeps): Router; } export interface CreatePluginFn { < V extends AnySchema, S extends AnySchema, TContract extends AnyContractRouter, TRequestContext extends AnySchema | undefined = undefined, TDeps extends Record = Record, P extends Record = Record, >( config: PluginDefinition, ): LoadedPluginWithBinding; withPlugins:

>() => CreatePluginWithPlugins

; } export const createPlugin: CreatePluginFn = function createPlugin< V extends AnySchema, S extends AnySchema, TContract extends AnyContractRouter, TRequestContext extends AnySchema | undefined = undefined, TDeps extends Record = Record, P extends Record = Record, >(config: PluginDefinition) { const configSchema: PluginConfigFor = { variables: config.variables, secrets: config.secrets, context: config.context as TRequestContext, }; class CreatedPlugin implements Plugin { /** set during instantiation - registry key */ id!: string; readonly contract = config.contract; readonly configSchema = configSchema; private _deps: TDeps | null = null; initialize( pluginConfig: PluginInitializeInput, plugins: Record = {}, tools: PluginServicesTools, ): Effect.Effect { const init = config.initialize ?? (() => Effect.succeed({} as TDeps)); return init(pluginConfig, plugins as P, tools).pipe( Effect.tap((deps) => Effect.sync(() => { this._deps = deps; }), ), Effect.map(() => this._deps as TDeps), Effect.mapError((error) => error as unknown), ); } shutdown(): Effect.Effect { const self = this; return Effect.gen(function* () { if (config.shutdown && self._deps) { yield* config .shutdown(self._deps) .pipe( Effect.catchAll((error) => Effect.logWarning(`Plugin shutdown hook failed for ${self.id}`, error), ), ); } self._deps = null; }); } createRouter(deps: TDeps): Router { const base = implement(config.contract).$context>(); const errorMiddleware = onError((error: unknown) => { const unwrapped = extractFromFiberFailure(error); if (unwrapped !== error && unwrapped instanceof ORPCError) { throw unwrapped; } const formatted = formatORPCError(error); if (formatted) console.error(formatted); throw error; }) as any; const builder = (base as any).use(errorMiddleware); const router = config.createRouter(deps, builder as any); return router as Router; } } const PluginConstructor = CreatedPlugin as unknown as { new (): Plugin; binding: { contract: TContract; variables: V; secrets: S; context: TRequestContext; }; }; PluginConstructor.binding = { contract: config.contract, variables: config.variables, secrets: config.secrets, context: config.context as TRequestContext, }; return PluginConstructor as LoadedPluginWithBinding; }; export type CreatePluginWithPlugins

> = < V extends AnySchema, S extends AnySchema, TContract extends AnyContractRouter, TRequestContext extends AnySchema | undefined = undefined, TDeps extends Record = Record, >( config: PluginDefinition, ) => LoadedPluginWithBinding; export function withPlugins

>(): CreatePluginWithPlugins

{ return < V extends AnySchema, S extends AnySchema, TContract extends AnyContractRouter, TRequestContext extends AnySchema | undefined = undefined, TDeps extends Record = Record, >( config: PluginDefinition, ) => createPlugin(config as any); } createPlugin.withPlugins = withPlugins;