orpc-fn 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +250 -0
  3. package/dist/bound-call.d.ts +23 -0
  4. package/dist/bound-call.d.ts.map +1 -0
  5. package/dist/bound-call.js +23 -0
  6. package/dist/bound-call.js.map +1 -0
  7. package/dist/client.d.ts +44 -0
  8. package/dist/client.d.ts.map +1 -0
  9. package/dist/client.js +49 -0
  10. package/dist/client.js.map +1 -0
  11. package/dist/create-fn.d.ts +97 -0
  12. package/dist/create-fn.d.ts.map +1 -0
  13. package/dist/create-fn.js +224 -0
  14. package/dist/create-fn.js.map +1 -0
  15. package/dist/expected-client-error.d.ts +11 -0
  16. package/dist/expected-client-error.d.ts.map +1 -0
  17. package/dist/expected-client-error.js +21 -0
  18. package/dist/expected-client-error.js.map +1 -0
  19. package/dist/expo.d.ts +66 -0
  20. package/dist/expo.d.ts.map +1 -0
  21. package/dist/expo.js +74 -0
  22. package/dist/expo.js.map +1 -0
  23. package/dist/hono.d.ts +81 -0
  24. package/dist/hono.d.ts.map +1 -0
  25. package/dist/hono.js +138 -0
  26. package/dist/hono.js.map +1 -0
  27. package/dist/index.d.ts +10 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +8 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/json-schema.d.ts +47 -0
  32. package/dist/json-schema.d.ts.map +1 -0
  33. package/dist/json-schema.js +44 -0
  34. package/dist/json-schema.js.map +1 -0
  35. package/dist/live/codec.d.ts +9 -0
  36. package/dist/live/codec.d.ts.map +1 -0
  37. package/dist/live/codec.js +27 -0
  38. package/dist/live/codec.js.map +1 -0
  39. package/dist/live/fn-live.d.ts +155 -0
  40. package/dist/live/fn-live.d.ts.map +1 -0
  41. package/dist/live/fn-live.js +210 -0
  42. package/dist/live/fn-live.js.map +1 -0
  43. package/dist/live/index.d.ts +4 -0
  44. package/dist/live/index.d.ts.map +1 -0
  45. package/dist/live/index.js +5 -0
  46. package/dist/live/index.js.map +1 -0
  47. package/dist/live/ioredis.d.ts +29 -0
  48. package/dist/live/ioredis.d.ts.map +1 -0
  49. package/dist/live/ioredis.js +49 -0
  50. package/dist/live/ioredis.js.map +1 -0
  51. package/dist/live/memory.d.ts +10 -0
  52. package/dist/live/memory.d.ts.map +1 -0
  53. package/dist/live/memory.js +45 -0
  54. package/dist/live/memory.js.map +1 -0
  55. package/dist/live/pub-sub.d.ts +211 -0
  56. package/dist/live/pub-sub.d.ts.map +1 -0
  57. package/dist/live/pub-sub.js +529 -0
  58. package/dist/live/pub-sub.js.map +1 -0
  59. package/dist/live/redis-bun.d.ts +26 -0
  60. package/dist/live/redis-bun.d.ts.map +1 -0
  61. package/dist/live/redis-bun.js +59 -0
  62. package/dist/live/redis-bun.js.map +1 -0
  63. package/dist/live/redis.d.ts +45 -0
  64. package/dist/live/redis.d.ts.map +1 -0
  65. package/dist/live/redis.js +214 -0
  66. package/dist/live/redis.js.map +1 -0
  67. package/dist/live/transport.d.ts +35 -0
  68. package/dist/live/transport.d.ts.map +1 -0
  69. package/dist/live/transport.js +2 -0
  70. package/dist/live/transport.js.map +1 -0
  71. package/dist/logger.d.ts +15 -0
  72. package/dist/logger.d.ts.map +1 -0
  73. package/dist/logger.js +15 -0
  74. package/dist/logger.js.map +1 -0
  75. package/dist/mastra.d.ts +44 -0
  76. package/dist/mastra.d.ts.map +1 -0
  77. package/dist/mastra.js +78 -0
  78. package/dist/mastra.js.map +1 -0
  79. package/dist/mcp.d.ts +39 -0
  80. package/dist/mcp.d.ts.map +1 -0
  81. package/dist/mcp.js +38 -0
  82. package/dist/mcp.js.map +1 -0
  83. package/dist/meta.d.ts +35 -0
  84. package/dist/meta.d.ts.map +1 -0
  85. package/dist/meta.js +30 -0
  86. package/dist/meta.js.map +1 -0
  87. package/dist/otel.d.ts +66 -0
  88. package/dist/otel.d.ts.map +1 -0
  89. package/dist/otel.js +63 -0
  90. package/dist/otel.js.map +1 -0
  91. package/dist/router.d.ts +13 -0
  92. package/dist/router.d.ts.map +1 -0
  93. package/dist/router.js +14 -0
  94. package/dist/router.js.map +1 -0
  95. package/dist/stream.d.ts +13 -0
  96. package/dist/stream.d.ts.map +1 -0
  97. package/dist/stream.js +44 -0
  98. package/dist/stream.js.map +1 -0
  99. package/dist/types.d.ts +196 -0
  100. package/dist/types.d.ts.map +1 -0
  101. package/dist/types.js +29 -0
  102. package/dist/types.js.map +1 -0
  103. package/package.json +157 -0
  104. package/src/bound-call.ts +50 -0
  105. package/src/client.ts +82 -0
  106. package/src/create-fn.ts +461 -0
  107. package/src/expected-client-error.ts +23 -0
  108. package/src/expo.ts +127 -0
  109. package/src/hono.ts +247 -0
  110. package/src/index.ts +45 -0
  111. package/src/json-schema.ts +82 -0
  112. package/src/live/codec.ts +31 -0
  113. package/src/live/fn-live.ts +470 -0
  114. package/src/live/index.ts +31 -0
  115. package/src/live/ioredis.ts +74 -0
  116. package/src/live/memory.ts +50 -0
  117. package/src/live/pub-sub.ts +828 -0
  118. package/src/live/redis-bun.ts +76 -0
  119. package/src/live/redis.ts +255 -0
  120. package/src/live/transport.ts +41 -0
  121. package/src/logger.ts +28 -0
  122. package/src/mastra.ts +140 -0
  123. package/src/mcp.ts +73 -0
  124. package/src/meta.ts +72 -0
  125. package/src/otel.ts +151 -0
  126. package/src/router.ts +15 -0
  127. package/src/stream.ts +54 -0
  128. package/src/types.ts +389 -0
package/src/otel.ts ADDED
@@ -0,0 +1,151 @@
1
+ import {
2
+ isAsyncIteratorObject,
3
+ onStreamEnd,
4
+ type StreamScope
5
+ } from './stream.js'
6
+
7
+ /** Attribute values accepted by OpenTelemetry spans. */
8
+ export type AttributeValue = string | number | boolean
9
+
10
+ /** The part of an OpenTelemetry `Span` this package uses. */
11
+ export interface SpanLike {
12
+ setAttribute(key: string, value: AttributeValue): unknown
13
+ setStatus(status: { code: number; message?: string }): unknown
14
+ recordException(exception: Error): unknown
15
+ end(): unknown
16
+ }
17
+
18
+ type SpanOptionsLike = {
19
+ kind?: number
20
+ attributes?: Record<string, AttributeValue>
21
+ }
22
+
23
+ interface TracerLike<TSpan> {
24
+ startActiveSpan<F extends (span: TSpan) => unknown>(
25
+ name: string,
26
+ options: SpanOptionsLike,
27
+ fn: F
28
+ ): ReturnType<F>
29
+ startSpan(name: string, options?: SpanOptionsLike): TSpan
30
+ }
31
+
32
+ /**
33
+ * The part of `@opentelemetry/api` this package uses. Pass the module itself:
34
+ * `import * as otel from '@opentelemetry/api'` then `createFn({ otel })`.
35
+ * Without it no spans are created and handlers receive `span: undefined`.
36
+ */
37
+ export interface OtelApiLike<TSpan extends SpanLike = SpanLike> {
38
+ trace: { getTracer(name: string): TracerLike<TSpan> }
39
+ SpanStatusCode: { OK: number; ERROR: number }
40
+ SpanKind: {
41
+ INTERNAL: number
42
+ SERVER: number
43
+ PRODUCER: number
44
+ }
45
+ /** Restores a span's context for each step of a stream it returned. */
46
+ context: {
47
+ active(): unknown
48
+ with<T>(context: never, fn: () => T): T
49
+ }
50
+ }
51
+
52
+ /** The span type of an OpenTelemetry API, or `SpanLike` when absent. */
53
+ export type SpanOf<TOtel> =
54
+ TOtel extends OtelApiLike<infer TSpan> ? TSpan : SpanLike
55
+
56
+ /** Called once when a span's work is done, before the span ends. */
57
+ export type OnSettled = (
58
+ span: SpanLike | undefined,
59
+ ...error: [] | [unknown]
60
+ ) => void
61
+
62
+ export type Tracing = {
63
+ /**
64
+ * Run `fn` inside an active span: OK when it resolves, ERROR when it throws,
65
+ * ended either way. When `fn` resolves to a stream, the span stays open -
66
+ * and active for each step of the stream - until the stream ends.
67
+ * `onSettled` runs once at that point, before the span ends. Without
68
+ * OpenTelemetry `span` is undefined and `onSettled` still runs.
69
+ */
70
+ inSpan<T>(
71
+ name: string,
72
+ kind: 'INTERNAL' | 'SERVER' | 'PRODUCER',
73
+ fn: (span: SpanLike | undefined) => Promise<T>,
74
+ onSettled?: OnSettled
75
+ ): Promise<T>
76
+ /** Start a span that the caller ends with `end` (long-lived streams). */
77
+ startSpan(
78
+ name: string,
79
+ kind: 'SERVER',
80
+ attributes: Record<string, AttributeValue>
81
+ ): SpanLike | undefined
82
+ /** Set OK, or ERROR with `error`, and end the span. */
83
+ end(span: SpanLike | undefined, error?: unknown): void
84
+ }
85
+
86
+ export function createTracing(otel: OtelApiLike | undefined): Tracing {
87
+ const tracer = otel?.trace.getTracer('orpc-fn')
88
+ const end: Tracing['end'] = (span, ...error) => {
89
+ if (!span || !otel) return
90
+ if (error.length === 0) {
91
+ span.setStatus({ code: otel.SpanStatusCode.OK })
92
+ } else {
93
+ span.setStatus({
94
+ code: otel.SpanStatusCode.ERROR,
95
+ message: errorMessageOf(error[0])
96
+ })
97
+ if (error[0] instanceof Error) span.recordException(error[0])
98
+ }
99
+ span.end()
100
+ }
101
+ return {
102
+ inSpan(name, kind, fn, onSettled) {
103
+ const settle = (span: SpanLike | undefined, ...error: [] | [unknown]) => {
104
+ try {
105
+ onSettled?.(span, ...error)
106
+ } finally {
107
+ end(span, ...error)
108
+ }
109
+ }
110
+ const run = async (
111
+ span: SpanLike | undefined,
112
+ scope?: StreamScope
113
+ ): Promise<Awaited<ReturnType<typeof fn>>> => {
114
+ let result: Awaited<ReturnType<typeof fn>>
115
+ try {
116
+ result = await fn(span)
117
+ } catch (error) {
118
+ settle(span, error)
119
+ throw error
120
+ }
121
+ if (isAsyncIteratorObject(result)) {
122
+ return onStreamEnd(
123
+ result,
124
+ (...error) => settle(span, ...error),
125
+ scope
126
+ ) as typeof result
127
+ }
128
+ settle(span)
129
+ return result
130
+ }
131
+ if (!tracer || !otel) return run(undefined)
132
+ return tracer.startActiveSpan(
133
+ name,
134
+ { kind: otel.SpanKind[kind] },
135
+ (span) => {
136
+ const active = otel.context.active()
137
+ return run(span, (step) => otel.context.with(active as never, step))
138
+ }
139
+ )
140
+ },
141
+ startSpan(name, kind, attributes) {
142
+ if (!tracer || !otel) return undefined
143
+ return tracer.startSpan(name, { kind: otel.SpanKind[kind], attributes })
144
+ },
145
+ end
146
+ }
147
+ }
148
+
149
+ export function errorMessageOf(error: unknown): string {
150
+ return error instanceof Error ? error.message : String(error)
151
+ }
package/src/router.ts ADDED
@@ -0,0 +1,15 @@
1
+ import type { AnyRouter } from '@orpc/server'
2
+
3
+ /**
4
+ * Creates a router object from routes or nested routers.
5
+ * Simply returns the routes as a const object for type inference.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const items = createRouter({ getItem, updateItem })
10
+ * export const api = createRouter({ metabase, items })
11
+ * ```
12
+ */
13
+ export function createRouter<T extends AnyRouter>(routes: T): Readonly<T> {
14
+ return routes
15
+ }
package/src/stream.ts ADDED
@@ -0,0 +1,54 @@
1
+ /** A value oRPC streams to the client (an async generator or iterator). */
2
+ export function isAsyncIteratorObject(
3
+ value: unknown
4
+ ): value is AsyncIterator<unknown> & AsyncIterable<unknown> {
5
+ return (
6
+ typeof value === 'object' &&
7
+ value !== null &&
8
+ typeof (value as AsyncIterable<unknown>)[Symbol.asyncIterator] ===
9
+ 'function' &&
10
+ typeof (value as AsyncIterator<unknown>).next === 'function'
11
+ )
12
+ }
13
+
14
+ /** Runs one step of a stream inside some context (e.g. its tracing span). */
15
+ export type StreamScope = <T>(step: () => T) => T
16
+
17
+ /**
18
+ * Wraps a stream so `finish` runs once when it ends: after the last item,
19
+ * when it throws (with the error), or when the consumer stops early with
20
+ * `return()` - also before the first `next()`, which a plain async generator
21
+ * wrapper would miss. With `scope`, every `next`/`return`/`throw` runs inside
22
+ * it: a generator's body runs on those calls, long after it was created.
23
+ */
24
+ export function onStreamEnd<T>(
25
+ stream: AsyncIterator<T> & AsyncIterable<T>,
26
+ finish: (...error: [] | [unknown]) => void,
27
+ scope: StreamScope = (step) => step()
28
+ ): AsyncIterator<T> & AsyncIterable<T> {
29
+ let finished = false
30
+ const done = (...error: [] | [unknown]) => {
31
+ if (finished) return
32
+ finished = true
33
+ finish(...error)
34
+ }
35
+ const settle = async (step: () => Promise<IteratorResult<T>> | undefined) => {
36
+ try {
37
+ const result = await step()
38
+ if (!result || result.done) done()
39
+ return result ?? { done: true as const, value: undefined }
40
+ } catch (error) {
41
+ done(error)
42
+ throw error
43
+ }
44
+ }
45
+ const wrapped: AsyncIterator<T> & AsyncIterable<T> = {
46
+ next: (...args) => settle(() => scope(() => stream.next(...args))),
47
+ return: (value) =>
48
+ settle(() => scope(() => stream.return?.(value))).finally(() => done()),
49
+ throw: (error) =>
50
+ settle(() => scope(() => stream.throw?.(error) ?? Promise.reject(error))),
51
+ [Symbol.asyncIterator]: () => wrapped
52
+ }
53
+ return wrapped
54
+ }
package/src/types.ts ADDED
@@ -0,0 +1,389 @@
1
+ import type {
2
+ AnySchema,
3
+ ErrorMap,
4
+ InferSchemaInput,
5
+ InferSchemaOutput,
6
+ Meta,
7
+ Schema
8
+ } from '@orpc/contract'
9
+ import type {
10
+ AnyProcedure,
11
+ DecoratedProcedure,
12
+ Procedure,
13
+ Route
14
+ } from '@orpc/server'
15
+ import type { ZodType, z } from 'zod'
16
+ import type { BoundCall } from './bound-call.js'
17
+ import type { FnLogger } from './logger.js'
18
+ import type { SpanLike } from './otel.js'
19
+
20
+ export type MaybePromise<T> = T | Promise<T>
21
+
22
+ /**
23
+ * An oRPC builder without input or output schema: `os`, `os.$context<T>()`,
24
+ * `os.use(...)`. A schema set on the builder is rejected by `createFn`; set it
25
+ * on the route instead.
26
+ */
27
+ /* biome-ignore-start lint/suspicious/noExplicitAny: matches every builder's generic methods */
28
+ export type BuilderLike = {
29
+ route(route: any): unknown
30
+ meta(meta: any): unknown
31
+ input(schema: any): unknown
32
+ output(schema: any): unknown
33
+ handler(handler: any): Procedure<any, any, any, any, any, any>
34
+ }
35
+ /* biome-ignore-end lint/suspicious/noExplicitAny: matches every builder's generic methods */
36
+
37
+ /** Input a procedure is called with (before its input schema runs). */
38
+ export type ProcedureInput<T> =
39
+ T extends Procedure<
40
+ infer _TInitial,
41
+ infer _TCurrent,
42
+ infer TInputSchema extends AnySchema,
43
+ infer _TOut,
44
+ infer _TErrors,
45
+ infer _TMeta
46
+ >
47
+ ? InferSchemaInput<TInputSchema>
48
+ : unknown
49
+
50
+ /** What a procedure resolves to (after its output schema runs). */
51
+ export type ProcedureOutput<T> =
52
+ T extends Procedure<
53
+ infer _TInitial,
54
+ infer _TCurrent,
55
+ infer _TIn,
56
+ infer TOutputSchema extends AnySchema,
57
+ infer _TErrors,
58
+ infer _TMeta
59
+ >
60
+ ? InferSchemaOutput<TOutputSchema>
61
+ : unknown
62
+
63
+ /** Context a procedure must be called with. */
64
+ export type ProcedureContext<T> =
65
+ T extends Procedure<
66
+ infer TContext,
67
+ infer _TCurrent,
68
+ infer _TIn,
69
+ infer _TOut,
70
+ infer _TErrors,
71
+ infer _TMeta
72
+ >
73
+ ? TContext
74
+ : never
75
+
76
+ /** `Omit` that keeps each member of a union (handler params narrow by discriminant). */
77
+ export type DistributiveOmit<T, K extends PropertyKey> = T extends unknown
78
+ ? Omit<T, K>
79
+ : never
80
+
81
+ /** `fn()` options passed to oRPC's `.route()`; `RouteConfig` is derived from it. */
82
+ export const ROUTE_OPTION_KEYS = [
83
+ 'path',
84
+ 'method',
85
+ 'summary',
86
+ 'description',
87
+ 'deprecated',
88
+ 'successStatus',
89
+ 'successDescription',
90
+ 'inputStructure',
91
+ 'outputStructure',
92
+ 'tags'
93
+ ] as const satisfies readonly (keyof Route)[]
94
+
95
+ /**
96
+ * `fn()` option names owned by the route itself. Guard and meta keys must not
97
+ * reuse them: such an option would be read as the route option and the guard
98
+ * would never run.
99
+ */
100
+ export const RESERVED_OPTION_KEYS = [
101
+ ...ROUTE_OPTION_KEYS,
102
+ 'name',
103
+ 'procedure',
104
+ 'input',
105
+ 'output',
106
+ 'handler',
107
+ 'live',
108
+ 'operationId'
109
+ ] as const
110
+
111
+ export type ReservedOptionKey = (typeof RESERVED_OPTION_KEYS)[number]
112
+
113
+ /** @internal Builds one route from a `procedures` builder; shared by `fn` and `createPubSub`. */
114
+ export type BuildProcedure = (options: {
115
+ name: string
116
+ procedure: string | undefined
117
+ route: Record<string, unknown>
118
+ meta: Record<string, unknown>
119
+ input?: AnySchema | undefined
120
+ output?: AnySchema | undefined
121
+ handler: (options: {
122
+ input: unknown
123
+ context: Record<string, unknown>
124
+ signal?: AbortSignal
125
+ }) => unknown
126
+ }) => AnyProcedure
127
+
128
+ /** Turns every key of `T` found in `K` into a readable type error. */
129
+ export type RejectKeys<T, K extends PropertyKey, TMessage extends string> = {
130
+ [P in Extract<keyof T, K>]: TMessage
131
+ }
132
+
133
+ /**
134
+ * Handler extras must have known keys: an index signature would claim every
135
+ * handler param. Intersected into `createFn`'s options to reject it.
136
+ */
137
+ export type FiniteExtras<T> = string extends keyof T
138
+ ? { extras: 'orpc-fn: extras need known keys, not an index signature' }
139
+ : number extends keyof T
140
+ ? { extras: 'orpc-fn: extras need known keys, not an index signature' }
141
+ : unknown
142
+
143
+ type ProcedureOf<TBuilder> = TBuilder extends {
144
+ handler(handler: never): infer TProcedure
145
+ }
146
+ ? TProcedure
147
+ : never
148
+
149
+ /** Context a route built from this builder is called with. */
150
+ export type InitialContextOf<TBuilder> = ProcedureContext<ProcedureOf<TBuilder>>
151
+
152
+ /** Context the handler sees, after the builder's middleware. */
153
+ export type CurrentContextOf<TBuilder> =
154
+ ProcedureOf<TBuilder> extends Procedure<
155
+ infer _TInitial,
156
+ infer TContext,
157
+ infer _TIn,
158
+ infer _TOut,
159
+ infer _TErrors,
160
+ infer _TMeta
161
+ >
162
+ ? TContext
163
+ : never
164
+
165
+ type ErrorMapOf<TBuilder> =
166
+ ProcedureOf<TBuilder> extends {
167
+ '~orpc': { errorMap: infer T extends ErrorMap }
168
+ }
169
+ ? T
170
+ : ErrorMap
171
+ type MetaOf<TBuilder> =
172
+ ProcedureOf<TBuilder> extends { '~orpc': { meta: infer T extends Meta } }
173
+ ? T
174
+ : Meta
175
+
176
+ /**
177
+ * Everything `createFn` inferred from its options. Route types read from it;
178
+ * apps rarely spell it out.
179
+ */
180
+ export type FnDefinition = {
181
+ procedures: Record<string, BuilderLike>
182
+ default: string
183
+ extras: object
184
+ guards: object
185
+ meta: object
186
+ tag: string
187
+ span: SpanLike
188
+ logger: FnLogger
189
+ }
190
+
191
+ export type ProcedureKey<TDef extends FnDefinition> = keyof TDef['procedures'] &
192
+ string
193
+
194
+ /** Context the handler of a `procedure: TKey` route receives. */
195
+ export type FnContext<
196
+ TDef extends FnDefinition,
197
+ TKey extends ProcedureKey<TDef>
198
+ > = CurrentContextOf<TDef['procedures'][TKey]>
199
+
200
+ /** Union of every procedure's handler context: what guards and extras see. */
201
+ export type AnyFnContext<TDef extends FnDefinition> = {
202
+ [K in ProcedureKey<TDef>]: FnContext<TDef, K>
203
+ }[ProcedureKey<TDef>]
204
+
205
+ export type GuardParams<TContext, TMeta> = {
206
+ context: TContext
207
+ input: unknown
208
+ /** Route name (operationId). */
209
+ name: string
210
+ /** Procedures key the route was built from. */
211
+ procedure: string
212
+ meta: Partial<TMeta>
213
+ signal: AbortSignal | undefined
214
+ }
215
+
216
+ /**
217
+ * A check run before the handler. Its first parameter becomes an option on
218
+ * `fn({...})`; the guard runs only on routes that set it. Throw (usually an
219
+ * `ORPCError`) to refuse the call.
220
+ */
221
+ export type Guard<TValue, TParams> = (value: TValue, params: TParams) => unknown
222
+
223
+ export type GuardOptions<TGuards> = {
224
+ [K in keyof TGuards]?: TGuards[K] extends (
225
+ value: infer TValue,
226
+ params: never
227
+ ) => unknown
228
+ ? TValue
229
+ : never
230
+ }
231
+
232
+ export type HandlerParams<
233
+ TDef extends FnDefinition,
234
+ TInput,
235
+ TKey extends ProcedureKey<TDef>
236
+ > = DistributiveOmit<
237
+ TDef['extras'],
238
+ 'input' | 'context' | 'call' | 'signal' | 'span' | 'logger'
239
+ > & {
240
+ input: TInput
241
+ context: FnContext<TDef, TKey>
242
+ /** Calls another procedure with this context and signal, in a child span. */
243
+ call: BoundCall<FnContext<TDef, TKey>>
244
+ /** Cancellation is inherited by nested calls; pass to interruptible I/O. */
245
+ signal: AbortSignal | undefined
246
+ /** Active OpenTelemetry span, undefined when `createFn({ otel })` is not set. */
247
+ span: TDef['span'] | undefined
248
+ logger: TDef['logger']
249
+ }
250
+
251
+ /** Route options shared by every `fn()` overload. */
252
+ export type RouteConfig<TTag extends string = string> = Pick<
253
+ Route,
254
+ Exclude<(typeof ROUTE_OPTION_KEYS)[number], 'tags'>
255
+ > & {
256
+ /** Required. Becomes the OpenAPI operationId and the span name (e.g. 'user.create'). */
257
+ name: string
258
+ tags?: TTag[]
259
+ }
260
+
261
+ export type FnRouteOptions<
262
+ TDef extends FnDefinition,
263
+ TKey extends ProcedureKey<TDef>
264
+ > = RouteConfig<TDef['tag']> &
265
+ GuardOptions<TDef['guards']> &
266
+ TDef['meta'] &
267
+ ProcedureOption<TDef, TKey>
268
+
269
+ /**
270
+ * Which `createFn({ procedures })` builder a route uses. Optional when
271
+ * `createFn` has a `default`; required on every route when it has none.
272
+ */
273
+ export type ProcedureOption<
274
+ TDef extends FnDefinition,
275
+ TKey extends ProcedureKey<TDef>
276
+ > = [TDef['default']] extends [never]
277
+ ? { procedure: TKey }
278
+ : { procedure?: TKey }
279
+
280
+ export type FnProcedure<
281
+ TDef extends FnDefinition,
282
+ TKey extends ProcedureKey<TDef>,
283
+ TInputSchema extends Schema<unknown, unknown>,
284
+ TOutputSchema extends Schema<unknown, unknown>
285
+ > = DecoratedProcedure<
286
+ InitialContextOf<TDef['procedures'][TKey]>,
287
+ FnContext<TDef, TKey>,
288
+ TInputSchema,
289
+ TOutputSchema,
290
+ ErrorMapOf<TDef['procedures'][TKey]>,
291
+ MetaOf<TDef['procedures'][TKey]>
292
+ >
293
+
294
+ type InputOf<T> = [T] extends [ZodType] ? z.output<T> : unknown
295
+ type ReturnOf<TOut, TReturn> = [TOut] extends [ZodType]
296
+ ? z.input<TOut>
297
+ : TReturn
298
+
299
+ /** `fn()`: four overloads for input/output schema × inferred. */
300
+ export interface Fn<TDef extends FnDefinition> {
301
+ // 1. With input + output schema
302
+ <
303
+ TInput extends ZodType,
304
+ TOut extends ZodType,
305
+ TKey extends ProcedureKey<TDef> = TDef['default']
306
+ >(
307
+ options: {
308
+ input: TInput
309
+ output: TOut
310
+ // The output schema parses what the handler returns: its input type.
311
+ handler: (
312
+ params: HandlerParams<TDef, z.output<TInput>, NoInfer<TKey>>
313
+ ) => MaybePromise<z.input<TOut>>
314
+ } & FnRouteOptions<TDef, TKey>
315
+ ): FnProcedure<TDef, TKey, TInput, TOut>
316
+
317
+ // 2. With input, inferred output
318
+ <
319
+ TInput extends ZodType,
320
+ TReturn,
321
+ TKey extends ProcedureKey<TDef> = TDef['default']
322
+ >(
323
+ options: {
324
+ input: TInput
325
+ output?: undefined
326
+ handler: (
327
+ params: HandlerParams<TDef, z.output<TInput>, NoInfer<TKey>>
328
+ ) => MaybePromise<TReturn>
329
+ } & FnRouteOptions<TDef, TKey>
330
+ ): FnProcedure<TDef, TKey, TInput, Schema<TReturn, TReturn>>
331
+
332
+ // 3. No input, with output schema
333
+ <TOut extends ZodType, TKey extends ProcedureKey<TDef> = TDef['default']>(
334
+ options: {
335
+ input?: undefined
336
+ output: TOut
337
+ handler: (
338
+ params: HandlerParams<TDef, unknown, NoInfer<TKey>>
339
+ ) => MaybePromise<z.input<TOut>>
340
+ } & FnRouteOptions<TDef, TKey>
341
+ ): FnProcedure<TDef, TKey, Schema<unknown, unknown>, TOut>
342
+
343
+ // 4. No input, inferred output
344
+ <TReturn, TKey extends ProcedureKey<TDef> = TDef['default']>(
345
+ options: {
346
+ input?: undefined
347
+ output?: undefined
348
+ handler: (
349
+ params: HandlerParams<TDef, unknown, NoInfer<TKey>>
350
+ ) => MaybePromise<TReturn>
351
+ } & FnRouteOptions<TDef, TKey>
352
+ ): FnProcedure<TDef, TKey, Schema<unknown, unknown>, Schema<TReturn, TReturn>>
353
+
354
+ // 5. Never matches valid code first; it only words the type errors, which
355
+ // TypeScript reports against the last overload.
356
+ <
357
+ TReturn,
358
+ TInput extends ZodType | undefined = undefined,
359
+ TOut extends ZodType | undefined = undefined,
360
+ TKey extends ProcedureKey<TDef> = TDef['default']
361
+ >(
362
+ options: {
363
+ input?: TInput
364
+ output?: TOut
365
+ // The output schema parses what the handler returns: its input type.
366
+ handler: (
367
+ params: HandlerParams<TDef, InputOf<TInput>, NoInfer<TKey>>
368
+ ) => MaybePromise<ReturnOf<TOut, TReturn>>
369
+ } & FnRouteOptions<TDef, TKey>
370
+ ): FnProcedure<
371
+ TDef,
372
+ TKey,
373
+ [TInput] extends [ZodType] ? TInput : Schema<unknown, unknown>,
374
+ [TOut] extends [ZodType] ? TOut : Schema<TReturn, TReturn>
375
+ >
376
+ }
377
+
378
+ /** What `fn.completed` reports, and what `onCompleted` receives. */
379
+ export type FnCompletedEvent<TContext = unknown, TSpan = SpanLike> = {
380
+ name: string
381
+ procedure: string
382
+ durationMs: number
383
+ success: boolean
384
+ error: unknown
385
+ input: unknown
386
+ context: TContext
387
+ span: TSpan | undefined
388
+ meta: Record<string, unknown>
389
+ }