| 1 | /** |
| 2 | * Collects typed RPC definitions and server handlers. |
| 3 | * |
| 4 | * An `RpcGroup` stores RPC definitions by tag and keeps annotations shared by |
| 5 | * the group. This module provides helpers for composing groups, applying |
| 6 | * middleware or annotations, deriving handler types, and turning handler objects |
| 7 | * into `Context` or `Layer` values used by RPC servers. |
| 8 | * |
| 9 | * @stability unstable |
| 10 | * @since 4.0.0 |
| 11 | */ |
| 12 | import type * as Cause from "../Cause.ts" |
| 13 | import * as Context from "../Context.ts" |
| 14 | import * as Effect from "../Effect.ts" |
| 15 | import { identity } from "../Function.ts" |
| 16 | import type { Headers } from "../http/Headers.ts" |
| 17 | import * as Layer from "../Layer.ts" |
| 18 | import type { Pipeable } from "../Pipeable.ts" |
| 19 | import type * as Queue from "../Queue.ts" |
| 20 | import type * as Record from "../Record.ts" |
| 21 | import type { Scope } from "../Scope.ts" |
| 22 | import * as Stream from "../Stream.ts" |
| 23 | import * as Rpc from "./Rpc.ts" |
| 24 | import type { RequestId } from "./RpcMessage.ts" |
| 25 | import type * as RpcMiddleware from "./RpcMiddleware.ts" |
| 26 | |
| 27 | const TypeId = "~effect/rpc/RpcGroup" |
| 28 | |
| 29 | /** |
| 30 | * A collection of RPC definitions that can be composed, annotated, and |
| 31 | * converted into server handlers or layers. |
| 32 | * |
| 33 | * @stability unstable |
| 34 | * @category models |
| 35 | * @since 4.0.0 |
| 36 | */ |
| 37 | export interface RpcGroup<in out R extends Rpc.Any> extends Pipeable { |
| 38 | new(_: never): {} |
| 39 | |
| 40 | readonly [TypeId]: typeof TypeId |
| 41 | readonly requests: ReadonlyMap<string, R> |
| 42 | readonly annotations: Context.Context<never> |
| 43 | |
| 44 | /** |
| 45 | * Add one or more procedures to the group. |
| 46 | */ |
| 47 | add<const Rpcs2 extends ReadonlyArray<Rpc.Any>>( |
| 48 | ...rpcs: Rpcs2 |
| 49 | ): RpcGroup<R | Rpcs2[number]> |
| 50 | |
| 51 | /** |
| 52 | * Merge this group with one or more other groups. |
| 53 | */ |
| 54 | merge<const Groups extends ReadonlyArray<Any>>( |
| 55 | ...groups: Groups |
| 56 | ): RpcGroup<R | Rpcs<Groups[number]>> |
| 57 | |
| 58 | /** |
| 59 | * Omit one or more procedures from the group. |
| 60 | */ |
| 61 | omit<const Tags extends ReadonlyArray<R["_tag"]>>( |
| 62 | ...tags: Tags |
| 63 | ): RpcGroup<Exclude<R, { readonly _tag: Tags[number] }>> |
| 64 | |
| 65 | /** |
| 66 | * Add middleware to all the procedures added to the group until this point. |
| 67 | */ |
| 68 | middleware<M extends RpcMiddleware.AnyService>(middleware: M): RpcGroup<Rpc.AddMiddleware<R, M>> |
| 69 | |
| 70 | /** |
| 71 | * Add a prefix to the procedures in this group, returning a new group |
| 72 | */ |
| 73 | prefix<const Prefix extends string>(prefix: Prefix): RpcGroup<Rpc.Prefixed<R, Prefix>> |
| 74 | |
| 75 | /** |
| 76 | * Implement the handlers for the procedures in this group, returning a |
| 77 | * context object. |
| 78 | */ |
| 79 | toHandlers< |
| 80 | Handlers extends HandlersFrom<R>, |
| 81 | EX = never, |
| 82 | RX = never |
| 83 | >( |
| 84 | build: |
| 85 | | Handlers |
| 86 | | Effect.Effect<Handlers, EX, RX> |
| 87 | ): Effect.Effect< |
| 88 | Context.Context<Rpc.ToHandler<R>>, |
| 89 | EX, |
| 90 | | RX |
| 91 | | HandlersServices<R, Handlers> |
| 92 | > |
| 93 | |
| 94 | /** |
| 95 | * Implement the handlers for the procedures in this group. |
| 96 | */ |
| 97 | toLayer< |
| 98 | Handlers extends HandlersFrom<R>, |
| 99 | EX = never, |
| 100 | RX = never |
| 101 | >( |
| 102 | build: |
| 103 | | Handlers |
| 104 | | Effect.Effect<Handlers, EX, RX> |
| 105 | ): Layer.Layer< |
| 106 | Rpc.ToHandler<R>, |
| 107 | EX, |
| 108 | | Exclude<RX, Scope> |
| 109 | | HandlersServices<R, Handlers> |
| 110 | > |
| 111 | |
| 112 | of<const Handlers extends HandlersFrom<R>>(handlers: Handlers): Handlers |
| 113 | |
| 114 | /** |
| 115 | * Implement a single handler from the group. |
| 116 | */ |
| 117 | toLayerHandler< |
| 118 | const Tag extends R["_tag"], |
| 119 | Handler extends HandlerFrom<R, Tag>, |
| 120 | EX = never, |
| 121 | RX = never |
| 122 | >( |
| 123 | tag: Tag, |
| 124 | build: |
| 125 | | Handler |
| 126 | | Effect.Effect<Handler, EX, RX> |
| 127 | ): Layer.Layer< |
| 128 | Rpc.Handler<Tag>, |
| 129 | EX, |
| 130 | | Exclude<RX, Scope> |
| 131 | | HandlerServices<R, Tag, Handler> |
| 132 | > |
| 133 | |
| 134 | /** |
| 135 | * Retrieve a handler for a specific procedure in the group. |
| 136 | */ |
| 137 | accessHandler<const Tag extends R["_tag"]>(tag: Tag): Effect.Effect< |
| 138 | ( |
| 139 | payload: Rpc.Payload<Extract<R, { readonly _tag: Tag }>>, |
| 140 | options: { |
| 141 | readonly client: Rpc.ServerClient |
| 142 | readonly requestId: RequestId |
| 143 | readonly headers: Headers |
| 144 | } |
| 145 | ) => Rpc.ResultFrom<Extract<R, { readonly _tag: Tag }>, never>, |
| 146 | never, |
| 147 | Rpc.Handler<Tag> |
| 148 | > |
| 149 | |
| 150 | /** |
| 151 | * Annotate the group with a value. |
| 152 | */ |
| 153 | annotate<I, S>(service: Context.Key<I, S>, value: S): RpcGroup<R> |
| 154 | |
| 155 | /** |
| 156 | * Annotate the Rpc's above this point with a value. |
| 157 | */ |
| 158 | annotateRpcs<I, S>(service: Context.Key<I, S>, value: S): RpcGroup<R> |
| 159 | |
| 160 | /** |
| 161 | * Annotate the group with the provided annotations. |
| 162 | */ |
| 163 | annotateMerge<S>(annotations: Context.Context<S>): RpcGroup<R> |
| 164 | |
| 165 | /** |
| 166 | * Annotate the Rpc's above this point with the provided annotations. |
| 167 | */ |
| 168 | annotateRpcsMerge<S>(annotations: Context.Context<S>): RpcGroup<R> |
| 169 | } |
| 170 | |
| 171 | /** |
| 172 | * An erased `RpcGroup` type for APIs that only need to know that a value is an |
| 173 | * RPC group. |
| 174 | * |
| 175 | * @stability unstable |
| 176 | * @category utility types |
| 177 | * @since 4.0.0 |
| 178 | */ |
| 179 | export interface Any { |
| 180 | readonly [TypeId]: typeof TypeId |
| 181 | } |
| 182 | |
| 183 | /** |
| 184 | * Builds the object type of server handler functions required to implement each |
| 185 | * RPC in a union. |
| 186 | * |
| 187 | * @stability unstable |
| 188 | * @category utility types |
| 189 | * @since 4.0.0 |
| 190 | */ |
| 191 | export type HandlersFrom<Rpc extends Rpc.Any> = { |
| 192 | readonly [Current in Rpc as Current["_tag"]]: Rpc.ToHandlerFn<Current> |
| 193 | } |
| 194 | |
| 195 | /** |
| 196 | * Extracts the server handler function type for a specific RPC tag from an RPC |
| 197 | * union. |
| 198 | * |
| 199 | * @stability unstable |
| 200 | * @category utility types |
| 201 | * @since 4.0.0 |
| 202 | */ |
| 203 | export type HandlerFrom<Rpc extends Rpc.Any, Tag extends Rpc["_tag"]> = Extract<Rpc, { readonly _tag: Tag }> extends |
| 204 | infer Current ? Current extends Rpc.Any ? Rpc.ToHandlerFn<Current> : never : never |
| 205 | |
| 206 | /** |
| 207 | * Computes the services required by all handlers in a handler object for an RPC |
| 208 | * union. |
| 209 | * |
| 210 | * @stability unstable |
| 211 | * @category utility types |
| 212 | * @since 4.0.0 |
| 213 | */ |
| 214 | export type HandlersServices<Rpcs extends Rpc.Any, Handlers> = keyof Handlers extends infer K ? |
| 215 | K extends keyof Handlers & string ? HandlerServices<Rpcs, K, Handlers[K]> : never : |
| 216 | never |
| 217 | |
| 218 | /** |
| 219 | * Computes the services required by a single RPC handler, excluding services |
| 220 | * provided by middleware and `Scope` where the server supplies it. |
| 221 | * |
| 222 | * @stability unstable |
| 223 | * @category utility types |
| 224 | * @since 4.0.0 |
| 225 | */ |
| 226 | export type HandlerServices<Rpcs extends Rpc.Any, K extends Rpcs["_tag"], Handler> = true extends |
| 227 | Rpc.IsStream<Rpcs, K> ? Handler extends (...args: any) => |
| 228 | | Stream.Stream<infer _A, infer _E, infer _R> |
| 229 | | Rpc.Wrapper<Stream.Stream<infer _A, infer _E, infer _R>> |
| 230 | | Effect.Effect< |
| 231 | Queue.Dequeue<infer _A, infer _E | Cause.Done>, |
| 232 | infer _EX, |
| 233 | infer _R |
| 234 | > |
| 235 | | Rpc.Wrapper< |
| 236 | Effect.Effect< |
| 237 | Queue.Dequeue<infer _A, infer _E | Cause.Done>, |
| 238 | infer _EX, |
| 239 | infer _R |
| 240 | > |
| 241 | > ? Exclude<Rpc.ExcludeProvides<_R, Rpcs, K>, Scope> | Rpc.ExtractRequires<Rpcs, K> : |
| 242 | never : |
| 243 | Handler extends ( |
| 244 | ...args: any |
| 245 | ) => Effect.Effect<infer _A, infer _E, infer _R> | Rpc.Wrapper<Effect.Effect<infer _A, infer _E, infer _R>> ? |
| 246 | Exclude<Rpc.ExcludeProvides<_R, Rpcs, K>, Scope> | Rpc.ExtractRequires<Rpcs, K> |
| 247 | : never |
| 248 | |
| 249 | /** |
| 250 | * Extracts the union of RPC definitions from an `RpcGroup`. |
| 251 | * |
| 252 | * @stability unstable |
| 253 | * @category utility types |
| 254 | * @since 4.0.0 |
| 255 | */ |
| 256 | export type Rpcs<Group> = Group extends RpcGroup<infer R> ? string extends R["_tag"] ? never : R : never |
| 257 | |
| 258 | const RpcGroupProto = { |
| 259 | add(this: RpcGroup<any>, ...rpcs: Array<Rpc.Any>) { |
| 260 | const requests = new Map(this.requests) |
| 261 | for (const rpc of rpcs) { |
| 262 | requests.set(rpc._tag, rpc) |
| 263 | } |
| 264 | return makeProto({ |
| 265 | requests, |
| 266 | annotations: this.annotations |
| 267 | }) |
| 268 | }, |
| 269 | merge(this: RpcGroup<any>, ...groups: ReadonlyArray<RpcGroup<any>>) { |
| 270 | const requests = new Map(this.requests) |
| 271 | const annotations = new Map(this.annotations.mapUnsafe) |
| 272 | |
| 273 | for (const group of groups) { |
| 274 | for (const [tag, rpc] of group.requests) { |
| 275 | requests.set(tag, rpc) |
| 276 | } |
| 277 | for (const [key, value] of group.annotations.mapUnsafe) { |
| 278 | annotations.set(key, value) |
| 279 | } |
| 280 | } |
| 281 | |
| 282 | return makeProto({ |
| 283 | requests, |
| 284 | annotations: Context.makeUnsafe(annotations) |
| 285 | }) |
| 286 | }, |
| 287 | omit(this: RpcGroup<any>, ...tags: Array<string>) { |
| 288 | const requests = new Map(this.requests) |
| 289 | for (const tag of tags) { |
| 290 | requests.delete(tag) |
| 291 | } |
| 292 | return makeProto({ |
| 293 | requests, |
| 294 | annotations: this.annotations |
| 295 | }) |
| 296 | }, |
| 297 | middleware(this: RpcGroup<any>, middleware: RpcMiddleware.AnyService) { |
| 298 | const requests = new Map<string, any>() |
| 299 | for (const [tag, rpc] of this.requests) { |
| 300 | requests.set(tag, rpc.middleware(middleware)) |
| 301 | } |
| 302 | return makeProto({ |
| 303 | requests, |
| 304 | annotations: this.annotations |
| 305 | }) |
| 306 | }, |
| 307 | toHandlers(this: RpcGroup<any>, build: Effect.Effect<Record<string, (request: any) => any>>) { |
| 308 | // oxlint-disable-next-line no-this-alias |
| 309 | const self = this |
| 310 | return Effect.gen(function*() { |
| 311 | const services = yield* Effect.context<never>() |
| 312 | const handlers = Effect.isEffect(build) ? yield* build : build |
| 313 | const contextMap = new Map<string, unknown>() |
| 314 | self.requests.forEach((rpc, tag) => { |
| 315 | contextMap.set(rpc.key, { |
| 316 | tag: rpc._tag, |
| 317 | handler: handlers[tag], |
| 318 | context: services |
| 319 | }) |
| 320 | }) |
| 321 | return Context.makeUnsafe(contextMap) |
| 322 | }) |
| 323 | }, |
| 324 | prefix<const Prefix extends string>(this: RpcGroup<any>, prefix: Prefix) { |
| 325 | const requests = new Map<string, any>() |
| 326 | for (const rpc of this.requests.values()) { |
| 327 | const newRpc = rpc.prefix(prefix) |
| 328 | requests.set(newRpc._tag, newRpc) |
| 329 | } |
| 330 | return makeProto({ |
| 331 | requests, |
| 332 | annotations: this.annotations |
| 333 | }) |
| 334 | }, |
| 335 | toLayer(this: RpcGroup<any>, build: Effect.Effect<Record<string, (request: any) => any>>) { |
| 336 | return Layer.effectContext(this.toHandlers(build)) |
| 337 | }, |
| 338 | of: identity, |
| 339 | toLayerHandler(this: RpcGroup<any>, service: string, build: Effect.Effect<Record<string, (request: any) => any>>) { |
| 340 | // oxlint-disable-next-line no-this-alias |
| 341 | const self = this |
| 342 | return Layer.effectContext(Effect.gen(function*() { |
| 343 | const services = yield* Effect.context<never>() |
| 344 | const handler = Effect.isEffect(build) ? yield* build : build |
| 345 | const contextMap = new Map<string, unknown>() |
| 346 | const rpc = self.requests.get(service)! |
| 347 | contextMap.set(rpc.key, { |
| 348 | handler, |
| 349 | context: services |
| 350 | }) |
| 351 | return Context.makeUnsafe(contextMap) |
| 352 | })) |
| 353 | }, |
| 354 | accessHandler(this: RpcGroup<any>, service: string) { |
| 355 | return Effect.contextWith((parentContext: Context.Context<any>) => { |
| 356 | const rpc = this.requests.get(service)! |
| 357 | const { handler, context } = Context.getOrUndefinedUnsafe(parentContext, rpc.key) as Rpc.Handler<any> |
| 358 | return Effect.succeed((payload: Rpc.Payload<any>, options: any) => { |
| 359 | options.rpc = rpc |
| 360 | const result = handler(payload, options) |
| 361 | const effectOrStream = Rpc.isWrapper(result) ? result.value : result |
| 362 | return Effect.isEffect(effectOrStream) |
| 363 | ? Effect.provide(effectOrStream, context) |
| 364 | : Stream.provideContext(effectOrStream, context) |
| 365 | }) |
| 366 | }) |
| 367 | }, |
| 368 | annotate(this: RpcGroup<any>, service: Context.Key<any, any>, value: any) { |
| 369 | return makeProto({ |
| 370 | requests: this.requests, |
| 371 | annotations: Context.add(this.annotations, service, value) |
| 372 | }) |
| 373 | }, |
| 374 | annotateRpcs(this: RpcGroup<any>, service: Context.Key<any, any>, value: any) { |
| 375 | return this.annotateRpcsMerge(Context.make(service, value)) |
| 376 | }, |
| 377 | annotateMerge(this: RpcGroup<any>, context: Context.Context<any>) { |
| 378 | return makeProto({ |
| 379 | requests: this.requests, |
| 380 | annotations: Context.merge(this.annotations, context) |
| 381 | }) |
| 382 | }, |
| 383 | annotateRpcsMerge(this: RpcGroup<any>, context: Context.Context<any>) { |
| 384 | const requests = new Map<string, any>() |
| 385 | for (const [tag, rpc] of this.requests) { |
| 386 | requests.set(tag, rpc.annotateMerge(Context.merge(context, rpc.annotations))) |
| 387 | } |
| 388 | return makeProto({ |
| 389 | requests, |
| 390 | annotations: this.annotations |
| 391 | }) |
| 392 | } |
| 393 | } |
| 394 | |
| 395 | const makeProto = <Rpcs extends Rpc.Any>(options: { |
| 396 | readonly requests: ReadonlyMap<string, Rpcs> |
| 397 | readonly annotations: Context.Context<never> |
| 398 | }): RpcGroup<Rpcs> => |
| 399 | Object.assign(function() {}, RpcGroupProto, { |
| 400 | requests: options.requests, |
| 401 | annotations: options.annotations |
| 402 | }) as any |
| 403 | |
| 404 | /** |
| 405 | * Creates an `RpcGroup` from one or more RPC definitions. |
| 406 | * |
| 407 | * @stability unstable |
| 408 | * @category constructors |
| 409 | * @since 4.0.0 |
| 410 | */ |
| 411 | export const make = <const Rpcs extends ReadonlyArray<Rpc.Any>>( |
| 412 | ...rpcs: Rpcs |
| 413 | ): RpcGroup<Rpcs[number]> => |
| 414 | makeProto({ |
| 415 | requests: new Map(rpcs.map((rpc) => [rpc._tag, rpc])), |
| 416 | annotations: Context.empty() |
| 417 | }) |