| 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 | */ |
| 13 | import * as Context from "../Context.ts" |
| 14 | import * as Effect from "../Effect.ts" |
| 15 | import type { Headers } from "../http/Headers.ts" |
| 16 | import { getStackTraceLimit, setStackTraceLimit } from "../internal/stackTraceLimit.ts" |
| 17 | import * as Layer from "../Layer.ts" |
| 18 | import * as Schema from "../Schema.ts" |
| 19 | import { Scope } from "../Scope.ts" |
| 20 | import type { Mutable, unhandled } from "../Types.ts" |
| 21 | import type * as Rpc from "./Rpc.ts" |
| 22 | import 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 | */ |
| 31 | export 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 | */ |
| 40 | export 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 | */ |
| 51 | export 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 | */ |
| 72 | export 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 | */ |
| 84 | export 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 | */ |
| 100 | export 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 | */ |
| 113 | export 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 | */ |
| 134 | export 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 | */ |
| 151 | export 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 | */ |
| 181 | export 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 | */ |
| 190 | export 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 | */ |
| 200 | export 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 | */ |
| 209 | export 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 | */ |
| 220 | export 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 | */ |
| 229 | export 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 | */ |
| 238 | export 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 | */ |
| 247 | export 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 | */ |
| 262 | export 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 | */ |
| 278 | export 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 | */ |
| 347 | export 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 | })) |