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 */
12import type * as Cause from "../Cause.ts"
13import * as Context from "../Context.ts"
14import * as Effect from "../Effect.ts"
15import { identity } from "../Function.ts"
16import type { Headers } from "../http/Headers.ts"
17import * as Layer from "../Layer.ts"
18import type { Pipeable } from "../Pipeable.ts"
19import type * as Queue from "../Queue.ts"
20import type * as Record from "../Record.ts"
21import type { Scope } from "../Scope.ts"
22import * as Stream from "../Stream.ts"
23import * as Rpc from "./Rpc.ts"
24import type { RequestId } from "./RpcMessage.ts"
25import type * as RpcMiddleware from "./RpcMiddleware.ts"
26
27const 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 */
37export 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 */
179export 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 */
191export 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 */
203export 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 */
214export 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 */
226export 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 */
256export type Rpcs<Group> = Group extends RpcGroup<infer R> ? string extends R["_tag"] ? never : R : never
257
258const 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
395const 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 */
411export 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 })