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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hans Kristoffer Hansen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,250 @@
1
+ # orpc-fn
2
+
3
+ Define a backend function once. You get a typed [oRPC](https://orpc.dev) procedure with tracing, structured logging, guards, handler extras from your app, nested calls that keep the context, and optional live queries over Redis. Adapters expose the same procedure as a Mastra tool, an MCP tool, or a route on Hono, and the client helpers call it from Vue, React or Expo.
4
+
5
+ Your app keeps everything app-specific: auth and session, context shape, database, queues, i18n, feature flags and permissions. You inject them once with `createFn`.
6
+
7
+ ```sh
8
+ bun add orpc-fn @orpc/server @orpc/contract @orpc/client zod
9
+ ```
10
+
11
+ ESM only, Node 20+ or Bun, TypeScript 5.7+, oRPC 1.14+ (`@orpc/client` is already a dependency of `@orpc/server`), Zod 4. Optional peers per subpath are listed below.
12
+
13
+ ## 30 seconds
14
+
15
+ ```ts
16
+ // lib/fn.ts — once per app
17
+ import { ORPCError, os } from '@orpc/server'
18
+ import * as otel from '@opentelemetry/api'
19
+ import { createFn } from 'orpc-fn'
20
+
21
+ const publicProcedure = os.$context<{ headers: Headers; user?: User }>()
22
+ const protectedProcedure = publicProcedure.use(({ context, next }) => {
23
+ if (!context.user) throw new ORPCError('UNAUTHORIZED')
24
+ return next({ context: { user: context.user } })
25
+ })
26
+
27
+ export const { fn, fnLive, createPubSub, createRouter } = createFn({
28
+ procedures: { public: publicProcedure, protected: protectedProcedure },
29
+ default: 'protected',
30
+ extras: () => ({ db }),
31
+ guards: {
32
+ permission: (required: Permission, { context }) => context.user?.can(required)
33
+ },
34
+ otel
35
+ })
36
+ ```
37
+
38
+ ```ts
39
+ // features/orders/get-order.ts
40
+ export const getOrder = fn({
41
+ name: 'order.get',
42
+ method: 'GET',
43
+ path: '/orders/{id}',
44
+ permission: 'orders:read',
45
+ input: z.object({ id: z.string() }),
46
+ handler: async ({ input, context, db, call, logger }) => {
47
+ logger.info('loading order', { id: input.id }) // context.user is typed: protected
48
+ const order = await db.order.find(input.id)
49
+ const customer = await call(getCustomer, { id: order.customerId }) // child span, same context
50
+ return { ...order, customer }
51
+ }
52
+ })
53
+ ```
54
+
55
+ Every call runs in an OpenTelemetry span named `order.get` (for a streaming handler, until the stream ends), runs the `permission` guard, writes `handler_ms` back to `context.timing`, and logs one `fn.completed` line at `info`, `warn` (4xx) or `error` (defects).
56
+
57
+ ## `orpc-fn`: `createFn`
58
+
59
+ ```ts
60
+ createFn({
61
+ procedures, // named oRPC builders; the handler context is the builder's context
62
+ default, // builder used when a route omits `procedure`; leave out to require `procedure` on every route
63
+ extras, // ({ context, span, signal, name, procedure }) => values (or a promise) merged into handler params
64
+ guards, // { key: (value, { context, input, name, meta, signal }) => void } — each key is a typed fn() option
65
+ meta, // `{} as { readOnly?: boolean }` — typed fn() options stored on the procedure
66
+ tags, // ['internal', 'external'] — allowed route tags
67
+ otel, // `import * as otel from '@opentelemetry/api'`; omit for no tracing
68
+ logger, // (scope, span) => your logger (debug/info/warn/error)
69
+ spanAttributes, // ({ context, name, procedure, meta }) => extra span attributes
70
+ onCompleted, // (event) => extra attributes for the fn.completed log
71
+ isExpectedError, // default: 4xx ORPCError or AbortError
72
+ pubsub // { transport, onDrop?, maxQueueSize? } for fnLive / createPubSub
73
+ })
74
+ ```
75
+
76
+ It returns `fn`, `fnLive`, `createPubSub`, `createPublisher`, `createRouter`, `readMeta`, `createCall`, `drainPubSubSubscribers` and `activePubSubSubscriberCount`.
77
+
78
+ Procedure builders must not set an input or output schema (`os.input(...)`); put schemas on routes. Guard and meta keys must not reuse a route option name (`name`, `tags`, `input`, …), and guards and meta must not share a key. Both are type errors, and a reserved guard name also throws at startup.
79
+
80
+ **`fn(options)`** has four overloads (input and output schema, each given or inferred). Route options: `name` (required; operationId and span name), `procedure`, `path`, `method`, `summary`, `description`, `tags`, `deprecated`, `successStatus`, `successDescription`, `inputStructure`, `outputStructure`, every guard key and every meta key. The handler receives `input`, `context`, `call`, `signal`, `span`, `logger` and your extras. With an output schema, the handler returns the schema's input type and callers receive its output type, so `output: z.string().transform(Number)` means the handler returns a string and callers get a number.
81
+
82
+ **Guards** run inside the span, before the handler, and only on routes that set their option. Throw (usually an `ORPCError`) to refuse.
83
+
84
+ **`call(procedure, input)`** calls another procedure with the caller's context and abort signal, inside a `call: <name>` child span. Input and output are typed from the callee. Calling a procedure whose context the caller's context does not satisfy is a type error. Outside a handler (seeders, scripts, tests), use `createCall(context, signal?)` from `createFn`.
85
+
86
+ **Meta** is stored in oRPC's own procedure meta. It survives `os.router()`, prefixes and lazy routers. Read it with `readMeta(procedure)` (typed) or `readFnMeta(procedure)`.
87
+
88
+ Also exported: `isExpectedClientError`, `createBoundedEventQueue`, `createRouter`, `createBoundCall`, and the types `HandlerParams`, `RouteConfig`, `BoundCall`, `FnMeta`, `FnContext`, `FnCompletedEvent`, `ProcedureInput`, `ProcedureOutput`, `ProcedureContext`, `InitialContextOf`, `CurrentContextOf` and `GuardOptions`.
89
+
90
+ ## `orpc-fn/live`: pub/sub and live queries
91
+
92
+ ```ts
93
+ import { createFn } from 'orpc-fn'
94
+ import { bunRedisTransport } from 'orpc-fn/live/redis-bun' // or ioredis / memory
95
+
96
+ const transport = bunRedisTransport(new RedisClient(process.env.REDIS_URL))
97
+ export const { fnLive, createPubSub, drainPubSubSubscribers } = createFn({
98
+ /* … */,
99
+ pubsub: { transport, onDrop: (count) => dropCounter.add(count) }
100
+ })
101
+
102
+ export const listOrders = fnLive({
103
+ name: 'order.list',
104
+ input: z.object({ organizationId: z.string() }),
105
+ handler: ({ input, db }) => db.order.list(input.organizationId),
106
+ live: {
107
+ eventSchema: z.object({ organizationId: z.string(), orderId: z.string() }),
108
+ channel: ({ organizationId }) => `orders:${organizationId}`,
109
+ coalesceMs: 50
110
+ }
111
+ })
112
+ // router: { list: listOrders.procedure, subscribe: listOrders.subscribe }
113
+ await listOrders.publish({ organizationId, orderId }) // after a write
114
+ ```
115
+
116
+ The `subscribe` route first runs `authFn` and subscribes to the channel, then streams the initial snapshot, then a new one per event. A refused subscriber receives nothing, and events published while the snapshot loads are not lost. Snapshots go through the output schema, like the route's own result. Options:
117
+
118
+ - `shouldUpdate` skips an event.
119
+ - `transformerFn` folds an event into `previous` instead of re-running the handler. It may return `fnLivePatch(state, emit)` to send a small payload and keep the full state.
120
+ - `coalesceMs` batches events that arrive close together.
121
+ - `authFn` and `useBacklog`/`backlogSize`/`backlogTtl` work as on `createPubSub`.
122
+ - `overflowMarker` is enqueued when a slow subscriber's queue overflows, once per overflow episode, so the client can resync.
123
+ - `safePublish` logs publish errors instead of throwing them.
124
+
125
+ **`createPubSub`** returns a typed `subscribe` route plus `publish`, `publishMany` (one atomic round-trip across channels) and `getChannelName`. Publishers pass the event schema's input type; subscribers, filters and channel resolvers get its output type. The raw event travels in oRPC's JSON format, so `Date`, `Map`, `Set` and `BigInt` survive, and each receiver parses it once. Options: `filterFn`, `authFn`, `mirrorChannel`, `overflowMarker`, `procedure`. Each pub/sub definition subscribes to each channel once and parses each payload once for all local subscribers. Each subscriber then runs its filter in arrival order as it reads; its bounded queue holds events before filtering, so `onDrop` counts unfiltered events. Abort and drain release a subscription at once, even one that is not being read. A lost subscription is retried with exponential backoff and jitter. **`createPublisher`** is the publish-only half.
126
+
127
+ **Graceful shutdown:** call `drainPubSubSubscribers()` on SIGTERM. Open streams end cleanly and clients reconnect to the new deployment. Draining is per `createFn` instance.
128
+
129
+ **Transports** implement `PubSubTransport` (`publish(messages, backlog?)`, `readBacklog(channel)`, `subscribe(channel, listener, onLost)`):
130
+
131
+ | Subpath | Client | Notes |
132
+ |---|---|---|
133
+ | `orpc-fn/live/redis-bun` | Bun `RedisClient` | Dedicated subscriber via `duplicate()`; re-subscribes after Bun's reconnects |
134
+ | `orpc-fn/live/ioredis` | ioredis 5 | Dedicated subscriber via `duplicate()`; ioredis re-subscribes itself |
135
+ | `orpc-fn/live/memory` | none | Tests and single-process dev |
136
+
137
+ A subscriber connection that is lost, or cannot restore its channels after a reconnect, is closed and replaced; nothing it still emits is delivered. The Redis transports publish and maintain backlogs with one Lua script, sent by SHA, with a fallback to `EVAL` when Redis returns `NOSCRIPT`. Call `transport.close()` on shutdown.
138
+
139
+ `streamLiveSnapshots`, `fnLivePatch` and `throwInitialSnapshotError` are exported for custom streams.
140
+
141
+ ## `orpc-fn/mastra`
142
+
143
+ Optional peers: `@mastra/core` ^1.51, `@orpc/zod`, `@orpc/json-schema`.
144
+
145
+ ```ts
146
+ import { createMastraTool } from 'orpc-fn/mastra'
147
+
148
+ const tool = createMastraTool(getOrder, { requireApproval: true })
149
+ // at run time: requestContext.set('orpcContext', context)
150
+ ```
151
+
152
+ Options: `id`, `description`, `requireApproval`, `allowMissingInputSchema`, `onExecuteFinish`, `contextKey` (default `'orpcContext'`). Execution goes through oRPC `call`, so middleware and guards apply.
153
+
154
+ The tool takes the procedure's raw input and returns its parsed output, and `InferToolInput`/`InferToolOutput`/`InferUITools` see exactly those types. Mastra validates the input (coercing date strings) but passes the raw value on, and the procedure parses that raw value: a transform may run twice, but never on its own output. Agent tools need an object input: any other input is a type error and throws.
155
+
156
+ ## `orpc-fn/mcp`
157
+
158
+ Optional peers: `@orpc/zod`, `@orpc/json-schema`.
159
+
160
+ ```ts
161
+ import { hasTag, listTools } from 'orpc-fn/mcp'
162
+
163
+ for (const { name, procedure, config } of listTools(router, { filter: hasTag('external') })) {
164
+ server.registerTool(name, config, (args) => call(procedure, args, { context }))
165
+ }
166
+ ```
167
+
168
+ Tool names are the sanitized `fn` names (`user.me` becomes `user-me`). Input schemas carry the same JSON Schema as your OpenAPI docs (dates become `string`/`date-time`), and validation turns them back into `Date`s. Validation returns the raw arguments, and `call(procedure, args, { context })` parses those: a transform may run twice, but never on its own output. `readOnlyHint` defaults to GET routes; pass `readOnly` to change it. `hasTag(tag)` also works as an oRPC `OpenAPIHandler` filter.
169
+
170
+ ## `orpc-fn/hono`
171
+
172
+ Optional peers: `hono`, `@orpc/openapi`, `@orpc/zod`, `@orpc/json-schema`.
173
+
174
+ ```ts
175
+ import { mountOrpc, normalizeExpoOrigin } from 'orpc-fn/hono'
176
+
177
+ mountOrpc(app, {
178
+ router,
179
+ rpcPrefix: '/rpc',
180
+ openapi: { prefix: '/api', info: { title: 'API', version: '1.0.0' } },
181
+ context: async (c, { headers, timing }) => ({ headers, timing, session: await getSession(headers) }),
182
+ normalizeHeaders: normalizeExpoOrigin,
183
+ onError: (error, request) => errorTracker.capture(error, request)
184
+ })
185
+ ```
186
+
187
+ Behaviour:
188
+
189
+ - The response gets a `Server-Timing` header built from `context.timing`: queue, auth and context if you record them, and handler from `fn`.
190
+ - SSE responses get keep-alive comments (15 s) and anti-buffering headers.
191
+ - RPC calls are batched (`BatchHandlerPlugin`).
192
+ - Scalar docs are served at the OpenAPI prefix.
193
+ - `onError` sees only unexpected errors.
194
+ - `openapi.filter`, `openapi.smartCoercion` and `openapi.spec` (for security schemes) cover an external API with its own auth.
195
+ - `context` builds the router's context and is checked against it: it is required when `{ headers, timing }` alone does not satisfy the router. It may return a `Response`, for example a 401.
196
+
197
+ ## `orpc-fn/client`
198
+
199
+ Has no server code, so it is safe in frontends (Vue, React, Expo).
200
+
201
+ ```ts
202
+ import { createORPCClient } from '@orpc/client'
203
+ import { createTanstackQueryUtils } from '@orpc/tanstack-query'
204
+ import { createRpcLink, hasOrpcErrorCode } from 'orpc-fn/client'
205
+ import type { RouterClient } from '@orpc/server'
206
+ import type { router } from '@app/backend' // type-only import
207
+
208
+ const link = createRpcLink({
209
+ url: `${apiOrigin}/rpc`,
210
+ fetch: (request, init) => fetch(request, { ...init, credentials: 'include', cache: 'no-store' })
211
+ })
212
+ export const client: RouterClient<typeof router> = createORPCClient(link)
213
+ export const orpc = createTanstackQueryUtils(client) // Vue Query or React Query
214
+
215
+ if (hasOrpcErrorCode(error, 'NOT_FOUND')) { /* … */ }
216
+ ```
217
+
218
+ `createRpcLink` takes every `RPCLink` option. It also batches parallel calls into one request, matching `mountOrpc`'s server-side batching (`batch: { maxSize, exclude, groups }`, or `false` to turn it off). A batch is one request with one client context, so by default only calls without a client context are batched; pass `groups` to batch calls that share one. A streaming response cannot be batched, and the request doesn't say whether the response will stream, so streaming routes are recognised by name: any path segment containing `subscribe` is sent on its own (`isSubscriptionPath`).
219
+
220
+ ## `orpc-fn/expo`
221
+
222
+ Expo itself is passed in, not imported. The fetch must resolve to a response with the members oRPC reads (`expo/fetch` does).
223
+
224
+ ```ts
225
+ import { fetch } from 'expo/fetch'
226
+ import { Platform } from 'react-native'
227
+ import { createExpoLink } from 'orpc-fn/expo'
228
+
229
+ const link = createExpoLink({
230
+ url: `${baseUrl}/api/rpc`,
231
+ fetch,
232
+ native: Platform.OS !== 'web',
233
+ getCookie: () => authClient.getCookie(), // Better Auth Expo
234
+ getExpoOrigin,
235
+ headers: () => ({ 'x-orpc-source': 'expo-react' })
236
+ })
237
+ ```
238
+
239
+ On native, calls go through `expo/fetch`, the only fetch that streams there. The bridge forwards the request body and the abort signal. Without the signal, cancelled subscriptions keep their sockets open, the device's HTTP pool runs out, and later requests hang. Cookies are sent in a header (`credentials: 'omit'`), together with `expo-origin` and `x-skip-oauth-proxy`, the same headers `@better-auth/expo` sends. On the server, `mountOrpc({ normalizeHeaders: normalizeExpoOrigin })` copies `expo-origin` to `origin`. On web, the link uses the platform fetch with cookies. `createExpoFetch` is exported for a custom link.
240
+
241
+ ## Docs
242
+
243
+ - [Migrating an app from an in-repo `lib/fn`](docs/migration.md)
244
+ - [Decisions: what oRPC already ships and why this exists](docs/decisions.md)
245
+ - [Releasing](docs/releasing.md)
246
+ - [Example: Hono, in-memory pub/sub, two routes and a live query](examples/basic/server.ts)
247
+
248
+ ## License
249
+
250
+ MIT
@@ -0,0 +1,23 @@
1
+ import { type AnyProcedure } from '@orpc/server';
2
+ import { type Tracing } from './otel.js';
3
+ import type { ProcedureContext, ProcedureInput, ProcedureOutput } from './types.js';
4
+ /** Rejects a procedure whose context the caller's context does not satisfy. */
5
+ type ContextCheck<TContext, T> = [TContext] extends [ProcedureContext<T>] ? unknown : {
6
+ 'orpc-fn: the calling context does not satisfy this procedure': ProcedureContext<T>;
7
+ };
8
+ /**
9
+ * Calls another procedure with the caller's context and signal. `TContext` is
10
+ * the caller's context; the callee must accept it.
11
+ */
12
+ export type BoundCall<TContext = any> = <T extends AnyProcedure>(procedure: T & ContextCheck<TContext, T>, input: ProcedureInput<T>) => Promise<ProcedureOutput<T>>;
13
+ /**
14
+ * Creates a bound call: nested calls keep the caller's context and abort
15
+ * signal and run inside a `call: <name>` child span, so the trace shows the
16
+ * whole call tree. oRPC's `call` runs the callee's own middleware and guards.
17
+ *
18
+ * Handlers get one as `call`. Outside a handler (seeders, tests) use
19
+ * `createCall` from `createFn`, which traces with the instance's OpenTelemetry.
20
+ */
21
+ export declare function createBoundCall<TContext>(context: TContext, signal?: AbortSignal, tracing?: Tracing): BoundCall<TContext>;
22
+ export {};
23
+ //# sourceMappingURL=bound-call.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bound-call.d.ts","sourceRoot":"","sources":["../src/bound-call.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAQ,MAAM,cAAc,CAAA;AAEtD,OAAO,EAAiB,KAAK,OAAO,EAAE,MAAM,WAAW,CAAA;AACvD,OAAO,KAAK,EACX,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,MAAM,YAAY,CAAA;AAEnB,+EAA+E;AAC/E,KAAK,YAAY,CAAC,QAAQ,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,GACtE,OAAO,GACP;IACA,8DAA8D,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAA;CACnF,CAAA;AAEH;;;GAGG;AAEH,MAAM,MAAM,SAAS,CAAC,QAAQ,GAAG,GAAG,IAAI,CAAC,CAAC,SAAS,YAAY,EAC9D,SAAS,EAAE,CAAC,GAAG,YAAY,CAAC,QAAQ,EAAE,CAAC,CAAC,EACxC,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,KACpB,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAA;AAIhC;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EACvC,OAAO,EAAE,QAAQ,EACjB,MAAM,CAAC,EAAE,WAAW,EACpB,OAAO,GAAE,OAAmB,GAC1B,SAAS,CAAC,QAAQ,CAAC,CASrB"}
@@ -0,0 +1,23 @@
1
+ import { call } from '@orpc/server';
2
+ import { readFnMeta } from './meta.js';
3
+ import { createTracing } from './otel.js';
4
+ const noTracing = createTracing(undefined);
5
+ /**
6
+ * Creates a bound call: nested calls keep the caller's context and abort
7
+ * signal and run inside a `call: <name>` child span, so the trace shows the
8
+ * whole call tree. oRPC's `call` runs the callee's own middleware and guards.
9
+ *
10
+ * Handlers get one as `call`. Outside a handler (seeders, tests) use
11
+ * `createCall` from `createFn`, which traces with the instance's OpenTelemetry.
12
+ */
13
+ export function createBoundCall(context, signal, tracing = noTracing) {
14
+ return ((procedure, input) => {
15
+ const name = readFnMeta(procedure).name ?? 'unknown_procedure';
16
+ return tracing.inSpan(`call: ${name}`, 'INTERNAL', async (span) => {
17
+ signal?.throwIfAborted();
18
+ span?.setAttribute('rpc.method', name);
19
+ return call(procedure, input, signal ? { context, signal } : { context });
20
+ });
21
+ });
22
+ }
23
+ //# sourceMappingURL=bound-call.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bound-call.js","sourceRoot":"","sources":["../src/bound-call.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,IAAI,EAAE,MAAM,cAAc,CAAA;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AACtC,OAAO,EAAE,aAAa,EAAgB,MAAM,WAAW,CAAA;AAwBvD,MAAM,SAAS,GAAG,aAAa,CAAC,SAAS,CAAC,CAAA;AAE1C;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAC9B,OAAiB,EACjB,MAAoB,EACpB,OAAO,GAAY,SAAS;IAE5B,OAAO,CAAC,CAAC,SAAuB,EAAE,KAAc,EAAE,EAAE;QACnD,MAAM,IAAI,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,IAAI,IAAI,mBAAmB,CAAA;QAC9D,OAAO,OAAO,CAAC,MAAM,CAAC,SAAS,IAAI,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;YACjE,MAAM,EAAE,cAAc,EAAE,CAAA;YACxB,IAAI,EAAE,YAAY,CAAC,YAAY,EAAE,IAAI,CAAC,CAAA;YACtC,OAAO,IAAI,CAAC,SAAS,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAA;QAC1E,CAAC,CAAC,CAAA;IACH,CAAC,CAAwB,CAAA;AAC1B,CAAC","sourcesContent":["import { type AnyProcedure, call } from '@orpc/server'\nimport { readFnMeta } from './meta.js'\nimport { createTracing, type Tracing } from './otel.js'\nimport type {\n\tProcedureContext,\n\tProcedureInput,\n\tProcedureOutput\n} from './types.js'\n\n/** Rejects a procedure whose context the caller's context does not satisfy. */\ntype ContextCheck<TContext, T> = [TContext] extends [ProcedureContext<T>]\n\t? unknown\n\t: {\n\t\t\t'orpc-fn: the calling context does not satisfy this procedure': ProcedureContext<T>\n\t\t}\n\n/**\n * Calls another procedure with the caller's context and signal. `TContext` is\n * the caller's context; the callee must accept it.\n */\n// biome-ignore lint/suspicious/noExplicitAny: an unbound call accepts any procedure\nexport type BoundCall<TContext = any> = <T extends AnyProcedure>(\n\tprocedure: T & ContextCheck<TContext, T>,\n\tinput: ProcedureInput<T>\n) => Promise<ProcedureOutput<T>>\n\nconst noTracing = createTracing(undefined)\n\n/**\n * Creates a bound call: nested calls keep the caller's context and abort\n * signal and run inside a `call: <name>` child span, so the trace shows the\n * whole call tree. oRPC's `call` runs the callee's own middleware and guards.\n *\n * Handlers get one as `call`. Outside a handler (seeders, tests) use\n * `createCall` from `createFn`, which traces with the instance's OpenTelemetry.\n */\nexport function createBoundCall<TContext>(\n\tcontext: TContext,\n\tsignal?: AbortSignal,\n\ttracing: Tracing = noTracing\n): BoundCall<TContext> {\n\treturn ((procedure: AnyProcedure, input: unknown) => {\n\t\tconst name = readFnMeta(procedure).name ?? 'unknown_procedure'\n\t\treturn tracing.inSpan(`call: ${name}`, 'INTERNAL', async (span) => {\n\t\t\tsignal?.throwIfAborted()\n\t\t\tspan?.setAttribute('rpc.method', name)\n\t\t\treturn call(procedure, input, signal ? { context, signal } : { context })\n\t\t})\n\t}) as BoundCall<TContext>\n}\n"]}
@@ -0,0 +1,44 @@
1
+ import { type ClientContext, type ORPCErrorCode } from '@orpc/client';
2
+ import { RPCLink, type RPCLinkOptions } from '@orpc/client/fetch';
3
+ import { BatchLinkPlugin } from '@orpc/client/plugins';
4
+ type BatchOptions<T extends ClientContext> = ConstructorParameters<typeof BatchLinkPlugin<T>>[0];
5
+ export type CreateRpcLinkOptions<T extends ClientContext> = RPCLinkOptions<T> & {
6
+ /**
7
+ * Collapse parallel calls into one HTTP request, matching `mountOrpc`'s
8
+ * server-side batching. Default: true (25 calls). Pass false to turn off.
9
+ *
10
+ * A batch is one HTTP request, so it carries one client context. By
11
+ * default only calls without a client context are batched; pass
12
+ * `groups` to batch calls that share a context (e.g. a token).
13
+ */
14
+ batch?: boolean | {
15
+ maxSize?: number;
16
+ /** Calls that must not be batched. Default: {@link isSubscriptionPath}. */
17
+ exclude?: (path: readonly string[]) => boolean;
18
+ groups?: BatchOptions<T>['groups'];
19
+ };
20
+ };
21
+ /**
22
+ * Streaming responses cannot be batched, and the request does not say a
23
+ * response will stream - so streaming routes are recognised by name. The
24
+ * default treats any path segment containing `subscribe` as one (the
25
+ * convention `fnLive` routers use: `{ list, subscribe }`).
26
+ */
27
+ export declare const isSubscriptionPath: (path: readonly string[]) => boolean;
28
+ /**
29
+ * `RPCLink` with request batching: a page-load burst becomes one request
30
+ * instead of many queued on the server's event loop.
31
+ *
32
+ * ```ts
33
+ * const client: RouterClient<typeof router> = createORPCClient(createRpcLink({ url }))
34
+ * export const orpc = createTanstackQueryUtils(client)
35
+ * ```
36
+ */
37
+ export declare function createRpcLink<T extends ClientContext = ClientContext>(options: CreateRpcLinkOptions<T>): RPCLink<T>;
38
+ /**
39
+ * Typed check for a route error on the client. The RPC link rebuilds thrown
40
+ * route errors as `ORPCError`, so `instanceof` plus `code` is the contract.
41
+ */
42
+ export declare function hasOrpcErrorCode(error: unknown, code: ORPCErrorCode): boolean;
43
+ export {};
44
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAa,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;AAChF,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAEtD,KAAK,YAAY,CAAC,CAAC,SAAS,aAAa,IAAI,qBAAqB,CACjE,OAAO,eAAe,CAAC,CAAC,CAAC,CACzB,CAAC,CAAC,CAAC,CAAA;AAEJ,MAAM,MAAM,oBAAoB,CAAC,CAAC,SAAS,aAAa,IACvD,cAAc,CAAC,CAAC,CAAC,GAAG;IACnB;;;;;;;OAOG;IACH,KAAK,CAAC,EACH,OAAO,GACP;QACA,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,2EAA2E;QAC3E,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,OAAO,CAAA;QAC9C,MAAM,CAAC,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAA;KACjC,CAAA;CACJ,CAAA;AAEF;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,SAAU,SAAS,MAAM,EAAE,YACU,CAAA;AAEpE;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,aAAa,GAAG,aAAa,EACpE,OAAO,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAC9B,OAAO,CAAC,CAAC,CAAC,CAyBZ;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,GAAG,OAAO,CAE7E"}
package/dist/client.js ADDED
@@ -0,0 +1,49 @@
1
+ import { ORPCError } from '@orpc/client';
2
+ import { RPCLink } from '@orpc/client/fetch';
3
+ import { BatchLinkPlugin } from '@orpc/client/plugins';
4
+ /**
5
+ * Streaming responses cannot be batched, and the request does not say a
6
+ * response will stream - so streaming routes are recognised by name. The
7
+ * default treats any path segment containing `subscribe` as one (the
8
+ * convention `fnLive` routers use: `{ list, subscribe }`).
9
+ */
10
+ export const isSubscriptionPath = (path) => path.some((segment) => segment.toLowerCase().includes('subscribe'));
11
+ /**
12
+ * `RPCLink` with request batching: a page-load burst becomes one request
13
+ * instead of many queued on the server's event loop.
14
+ *
15
+ * ```ts
16
+ * const client: RouterClient<typeof router> = createORPCClient(createRpcLink({ url }))
17
+ * export const orpc = createTanstackQueryUtils(client)
18
+ * ```
19
+ */
20
+ export function createRpcLink(options) {
21
+ const { batch = true, plugins = [], ...rest } = options;
22
+ if (batch === false)
23
+ return new RPCLink({ ...rest, plugins });
24
+ const { maxSize = 25, exclude = isSubscriptionPath, groups } = batch === true ? {} : batch;
25
+ return new RPCLink({
26
+ ...rest,
27
+ plugins: [
28
+ new BatchLinkPlugin({
29
+ maxSize,
30
+ groups: groups ?? [
31
+ // Only context-free calls reach this group (see `exclude` below),
32
+ // so the batch's empty context is exactly theirs.
33
+ { condition: () => true, context: {} }
34
+ ],
35
+ exclude: ({ path, context }) => exclude(path) ||
36
+ (groups === undefined && Object.keys(context).length > 0)
37
+ }),
38
+ ...plugins
39
+ ]
40
+ });
41
+ }
42
+ /**
43
+ * Typed check for a route error on the client. The RPC link rebuilds thrown
44
+ * route errors as `ORPCError`, so `instanceof` plus `code` is the contract.
45
+ */
46
+ export function hasOrpcErrorCode(error, code) {
47
+ return error instanceof ORPCError && error.code === code;
48
+ }
49
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,SAAS,EAAsB,MAAM,cAAc,CAAA;AAChF,OAAO,EAAE,OAAO,EAAuB,MAAM,oBAAoB,CAAA;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AA0BtD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,IAAuB,EAAE,EAAE,CAC7D,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAA;AAEpE;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAC5B,OAAgC;IAEhC,MAAM,EAAE,KAAK,GAAG,IAAI,EAAE,OAAO,GAAG,EAAE,EAAE,GAAG,IAAI,EAAE,GAAG,OAAO,CAAA;IACvD,IAAI,KAAK,KAAK,KAAK;QAAE,OAAO,IAAI,OAAO,CAAI,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,CAAC,CAAA;IAChE,MAAM,EACL,OAAO,GAAG,EAAE,EACZ,OAAO,GAAG,kBAAkB,EAC5B,MAAM,EACN,GAAG,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAA;IAC/B,OAAO,IAAI,OAAO,CAAI;QACrB,GAAG,IAAI;QACP,OAAO,EAAE;YACR,IAAI,eAAe,CAAI;gBACtB,OAAO;gBACP,MAAM,EAAE,MAAM,IAAI;oBACjB,kEAAkE;oBAClE,kDAAkD;oBAClD,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,EAAO,EAAE;iBAC3C;gBACD,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,CAC9B,OAAO,CAAC,IAAI,CAAC;oBACb,CAAC,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;aAC1D,CAAC;YACF,GAAG,OAAO;SACV;KACD,CAAC,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAc,EAAE,IAAmB;IACnE,OAAO,KAAK,YAAY,SAAS,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI,CAAA;AACzD,CAAC","sourcesContent":["import { type ClientContext, ORPCError, type ORPCErrorCode } from '@orpc/client'\nimport { RPCLink, type RPCLinkOptions } from '@orpc/client/fetch'\nimport { BatchLinkPlugin } from '@orpc/client/plugins'\n\ntype BatchOptions<T extends ClientContext> = ConstructorParameters<\n\ttypeof BatchLinkPlugin<T>\n>[0]\n\nexport type CreateRpcLinkOptions<T extends ClientContext> =\n\tRPCLinkOptions<T> & {\n\t\t/**\n\t\t * Collapse parallel calls into one HTTP request, matching `mountOrpc`'s\n\t\t * server-side batching. Default: true (25 calls). Pass false to turn off.\n\t\t *\n\t\t * A batch is one HTTP request, so it carries one client context. By\n\t\t * default only calls without a client context are batched; pass\n\t\t * `groups` to batch calls that share a context (e.g. a token).\n\t\t */\n\t\tbatch?:\n\t\t\t| boolean\n\t\t\t| {\n\t\t\t\t\tmaxSize?: number\n\t\t\t\t\t/** Calls that must not be batched. Default: {@link isSubscriptionPath}. */\n\t\t\t\t\texclude?: (path: readonly string[]) => boolean\n\t\t\t\t\tgroups?: BatchOptions<T>['groups']\n\t\t\t }\n\t}\n\n/**\n * Streaming responses cannot be batched, and the request does not say a\n * response will stream - so streaming routes are recognised by name. The\n * default treats any path segment containing `subscribe` as one (the\n * convention `fnLive` routers use: `{ list, subscribe }`).\n */\nexport const isSubscriptionPath = (path: readonly string[]) =>\n\tpath.some((segment) => segment.toLowerCase().includes('subscribe'))\n\n/**\n * `RPCLink` with request batching: a page-load burst becomes one request\n * instead of many queued on the server's event loop.\n *\n * ```ts\n * const client: RouterClient<typeof router> = createORPCClient(createRpcLink({ url }))\n * export const orpc = createTanstackQueryUtils(client)\n * ```\n */\nexport function createRpcLink<T extends ClientContext = ClientContext>(\n\toptions: CreateRpcLinkOptions<T>\n): RPCLink<T> {\n\tconst { batch = true, plugins = [], ...rest } = options\n\tif (batch === false) return new RPCLink<T>({ ...rest, plugins })\n\tconst {\n\t\tmaxSize = 25,\n\t\texclude = isSubscriptionPath,\n\t\tgroups\n\t} = batch === true ? {} : batch\n\treturn new RPCLink<T>({\n\t\t...rest,\n\t\tplugins: [\n\t\t\tnew BatchLinkPlugin<T>({\n\t\t\t\tmaxSize,\n\t\t\t\tgroups: groups ?? [\n\t\t\t\t\t// Only context-free calls reach this group (see `exclude` below),\n\t\t\t\t\t// so the batch's empty context is exactly theirs.\n\t\t\t\t\t{ condition: () => true, context: {} as T }\n\t\t\t\t],\n\t\t\t\texclude: ({ path, context }) =>\n\t\t\t\t\texclude(path) ||\n\t\t\t\t\t(groups === undefined && Object.keys(context).length > 0)\n\t\t\t}),\n\t\t\t...plugins\n\t\t]\n\t})\n}\n\n/**\n * Typed check for a route error on the client. The RPC link rebuilds thrown\n * route errors as `ORPCError`, so `instanceof` plus `code` is the contract.\n */\nexport function hasOrpcErrorCode(error: unknown, code: ORPCErrorCode): boolean {\n\treturn error instanceof ORPCError && error.code === code\n}\n"]}
@@ -0,0 +1,97 @@
1
+ import type { AnyProcedure } from '@orpc/server';
2
+ import { type BoundCall } from './bound-call.js';
3
+ import { type FnLive } from './live/fn-live.js';
4
+ import { type CreatePublisher, type CreatePubSub, type PubSubRuntimeOptions } from './live/pub-sub.js';
5
+ import { type FnLogger, type LogAttributes } from './logger.js';
6
+ import { type FnMeta } from './meta.js';
7
+ import { type AttributeValue, type OtelApiLike, type SpanLike, type SpanOf } from './otel.js';
8
+ import { createRouter } from './router.js';
9
+ import { type AnyFnContext, type BuilderLike, type FiniteExtras, type Fn, type FnCompletedEvent, type FnDefinition, type Guard, type GuardParams, type MaybePromise, type RejectKeys, type ReservedOptionKey } from './types.js';
10
+ type UnionContext<TProcedures extends Record<string, BuilderLike>> = AnyFnContext<{
11
+ procedures: TProcedures;
12
+ default: string;
13
+ extras: object;
14
+ guards: object;
15
+ meta: object;
16
+ tag: string;
17
+ span: SpanLike;
18
+ logger: FnLogger;
19
+ }>;
20
+ export type CreateFnOptions<TProcedures extends Record<string, BuilderLike>, TDefault extends keyof TProcedures & string, TExtras extends object, TGuards, TMeta extends object, TTag extends string, TOtel extends OtelApiLike | undefined, TLogger extends FnLogger> = {
21
+ /** Named oRPC builders, e.g. `{ public: os.$context<Ctx>(), protected: authed }`. */
22
+ procedures: TProcedures;
23
+ /**
24
+ * Builder used when a route omits `procedure`. Leave it out to make every
25
+ * route name its procedure, so none gets one by accident.
26
+ */
27
+ default?: TDefault;
28
+ /** Per-call values merged into every handler's params; may be async. */
29
+ extras?: (params: {
30
+ context: UnionContext<TProcedures>;
31
+ span: SpanOf<TOtel> | undefined;
32
+ signal: AbortSignal | undefined;
33
+ name: string;
34
+ procedure: string;
35
+ }) => MaybePromise<TExtras>;
36
+ /**
37
+ * Checks run before the handler; each key becomes a typed `fn()` option.
38
+ * Keys must not reuse route options or meta keys.
39
+ */
40
+ guards?: TGuards & Record<string, Guard<never, GuardParams<UnionContext<TProcedures>, TMeta>>> & RejectKeys<NoInfer<TGuards>, ReservedOptionKey | keyof NoInfer<TMeta>, 'orpc-fn: this guard name is a route option or meta key'>;
41
+ /**
42
+ * Typed route metadata: `meta: {} as { readOnly?: boolean }`. Keys become
43
+ * `fn()` options and must not reuse route options.
44
+ */
45
+ meta?: TMeta & RejectKeys<NoInfer<TMeta>, ReservedOptionKey, 'orpc-fn: this meta key is a route option'>;
46
+ /** Allowed route tags: `tags: ['internal', 'external']`. */
47
+ tags?: readonly TTag[];
48
+ /** `import * as otel from '@opentelemetry/api'`. Omit for no tracing. */
49
+ otel?: TOtel;
50
+ logger?: (scope: string, span: SpanOf<TOtel> | undefined) => TLogger;
51
+ /** Extra span attributes per call; undefined values are skipped. */
52
+ spanAttributes?: (params: {
53
+ context: UnionContext<TProcedures>;
54
+ name: string;
55
+ procedure: string;
56
+ meta: Partial<TMeta>;
57
+ }) => Record<string, AttributeValue | undefined>;
58
+ /** Called after every handler; returned attributes join the `fn.completed` log. */
59
+ onCompleted?: (event: FnCompletedEvent<UnionContext<TProcedures>, SpanOf<TOtel>>) => LogAttributes | undefined | void;
60
+ /** Errors that are the caller's fault, logged at `warn`. Default: 4xx `ORPCError` or `AbortError`. */
61
+ isExpectedError?: (error: unknown) => boolean;
62
+ /** Needed for `fnLive` and `createPubSub`. */
63
+ pubsub?: PubSubRuntimeOptions;
64
+ };
65
+ type Definition<TProcedures extends Record<string, BuilderLike>, TDefault extends string, TExtras extends object, TGuards, TMeta extends object, TTag extends string, TOtel, TLogger extends FnLogger> = {
66
+ procedures: TProcedures;
67
+ default: TDefault;
68
+ extras: TExtras;
69
+ guards: TGuards;
70
+ meta: TMeta;
71
+ tag: TTag;
72
+ span: SpanOf<TOtel>;
73
+ logger: TLogger;
74
+ };
75
+ export type FnFactory<TDef extends FnDefinition> = {
76
+ fn: Fn<TDef>;
77
+ fnLive: FnLive<TDef>;
78
+ createPubSub: CreatePubSub<TDef>;
79
+ createPublisher: CreatePublisher;
80
+ createRouter: typeof createRouter;
81
+ readMeta: (procedure: AnyProcedure) => FnMeta<TDef['meta']>;
82
+ /**
83
+ * A `call` outside any handler (seeders, scripts, tests), traced with this
84
+ * instance's OpenTelemetry.
85
+ */
86
+ createCall: <TContext>(context: TContext, signal?: AbortSignal) => BoundCall<TContext>;
87
+ /** End every open subscription of this instance; returns how many closed. */
88
+ drainPubSubSubscribers: () => number;
89
+ activePubSubSubscriberCount: () => number;
90
+ };
91
+ /**
92
+ * Builds the app's `fn` and friends once. Everything app-specific (auth,
93
+ * context, database, queues, i18n, permissions) is injected here.
94
+ */
95
+ export declare function createFn<const TProcedures extends Record<string, BuilderLike>, TDefault extends keyof TProcedures & string = never, TExtras extends object = Record<never, never>, TGuards extends object = Record<never, never>, TMeta extends object = Record<never, never>, const TTag extends string = string, TOtel extends OtelApiLike | undefined = undefined, TLogger extends FnLogger = FnLogger>(options: CreateFnOptions<TProcedures, TDefault, TExtras, TGuards, TMeta, TTag, TOtel, TLogger> & FiniteExtras<NoInfer<TExtras>>): FnFactory<Definition<TProcedures, TDefault, TExtras, TGuards, TMeta, TTag, TOtel, TLogger>>;
96
+ export {};
97
+ //# sourceMappingURL=create-fn.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-fn.d.ts","sourceRoot":"","sources":["../src/create-fn.ts"],"names":[],"mappings":"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"}