orpc-fn 0.1.0 → 0.3.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.
- package/README.md +70 -33
- package/dist/assemble-procedure.d.ts +13 -0
- package/dist/assemble-procedure.d.ts.map +1 -0
- package/dist/assemble-procedure.js +29 -0
- package/dist/assemble-procedure.js.map +1 -0
- package/dist/bound-call.d.ts +1 -1
- package/dist/bound-call.d.ts.map +1 -1
- package/dist/bound-call.js.map +1 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +56 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +4 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +4 -2
- package/dist/client.js.map +1 -1
- package/dist/compatibility.d.ts +16 -0
- package/dist/compatibility.d.ts.map +1 -0
- package/dist/compatibility.js +4 -0
- package/dist/compatibility.js.map +1 -0
- package/dist/create-fn.d.ts +30 -34
- package/dist/create-fn.d.ts.map +1 -1
- package/dist/create-fn.js +177 -121
- package/dist/create-fn.js.map +1 -1
- package/dist/expo.d.ts +11 -25
- package/dist/expo.d.ts.map +1 -1
- package/dist/expo.js +25 -30
- package/dist/expo.js.map +1 -1
- package/dist/hono.d.ts +12 -4
- package/dist/hono.d.ts.map +1 -1
- package/dist/hono.js +29 -12
- package/dist/hono.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/json-schema.d.ts +2 -0
- package/dist/json-schema.d.ts.map +1 -1
- package/dist/json-schema.js +11 -0
- package/dist/json-schema.js.map +1 -1
- package/dist/live/codec.d.ts +6 -1
- package/dist/live/codec.d.ts.map +1 -1
- package/dist/live/codec.js +14 -3
- package/dist/live/codec.js.map +1 -1
- package/dist/live/fn-live.d.ts +67 -22
- package/dist/live/fn-live.d.ts.map +1 -1
- package/dist/live/fn-live.js +98 -16
- package/dist/live/fn-live.js.map +1 -1
- package/dist/live/index.d.ts +1 -1
- package/dist/live/index.d.ts.map +1 -1
- package/dist/live/index.js.map +1 -1
- package/dist/live/ioredis.d.ts +4 -1
- package/dist/live/ioredis.d.ts.map +1 -1
- package/dist/live/ioredis.js +21 -2
- package/dist/live/ioredis.js.map +1 -1
- package/dist/live/lifecycle.d.ts +7 -0
- package/dist/live/lifecycle.d.ts.map +1 -0
- package/dist/live/lifecycle.js +36 -0
- package/dist/live/lifecycle.js.map +1 -0
- package/dist/live/memory.d.ts.map +1 -1
- package/dist/live/memory.js +4 -0
- package/dist/live/memory.js.map +1 -1
- package/dist/live/pub-sub.d.ts +81 -13
- package/dist/live/pub-sub.d.ts.map +1 -1
- package/dist/live/pub-sub.js +437 -126
- package/dist/live/pub-sub.js.map +1 -1
- package/dist/live/redis-bun.d.ts +1 -1
- package/dist/live/redis-bun.d.ts.map +1 -1
- package/dist/live/redis-bun.js +3 -5
- package/dist/live/redis-bun.js.map +1 -1
- package/dist/live/redis.d.ts +2 -2
- package/dist/live/redis.d.ts.map +1 -1
- package/dist/live/redis.js +36 -9
- package/dist/live/redis.js.map +1 -1
- package/dist/live/transport.d.ts +5 -1
- package/dist/live/transport.d.ts.map +1 -1
- package/dist/live/transport.js.map +1 -1
- package/dist/logger.d.ts +2 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +21 -0
- package/dist/logger.js.map +1 -1
- package/dist/mastra.d.ts.map +1 -1
- package/dist/mastra.js +16 -7
- package/dist/mastra.js.map +1 -1
- package/dist/mcp/core.d.ts +41 -0
- package/dist/mcp/core.d.ts.map +1 -0
- package/dist/mcp/core.js +61 -0
- package/dist/mcp/core.js.map +1 -0
- package/dist/mcp/sdk.d.ts +12 -0
- package/dist/mcp/sdk.d.ts.map +1 -0
- package/dist/mcp/sdk.js +14 -0
- package/dist/mcp/sdk.js.map +1 -0
- package/dist/mcp/server.d.ts +11 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +13 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp.d.ts +3 -2
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +22 -7
- package/dist/mcp.js.map +1 -1
- package/dist/meta.d.ts +15 -7
- package/dist/meta.d.ts.map +1 -1
- package/dist/meta.js +9 -2
- package/dist/meta.js.map +1 -1
- package/dist/otel.d.ts.map +1 -1
- package/dist/otel.js +77 -6
- package/dist/otel.js.map +1 -1
- package/dist/route-options.d.ts +22 -0
- package/dist/route-options.d.ts.map +1 -0
- package/dist/route-options.js +67 -0
- package/dist/route-options.js.map +1 -0
- package/dist/router.d.ts +13 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +35 -0
- package/dist/router.js.map +1 -1
- package/dist/timing.d.ts +8 -0
- package/dist/timing.d.ts.map +1 -0
- package/dist/timing.js +4 -0
- package/dist/timing.js.map +1 -0
- package/dist/types.d.ts +96 -41
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -2
- package/dist/types.js.map +1 -1
- package/package.json +28 -3
- package/src/assemble-procedure.ts +42 -0
- package/src/bound-call.ts +4 -3
- package/src/cli.ts +68 -0
- package/src/client.ts +9 -2
- package/src/compatibility.ts +39 -0
- package/src/create-fn.ts +295 -182
- package/src/expo.ts +49 -44
- package/src/hono.ts +36 -17
- package/src/index.ts +7 -2
- package/src/json-schema.ts +16 -0
- package/src/live/codec.ts +22 -3
- package/src/live/fn-live.ts +248 -69
- package/src/live/index.ts +2 -0
- package/src/live/ioredis.ts +22 -2
- package/src/live/lifecycle.ts +44 -0
- package/src/live/memory.ts +4 -0
- package/src/live/pub-sub.ts +619 -168
- package/src/live/redis-bun.ts +7 -5
- package/src/live/redis.ts +37 -11
- package/src/live/transport.ts +4 -1
- package/src/logger.ts +20 -0
- package/src/mastra.ts +15 -7
- package/src/mcp/core.ts +102 -0
- package/src/mcp/sdk.ts +36 -0
- package/src/mcp/server.ts +35 -0
- package/src/mcp.ts +48 -20
- package/src/meta.ts +40 -24
- package/src/otel.ts +77 -10
- package/src/route-options.ts +100 -0
- package/src/router.ts +58 -1
- package/src/timing.ts +5 -0
- package/src/types.ts +186 -62
package/src/create-fn.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
type BuilderChain,
|
|
3
|
+
createProcedureAssembler
|
|
4
|
+
} from './assemble-procedure.js'
|
|
5
|
+
import { isAsyncIteratorObject } from './stream.js'
|
|
6
|
+
import { normalizeRoute } from './route-options.js'
|
|
7
|
+
import { type AnyProcedure, ORPCError, os } from '@orpc/server'
|
|
8
|
+
import { procedureDefinition } from './compatibility.js'
|
|
3
9
|
import { type BoundCall, createBoundCall } from './bound-call.js'
|
|
4
10
|
import { isExpectedClientError } from './expected-client-error.js'
|
|
5
11
|
import { createFnLive, type FnLive } from './live/fn-live.js'
|
|
@@ -11,15 +17,11 @@ import {
|
|
|
11
17
|
} from './live/pub-sub.js'
|
|
12
18
|
import {
|
|
13
19
|
createDefaultLogger,
|
|
20
|
+
protectLogger,
|
|
14
21
|
type FnLogger,
|
|
15
22
|
type LogAttributes
|
|
16
23
|
} from './logger.js'
|
|
17
|
-
import {
|
|
18
|
-
FN_META_KEY,
|
|
19
|
-
type FnMeta,
|
|
20
|
-
readFnMeta,
|
|
21
|
-
type StoredFnMeta
|
|
22
|
-
} from './meta.js'
|
|
24
|
+
import { readFnMeta } from './meta.js'
|
|
23
25
|
import {
|
|
24
26
|
type AttributeValue,
|
|
25
27
|
createTracing,
|
|
@@ -28,12 +30,15 @@ import {
|
|
|
28
30
|
type SpanLike,
|
|
29
31
|
type SpanOf
|
|
30
32
|
} from './otel.js'
|
|
33
|
+
import { RAW_INPUT, REQUEST_TIMING, type InvocationTiming } from './timing.js'
|
|
31
34
|
import { createRouter } from './router.js'
|
|
32
35
|
import {
|
|
33
|
-
type
|
|
36
|
+
type ProcedureHookParams,
|
|
37
|
+
type CompletedHookEvent,
|
|
38
|
+
type CurrentContextOf,
|
|
34
39
|
type BuilderLike,
|
|
35
|
-
type BuildProcedure,
|
|
36
40
|
type FiniteExtras,
|
|
41
|
+
type ScopedExtrasGuard,
|
|
37
42
|
type Fn,
|
|
38
43
|
type FnCompletedEvent,
|
|
39
44
|
type FnDefinition,
|
|
@@ -42,24 +47,18 @@ import {
|
|
|
42
47
|
type MaybePromise,
|
|
43
48
|
type RejectKeys,
|
|
44
49
|
RESERVED_OPTION_KEYS,
|
|
45
|
-
ROUTE_OPTION_KEYS,
|
|
46
50
|
type ReservedOptionKey
|
|
47
51
|
} from './types.js'
|
|
48
52
|
|
|
49
|
-
/** Builder methods `createFn` calls; `BuilderLike` is the public constraint. */
|
|
50
|
-
type BuilderChain = {
|
|
51
|
-
route(route: Record<string, unknown>): BuilderChain
|
|
52
|
-
meta(meta: Record<string, unknown>): BuilderChain
|
|
53
|
-
input(schema: AnySchema): BuilderChain
|
|
54
|
-
output(schema: AnySchema): BuilderChain
|
|
55
|
-
handler(handler: Parameters<BuildProcedure>[0]['handler']): AnyProcedure
|
|
56
|
-
}
|
|
57
|
-
|
|
58
53
|
/** `CreateFnOptions` with the generics erased: what the implementation reads. */
|
|
59
54
|
type RuntimeOptions = {
|
|
60
55
|
procedures: Record<string, BuilderChain>
|
|
61
56
|
default?: string
|
|
62
57
|
extras?: (params: Record<string, unknown>) => unknown
|
|
58
|
+
extrasByProcedure?: Record<
|
|
59
|
+
string,
|
|
60
|
+
(params: Record<string, unknown>) => unknown
|
|
61
|
+
>
|
|
63
62
|
guards?: Record<
|
|
64
63
|
string,
|
|
65
64
|
(value: unknown, params: GuardParams<unknown, unknown>) => unknown
|
|
@@ -77,18 +76,6 @@ type RuntimeOptions = {
|
|
|
77
76
|
pubsub?: PubSubRuntimeOptions
|
|
78
77
|
}
|
|
79
78
|
|
|
80
|
-
type UnionContext<TProcedures extends Record<string, BuilderLike>> =
|
|
81
|
-
AnyFnContext<{
|
|
82
|
-
procedures: TProcedures
|
|
83
|
-
default: string
|
|
84
|
-
extras: object
|
|
85
|
-
guards: object
|
|
86
|
-
meta: object
|
|
87
|
-
tag: string
|
|
88
|
-
span: SpanLike
|
|
89
|
-
logger: FnLogger
|
|
90
|
-
}>
|
|
91
|
-
|
|
92
79
|
export type CreateFnOptions<
|
|
93
80
|
TProcedures extends Record<string, BuilderLike>,
|
|
94
81
|
TDefault extends keyof TProcedures & string,
|
|
@@ -97,7 +84,11 @@ export type CreateFnOptions<
|
|
|
97
84
|
TMeta extends object,
|
|
98
85
|
TTag extends string,
|
|
99
86
|
TOtel extends OtelApiLike | undefined,
|
|
100
|
-
TLogger extends FnLogger
|
|
87
|
+
TLogger extends FnLogger,
|
|
88
|
+
TScoped extends Partial<Record<keyof TProcedures, object>> = Record<
|
|
89
|
+
never,
|
|
90
|
+
never
|
|
91
|
+
>
|
|
101
92
|
> = {
|
|
102
93
|
/** Named oRPC builders, e.g. `{ public: os.$context<Ctx>(), protected: authed }`. */
|
|
103
94
|
procedures: TProcedures
|
|
@@ -107,52 +98,65 @@ export type CreateFnOptions<
|
|
|
107
98
|
*/
|
|
108
99
|
default?: TDefault
|
|
109
100
|
/** Per-call values merged into every handler's params; may be async. */
|
|
110
|
-
extras?: (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
101
|
+
extras?: (
|
|
102
|
+
params: ProcedureHookParams<
|
|
103
|
+
TProcedures,
|
|
104
|
+
{
|
|
105
|
+
span: SpanOf<TOtel> | undefined
|
|
106
|
+
signal: AbortSignal | undefined
|
|
107
|
+
name: string
|
|
108
|
+
}
|
|
109
|
+
>
|
|
110
|
+
) => MaybePromise<TExtras>
|
|
111
|
+
extrasByProcedure?: {
|
|
112
|
+
[K in keyof TScoped & keyof TProcedures]: (params: {
|
|
113
|
+
context: CurrentContextOf<TProcedures[K]>
|
|
114
|
+
span: SpanOf<TOtel> | undefined
|
|
115
|
+
signal: AbortSignal | undefined
|
|
116
|
+
name: string
|
|
117
|
+
procedure: K
|
|
118
|
+
}) => MaybePromise<TScoped[K] & ScopedExtrasGuard<NoInfer<TScoped[K]>>>
|
|
119
|
+
}
|
|
117
120
|
/**
|
|
118
121
|
* Checks run before the handler; each key becomes a typed `fn()` option.
|
|
119
|
-
* Keys must not reuse route options or
|
|
122
|
+
* Keys must not reuse route options. False denies, true or void allows.
|
|
120
123
|
*/
|
|
121
124
|
guards?: TGuards &
|
|
122
125
|
Record<
|
|
123
126
|
string,
|
|
124
|
-
Guard<
|
|
127
|
+
Guard<
|
|
128
|
+
never,
|
|
129
|
+
ProcedureHookParams<
|
|
130
|
+
TProcedures,
|
|
131
|
+
Omit<GuardParams<unknown, TMeta>, 'context' | 'procedure'>
|
|
132
|
+
>
|
|
133
|
+
>
|
|
125
134
|
> &
|
|
126
135
|
RejectKeys<
|
|
127
136
|
NoInfer<TGuards>,
|
|
128
|
-
ReservedOptionKey
|
|
129
|
-
'orpc-fn: this guard name is a route option
|
|
137
|
+
ReservedOptionKey,
|
|
138
|
+
'orpc-fn: this guard name is a route option'
|
|
130
139
|
>
|
|
131
140
|
/**
|
|
132
|
-
* Typed route metadata: `meta:
|
|
133
|
-
*
|
|
141
|
+
* Typed route metadata contract: `meta: defineMeta<AppMeta>()`.
|
|
142
|
+
* Route declarations place values inside their own `meta` object.
|
|
134
143
|
*/
|
|
135
|
-
meta?: TMeta
|
|
136
|
-
RejectKeys<
|
|
137
|
-
NoInfer<TMeta>,
|
|
138
|
-
ReservedOptionKey,
|
|
139
|
-
'orpc-fn: this meta key is a route option'
|
|
140
|
-
>
|
|
144
|
+
meta?: TMeta
|
|
141
145
|
/** Allowed route tags: `tags: ['internal', 'external']`. */
|
|
142
146
|
tags?: readonly TTag[]
|
|
143
147
|
/** `import * as otel from '@opentelemetry/api'`. Omit for no tracing. */
|
|
144
148
|
otel?: TOtel
|
|
145
149
|
logger?: (scope: string, span: SpanOf<TOtel> | undefined) => TLogger
|
|
146
150
|
/** Extra span attributes per call; undefined values are skipped. */
|
|
147
|
-
spanAttributes?: (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
/** Called after
|
|
151
|
+
spanAttributes?: (
|
|
152
|
+
params: ProcedureHookParams<
|
|
153
|
+
TProcedures,
|
|
154
|
+
{ name: string; meta: Partial<TMeta> }
|
|
155
|
+
>
|
|
156
|
+
) => Record<string, AttributeValue | undefined>
|
|
157
|
+
/** Called after the procedure (or stream) settles; returned attributes join the `fn.completed` log. */
|
|
154
158
|
onCompleted?: (
|
|
155
|
-
event:
|
|
159
|
+
event: CompletedHookEvent<TProcedures, SpanOf<TOtel>>
|
|
156
160
|
// biome-ignore lint/suspicious/noConfusingVoidType: lets hooks that return nothing type-check
|
|
157
161
|
) => LogAttributes | undefined | void
|
|
158
162
|
/** Errors that are the caller's fault, logged at `warn`. Default: 4xx `ORPCError` or `AbortError`. */
|
|
@@ -169,11 +173,16 @@ type Definition<
|
|
|
169
173
|
TMeta extends object,
|
|
170
174
|
TTag extends string,
|
|
171
175
|
TOtel,
|
|
172
|
-
TLogger extends FnLogger
|
|
176
|
+
TLogger extends FnLogger,
|
|
177
|
+
TScoped extends Partial<Record<keyof TProcedures, object>> = Record<
|
|
178
|
+
never,
|
|
179
|
+
never
|
|
180
|
+
>
|
|
173
181
|
> = {
|
|
174
182
|
procedures: TProcedures
|
|
175
183
|
default: TDefault
|
|
176
184
|
extras: TExtras
|
|
185
|
+
extrasByProcedure: TScoped
|
|
177
186
|
guards: TGuards
|
|
178
187
|
meta: TMeta
|
|
179
188
|
tag: TTag
|
|
@@ -187,7 +196,7 @@ export type FnFactory<TDef extends FnDefinition> = {
|
|
|
187
196
|
createPubSub: CreatePubSub<TDef>
|
|
188
197
|
createPublisher: CreatePublisher
|
|
189
198
|
createRouter: typeof createRouter
|
|
190
|
-
readMeta:
|
|
199
|
+
readMeta: typeof readFnMeta
|
|
191
200
|
/**
|
|
192
201
|
* A `call` outside any handler (seeders, scripts, tests), traced with this
|
|
193
202
|
* instance's OpenTelemetry.
|
|
@@ -196,7 +205,8 @@ export type FnFactory<TDef extends FnDefinition> = {
|
|
|
196
205
|
context: TContext,
|
|
197
206
|
signal?: AbortSignal
|
|
198
207
|
) => BoundCall<TContext>
|
|
199
|
-
/**
|
|
208
|
+
/** Terminal, idempotent shutdown: close streams and await owned resources. */
|
|
209
|
+
shutdown: () => Promise<void>
|
|
200
210
|
drainPubSubSubscribers: () => number
|
|
201
211
|
activePubSubSubscriberCount: () => number
|
|
202
212
|
}
|
|
@@ -213,7 +223,11 @@ export function createFn<
|
|
|
213
223
|
TMeta extends object = Record<never, never>,
|
|
214
224
|
const TTag extends string = string,
|
|
215
225
|
TOtel extends OtelApiLike | undefined = undefined,
|
|
216
|
-
TLogger extends FnLogger = FnLogger
|
|
226
|
+
TLogger extends FnLogger = FnLogger,
|
|
227
|
+
TScoped extends Partial<Record<keyof TProcedures, object>> = Record<
|
|
228
|
+
never,
|
|
229
|
+
never
|
|
230
|
+
>
|
|
217
231
|
>(
|
|
218
232
|
options: CreateFnOptions<
|
|
219
233
|
TProcedures,
|
|
@@ -223,7 +237,8 @@ export function createFn<
|
|
|
223
237
|
TMeta,
|
|
224
238
|
TTag,
|
|
225
239
|
TOtel,
|
|
226
|
-
TLogger
|
|
240
|
+
TLogger,
|
|
241
|
+
TScoped
|
|
227
242
|
> &
|
|
228
243
|
FiniteExtras<NoInfer<TExtras>>
|
|
229
244
|
): FnFactory<
|
|
@@ -235,28 +250,42 @@ export function createFn<
|
|
|
235
250
|
TMeta,
|
|
236
251
|
TTag,
|
|
237
252
|
TOtel,
|
|
238
|
-
TLogger
|
|
253
|
+
TLogger,
|
|
254
|
+
TScoped
|
|
239
255
|
>
|
|
240
256
|
> {
|
|
241
257
|
const runtime = options as unknown as RuntimeOptions
|
|
242
258
|
const tracing = createTracing(runtime.otel)
|
|
243
|
-
const defaultLogger = createDefaultLogger('fn')
|
|
244
|
-
const createLogger =
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
259
|
+
const defaultLogger = protectLogger(createDefaultLogger('fn'))
|
|
260
|
+
const createLogger = (
|
|
261
|
+
scope: string,
|
|
262
|
+
span: SpanLike | undefined
|
|
263
|
+
): FnLogger => {
|
|
264
|
+
try {
|
|
265
|
+
return protectLogger(
|
|
266
|
+
runtime.logger?.(scope, span) ??
|
|
267
|
+
(scope === 'fn' ? defaultLogger : createDefaultLogger(scope))
|
|
268
|
+
)
|
|
269
|
+
} catch {
|
|
270
|
+
return defaultLogger
|
|
271
|
+
}
|
|
272
|
+
}
|
|
248
273
|
const isExpectedError = runtime.isExpectedError ?? isExpectedClientError
|
|
249
274
|
const guards = runtime.guards ?? {}
|
|
250
275
|
const isGuard = (option: string) => Object.hasOwn(guards, option)
|
|
251
276
|
for (const guard of Object.keys(guards)) {
|
|
277
|
+
if (typeof guards[guard] !== 'function')
|
|
278
|
+
throw new TypeError(`orpc-fn: guard ${guard} must be a function`)
|
|
252
279
|
if ((RESERVED_OPTION_KEYS as readonly string[]).includes(guard)) {
|
|
253
280
|
throw new Error(
|
|
254
281
|
`orpc-fn: guard "${guard}" reuses a route option name; rename it`
|
|
255
282
|
)
|
|
256
283
|
}
|
|
257
284
|
}
|
|
285
|
+
if (runtime.default && !Object.hasOwn(runtime.procedures, runtime.default))
|
|
286
|
+
throw new TypeError(`orpc-fn: unknown default procedure ${runtime.default}`)
|
|
258
287
|
for (const [key, builder] of Object.entries(runtime.procedures)) {
|
|
259
|
-
const def = (builder
|
|
288
|
+
const def = procedureDefinition(builder)
|
|
260
289
|
if (def?.inputSchema !== undefined || def?.outputSchema !== undefined) {
|
|
261
290
|
throw new Error(
|
|
262
291
|
`orpc-fn: procedure "${key}" sets an input or output schema; set schemas on routes instead`
|
|
@@ -274,139 +303,204 @@ export function createFn<
|
|
|
274
303
|
return key
|
|
275
304
|
}
|
|
276
305
|
|
|
277
|
-
const buildProcedure
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const stored: StoredFnMeta = {
|
|
282
|
-
name: route.name,
|
|
283
|
-
procedure: key,
|
|
284
|
-
meta: route.meta
|
|
285
|
-
}
|
|
286
|
-
builder = builder
|
|
287
|
-
.route({
|
|
288
|
-
operationId: route.name,
|
|
289
|
-
...Object.fromEntries(
|
|
290
|
-
Object.entries(route.route).filter(([, value]) => value !== undefined)
|
|
291
|
-
)
|
|
292
|
-
})
|
|
293
|
-
.meta({ [FN_META_KEY]: stored })
|
|
294
|
-
if (route.input) builder = builder.input(route.input)
|
|
295
|
-
if (route.output) builder = builder.output(route.output)
|
|
296
|
-
return builder.handler(route.handler)
|
|
297
|
-
}
|
|
306
|
+
const buildProcedure = createProcedureAssembler(
|
|
307
|
+
runtime.procedures,
|
|
308
|
+
resolveKey
|
|
309
|
+
)
|
|
298
310
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
311
|
+
const invocation = Symbol('orpc-fn.invocation')
|
|
312
|
+
type Invocation = {
|
|
313
|
+
span: SpanLike | undefined
|
|
314
|
+
logger: FnLogger
|
|
315
|
+
input: unknown
|
|
316
|
+
context: unknown
|
|
317
|
+
handlerStarted: boolean
|
|
318
|
+
rawInput: unknown
|
|
319
|
+
}
|
|
320
|
+
// The overload-to-runtime boundary is adapted once at the returned factory.
|
|
321
|
+
const fn = (routeOptions: Record<string, unknown>): AnyProcedure => {
|
|
322
|
+
const {
|
|
323
|
+
handler,
|
|
324
|
+
input,
|
|
325
|
+
output,
|
|
326
|
+
name,
|
|
327
|
+
procedure,
|
|
328
|
+
meta,
|
|
329
|
+
stream,
|
|
330
|
+
errors,
|
|
331
|
+
guardResolvers,
|
|
332
|
+
route,
|
|
333
|
+
guardChecks
|
|
334
|
+
} = normalizeRoute(routeOptions, isGuard)
|
|
302
335
|
const key = resolveKey(procedure, name)
|
|
303
|
-
const
|
|
304
|
-
const guardChecks: Array<[string, unknown]> = []
|
|
305
|
-
const meta: Record<string, unknown> = {}
|
|
306
|
-
for (const [option, value] of Object.entries(rest)) {
|
|
307
|
-
if (value === undefined) continue
|
|
308
|
-
if ((ROUTE_OPTION_KEYS as readonly string[]).includes(option))
|
|
309
|
-
route[option] = value
|
|
310
|
-
else if (isGuard(option)) guardChecks.push([option, value])
|
|
311
|
-
else meta[option] = value
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
return buildProcedure({
|
|
336
|
+
const built = buildProcedure({
|
|
315
337
|
name,
|
|
316
338
|
procedure: key,
|
|
317
339
|
route,
|
|
318
340
|
meta,
|
|
319
341
|
input,
|
|
320
342
|
output,
|
|
321
|
-
|
|
343
|
+
stream,
|
|
344
|
+
...(errors ? { errors } : {}),
|
|
345
|
+
handler: async (params) => {
|
|
322
346
|
const { context, signal } = params
|
|
323
|
-
const
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
347
|
+
const state = context[invocation] as Invocation
|
|
348
|
+
const { [invocation]: _invocation, ...observedContext } = context
|
|
349
|
+
state.context = observedContext
|
|
350
|
+
state.input = params.input
|
|
351
|
+
state.handlerStarted = true
|
|
352
|
+
try {
|
|
353
|
+
const attributes = runtime.spanAttributes?.({
|
|
354
|
+
context,
|
|
355
|
+
name,
|
|
356
|
+
procedure: key,
|
|
357
|
+
meta
|
|
358
|
+
})
|
|
359
|
+
for (const [attribute, value] of Object.entries(attributes ?? {}))
|
|
360
|
+
if (value !== undefined) state.span?.setAttribute(attribute, value)
|
|
361
|
+
} catch (error) {
|
|
362
|
+
state.logger.warn('spanAttributes hook failed', {
|
|
363
|
+
error: errorMessageOf(error)
|
|
364
|
+
})
|
|
365
|
+
}
|
|
366
|
+
const resolvedChecks = [...guardChecks]
|
|
367
|
+
for (const [guard, resolver] of Object.entries(guardResolvers)) {
|
|
368
|
+
const value = await resolver({ input: params.input, context, signal })
|
|
369
|
+
if (value !== undefined) resolvedChecks.push([guard, value])
|
|
370
|
+
}
|
|
371
|
+
for (const [guard, value] of resolvedChecks) {
|
|
372
|
+
const result = await guards[guard]?.(value, {
|
|
373
|
+
context,
|
|
374
|
+
input: params.input,
|
|
375
|
+
name,
|
|
376
|
+
procedure: key,
|
|
377
|
+
meta,
|
|
378
|
+
signal
|
|
379
|
+
})
|
|
380
|
+
if (result === false)
|
|
381
|
+
throw new ORPCError('FORBIDDEN', {
|
|
382
|
+
message: `Guard "${guard}" denied this call`
|
|
383
|
+
})
|
|
384
|
+
if (result !== undefined && result !== true)
|
|
385
|
+
throw new TypeError(
|
|
386
|
+
`orpc-fn: guard "${guard}" must return void or boolean`
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
const extraParams = {
|
|
390
|
+
context,
|
|
391
|
+
span: state.span,
|
|
392
|
+
signal,
|
|
393
|
+
name,
|
|
394
|
+
procedure: key
|
|
395
|
+
}
|
|
396
|
+
const shared = await runtime.extras?.(extraParams)
|
|
397
|
+
const scoped = await runtime.extrasByProcedure?.[key]?.(extraParams)
|
|
398
|
+
const extras = {
|
|
399
|
+
...(shared as object | undefined),
|
|
400
|
+
...(scoped as object | undefined)
|
|
401
|
+
}
|
|
402
|
+
if (extras && typeof extras === 'object') {
|
|
403
|
+
for (const reserved of [
|
|
404
|
+
'input',
|
|
405
|
+
'context',
|
|
406
|
+
'call',
|
|
407
|
+
'signal',
|
|
408
|
+
'span',
|
|
409
|
+
'logger',
|
|
410
|
+
'errors',
|
|
411
|
+
'lastEventId'
|
|
412
|
+
]) {
|
|
413
|
+
if (Object.hasOwn(extras, reserved))
|
|
414
|
+
throw new TypeError(
|
|
415
|
+
`orpc-fn: extra "${reserved}" shadows a handler parameter`
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return handler({
|
|
420
|
+
...(extras as object | undefined),
|
|
421
|
+
input: params.input,
|
|
422
|
+
context,
|
|
423
|
+
call: createBoundCall(context, signal, tracing),
|
|
424
|
+
signal,
|
|
425
|
+
span: state.span,
|
|
426
|
+
logger: state.logger,
|
|
427
|
+
errors: params.errors,
|
|
428
|
+
lastEventId: params.lastEventId,
|
|
429
|
+
[RAW_INPUT]: state.rawInput
|
|
430
|
+
})
|
|
431
|
+
}
|
|
432
|
+
})
|
|
433
|
+
// Prepending middleware shifts oRPC's validation indexes, covering auth,
|
|
434
|
+
// input validation, output validation, and the final stream lifetime.
|
|
435
|
+
return os
|
|
436
|
+
.use(async ({ context, next }, rawInput) => {
|
|
437
|
+
const started = performance.now()
|
|
438
|
+
const state: Invocation = {
|
|
439
|
+
span: undefined,
|
|
440
|
+
logger: defaultLogger,
|
|
441
|
+
input: undefined,
|
|
442
|
+
context,
|
|
443
|
+
handlerStarted: false,
|
|
444
|
+
rawInput
|
|
445
|
+
}
|
|
446
|
+
const result = await tracing.inSpan(
|
|
328
447
|
name,
|
|
329
448
|
'SERVER',
|
|
330
449
|
async (span) => {
|
|
331
|
-
|
|
450
|
+
state.span = span
|
|
451
|
+
state.logger = createLogger('fn', span)
|
|
332
452
|
span?.setAttribute('fn.operation', name)
|
|
333
453
|
span?.setAttribute('fn.procedure', key)
|
|
334
|
-
const attributes = runtime.spanAttributes?.({
|
|
335
|
-
context,
|
|
336
|
-
name,
|
|
337
|
-
procedure: key,
|
|
338
|
-
meta
|
|
339
|
-
})
|
|
340
|
-
for (const [attribute, value] of Object.entries(attributes ?? {})) {
|
|
341
|
-
if (value !== undefined) span?.setAttribute(attribute, value)
|
|
342
|
-
}
|
|
343
454
|
try {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
455
|
+
const result = await next({ context: { [invocation]: state } })
|
|
456
|
+
if (isAsyncIteratorObject(result.output) !== stream) {
|
|
457
|
+
if (isAsyncIteratorObject(result.output)) {
|
|
458
|
+
try {
|
|
459
|
+
await result.output.return?.()
|
|
460
|
+
} catch {
|
|
461
|
+
/* Release the unexpected stream. */
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
throw new TypeError(
|
|
465
|
+
`orpc-fn: ${name} returned a result inconsistent with stream; declare stream: true for iterators`
|
|
466
|
+
)
|
|
353
467
|
}
|
|
354
|
-
|
|
355
|
-
context,
|
|
356
|
-
span,
|
|
357
|
-
signal,
|
|
358
|
-
name,
|
|
359
|
-
procedure: key
|
|
360
|
-
})
|
|
361
|
-
return await handler({
|
|
362
|
-
...(extras as object | undefined),
|
|
363
|
-
input: params.input,
|
|
364
|
-
context,
|
|
365
|
-
call: createBoundCall(context, signal, tracing),
|
|
366
|
-
signal,
|
|
367
|
-
span,
|
|
368
|
-
logger
|
|
369
|
-
})
|
|
468
|
+
return result.output
|
|
370
469
|
} finally {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
470
|
+
const timing = (context as Record<symbol, unknown>)[
|
|
471
|
+
REQUEST_TIMING
|
|
472
|
+
] as InvocationTiming | undefined
|
|
473
|
+
if (timing && !(context as Record<symbol, unknown>)[invocation]) {
|
|
474
|
+
timing.procedureMs += performance.now() - started
|
|
475
|
+
timing.calls++
|
|
375
476
|
}
|
|
376
477
|
}
|
|
377
478
|
},
|
|
378
|
-
// After the result, or after a returned stream ends; before the span ends.
|
|
379
479
|
(span, ...error) => {
|
|
380
|
-
const durationMs =
|
|
480
|
+
const durationMs =
|
|
481
|
+
Math.round((performance.now() - started) * 100) / 100
|
|
381
482
|
span?.setAttribute('fn.duration_ms', durationMs)
|
|
382
|
-
logCompleted(logger, {
|
|
483
|
+
logCompleted(state.logger, {
|
|
383
484
|
name,
|
|
384
485
|
procedure: key,
|
|
385
486
|
durationMs,
|
|
386
487
|
success: error.length === 0,
|
|
387
488
|
error: error[0],
|
|
388
|
-
input:
|
|
389
|
-
context,
|
|
489
|
+
input: state.input,
|
|
490
|
+
context: state.context,
|
|
390
491
|
span,
|
|
391
|
-
meta
|
|
492
|
+
meta,
|
|
493
|
+
handlerStarted: state.handlerStarted
|
|
392
494
|
})
|
|
393
495
|
}
|
|
394
496
|
)
|
|
395
|
-
|
|
396
|
-
|
|
497
|
+
return { output: result, context }
|
|
498
|
+
})
|
|
499
|
+
.router({ route: built }).route
|
|
397
500
|
}
|
|
398
501
|
|
|
399
502
|
const logCompleted = (logger: FnLogger, event: FnCompletedEvent) => {
|
|
400
503
|
const { error, success } = event
|
|
401
|
-
const timing = (event.context as { timing?: unknown }).timing
|
|
402
|
-
const timingFields: LogAttributes = {}
|
|
403
|
-
if (typeof timing === 'object' && timing !== null) {
|
|
404
|
-
for (const [field, value] of Object.entries(timing)) {
|
|
405
|
-
if (field !== 'handler_ms' && typeof value === 'number') {
|
|
406
|
-
timingFields[field] = value
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
504
|
let extra: LogAttributes | undefined
|
|
411
505
|
try {
|
|
412
506
|
extra = runtime.onCompleted?.(event)
|
|
@@ -415,26 +509,31 @@ export function createFn<
|
|
|
415
509
|
error: errorMessageOf(hookError)
|
|
416
510
|
})
|
|
417
511
|
}
|
|
418
|
-
|
|
512
|
+
let expected = false
|
|
513
|
+
try {
|
|
514
|
+
expected = isExpectedError(error)
|
|
515
|
+
} catch {
|
|
516
|
+
expected = isExpectedClientError(error)
|
|
517
|
+
}
|
|
518
|
+
const level = success ? 'info' : expected ? 'warn' : 'error'
|
|
419
519
|
logger[level]('fn.completed', {
|
|
520
|
+
...extra,
|
|
420
521
|
event: 'fn.completed',
|
|
421
522
|
operation: event.name,
|
|
422
523
|
procedure: event.procedure,
|
|
423
524
|
duration_ms: event.durationMs,
|
|
424
525
|
status: success ? 'success' : 'failed',
|
|
425
|
-
...timingFields,
|
|
426
526
|
...(error === undefined
|
|
427
527
|
? {}
|
|
428
528
|
: {
|
|
429
529
|
error_type: error instanceof Error ? error.name : typeof error,
|
|
430
530
|
error_message: errorMessageOf(error)
|
|
431
|
-
})
|
|
432
|
-
...extra
|
|
531
|
+
})
|
|
433
532
|
})
|
|
434
533
|
}
|
|
435
534
|
|
|
436
535
|
const live = createLiveRuntime({
|
|
437
|
-
|
|
536
|
+
fn,
|
|
438
537
|
tracing,
|
|
439
538
|
createLogger,
|
|
440
539
|
pubsub: runtime.pubsub
|
|
@@ -443,19 +542,33 @@ export function createFn<
|
|
|
443
542
|
// The runtimes are untyped; `Fn`, `FnLive`, `CreatePubSub` and
|
|
444
543
|
// `CreatePublisher` are their typed surface.
|
|
445
544
|
return {
|
|
446
|
-
fn
|
|
545
|
+
fn,
|
|
447
546
|
fnLive: createFnLive({
|
|
448
547
|
fn,
|
|
449
548
|
createChannel: live.createChannel,
|
|
450
549
|
createLogger,
|
|
451
550
|
isGuard
|
|
452
|
-
})
|
|
453
|
-
createPubSub: live.createPubSub
|
|
454
|
-
createPublisher: live.createPublisher
|
|
551
|
+
}),
|
|
552
|
+
createPubSub: live.createPubSub,
|
|
553
|
+
createPublisher: live.createPublisher,
|
|
455
554
|
createRouter,
|
|
456
|
-
readMeta:
|
|
457
|
-
createCall: (context, signal) =>
|
|
555
|
+
readMeta: readFnMeta,
|
|
556
|
+
createCall: <TContext>(context: TContext, signal?: AbortSignal) =>
|
|
557
|
+
createBoundCall(context, signal, tracing),
|
|
558
|
+
shutdown: live.shutdown,
|
|
458
559
|
drainPubSubSubscribers: live.drainPubSubSubscribers,
|
|
459
560
|
activePubSubSubscriberCount: live.activePubSubSubscriberCount
|
|
460
|
-
}
|
|
561
|
+
} as unknown as FnFactory<
|
|
562
|
+
Definition<
|
|
563
|
+
TProcedures,
|
|
564
|
+
TDefault,
|
|
565
|
+
TExtras,
|
|
566
|
+
TGuards,
|
|
567
|
+
TMeta,
|
|
568
|
+
TTag,
|
|
569
|
+
TOtel,
|
|
570
|
+
TLogger,
|
|
571
|
+
TScoped
|
|
572
|
+
>
|
|
573
|
+
>
|
|
461
574
|
}
|