1/**
2 * Middleware services for the unstable RPC runtime.
3 *
4 * A middleware service wraps server handler execution and can also install a
5 * client-side wrapper for generated clients. Its metadata records the services
6 * provided to downstream handlers, the services required by the middleware
7 * implementation, the schema for server-visible failures, the client-only error
8 * type, and whether generated clients must require the matching client layer.
9 *
10 * @stability unstable
11 * @since 4.0.0
12 */
13import * as Context from "../Context.ts"
14import * as Effect from "../Effect.ts"
15import type { Headers } from "../http/Headers.ts"
16import { getStackTraceLimit, setStackTraceLimit } from "../internal/stackTraceLimit.ts"
17import * as Layer from "../Layer.ts"
18import * as Schema from "../Schema.ts"
19import { Scope } from "../Scope.ts"
20import type { Mutable, unhandled } from "../Types.ts"
21import type * as Rpc from "./Rpc.ts"
22import type { Request, RequestId } from "./RpcMessage.ts"
23
24/**
25 * The literal type id used to identify RPC middleware service classes.
26 *
27 * @stability unstable
28 * @category type IDs
29 * @since 4.0.0
30 */
31export type TypeId = "~effect/rpc/RpcMiddleware"
32
33/**
34 * The runtime type id used to attach and inspect RPC middleware metadata.
35 *
36 * @stability unstable
37 * @category type IDs
38 * @since 4.0.0
39 */
40export const TypeId: TypeId = "~effect/rpc/RpcMiddleware"
41
42/**
43 * The server-side RPC middleware function shape, wrapping a handler effect with
44 * access to request metadata and translating provided services into required
45 * services.
46 *
47 * @stability unstable
48 * @category models
49 * @since 4.0.0
50 */
51export interface RpcMiddleware<Provides, E, Requires> {
52 (
53 effect: Effect.Effect<SuccessValue, E | unhandled, Provides>,
54 options: {
55 readonly client: Rpc.ServerClient
56 readonly requestId: RequestId
57 readonly rpc: Rpc.AnyWithProps
58 readonly payload: unknown
59 readonly headers: Headers
60 }
61 ): Effect.Effect<SuccessValue, unhandled | E, Requires | Scope>
62}
63
64/**
65 * Marker success type used by RPC middleware to represent successful completion
66 * without exposing the handler's concrete success value.
67 *
68 * @stability unstable
69 * @category models
70 * @since 4.0.0
71 */
72export interface SuccessValue {
73 readonly _: unique symbol
74}
75
76/**
77 * The client-side RPC middleware function shape, allowing outgoing requests to
78 * be inspected or modified before calling `next`.
79 *
80 * @stability unstable
81 * @category models
82 * @since 4.0.0
83 */
84export interface RpcMiddlewareClient<E, CE, R> {
85 (options: {
86 readonly rpc: Rpc.AnyWithProps
87 readonly request: Request<Rpc.Any>
88 readonly next: (request: Request<Rpc.Any>) => Effect.Effect<SuccessValue, unhandled | E>
89 }): Effect.Effect<SuccessValue, unhandled | E | CE, R>
90}
91
92/**
93 * Marker service requirement indicating that a middleware has a client-side
94 * implementation available for an RPC client.
95 *
96 * @stability unstable
97 * @category models
98 * @since 4.0.0
99 */
100export interface ForClient<Id> {
101 readonly _: unique symbol
102 readonly id: Id
103}
104
105/**
106 * An erased server-side RPC middleware function, useful when the concrete
107 * provided services, errors, and requirements are not needed.
108 *
109 * @stability unstable
110 * @category models
111 * @since 4.0.0
112 */
113export interface Any {
114 (
115 effect: Effect.Effect<SuccessValue, any, any>,
116 options: {
117 readonly client: Rpc.ServerClient
118 readonly requestId: RequestId
119 readonly rpc: Rpc.AnyWithProps
120 readonly payload: unknown
121 readonly headers: Headers
122 }
123 ): Effect.Effect<SuccessValue, any, any>
124}
125
126/**
127 * A type-level carrier for RPC middleware metadata, including provided
128 * services, required services, error schema, and client error type.
129 *
130 * @stability unstable
131 * @category utility types
132 * @since 4.0.0
133 */
134export interface AnyId {
135 readonly [TypeId]: {
136 readonly provides: any
137 readonly requires: any
138 readonly error: Schema.Top
139 readonly clientError: any
140 }
141}
142
143/**
144 * The `Context.Service` class shape created for an RPC middleware, including
145 * its error schema, service metadata, and client-side requirement marker.
146 *
147 * @stability unstable
148 * @category services
149 * @since 4.0.0
150 */
151export interface ServiceClass<
152 Self,
153 Name extends string,
154 Provides,
155 E extends Schema.Constraint,
156 ClientError,
157 Requires,
158 RequiredForClient extends boolean
159> extends Context.Service<Self, RpcMiddleware<Provides, E["Type"], Requires>> {
160 new(_: never): Context.ServiceClass.Shape<Name, RpcMiddleware<Provides, E["Type"], Requires>> & {
161 readonly [TypeId]: {
162 readonly error: E
163 readonly provides: Provides
164 readonly requires: Requires
165 readonly clientError: ClientError
166 }
167 }
168 readonly [TypeId]: typeof TypeId
169 readonly error: E
170 readonly requiredForClient: RequiredForClient
171 readonly "~ClientError": ClientError
172}
173
174/**
175 * Extracts the services provided by an RPC middleware.
176 *
177 * @stability unstable
178 * @category utility types
179 * @since 4.0.0
180 */
181export type Provides<A> = A extends { readonly [TypeId]: { readonly provides: infer P } } ? P : never
182
183/**
184 * Extracts the services required by an RPC middleware.
185 *
186 * @stability unstable
187 * @category utility types
188 * @since 4.0.0
189 */
190export type Requires<A> = A extends { readonly [TypeId]: { readonly requires: infer R } } ? R : never
191
192/**
193 * Applies a middleware's service transformation to an RPC environment by
194 * removing services the middleware provides and adding services it requires.
195 *
196 * @stability unstable
197 * @category utility types
198 * @since 4.0.0
199 */
200export type ApplyServices<A, R> = Exclude<R, Provides<A>> | Requires<A>
201
202/**
203 * Extracts the error schema associated with an RPC middleware.
204 *
205 * @stability unstable
206 * @category utility types
207 * @since 4.0.0
208 */
209export type ErrorSchema<A> = A extends { readonly [TypeId]: { readonly error: infer E } }
210 ? E extends Schema.Constraint ? E : never
211 : never
212
213/**
214 * Extracts the decoded error type produced by an RPC middleware.
215 *
216 * @stability unstable
217 * @category utility types
218 * @since 4.0.0
219 */
220export type Error<A> = ErrorSchema<A>["Type"]
221
222/**
223 * Extracts the encoding services required by a middleware's error schema.
224 *
225 * @stability unstable
226 * @category utility types
227 * @since 4.0.0
228 */
229export type ErrorServicesEncode<A> = ErrorSchema<A>["EncodingServices"]
230
231/**
232 * Extracts the decoding services required by a middleware's error schema.
233 *
234 * @stability unstable
235 * @category utility types
236 * @since 4.0.0
237 */
238export type ErrorServicesDecode<A> = ErrorSchema<A>["DecodingServices"]
239
240/**
241 * An erased RPC middleware context key carrying middleware metadata.
242 *
243 * @stability unstable
244 * @category models
245 * @since 4.0.0
246 */
247export interface AnyService extends Context.Key<any, any> {
248 readonly [TypeId]: typeof TypeId
249 readonly error: Schema.Top
250 readonly requiredForClient: boolean
251 readonly "~ClientError": any
252}
253
254/**
255 * An erased RPC middleware context key whose service value is a server-side
256 * middleware function.
257 *
258 * @stability unstable
259 * @category models
260 * @since 4.0.0
261 */
262export interface AnyServiceWithProps extends Context.Key<any, RpcMiddleware<any, any, any>> {
263 readonly [TypeId]: typeof TypeId
264 readonly error: Schema.Top
265 readonly requiredForClient: boolean
266 readonly "~ClientError": any
267}
268
269/**
270 * Creates a typed RPC middleware service class, with optional service
271 * requirements, provided services, error schema, and client-side requirement
272 * metadata.
273 *
274 * @stability unstable
275 * @category constructors
276 * @since 4.0.0
277 */
278export const Service = <
279 Self,
280 Config extends {
281 requires?: any
282 provides?: any
283 clientError?: any
284 } = {
285 requires: never
286 provides: never
287 clientError: never
288 }
289>(): <
290 const Name extends string,
291 Error extends Schema.Top = Schema.Never,
292 const RequiredForClient extends boolean = false
293>(
294 id: Name,
295 options?: {
296 readonly error?: Error | undefined
297 readonly requiredForClient?: RequiredForClient | undefined
298 } | undefined
299) => ServiceClass<
300 Self,
301 Name,
302 "provides" extends keyof Config ? Config["provides"] : never,
303 Error,
304 "clientError" extends keyof Config ? Config["clientError"] : never,
305 "requires" extends keyof Config ? Config["requires"] : never,
306 RequiredForClient
307> =>
308(
309 id: string,
310 options?: {
311 readonly error?: Schema.Top | undefined
312 readonly requiredForClient?: boolean | undefined
313 }
314) => {
315 const limit = getStackTraceLimit()
316 let creationError: globalThis.Error | undefined
317 if (limit !== 0) {
318 setStackTraceLimit(2)
319 creationError = new globalThis.Error()
320 setStackTraceLimit(limit)
321 }
322
323 function ServiceClass() {}
324 const ServiceClass_ = ServiceClass as any as Mutable<AnyService>
325 Object.setPrototypeOf(ServiceClass, Object.getPrototypeOf(Context.Service<Self, any>(id)))
326 ServiceClass.key = id
327 Object.defineProperty(ServiceClass, "stack", {
328 get() {
329 return creationError?.stack
330 }
331 })
332 ServiceClass_[TypeId] = TypeId
333 ServiceClass_.error = options?.error ?? Schema.Never
334 ServiceClass_.requiredForClient = options?.requiredForClient ?? false
335 return ServiceClass as any
336}
337
338/**
339 * Provides the client-side implementation for an RPC middleware service,
340 * capturing the layer's environment and merging it into each middleware
341 * invocation.
342 *
343 * @stability unstable
344 * @category layers
345 * @since 4.0.0
346 */
347export const layerClient = <Id extends AnyId, S, R, EX = never, RX = never>(
348 tag: Context.Key<Id, S>,
349 service:
350 | RpcMiddlewareClient<Id[TypeId]["error"]["Type"], Id[TypeId]["clientError"], R>
351 | Effect.Effect<RpcMiddlewareClient<Id[TypeId]["error"]["Type"], Id[TypeId]["clientError"], R>, EX, RX>
352): Layer.Layer<ForClient<Id>, EX, R | Exclude<RX, Scope>> =>
353 Layer.effectContext(Effect.gen(function*() {
354 const services = (yield* Effect.context<R | Scope>()).pipe(
355 Context.omit(Scope)
356 ) as Context.Context<R>
357 const middleware = Effect.isEffect(service) ? yield* service : service
358 return Context.makeUnsafe(
359 new Map([[
360 `${tag.key}/Client`,
361 (options: any) =>
362 Effect.updateContext(
363 middleware(options),
364 (requestContext) => Context.merge(services, requestContext)
365 )
366 ]])
367 )
368 }))