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.
Files changed (156) hide show
  1. package/README.md +70 -33
  2. package/dist/assemble-procedure.d.ts +13 -0
  3. package/dist/assemble-procedure.d.ts.map +1 -0
  4. package/dist/assemble-procedure.js +29 -0
  5. package/dist/assemble-procedure.js.map +1 -0
  6. package/dist/bound-call.d.ts +1 -1
  7. package/dist/bound-call.d.ts.map +1 -1
  8. package/dist/bound-call.js.map +1 -1
  9. package/dist/cli.d.ts +3 -0
  10. package/dist/cli.d.ts.map +1 -0
  11. package/dist/cli.js +56 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/client.d.ts +4 -2
  14. package/dist/client.d.ts.map +1 -1
  15. package/dist/client.js +4 -2
  16. package/dist/client.js.map +1 -1
  17. package/dist/compatibility.d.ts +16 -0
  18. package/dist/compatibility.d.ts.map +1 -0
  19. package/dist/compatibility.js +4 -0
  20. package/dist/compatibility.js.map +1 -0
  21. package/dist/create-fn.d.ts +30 -34
  22. package/dist/create-fn.d.ts.map +1 -1
  23. package/dist/create-fn.js +177 -121
  24. package/dist/create-fn.js.map +1 -1
  25. package/dist/expo.d.ts +11 -25
  26. package/dist/expo.d.ts.map +1 -1
  27. package/dist/expo.js +25 -30
  28. package/dist/expo.js.map +1 -1
  29. package/dist/hono.d.ts +12 -4
  30. package/dist/hono.d.ts.map +1 -1
  31. package/dist/hono.js +29 -12
  32. package/dist/hono.js.map +1 -1
  33. package/dist/index.d.ts +2 -2
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +2 -2
  36. package/dist/index.js.map +1 -1
  37. package/dist/json-schema.d.ts +2 -0
  38. package/dist/json-schema.d.ts.map +1 -1
  39. package/dist/json-schema.js +11 -0
  40. package/dist/json-schema.js.map +1 -1
  41. package/dist/live/codec.d.ts +6 -1
  42. package/dist/live/codec.d.ts.map +1 -1
  43. package/dist/live/codec.js +14 -3
  44. package/dist/live/codec.js.map +1 -1
  45. package/dist/live/fn-live.d.ts +67 -22
  46. package/dist/live/fn-live.d.ts.map +1 -1
  47. package/dist/live/fn-live.js +98 -16
  48. package/dist/live/fn-live.js.map +1 -1
  49. package/dist/live/index.d.ts +1 -1
  50. package/dist/live/index.d.ts.map +1 -1
  51. package/dist/live/index.js.map +1 -1
  52. package/dist/live/ioredis.d.ts +4 -1
  53. package/dist/live/ioredis.d.ts.map +1 -1
  54. package/dist/live/ioredis.js +21 -2
  55. package/dist/live/ioredis.js.map +1 -1
  56. package/dist/live/lifecycle.d.ts +7 -0
  57. package/dist/live/lifecycle.d.ts.map +1 -0
  58. package/dist/live/lifecycle.js +36 -0
  59. package/dist/live/lifecycle.js.map +1 -0
  60. package/dist/live/memory.d.ts.map +1 -1
  61. package/dist/live/memory.js +4 -0
  62. package/dist/live/memory.js.map +1 -1
  63. package/dist/live/pub-sub.d.ts +81 -13
  64. package/dist/live/pub-sub.d.ts.map +1 -1
  65. package/dist/live/pub-sub.js +437 -126
  66. package/dist/live/pub-sub.js.map +1 -1
  67. package/dist/live/redis-bun.d.ts +1 -1
  68. package/dist/live/redis-bun.d.ts.map +1 -1
  69. package/dist/live/redis-bun.js +3 -5
  70. package/dist/live/redis-bun.js.map +1 -1
  71. package/dist/live/redis.d.ts +2 -2
  72. package/dist/live/redis.d.ts.map +1 -1
  73. package/dist/live/redis.js +36 -9
  74. package/dist/live/redis.js.map +1 -1
  75. package/dist/live/transport.d.ts +5 -1
  76. package/dist/live/transport.d.ts.map +1 -1
  77. package/dist/live/transport.js.map +1 -1
  78. package/dist/logger.d.ts +2 -0
  79. package/dist/logger.d.ts.map +1 -1
  80. package/dist/logger.js +21 -0
  81. package/dist/logger.js.map +1 -1
  82. package/dist/mastra.d.ts.map +1 -1
  83. package/dist/mastra.js +16 -7
  84. package/dist/mastra.js.map +1 -1
  85. package/dist/mcp/core.d.ts +41 -0
  86. package/dist/mcp/core.d.ts.map +1 -0
  87. package/dist/mcp/core.js +61 -0
  88. package/dist/mcp/core.js.map +1 -0
  89. package/dist/mcp/sdk.d.ts +12 -0
  90. package/dist/mcp/sdk.d.ts.map +1 -0
  91. package/dist/mcp/sdk.js +14 -0
  92. package/dist/mcp/sdk.js.map +1 -0
  93. package/dist/mcp/server.d.ts +11 -0
  94. package/dist/mcp/server.d.ts.map +1 -0
  95. package/dist/mcp/server.js +13 -0
  96. package/dist/mcp/server.js.map +1 -0
  97. package/dist/mcp.d.ts +3 -2
  98. package/dist/mcp.d.ts.map +1 -1
  99. package/dist/mcp.js +22 -7
  100. package/dist/mcp.js.map +1 -1
  101. package/dist/meta.d.ts +15 -7
  102. package/dist/meta.d.ts.map +1 -1
  103. package/dist/meta.js +9 -2
  104. package/dist/meta.js.map +1 -1
  105. package/dist/otel.d.ts.map +1 -1
  106. package/dist/otel.js +77 -6
  107. package/dist/otel.js.map +1 -1
  108. package/dist/route-options.d.ts +22 -0
  109. package/dist/route-options.d.ts.map +1 -0
  110. package/dist/route-options.js +67 -0
  111. package/dist/route-options.js.map +1 -0
  112. package/dist/router.d.ts +13 -1
  113. package/dist/router.d.ts.map +1 -1
  114. package/dist/router.js +35 -0
  115. package/dist/router.js.map +1 -1
  116. package/dist/timing.d.ts +8 -0
  117. package/dist/timing.d.ts.map +1 -0
  118. package/dist/timing.js +4 -0
  119. package/dist/timing.js.map +1 -0
  120. package/dist/types.d.ts +96 -41
  121. package/dist/types.d.ts.map +1 -1
  122. package/dist/types.js +6 -2
  123. package/dist/types.js.map +1 -1
  124. package/package.json +28 -3
  125. package/src/assemble-procedure.ts +42 -0
  126. package/src/bound-call.ts +4 -3
  127. package/src/cli.ts +68 -0
  128. package/src/client.ts +9 -2
  129. package/src/compatibility.ts +39 -0
  130. package/src/create-fn.ts +295 -182
  131. package/src/expo.ts +49 -44
  132. package/src/hono.ts +36 -17
  133. package/src/index.ts +7 -2
  134. package/src/json-schema.ts +16 -0
  135. package/src/live/codec.ts +22 -3
  136. package/src/live/fn-live.ts +248 -69
  137. package/src/live/index.ts +2 -0
  138. package/src/live/ioredis.ts +22 -2
  139. package/src/live/lifecycle.ts +44 -0
  140. package/src/live/memory.ts +4 -0
  141. package/src/live/pub-sub.ts +619 -168
  142. package/src/live/redis-bun.ts +7 -5
  143. package/src/live/redis.ts +37 -11
  144. package/src/live/transport.ts +4 -1
  145. package/src/logger.ts +20 -0
  146. package/src/mastra.ts +15 -7
  147. package/src/mcp/core.ts +102 -0
  148. package/src/mcp/sdk.ts +36 -0
  149. package/src/mcp/server.ts +35 -0
  150. package/src/mcp.ts +48 -20
  151. package/src/meta.ts +40 -24
  152. package/src/otel.ts +77 -10
  153. package/src/route-options.ts +100 -0
  154. package/src/router.ts +58 -1
  155. package/src/timing.ts +5 -0
  156. package/src/types.ts +186 -62
package/README.md CHANGED
@@ -29,7 +29,7 @@ export const { fn, fnLive, createPubSub, createRouter } = createFn({
29
29
  default: 'protected',
30
30
  extras: () => ({ db }),
31
31
  guards: {
32
- permission: (required: Permission, { context }) => context.user?.can(required)
32
+ permission: (required: Permission, { context }) => context.user?.can(required) ?? false
33
33
  },
34
34
  otel
35
35
  })
@@ -52,7 +52,7 @@ export const getOrder = fn({
52
52
  })
53
53
  ```
54
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).
55
+ Every call runs in an OpenTelemetry span named `order.get` (for a streaming handler, until the stream ends), runs the `permission` guard, observes input/output validation, and logs one `fn.completed` line at `info`, `warn` (4xx) or `error` (defects).
56
56
 
57
57
  ## `orpc-fn`: `createFn`
58
58
 
@@ -61,31 +61,34 @@ createFn({
61
61
  procedures, // named oRPC builders; the handler context is the builder's context
62
62
  default, // builder used when a route omits `procedure`; leave out to require `procedure` on every route
63
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
64
+ extrasByProcedure, // e.g. { protected: ({ context }) => ({ t: translator(context.user) }) }
65
+ guards, // { key: (value, params) => void | boolean | Promise<void | boolean> } — each key is a typed fn() option
66
+ meta, // defineMeta<AppMeta>() — allowed values of each route's meta object
66
67
  tags, // ['internal', 'external'] — allowed route tags
67
68
  otel, // `import * as otel from '@opentelemetry/api'`; omit for no tracing
68
69
  logger, // (scope, span) => your logger (debug/info/warn/error)
69
70
  spanAttributes, // ({ context, name, procedure, meta }) => extra span attributes
70
71
  onCompleted, // (event) => extra attributes for the fn.completed log
71
72
  isExpectedError, // default: 4xx ORPCError or AbortError
72
- pubsub // { transport, onDrop?, maxQueueSize? } for fnLive / createPubSub
73
+ pubsub // { transport (or () => transport, created on first use), namespace?, ownsTransport?, maxQueueSize?, maxIngressSize?, maxReplaySize?, initializationTimeoutMs?, onDrop?, onMetric? } for fnLive / createPubSub
73
74
  })
74
75
  ```
75
76
 
76
- It returns `fn`, `fnLive`, `createPubSub`, `createPublisher`, `createRouter`, `readMeta`, `createCall`, `drainPubSubSubscribers` and `activePubSubSubscriberCount`.
77
+ It returns `fn`, `fnLive`, `createPubSub`, `createPublisher`, `createRouter`, `readMeta`, `createCall`, `shutdown`, `drainPubSubSubscribers` and `activePubSubSubscriberCount`.
77
78
 
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
+ Procedure builders must not set an input or output schema (`os.input(...)`); put schemas on routes. Guard keys cannot reuse route option names. Application metadata lives in a typed `meta` object and may share names with guards or route options. Unknown top-level route options and extras that shadow built-in handler params are rejected. Scoped extras replace matching shared keys, with handler types reflecting that precedence.
79
80
 
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
+ **`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`, configured guard keys, `guardResolvers`, `meta`, `stream` and native `errors` declarations. Core schemas use Standard Schema; Zod 4 remains the default for tool/live adapters. The handler receives `input`, `context`, `call`, `signal`, `span`, `logger`, native typed `errors`, `lastEventId` 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
 
82
- **Guards** run inside the span, before the handler, and only on routes that set their option. Throw (usually an `ORPCError`) to refuse.
83
+ **Guards** run inside the span, before the handler, and only on routes that set their option. Return `false` or throw an `ORPCError` to refuse; `true` and `undefined` allow. Static guards run sequentially in route declaration order, followed by resolved guards in resolver order. Unsupported return values fail types and runtime. For input-dependent checks use `guardResolvers: { permission: ({ input, context }) => requirement }`; these see parsed input. A guard cannot set both a static value and a resolver.
83
84
 
84
85
  **`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
 
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
+ **Meta** is stored in oRPC's own procedure meta. It survives `os.router()`, prefixes and lazy routers. Declare the contract with `defineMeta<AppMeta>()` (no runtime defaults), then use `fn({ meta: { readOnly: true }, ... })`. Both `readMeta(procedure)` and `readFnMeta(procedure)` infer metadata from the passed procedure, including procedures from another factory.
87
88
 
88
- Also exported: `isExpectedClientError`, `createBoundedEventQueue`, `createRouter`, `createBoundCall`, and the types `HandlerParams`, `RouteConfig`, `BoundCall`, `FnMeta`, `FnContext`, `FnCompletedEvent`, `ProcedureInput`, `ProcedureOutput`, `ProcedureContext`, `InitialContextOf`, `CurrentContextOf` and `GuardOptions`.
89
+ Completion hooks discriminate by `procedure` and `handlerStarted`. Before handler context exists (auth/input failure), they receive the initial context. Logging, tracing, metrics and completion-hook failures preserve procedure results. Custom log fields cannot overwrite canonical completion fields. Stream completion runs once when the stream ends.
90
+
91
+ Also exported: `defineMeta`, `createStreamManifest`, `createStreamManifestAsync`, `renderStreamManifest`, `isExpectedClientError`, `createBoundedEventQueue`, `createRouter`, `createBoundCall`, and the types `HandlerParams`, `RouteConfig`, `BoundCall`, `FnMeta`, `FnContext`, `FnCompletedEvent`, `ProcedureInput`, `ProcedureOutput`, `ProcedureContext`, `InitialContextOf`, `CurrentContextOf` and `GuardOptions`.
89
92
 
90
93
  ## `orpc-fn/live`: pub/sub and live queries
91
94
 
@@ -93,10 +96,14 @@ Also exported: `isExpectedClientError`, `createBoundedEventQueue`, `createRouter
93
96
  import { createFn } from 'orpc-fn'
94
97
  import { bunRedisTransport } from 'orpc-fn/live/redis-bun' // or ioredis / memory
95
98
 
96
- const transport = bunRedisTransport(new RedisClient(process.env.REDIS_URL))
97
99
  export const { fnLive, createPubSub, drainPubSubSubscribers } = createFn({
98
100
  /* … */,
99
- pubsub: { transport, onDrop: (count) => dropCounter.add(count) }
101
+ pubsub: {
102
+ // A function defers the connection until the first publish or subscribe.
103
+ transport: () => bunRedisTransport(new RedisClient(process.env.REDIS_URL)),
104
+ ownsTransport: true,
105
+ onDrop: (count) => dropCounter.add(count)
106
+ }
100
107
  })
101
108
 
102
109
  export const listOrders = fnLive({
@@ -113,20 +120,23 @@ export const listOrders = fnLive({
113
120
  await listOrders.publish({ organizationId, orderId }) // after a write
114
121
  ```
115
122
 
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:
123
+ The `subscribe` route runs builder middleware and the same fn guards/extras/meta/completion policy, then `authFn` and subscribes to the channel, then streams the initial snapshot, then reloads when invalidated (once per coalescing batch). Queue bounds (`maxQueueSize`, `maxIngressSize`, `maxReplaySize`) can be overridden per pub/sub definition or live config. `maxBatchSize` bounds a live coalescing batch (default 1000). `pubsub.onMetric` reports typed queue depths, drops, successful reconnects and parsing failures; keep this synchronous callback fast. Startup waits for a successful broker subscription, bounded by `initializationTimeoutMs` (10000ms by default) and cancellation. 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
124
 
118
125
  - `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.
126
+ - `transformerFn` folds an event into initialized `previous`. It requires a `stateSchema` that validates parsed state. Patches returned by `fnLivePatch(state, emit)` additionally require `emitSchema`. These schemas should not transform already-parsed values. `rerun()` rechecks the current input through the procedure; it cannot change subscriber input.
127
+ - `coalesceMs` performs one rerun for an invalidation batch. Reducers process every event but emit their final full state; ordered patch payloads remain ordered. Batches are bounded (1000 events by default in `streamLiveSnapshots`).
121
128
  - `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.
129
+ - Live queries reload a full snapshot after overflow or reconnect. Raw pub/sub streams fail with `SERVICE_UNAVAILABLE` after detected loss, or emit an application-provided `overflowMarker` that must trigger recovery.
130
+ - `reauthorize` can check access before each live update. Initial authorization covers idle streams; apps that need immediate revocation should close those streams.
131
+ - `safePublish` logs publish errors instead of throwing them; it does not retry or guarantee delivery.
124
132
 
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.
133
+ **`createPubSub`** returns a typed `subscribe` route plus `publish`, `publishMany` (one atomic round-trip across channels) and typed `getSubscriptionChannelName({ input, context })` / `getPublishChannelName(event)` methods. The shared partial-input `getChannelName` method is deprecated. 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 include `filterFn`, `authFn`, `mirrorChannel`, `overflowMarker`, `procedure`, configured guards and `meta`. Channels may be static or use `channel: { subscribe: ({ input, context }) => key, publish: (parsedEvent) => key }`. Carry publishing tenant keys in the event. A factory `namespace` prefixes channels, mirrors and backlog keys. 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
134
 
127
- **Graceful shutdown:** call `drainPubSubSubscribers()` on SIGTERM. Open streams end cleanly and clients reconnect to the new deployment. Draining is per `createFn` instance.
135
+ **Graceful shutdown:** `await shutdown()` stops new subscriptions/publication, cancels initialization/retries, ends existing streams and awaits cleanup. It closes a supplied transport only with `ownsTransport: true`, and a lazy `transport: () => …` only if it was ever created; it is never created during or after shutdown. A custom transport whose pending `subscribe` cannot be cancelled may resolve later; that late subscription is immediately released. `drainPubSubSubscribers()` remains a synchronous, nonterminal operation. Both are per factory.
128
136
 
129
- **Transports** implement `PubSubTransport` (`publish(messages, backlog?)`, `readBacklog(channel)`, `subscribe(channel, listener, onLost)`):
137
+ Recent event IDs deduplicate replay/live overlap; legacy payloads remain readable. Replay is bounded and TTL-limited, so it is not durable resume. A snapshot may already include a queued delta: reducers must use revisions or idempotence. See [the revision example](examples/adoption/revisions.ts). Parsing ingress, replay staging and subscriber queues are bounded; positive queue/TTL/time options are validated.
138
+
139
+ **Transports** implement `PubSubTransport` (`publish(messages, backlog?)`, `readBacklog(channel)`, `subscribe(channel, listener, onLost, { onReconnect }?)`):
130
140
 
131
141
  | Subpath | Client | Notes |
132
142
  |---|---|---|
@@ -134,7 +144,7 @@ The `subscribe` route first runs `authFn` and subscribes to the channel, then st
134
144
  | `orpc-fn/live/ioredis` | ioredis 5 | Dedicated subscriber via `duplicate()`; ioredis re-subscribes itself |
135
145
  | `orpc-fn/live/memory` | none | Tests and single-process dev |
136
146
 
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.
147
+ 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`. Await `transport.close()` when the app owns its lifecycle. The Redis Lua batching adapter supports standalone Redis; Redis Cluster requires compatible key slots and is not promised by this adapter.
138
148
 
139
149
  `streamLiveSnapshots`, `fnLivePatch` and `throwInitialSnapshotError` are exported for custom streams.
140
150
 
@@ -151,21 +161,40 @@ const tool = createMastraTool(getOrder, { requireApproval: true })
151
161
 
152
162
  Options: `id`, `description`, `requireApproval`, `allowMissingInputSchema`, `onExecuteFinish`, `contextKey` (default `'orpcContext'`). Execution goes through oRPC `call`, so middleware and guards apply.
153
163
 
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.
164
+ 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: transforms/refinements must be pure: SDK validation and procedure execution may run them more than once, always on raw input. Agent tools accept objects and unions of object shapes. Other inputs and streams are rejected. Finish-hook failures preserve execution outcomes.
155
165
 
156
166
  ## `orpc-fn/mcp`
157
167
 
158
168
  Optional peers: `@orpc/zod`, `@orpc/json-schema`.
159
169
 
170
+ Inspection definitions from `listTools` use Standard Schema and are not `McpServer.registerTool` configs. Executable registration is a separate optional entry point per SDK major: `orpc-fn/mcp/sdk` for `@modelcontextprotocol/sdk` ^1.32.0 and `orpc-fn/mcp/server` for `@modelcontextprotocol/server` ^2.0.0:
171
+
172
+ ```ts
173
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js'
174
+ import { hasTag } from 'orpc-fn/mcp'
175
+ import { registerMcpTools } from 'orpc-fn/mcp/sdk'
176
+
177
+ const server = new Server({ name: 'api', version: '1.0.0' }, { capabilities: { tools: {} } })
178
+ registerMcpTools(server, router, {
179
+ filter: hasTag('external'),
180
+ context: ({ signal }) => authenticateToolRequest(signal)
181
+ })
182
+ // Connect your SDK transport; see examples/adoption/mcp.ts.
183
+ ```
184
+
185
+ For MCP SDK v2 (`@modelcontextprotocol/server` ^2.0.0), import the same `registerMcpTools` from `orpc-fn/mcp/server` instead; options and behaviour are identical:
186
+
160
187
  ```ts
161
- import { hasTag, listTools } from 'orpc-fn/mcp'
188
+ import { McpServer } from '@modelcontextprotocol/server'
189
+ import { registerMcpTools } from 'orpc-fn/mcp/server'
162
190
 
163
- for (const { name, procedure, config } of listTools(router, { filter: hasTag('external') })) {
164
- server.registerTool(name, config, (args) => call(procedure, args, { context }))
165
- }
191
+ const mcp = new McpServer({ name: 'api', version: '1.0.0' }, { capabilities: { tools: {} } })
192
+ registerMcpTools(mcp.server, router, { filter: hasTag('external'), context })
166
193
  ```
167
194
 
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.
195
+ The adapter installs the SDK list/call handlers on a low-level `Server` (also accessible as `McpServer.server`). Use one registration per server; these handlers own its tool list. It forwards cancellation, validates/coerces arguments, applies procedure authorization, and formats results as JSON/text MCP content. Override `formatResult` for images, structured content or other presentation. Tool selection is required. Object unions are supported; streams are rejected. Resolve lazy routers first with oRPC's `unlazyRouter`.
196
+
197
+ Sanitized tool-name collisions between distinct procedures throw; identical aliases are deduplicated. Override `name` to resolve deliberate collisions. 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: transforms/refinements must be pure: SDK validation and procedure execution may run them more than once, always on raw input. `readOnlyHint` defaults to GET routes; pass `readOnly` to change it. `hasTag(tag)` also works as an oRPC `OpenAPIHandler` filter.
169
198
 
170
199
  ## `orpc-fn/hono`
171
200
 
@@ -186,7 +215,7 @@ mountOrpc(app, {
186
215
 
187
216
  Behaviour:
188
217
 
189
- - The response gets a `Server-Timing` header built from `context.timing`: queue, auth and context if you record them, and handler from `fn`.
218
+ - The response gets a `Server-Timing` header built from the request timing collector: app-recorded queue/auth/context metrics plus library-owned procedure time. Nested calls do not overwrite parent timing; batches aggregate their top-level procedure durations (which may overlap). `fn` does not mutate an arbitrary `context.timing` field. `formatServerTiming` and `finishOrpcHeaders` are available for composed Fetch adapters.
190
219
  - SSE responses get keep-alive comments (15 s) and anti-buffering headers.
191
220
  - RPC calls are batched (`BatchHandlerPlugin`).
192
221
  - Scalar docs are served at the OpenAPI prefix.
@@ -215,7 +244,14 @@ export const orpc = createTanstackQueryUtils(client) // Vue Query or React Query
215
244
  if (hasOrpcErrorCode(error, 'NOT_FOUND')) { /* … */ }
216
245
  ```
217
246
 
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`).
247
+ `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 pass `streamPaths` generated from the actual router with `createStreamManifest` (or `createStreamManifestAsync` for lazy routers). Serialize it to a frontend module; importing that data needs no server runtime. The `orpc-fn` CLI does that, resolving lazy routers, sorting paths and writing only on change:
248
+
249
+ ```sh
250
+ orpc-fn stream-manifest src/router.ts#appRouter --out ../web/src/stream-paths.ts # export const streamPaths = [...]
251
+ orpc-fn stream-manifest src/router.ts#appRouter --out ../web/src/stream-paths.ts --check # CI: exit 1 when stale
252
+ ```
253
+
254
+ `--out x.json` writes JSON instead, and `--name` renames the export. The CLI imports the router, so that module's side effects (database clients, SDKs) still run; it exits when done rather than waiting on their open handles. Run TypeScript routers with `bun`, `tsx` or Node ≥ 22.18. Mark custom iterator routes with `stream: true` or use oRPC's `eventIterator` output schema. Generated live/pubsub routes are marked automatically. `batch.exclude` remains an escape hatch, and the legacy `subscribe` naming heuristic remains as a fallback.
219
255
 
220
256
  ## `orpc-fn/expo`
221
257
 
@@ -224,9 +260,9 @@ Expo itself is passed in, not imported. The fetch must resolve to a response wit
224
260
  ```ts
225
261
  import { fetch } from 'expo/fetch'
226
262
  import { Platform } from 'react-native'
227
- import { createExpoLink } from 'orpc-fn/expo'
263
+ import { createBetterAuthExpoLink } from 'orpc-fn/expo'
228
264
 
229
- const link = createExpoLink({
265
+ const link = createBetterAuthExpoLink({
230
266
  url: `${baseUrl}/api/rpc`,
231
267
  fetch,
232
268
  native: Platform.OS !== 'web',
@@ -236,13 +272,14 @@ const link = createExpoLink({
236
272
  })
237
273
  ```
238
274
 
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.
275
+ 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`. The generic `createExpoLink` forwards upstream headers (including async/context-aware resolvers) and adds no provider-specific headers. `createBetterAuthExpoLink` supplies the cookie/origin preset above. On web, the link uses the platform fetch with cookies, or your explicit `webFetch` override. `createExpoFetch` is exported for a custom link.
240
276
 
241
277
  ## Docs
242
278
 
243
279
  - [Migrating an app from an in-repo `lib/fn`](docs/migration.md)
244
280
  - [Decisions: what oRPC already ships and why this exists](docs/decisions.md)
245
281
  - [Releasing](docs/releasing.md)
282
+ - [Independent core, tenant, revision, MCP and Expo examples](examples/adoption/README.md)
246
283
  - [Example: Hono, in-memory pub/sub, two routes and a live query](examples/basic/server.ts)
247
284
 
248
285
  ## License
@@ -0,0 +1,13 @@
1
+ import type { AnySchema } from '@orpc/contract';
2
+ import type { AnyProcedure } from '@orpc/server';
3
+ import type { BuildProcedure } from './types.js';
4
+ export type BuilderChain = {
5
+ route(route: Record<string, unknown>): BuilderChain;
6
+ meta(meta: Record<string, unknown>): BuilderChain;
7
+ input(schema: AnySchema): BuilderChain;
8
+ output(schema: AnySchema): BuilderChain;
9
+ errors(errors: import('@orpc/contract').ErrorMap): BuilderChain;
10
+ handler(handler: Parameters<BuildProcedure>[0]['handler']): AnyProcedure;
11
+ };
12
+ export declare function createProcedureAssembler(procedures: Record<string, BuilderChain>, resolveKey: (key: string | undefined, name: string) => string): BuildProcedure;
13
+ //# sourceMappingURL=assemble-procedure.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assemble-procedure.d.ts","sourceRoot":"","sources":["../src/assemble-procedure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAEhD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAEhD,MAAM,MAAM,YAAY,GAAG;IAC1B,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,YAAY,CAAA;IACnD,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,YAAY,CAAA;IACjD,KAAK,CAAC,MAAM,EAAE,SAAS,GAAG,YAAY,CAAA;IACtC,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,YAAY,CAAA;IACvC,MAAM,CAAC,MAAM,EAAE,OAAO,gBAAgB,EAAE,QAAQ,GAAG,YAAY,CAAA;IAC/D,OAAO,CAAC,OAAO,EAAE,UAAU,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,YAAY,CAAA;CACxE,CAAA;AAED,wBAAgB,wBAAwB,CACvC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,EACxC,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,KAAK,MAAM,GAC3D,cAAc,CAwBhB"}
@@ -0,0 +1,29 @@
1
+ import { FN_META_KEY } from './meta.js';
2
+ export function createProcedureAssembler(procedures, resolveKey) {
3
+ return (route) => {
4
+ const key = resolveKey(route.procedure, route.name);
5
+ let builder = procedures[key];
6
+ if (!builder)
7
+ throw new Error(`orpc-fn: unknown procedure "${key}"`);
8
+ const stored = {
9
+ name: route.name,
10
+ procedure: key,
11
+ meta: route.meta,
12
+ stream: route.stream ?? false
13
+ };
14
+ builder = builder
15
+ .route({
16
+ operationId: route.name,
17
+ ...Object.fromEntries(Object.entries(route.route).filter(([, value]) => value !== undefined))
18
+ })
19
+ .meta({ [FN_META_KEY]: stored });
20
+ if (route.errors)
21
+ builder = builder.errors(route.errors);
22
+ if (route.input)
23
+ builder = builder.input(route.input);
24
+ if (route.output)
25
+ builder = builder.output(route.output);
26
+ return builder.handler(route.handler);
27
+ };
28
+ }
29
+ //# sourceMappingURL=assemble-procedure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assemble-procedure.js","sourceRoot":"","sources":["../src/assemble-procedure.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAqB,MAAM,WAAW,CAAA;AAY1D,MAAM,UAAU,wBAAwB,CACvC,UAAwC,EACxC,UAA6D;IAE7D,OAAO,CAAC,KAAK,EAAE,EAAE;QAChB,MAAM,GAAG,GAAG,UAAU,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,CAAC,CAAA;QACnD,IAAI,OAAO,GAAG,UAAU,CAAC,GAAG,CAAC,CAAA;QAC7B,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,+BAA+B,GAAG,GAAG,CAAC,CAAA;QACpE,MAAM,MAAM,GAAiB;YAC5B,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,SAAS,EAAE,GAAG;YACd,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,MAAM,EAAE,KAAK,CAAC,MAAM,IAAI,KAAK;SAC7B,CAAA;QACD,OAAO,GAAG,OAAO;aACf,KAAK,CAAC;YACN,WAAW,EAAE,KAAK,CAAC,IAAI;YACvB,GAAG,MAAM,CAAC,WAAW,CACpB,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,CAAC,CACtE;SACD,CAAC;aACD,IAAI,CAAC,EAAE,CAAC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC,CAAA;QACjC,IAAI,KAAK,CAAC,MAAM;YAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;QACxD,IAAI,KAAK,CAAC,KAAK;YAAE,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;QACrD,IAAI,KAAK,CAAC,MAAM;YAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;QACxD,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;IACtC,CAAC,CAAA;AACF,CAAC","sourcesContent":["import type { AnySchema } from '@orpc/contract'\nimport type { AnyProcedure } from '@orpc/server'\nimport { FN_META_KEY, type StoredFnMeta } from './meta.js'\nimport type { BuildProcedure } from './types.js'\n\nexport type BuilderChain = {\n\troute(route: Record<string, unknown>): BuilderChain\n\tmeta(meta: Record<string, unknown>): BuilderChain\n\tinput(schema: AnySchema): BuilderChain\n\toutput(schema: AnySchema): BuilderChain\n\terrors(errors: import('@orpc/contract').ErrorMap): BuilderChain\n\thandler(handler: Parameters<BuildProcedure>[0]['handler']): AnyProcedure\n}\n\nexport function createProcedureAssembler(\n\tprocedures: Record<string, BuilderChain>,\n\tresolveKey: (key: string | undefined, name: string) => string\n): BuildProcedure {\n\treturn (route) => {\n\t\tconst key = resolveKey(route.procedure, route.name)\n\t\tlet builder = procedures[key]\n\t\tif (!builder) throw new Error(`orpc-fn: unknown procedure \"${key}\"`)\n\t\tconst stored: StoredFnMeta = {\n\t\t\tname: route.name,\n\t\t\tprocedure: key,\n\t\t\tmeta: route.meta,\n\t\t\tstream: route.stream ?? false\n\t\t}\n\t\tbuilder = builder\n\t\t\t.route({\n\t\t\t\toperationId: route.name,\n\t\t\t\t...Object.fromEntries(\n\t\t\t\t\tObject.entries(route.route).filter(([, value]) => value !== undefined)\n\t\t\t\t)\n\t\t\t})\n\t\t\t.meta({ [FN_META_KEY]: stored })\n\t\tif (route.errors) builder = builder.errors(route.errors)\n\t\tif (route.input) builder = builder.input(route.input)\n\t\tif (route.output) builder = builder.output(route.output)\n\t\treturn builder.handler(route.handler)\n\t}\n}\n"]}
@@ -9,7 +9,7 @@ type ContextCheck<TContext, T> = [TContext] extends [ProcedureContext<T>] ? unkn
9
9
  * Calls another procedure with the caller's context and signal. `TContext` is
10
10
  * the caller's context; the callee must accept it.
11
11
  */
12
- export type BoundCall<TContext = any> = <T extends AnyProcedure>(procedure: T & ContextCheck<TContext, T>, input: ProcedureInput<T>) => Promise<ProcedureOutput<T>>;
12
+ export type BoundCall<TContext> = <T extends AnyProcedure>(procedure: T & ContextCheck<TContext, T>, ...args: undefined extends ProcedureInput<T> ? [input?: ProcedureInput<T>] : [input: ProcedureInput<T>]) => Promise<ProcedureOutput<T>>;
13
13
  /**
14
14
  * Creates a bound call: nested calls keep the caller's context and abort
15
15
  * signal and run inside a `call: <name>` child span, so the trace shows the
@@ -1 +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"}
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;AACH,MAAM,MAAM,SAAS,CAAC,QAAQ,IAAI,CAAC,CAAC,SAAS,YAAY,EACxD,SAAS,EAAE,CAAC,GAAG,YAAY,CAAC,QAAQ,EAAE,CAAC,CAAC,EACxC,GAAG,IAAI,EAAE,SAAS,SAAS,cAAc,CAAC,CAAC,CAAC,GACzC,CAAC,KAAK,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC,GAC3B,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC,KACzB,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"}
@@ -1 +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"]}
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;AAyBvD,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 */\nexport type BoundCall<TContext> = <T extends AnyProcedure>(\n\tprocedure: T & ContextCheck<TContext, T>,\n\t...args: undefined extends ProcedureInput<T>\n\t\t? [input?: ProcedureInput<T>]\n\t\t: [input: 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"]}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ // biome-ignore-all lint/suspicious/noConsole: a CLI reports on the console
3
+ import { readFile, writeFile } from 'node:fs/promises';
4
+ import { resolve } from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+ import { parseArgs } from 'node:util';
7
+ import { createStreamManifestAsync, renderStreamManifest } from './router.js';
8
+ const usage = `Usage: orpc-fn stream-manifest <module>[#export] --out <file.ts|file.json> [--name streamPaths] [--check]
9
+
10
+ Imports the router (export "default" unless #export is given) and writes the
11
+ paths of its streaming routes for createRpcLink({ streamPaths }). The file is
12
+ only written when its content changes. --check writes nothing and exits 1 when
13
+ the file is missing or stale. Importing the router runs its module code; run
14
+ TypeScript routers with a runtime that loads .ts (bun, tsx, node >= 22.18).`;
15
+ async function run(argv) {
16
+ const { positionals, values } = parseArgs({
17
+ args: [...argv],
18
+ allowPositionals: true,
19
+ options: {
20
+ out: { type: 'string' },
21
+ name: { type: 'string' },
22
+ check: { type: 'boolean' },
23
+ help: { type: 'boolean', short: 'h' }
24
+ }
25
+ });
26
+ const [command, target] = positionals;
27
+ if (values.help || command !== 'stream-manifest' || !target || !values.out) {
28
+ console.error(usage);
29
+ return values.help ? 0 : 2;
30
+ }
31
+ const [modulePath = '', exportName = 'default'] = target.split('#');
32
+ const loaded = await import(pathToFileURL(resolve(modulePath)).href);
33
+ const router = loaded[exportName];
34
+ if (!router || typeof router !== 'object') {
35
+ console.error(`orpc-fn: ${modulePath} has no router export "${exportName}"`);
36
+ return 2;
37
+ }
38
+ const out = resolve(values.out);
39
+ const source = renderStreamManifest(await createStreamManifestAsync(router), { format: out.endsWith('.json') ? 'json' : 'ts', name: values.name });
40
+ const current = await readFile(out, 'utf8').catch(() => undefined);
41
+ if (current === source)
42
+ return 0;
43
+ if (values.check) {
44
+ console.error(`orpc-fn: ${values.out} is stale; rerun without --check to update it`);
45
+ return 1;
46
+ }
47
+ await writeFile(out, source);
48
+ console.log(`orpc-fn: wrote ${values.out}`);
49
+ return 0;
50
+ }
51
+ // Exit explicitly: the imported router may hold open database or broker handles.
52
+ run(process.argv.slice(2)).then((code) => process.exit(code), (error) => {
53
+ console.error(error);
54
+ process.exit(1);
55
+ });
56
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,2EAA2E;AAC3E,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAErC,OAAO,EAAE,yBAAyB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAE7E,MAAM,KAAK,GAAG;;;;;;4EAM8D,CAAA;AAE5E,KAAK,UAAU,GAAG,CAAC,IAAuB;IACzC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;QACzC,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;QACf,gBAAgB,EAAE,IAAI;QACtB,OAAO,EAAE;YACR,GAAG,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;YACvB,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;YACxB,KAAK,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;YAC1B,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE;SACrC;KACD,CAAC,CAAA;IACF,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,GAAG,WAAW,CAAA;IACrC,IAAI,MAAM,CAAC,IAAI,IAAI,OAAO,KAAK,iBAAiB,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC;QAC5E,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;QACpB,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAC3B,CAAC;IACD,MAAM,CAAC,UAAU,GAAG,EAAE,EAAE,UAAU,GAAG,SAAS,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IACnE,MAAM,MAAM,GAA4B,MAAM,MAAM,CACnD,aAAa,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CACvC,CAAA;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,CAAA;IACjC,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC3C,OAAO,CAAC,KAAK,CAAC,YAAY,UAAU,0BAA0B,UAAU,GAAG,CAAC,CAAA;QAC5E,OAAO,CAAC,CAAA;IACT,CAAC;IACD,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IAC/B,MAAM,MAAM,GAAG,oBAAoB,CAClC,MAAM,yBAAyB,CAAC,MAAmB,CAAC,EACpD,EAAE,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CACpE,CAAA;IACD,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;IAClE,IAAI,OAAO,KAAK,MAAM;QAAE,OAAO,CAAC,CAAA;IAChC,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QAClB,OAAO,CAAC,KAAK,CACZ,YAAY,MAAM,CAAC,GAAG,+CAA+C,CACrE,CAAA;QACD,OAAO,CAAC,CAAA;IACT,CAAC;IACD,MAAM,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;IAC5B,OAAO,CAAC,GAAG,CAAC,kBAAkB,MAAM,CAAC,GAAG,EAAE,CAAC,CAAA;IAC3C,OAAO,CAAC,CAAA;AACT,CAAC;AAED,iFAAiF;AACjF,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAC9B,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAC5B,CAAC,KAAK,EAAE,EAAE;IACT,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IACpB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AAChB,CAAC,CACD,CAAA","sourcesContent":["#!/usr/bin/env node\n// biome-ignore-all lint/suspicious/noConsole: a CLI reports on the console\nimport { readFile, writeFile } from 'node:fs/promises'\nimport { resolve } from 'node:path'\nimport { pathToFileURL } from 'node:url'\nimport { parseArgs } from 'node:util'\nimport type { AnyRouter } from '@orpc/server'\nimport { createStreamManifestAsync, renderStreamManifest } from './router.js'\n\nconst usage = `Usage: orpc-fn stream-manifest <module>[#export] --out <file.ts|file.json> [--name streamPaths] [--check]\n\nImports the router (export \"default\" unless #export is given) and writes the\npaths of its streaming routes for createRpcLink({ streamPaths }). The file is\nonly written when its content changes. --check writes nothing and exits 1 when\nthe file is missing or stale. Importing the router runs its module code; run\nTypeScript routers with a runtime that loads .ts (bun, tsx, node >= 22.18).`\n\nasync function run(argv: readonly string[]): Promise<number> {\n\tconst { positionals, values } = parseArgs({\n\t\targs: [...argv],\n\t\tallowPositionals: true,\n\t\toptions: {\n\t\t\tout: { type: 'string' },\n\t\t\tname: { type: 'string' },\n\t\t\tcheck: { type: 'boolean' },\n\t\t\thelp: { type: 'boolean', short: 'h' }\n\t\t}\n\t})\n\tconst [command, target] = positionals\n\tif (values.help || command !== 'stream-manifest' || !target || !values.out) {\n\t\tconsole.error(usage)\n\t\treturn values.help ? 0 : 2\n\t}\n\tconst [modulePath = '', exportName = 'default'] = target.split('#')\n\tconst loaded: Record<string, unknown> = await import(\n\t\tpathToFileURL(resolve(modulePath)).href\n\t)\n\tconst router = loaded[exportName]\n\tif (!router || typeof router !== 'object') {\n\t\tconsole.error(`orpc-fn: ${modulePath} has no router export \"${exportName}\"`)\n\t\treturn 2\n\t}\n\tconst out = resolve(values.out)\n\tconst source = renderStreamManifest(\n\t\tawait createStreamManifestAsync(router as AnyRouter),\n\t\t{ format: out.endsWith('.json') ? 'json' : 'ts', name: values.name }\n\t)\n\tconst current = await readFile(out, 'utf8').catch(() => undefined)\n\tif (current === source) return 0\n\tif (values.check) {\n\t\tconsole.error(\n\t\t\t`orpc-fn: ${values.out} is stale; rerun without --check to update it`\n\t\t)\n\t\treturn 1\n\t}\n\tawait writeFile(out, source)\n\tconsole.log(`orpc-fn: wrote ${values.out}`)\n\treturn 0\n}\n\n// Exit explicitly: the imported router may hold open database or broker handles.\nrun(process.argv.slice(2)).then(\n\t(code) => process.exit(code),\n\t(error) => {\n\t\tconsole.error(error)\n\t\tprocess.exit(1)\n\t}\n)\n"]}
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type ClientContext, type ORPCErrorCode } from '@orpc/client';
1
+ import { type ClientContext, ORPCError, type ORPCErrorCode } from '@orpc/client';
2
2
  import { RPCLink, type RPCLinkOptions } from '@orpc/client/fetch';
3
3
  import { BatchLinkPlugin } from '@orpc/client/plugins';
4
4
  type BatchOptions<T extends ClientContext> = ConstructorParameters<typeof BatchLinkPlugin<T>>[0];
@@ -11,6 +11,8 @@ export type CreateRpcLinkOptions<T extends ClientContext> = RPCLinkOptions<T> &
11
11
  * default only calls without a client context are batched; pass
12
12
  * `groups` to batch calls that share a context (e.g. a token).
13
13
  */
14
+ /** Client-safe manifest generated by createStreamManifest(router). */
15
+ streamPaths?: readonly (readonly string[])[];
14
16
  batch?: boolean | {
15
17
  maxSize?: number;
16
18
  /** Calls that must not be batched. Default: {@link isSubscriptionPath}. */
@@ -39,6 +41,6 @@ export declare function createRpcLink<T extends ClientContext = ClientContext>(o
39
41
  * Typed check for a route error on the client. The RPC link rebuilds thrown
40
42
  * route errors as `ORPCError`, so `instanceof` plus `code` is the contract.
41
43
  */
42
- export declare function hasOrpcErrorCode(error: unknown, code: ORPCErrorCode): boolean;
44
+ export declare function hasOrpcErrorCode<TCode extends ORPCErrorCode>(error: unknown, code: TCode): error is ORPCError<TCode, unknown>;
43
45
  export {};
44
46
  //# sourceMappingURL=client.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAE,SAAS,EAAE,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,sEAAsE;IACtE,WAAW,CAAC,EAAE,SAAS,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,CAAA;IAC5C,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,CA2BZ;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,SAAS,aAAa,EAC3D,KAAK,EAAE,OAAO,EACd,IAAI,EAAE,KAAK,GACT,KAAK,IAAI,SAAS,CAAC,KAAK,EAAE,OAAO,CAAC,CAEpC"}
package/dist/client.js CHANGED
@@ -18,7 +18,8 @@ export const isSubscriptionPath = (path) => path.some((segment) => segment.toLow
18
18
  * ```
19
19
  */
20
20
  export function createRpcLink(options) {
21
- const { batch = true, plugins = [], ...rest } = options;
21
+ const { batch = true, plugins = [], streamPaths, ...rest } = options;
22
+ const streams = new Set(streamPaths?.map((path) => JSON.stringify(path)));
22
23
  if (batch === false)
23
24
  return new RPCLink({ ...rest, plugins });
24
25
  const { maxSize = 25, exclude = isSubscriptionPath, groups } = batch === true ? {} : batch;
@@ -32,7 +33,8 @@ export function createRpcLink(options) {
32
33
  // so the batch's empty context is exactly theirs.
33
34
  { condition: () => true, context: {} }
34
35
  ],
35
- exclude: ({ path, context }) => exclude(path) ||
36
+ exclude: ({ path, context }) => streams.has(JSON.stringify(path)) ||
37
+ exclude(path) ||
36
38
  (groups === undefined && Object.keys(context).length > 0)
37
39
  }),
38
40
  ...plugins
@@ -1 +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"]}
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;AA4BtD;;;;;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,WAAW,EAAE,GAAG,IAAI,EAAE,GAAG,OAAO,CAAA;IACpE,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACzE,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,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;oBACjC,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,CAC/B,KAAc,EACd,IAAW;IAEX,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\t/** Client-safe manifest generated by createStreamManifest(router). */\n\t\tstreamPaths?: readonly (readonly string[])[]\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 = [], streamPaths, ...rest } = options\n\tconst streams = new Set(streamPaths?.map((path) => JSON.stringify(path)))\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\tstreams.has(JSON.stringify(path)) ||\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<TCode extends ORPCErrorCode>(\n\terror: unknown,\n\tcode: TCode\n): error is ORPCError<TCode, unknown> {\n\treturn error instanceof ORPCError && error.code === code\n}\n"]}
@@ -0,0 +1,16 @@
1
+ import type { AnySchema, ErrorMap, Meta } from '@orpc/contract';
2
+ import type { Procedure, Route } from '@orpc/server';
3
+ /** All runtime access to oRPC's implementation namespace is audited here. */
4
+ export type ProcedureDefinition = {
5
+ meta?: Record<string, unknown>;
6
+ route?: Route;
7
+ inputSchema?: AnySchema;
8
+ outputSchema?: AnySchema;
9
+ };
10
+ export declare function procedureDefinition(procedure: object): ProcedureDefinition;
11
+ export type NativeMeta<T> = T extends Procedure<infer _I, infer _C, infer _In, infer _Out, infer _E, infer M extends Meta> ? M : Meta;
12
+ export type NativeErrors<T> = T extends Procedure<infer _I, infer _C, infer _In, infer _Out, infer E extends ErrorMap, infer _M> ? E : ErrorMap;
13
+ export type LibraryMeta<T> = NativeMeta<T> extends {
14
+ 'orpc-fn': infer M;
15
+ } ? M : never;
16
+ //# sourceMappingURL=compatibility.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compatibility.d.ts","sourceRoot":"","sources":["../src/compatibility.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAA;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,cAAc,CAAA;AAEpD,6EAA6E;AAC7E,MAAM,MAAM,mBAAmB,GAAG;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,KAAK,CAAC,EAAE,KAAK,CAAA;IACb,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB,YAAY,CAAC,EAAE,SAAS,CAAA;CACxB,CAAA;AAED,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,mBAAmB,CAE1E;AAED,MAAM,MAAM,UAAU,CAAC,CAAC,IACvB,CAAC,SAAS,SAAS,CAClB,MAAM,EAAE,EACR,MAAM,EAAE,EACR,MAAM,GAAG,EACT,MAAM,IAAI,EACV,MAAM,EAAE,EACR,MAAM,CAAC,SAAS,IAAI,CACpB,GACE,CAAC,GACD,IAAI,CAAA;AACR,MAAM,MAAM,YAAY,CAAC,CAAC,IACzB,CAAC,SAAS,SAAS,CAClB,MAAM,EAAE,EACR,MAAM,EAAE,EACR,MAAM,GAAG,EACT,MAAM,IAAI,EACV,MAAM,CAAC,SAAS,QAAQ,EACxB,MAAM,EAAE,CACR,GACE,CAAC,GACD,QAAQ,CAAA;AACZ,MAAM,MAAM,WAAW,CAAC,CAAC,IACxB,UAAU,CAAC,CAAC,CAAC,SAAS;IAAE,SAAS,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,CAAC,GAAG,KAAK,CAAA"}
@@ -0,0 +1,4 @@
1
+ export function procedureDefinition(procedure) {
2
+ return procedure['~orpc'] ?? {};
3
+ }
4
+ //# sourceMappingURL=compatibility.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compatibility.js","sourceRoot":"","sources":["../src/compatibility.ts"],"names":[],"mappings":"AAWA,MAAM,UAAU,mBAAmB,CAAC,SAAiB;IACpD,OAAQ,SAA+C,CAAC,OAAO,CAAC,IAAI,EAAE,CAAA;AACvE,CAAC","sourcesContent":["import type { AnySchema, ErrorMap, Meta } from '@orpc/contract'\nimport type { Procedure, Route } from '@orpc/server'\n\n/** All runtime access to oRPC's implementation namespace is audited here. */\nexport type ProcedureDefinition = {\n\tmeta?: Record<string, unknown>\n\troute?: Route\n\tinputSchema?: AnySchema\n\toutputSchema?: AnySchema\n}\n\nexport function procedureDefinition(procedure: object): ProcedureDefinition {\n\treturn (procedure as { '~orpc'?: ProcedureDefinition })['~orpc'] ?? {}\n}\n\nexport type NativeMeta<T> =\n\tT extends Procedure<\n\t\tinfer _I,\n\t\tinfer _C,\n\t\tinfer _In,\n\t\tinfer _Out,\n\t\tinfer _E,\n\t\tinfer M extends Meta\n\t>\n\t\t? M\n\t\t: Meta\nexport type NativeErrors<T> =\n\tT extends Procedure<\n\t\tinfer _I,\n\t\tinfer _C,\n\t\tinfer _In,\n\t\tinfer _Out,\n\t\tinfer E extends ErrorMap,\n\t\tinfer _M\n\t>\n\t\t? E\n\t\t: ErrorMap\nexport type LibraryMeta<T> =\n\tNativeMeta<T> extends { 'orpc-fn': infer M } ? M : never\n"]}