orpc-fn 0.1.0 → 0.2.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 +48 -32
- 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/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 +63 -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 +76 -12
- package/dist/live/pub-sub.d.ts.map +1 -1
- package/dist/live/pub-sub.js +429 -125
- 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/sdk.d.ts +20 -0
- package/dist/mcp/sdk.d.ts.map +1 -0
- package/dist/mcp/sdk.js +82 -0
- package/dist/mcp/sdk.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 +5 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +22 -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 +86 -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 +15 -3
- package/src/assemble-procedure.ts +42 -0
- package/src/bound-call.ts +4 -3
- package/src/client.ts +9 -2
- package/src/compatibility.ts +39 -0
- package/src/create-fn.ts +297 -183
- package/src/expo.ts +49 -44
- package/src/hono.ts +36 -17
- package/src/index.ts +6 -2
- package/src/json-schema.ts +16 -0
- package/src/live/codec.ts +22 -3
- package/src/live/fn-live.ts +244 -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 +608 -166
- 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/sdk.ts +118 -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 +39 -1
- package/src/timing.ts +5 -0
- package/src/types.ts +204 -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 FiniteScopedExtras,
|
|
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]>
|
|
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,9 +237,11 @@ export function createFn<
|
|
|
223
237
|
TMeta,
|
|
224
238
|
TTag,
|
|
225
239
|
TOtel,
|
|
226
|
-
TLogger
|
|
240
|
+
TLogger,
|
|
241
|
+
TScoped
|
|
227
242
|
> &
|
|
228
|
-
FiniteExtras<NoInfer<TExtras>>
|
|
243
|
+
FiniteExtras<NoInfer<TExtras>> &
|
|
244
|
+
FiniteScopedExtras<NoInfer<TScoped>>
|
|
229
245
|
): FnFactory<
|
|
230
246
|
Definition<
|
|
231
247
|
TProcedures,
|
|
@@ -235,28 +251,42 @@ export function createFn<
|
|
|
235
251
|
TMeta,
|
|
236
252
|
TTag,
|
|
237
253
|
TOtel,
|
|
238
|
-
TLogger
|
|
254
|
+
TLogger,
|
|
255
|
+
TScoped
|
|
239
256
|
>
|
|
240
257
|
> {
|
|
241
258
|
const runtime = options as unknown as RuntimeOptions
|
|
242
259
|
const tracing = createTracing(runtime.otel)
|
|
243
|
-
const defaultLogger = createDefaultLogger('fn')
|
|
244
|
-
const createLogger =
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
260
|
+
const defaultLogger = protectLogger(createDefaultLogger('fn'))
|
|
261
|
+
const createLogger = (
|
|
262
|
+
scope: string,
|
|
263
|
+
span: SpanLike | undefined
|
|
264
|
+
): FnLogger => {
|
|
265
|
+
try {
|
|
266
|
+
return protectLogger(
|
|
267
|
+
runtime.logger?.(scope, span) ??
|
|
268
|
+
(scope === 'fn' ? defaultLogger : createDefaultLogger(scope))
|
|
269
|
+
)
|
|
270
|
+
} catch {
|
|
271
|
+
return defaultLogger
|
|
272
|
+
}
|
|
273
|
+
}
|
|
248
274
|
const isExpectedError = runtime.isExpectedError ?? isExpectedClientError
|
|
249
275
|
const guards = runtime.guards ?? {}
|
|
250
276
|
const isGuard = (option: string) => Object.hasOwn(guards, option)
|
|
251
277
|
for (const guard of Object.keys(guards)) {
|
|
278
|
+
if (typeof guards[guard] !== 'function')
|
|
279
|
+
throw new TypeError(`orpc-fn: guard ${guard} must be a function`)
|
|
252
280
|
if ((RESERVED_OPTION_KEYS as readonly string[]).includes(guard)) {
|
|
253
281
|
throw new Error(
|
|
254
282
|
`orpc-fn: guard "${guard}" reuses a route option name; rename it`
|
|
255
283
|
)
|
|
256
284
|
}
|
|
257
285
|
}
|
|
286
|
+
if (runtime.default && !Object.hasOwn(runtime.procedures, runtime.default))
|
|
287
|
+
throw new TypeError(`orpc-fn: unknown default procedure ${runtime.default}`)
|
|
258
288
|
for (const [key, builder] of Object.entries(runtime.procedures)) {
|
|
259
|
-
const def = (builder
|
|
289
|
+
const def = procedureDefinition(builder)
|
|
260
290
|
if (def?.inputSchema !== undefined || def?.outputSchema !== undefined) {
|
|
261
291
|
throw new Error(
|
|
262
292
|
`orpc-fn: procedure "${key}" sets an input or output schema; set schemas on routes instead`
|
|
@@ -274,139 +304,204 @@ export function createFn<
|
|
|
274
304
|
return key
|
|
275
305
|
}
|
|
276
306
|
|
|
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
|
-
}
|
|
307
|
+
const buildProcedure = createProcedureAssembler(
|
|
308
|
+
runtime.procedures,
|
|
309
|
+
resolveKey
|
|
310
|
+
)
|
|
298
311
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
312
|
+
const invocation = Symbol('orpc-fn.invocation')
|
|
313
|
+
type Invocation = {
|
|
314
|
+
span: SpanLike | undefined
|
|
315
|
+
logger: FnLogger
|
|
316
|
+
input: unknown
|
|
317
|
+
context: unknown
|
|
318
|
+
handlerStarted: boolean
|
|
319
|
+
rawInput: unknown
|
|
320
|
+
}
|
|
321
|
+
// The overload-to-runtime boundary is adapted once at the returned factory.
|
|
322
|
+
const fn = (routeOptions: Record<string, unknown>): AnyProcedure => {
|
|
323
|
+
const {
|
|
324
|
+
handler,
|
|
325
|
+
input,
|
|
326
|
+
output,
|
|
327
|
+
name,
|
|
328
|
+
procedure,
|
|
329
|
+
meta,
|
|
330
|
+
stream,
|
|
331
|
+
errors,
|
|
332
|
+
guardResolvers,
|
|
333
|
+
route,
|
|
334
|
+
guardChecks
|
|
335
|
+
} = normalizeRoute(routeOptions, isGuard)
|
|
302
336
|
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({
|
|
337
|
+
const built = buildProcedure({
|
|
315
338
|
name,
|
|
316
339
|
procedure: key,
|
|
317
340
|
route,
|
|
318
341
|
meta,
|
|
319
342
|
input,
|
|
320
343
|
output,
|
|
321
|
-
|
|
344
|
+
stream,
|
|
345
|
+
...(errors ? { errors } : {}),
|
|
346
|
+
handler: async (params) => {
|
|
322
347
|
const { context, signal } = params
|
|
323
|
-
const
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
348
|
+
const state = context[invocation] as Invocation
|
|
349
|
+
const { [invocation]: _invocation, ...observedContext } = context
|
|
350
|
+
state.context = observedContext
|
|
351
|
+
state.input = params.input
|
|
352
|
+
state.handlerStarted = true
|
|
353
|
+
try {
|
|
354
|
+
const attributes = runtime.spanAttributes?.({
|
|
355
|
+
context,
|
|
356
|
+
name,
|
|
357
|
+
procedure: key,
|
|
358
|
+
meta
|
|
359
|
+
})
|
|
360
|
+
for (const [attribute, value] of Object.entries(attributes ?? {}))
|
|
361
|
+
if (value !== undefined) state.span?.setAttribute(attribute, value)
|
|
362
|
+
} catch (error) {
|
|
363
|
+
state.logger.warn('spanAttributes hook failed', {
|
|
364
|
+
error: errorMessageOf(error)
|
|
365
|
+
})
|
|
366
|
+
}
|
|
367
|
+
const resolvedChecks = [...guardChecks]
|
|
368
|
+
for (const [guard, resolver] of Object.entries(guardResolvers)) {
|
|
369
|
+
const value = await resolver({ input: params.input, context, signal })
|
|
370
|
+
if (value !== undefined) resolvedChecks.push([guard, value])
|
|
371
|
+
}
|
|
372
|
+
for (const [guard, value] of resolvedChecks) {
|
|
373
|
+
const result = await guards[guard]?.(value, {
|
|
374
|
+
context,
|
|
375
|
+
input: params.input,
|
|
376
|
+
name,
|
|
377
|
+
procedure: key,
|
|
378
|
+
meta,
|
|
379
|
+
signal
|
|
380
|
+
})
|
|
381
|
+
if (result === false)
|
|
382
|
+
throw new ORPCError('FORBIDDEN', {
|
|
383
|
+
message: `Guard "${guard}" denied this call`
|
|
384
|
+
})
|
|
385
|
+
if (result !== undefined && result !== true)
|
|
386
|
+
throw new TypeError(
|
|
387
|
+
`orpc-fn: guard "${guard}" must return void or boolean`
|
|
388
|
+
)
|
|
389
|
+
}
|
|
390
|
+
const extraParams = {
|
|
391
|
+
context,
|
|
392
|
+
span: state.span,
|
|
393
|
+
signal,
|
|
394
|
+
name,
|
|
395
|
+
procedure: key
|
|
396
|
+
}
|
|
397
|
+
const shared = await runtime.extras?.(extraParams)
|
|
398
|
+
const scoped = await runtime.extrasByProcedure?.[key]?.(extraParams)
|
|
399
|
+
const extras = {
|
|
400
|
+
...(shared as object | undefined),
|
|
401
|
+
...(scoped as object | undefined)
|
|
402
|
+
}
|
|
403
|
+
if (extras && typeof extras === 'object') {
|
|
404
|
+
for (const reserved of [
|
|
405
|
+
'input',
|
|
406
|
+
'context',
|
|
407
|
+
'call',
|
|
408
|
+
'signal',
|
|
409
|
+
'span',
|
|
410
|
+
'logger',
|
|
411
|
+
'errors',
|
|
412
|
+
'lastEventId'
|
|
413
|
+
]) {
|
|
414
|
+
if (Object.hasOwn(extras, reserved))
|
|
415
|
+
throw new TypeError(
|
|
416
|
+
`orpc-fn: extra "${reserved}" shadows a handler parameter`
|
|
417
|
+
)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return handler({
|
|
421
|
+
...(extras as object | undefined),
|
|
422
|
+
input: params.input,
|
|
423
|
+
context,
|
|
424
|
+
call: createBoundCall(context, signal, tracing),
|
|
425
|
+
signal,
|
|
426
|
+
span: state.span,
|
|
427
|
+
logger: state.logger,
|
|
428
|
+
errors: params.errors,
|
|
429
|
+
lastEventId: params.lastEventId,
|
|
430
|
+
[RAW_INPUT]: state.rawInput
|
|
431
|
+
})
|
|
432
|
+
}
|
|
433
|
+
})
|
|
434
|
+
// Prepending middleware shifts oRPC's validation indexes, covering auth,
|
|
435
|
+
// input validation, output validation, and the final stream lifetime.
|
|
436
|
+
return os
|
|
437
|
+
.use(async ({ context, next }, rawInput) => {
|
|
438
|
+
const started = performance.now()
|
|
439
|
+
const state: Invocation = {
|
|
440
|
+
span: undefined,
|
|
441
|
+
logger: defaultLogger,
|
|
442
|
+
input: undefined,
|
|
443
|
+
context,
|
|
444
|
+
handlerStarted: false,
|
|
445
|
+
rawInput
|
|
446
|
+
}
|
|
447
|
+
const result = await tracing.inSpan(
|
|
328
448
|
name,
|
|
329
449
|
'SERVER',
|
|
330
450
|
async (span) => {
|
|
331
|
-
|
|
451
|
+
state.span = span
|
|
452
|
+
state.logger = createLogger('fn', span)
|
|
332
453
|
span?.setAttribute('fn.operation', name)
|
|
333
454
|
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
455
|
try {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
456
|
+
const result = await next({ context: { [invocation]: state } })
|
|
457
|
+
if (isAsyncIteratorObject(result.output) !== stream) {
|
|
458
|
+
if (isAsyncIteratorObject(result.output)) {
|
|
459
|
+
try {
|
|
460
|
+
await result.output.return?.()
|
|
461
|
+
} catch {
|
|
462
|
+
/* Release the unexpected stream. */
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
throw new TypeError(
|
|
466
|
+
`orpc-fn: ${name} returned a result inconsistent with stream; declare stream: true for iterators`
|
|
467
|
+
)
|
|
353
468
|
}
|
|
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
|
-
})
|
|
469
|
+
return result.output
|
|
370
470
|
} finally {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
471
|
+
const timing = (context as Record<symbol, unknown>)[
|
|
472
|
+
REQUEST_TIMING
|
|
473
|
+
] as InvocationTiming | undefined
|
|
474
|
+
if (timing && !(context as Record<symbol, unknown>)[invocation]) {
|
|
475
|
+
timing.procedureMs += performance.now() - started
|
|
476
|
+
timing.calls++
|
|
375
477
|
}
|
|
376
478
|
}
|
|
377
479
|
},
|
|
378
|
-
// After the result, or after a returned stream ends; before the span ends.
|
|
379
480
|
(span, ...error) => {
|
|
380
|
-
const durationMs =
|
|
481
|
+
const durationMs =
|
|
482
|
+
Math.round((performance.now() - started) * 100) / 100
|
|
381
483
|
span?.setAttribute('fn.duration_ms', durationMs)
|
|
382
|
-
logCompleted(logger, {
|
|
484
|
+
logCompleted(state.logger, {
|
|
383
485
|
name,
|
|
384
486
|
procedure: key,
|
|
385
487
|
durationMs,
|
|
386
488
|
success: error.length === 0,
|
|
387
489
|
error: error[0],
|
|
388
|
-
input:
|
|
389
|
-
context,
|
|
490
|
+
input: state.input,
|
|
491
|
+
context: state.context,
|
|
390
492
|
span,
|
|
391
|
-
meta
|
|
493
|
+
meta,
|
|
494
|
+
handlerStarted: state.handlerStarted
|
|
392
495
|
})
|
|
393
496
|
}
|
|
394
497
|
)
|
|
395
|
-
|
|
396
|
-
|
|
498
|
+
return { output: result, context }
|
|
499
|
+
})
|
|
500
|
+
.router({ route: built }).route
|
|
397
501
|
}
|
|
398
502
|
|
|
399
503
|
const logCompleted = (logger: FnLogger, event: FnCompletedEvent) => {
|
|
400
504
|
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
505
|
let extra: LogAttributes | undefined
|
|
411
506
|
try {
|
|
412
507
|
extra = runtime.onCompleted?.(event)
|
|
@@ -415,26 +510,31 @@ export function createFn<
|
|
|
415
510
|
error: errorMessageOf(hookError)
|
|
416
511
|
})
|
|
417
512
|
}
|
|
418
|
-
|
|
513
|
+
let expected = false
|
|
514
|
+
try {
|
|
515
|
+
expected = isExpectedError(error)
|
|
516
|
+
} catch {
|
|
517
|
+
expected = isExpectedClientError(error)
|
|
518
|
+
}
|
|
519
|
+
const level = success ? 'info' : expected ? 'warn' : 'error'
|
|
419
520
|
logger[level]('fn.completed', {
|
|
521
|
+
...extra,
|
|
420
522
|
event: 'fn.completed',
|
|
421
523
|
operation: event.name,
|
|
422
524
|
procedure: event.procedure,
|
|
423
525
|
duration_ms: event.durationMs,
|
|
424
526
|
status: success ? 'success' : 'failed',
|
|
425
|
-
...timingFields,
|
|
426
527
|
...(error === undefined
|
|
427
528
|
? {}
|
|
428
529
|
: {
|
|
429
530
|
error_type: error instanceof Error ? error.name : typeof error,
|
|
430
531
|
error_message: errorMessageOf(error)
|
|
431
|
-
})
|
|
432
|
-
...extra
|
|
532
|
+
})
|
|
433
533
|
})
|
|
434
534
|
}
|
|
435
535
|
|
|
436
536
|
const live = createLiveRuntime({
|
|
437
|
-
|
|
537
|
+
fn,
|
|
438
538
|
tracing,
|
|
439
539
|
createLogger,
|
|
440
540
|
pubsub: runtime.pubsub
|
|
@@ -443,19 +543,33 @@ export function createFn<
|
|
|
443
543
|
// The runtimes are untyped; `Fn`, `FnLive`, `CreatePubSub` and
|
|
444
544
|
// `CreatePublisher` are their typed surface.
|
|
445
545
|
return {
|
|
446
|
-
fn
|
|
546
|
+
fn,
|
|
447
547
|
fnLive: createFnLive({
|
|
448
548
|
fn,
|
|
449
549
|
createChannel: live.createChannel,
|
|
450
550
|
createLogger,
|
|
451
551
|
isGuard
|
|
452
|
-
})
|
|
453
|
-
createPubSub: live.createPubSub
|
|
454
|
-
createPublisher: live.createPublisher
|
|
552
|
+
}),
|
|
553
|
+
createPubSub: live.createPubSub,
|
|
554
|
+
createPublisher: live.createPublisher,
|
|
455
555
|
createRouter,
|
|
456
|
-
readMeta:
|
|
457
|
-
createCall: (context, signal) =>
|
|
556
|
+
readMeta: readFnMeta,
|
|
557
|
+
createCall: <TContext>(context: TContext, signal?: AbortSignal) =>
|
|
558
|
+
createBoundCall(context, signal, tracing),
|
|
559
|
+
shutdown: live.shutdown,
|
|
458
560
|
drainPubSubSubscribers: live.drainPubSubSubscribers,
|
|
459
561
|
activePubSubSubscriberCount: live.activePubSubSubscriberCount
|
|
460
|
-
}
|
|
562
|
+
} as unknown as FnFactory<
|
|
563
|
+
Definition<
|
|
564
|
+
TProcedures,
|
|
565
|
+
TDefault,
|
|
566
|
+
TExtras,
|
|
567
|
+
TGuards,
|
|
568
|
+
TMeta,
|
|
569
|
+
TTag,
|
|
570
|
+
TOtel,
|
|
571
|
+
TLogger,
|
|
572
|
+
TScoped
|
|
573
|
+
>
|
|
574
|
+
>
|
|
461
575
|
}
|