orpc-fn 0.2.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 (49) hide show
  1. package/README.md +28 -7
  2. package/dist/cli.d.ts +3 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +56 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/create-fn.d.ts +3 -3
  7. package/dist/create-fn.d.ts.map +1 -1
  8. package/dist/create-fn.js.map +1 -1
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/live/fn-live.d.ts +9 -5
  14. package/dist/live/fn-live.d.ts.map +1 -1
  15. package/dist/live/fn-live.js.map +1 -1
  16. package/dist/live/pub-sub.d.ts +5 -1
  17. package/dist/live/pub-sub.d.ts.map +1 -1
  18. package/dist/live/pub-sub.js +9 -2
  19. package/dist/live/pub-sub.js.map +1 -1
  20. package/dist/mcp/core.d.ts +41 -0
  21. package/dist/mcp/core.d.ts.map +1 -0
  22. package/dist/mcp/core.js +61 -0
  23. package/dist/mcp/core.js.map +1 -0
  24. package/dist/mcp/sdk.d.ts +8 -16
  25. package/dist/mcp/sdk.d.ts.map +1 -1
  26. package/dist/mcp/sdk.js +10 -78
  27. package/dist/mcp/sdk.js.map +1 -1
  28. package/dist/mcp/server.d.ts +11 -0
  29. package/dist/mcp/server.d.ts.map +1 -0
  30. package/dist/mcp/server.js +13 -0
  31. package/dist/mcp/server.js.map +1 -0
  32. package/dist/router.d.ts +8 -0
  33. package/dist/router.d.ts.map +1 -1
  34. package/dist/router.js +13 -0
  35. package/dist/router.js.map +1 -1
  36. package/dist/types.d.ts +16 -6
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/types.js.map +1 -1
  39. package/package.json +15 -2
  40. package/src/cli.ts +68 -0
  41. package/src/create-fn.ts +3 -4
  42. package/src/index.ts +2 -1
  43. package/src/live/fn-live.ts +9 -5
  44. package/src/live/pub-sub.ts +13 -4
  45. package/src/mcp/core.ts +102 -0
  46. package/src/mcp/sdk.ts +20 -102
  47. package/src/mcp/server.ts +35 -0
  48. package/src/router.ts +19 -0
  49. package/src/types.ts +24 -42
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orpc-fn",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Define a backend function once: a typed oRPC procedure with tracing, logging, guards, injected handler extras, nested calls and live queries over Redis.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -74,9 +74,16 @@
74
74
  "./mcp/sdk": {
75
75
  "types": "./dist/mcp/sdk.d.ts",
76
76
  "import": "./dist/mcp/sdk.js"
77
+ },
78
+ "./mcp/server": {
79
+ "types": "./dist/mcp/server.d.ts",
80
+ "import": "./dist/mcp/server.js"
77
81
  }
78
82
  },
79
83
  "types": "./dist/index.d.ts",
84
+ "bin": {
85
+ "orpc-fn": "./dist/cli.js"
86
+ },
80
87
  "files": [
81
88
  "dist",
82
89
  "LICENSE",
@@ -113,7 +120,8 @@
113
120
  "hono": "^4.0.0",
114
121
  "ioredis": "^5.0.0",
115
122
  "zod": "^4.0.0",
116
- "@modelcontextprotocol/sdk": "^1.32.0"
123
+ "@modelcontextprotocol/sdk": "^1.32.0",
124
+ "@modelcontextprotocol/server": "^2.0.0"
117
125
  },
118
126
  "peerDependenciesMeta": {
119
127
  "@mastra/core": {
@@ -139,13 +147,18 @@
139
147
  },
140
148
  "@modelcontextprotocol/sdk": {
141
149
  "optional": true
150
+ },
151
+ "@modelcontextprotocol/server": {
152
+ "optional": true
142
153
  }
143
154
  },
144
155
  "devDependencies": {
145
156
  "@arethetypeswrong/cli": "^0.18.2",
146
157
  "@biomejs/biome": "^2.2.4",
147
158
  "@mastra/core": "1.51.0",
159
+ "@modelcontextprotocol/client": "2.3.0",
148
160
  "@modelcontextprotocol/sdk": "1.32.0",
161
+ "@modelcontextprotocol/server": "2.3.0",
149
162
  "@opentelemetry/api": "^1.9.0",
150
163
  "@orpc/client": "1.14.8",
151
164
  "@orpc/contract": "1.14.8",
package/src/cli.ts ADDED
@@ -0,0 +1,68 @@
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 type { AnyRouter } from '@orpc/server'
8
+ import { createStreamManifestAsync, renderStreamManifest } from './router.js'
9
+
10
+ const usage = `Usage: orpc-fn stream-manifest <module>[#export] --out <file.ts|file.json> [--name streamPaths] [--check]
11
+
12
+ Imports the router (export "default" unless #export is given) and writes the
13
+ paths of its streaming routes for createRpcLink({ streamPaths }). The file is
14
+ only written when its content changes. --check writes nothing and exits 1 when
15
+ the file is missing or stale. Importing the router runs its module code; run
16
+ TypeScript routers with a runtime that loads .ts (bun, tsx, node >= 22.18).`
17
+
18
+ async function run(argv: readonly string[]): Promise<number> {
19
+ const { positionals, values } = parseArgs({
20
+ args: [...argv],
21
+ allowPositionals: true,
22
+ options: {
23
+ out: { type: 'string' },
24
+ name: { type: 'string' },
25
+ check: { type: 'boolean' },
26
+ help: { type: 'boolean', short: 'h' }
27
+ }
28
+ })
29
+ const [command, target] = positionals
30
+ if (values.help || command !== 'stream-manifest' || !target || !values.out) {
31
+ console.error(usage)
32
+ return values.help ? 0 : 2
33
+ }
34
+ const [modulePath = '', exportName = 'default'] = target.split('#')
35
+ const loaded: Record<string, unknown> = await import(
36
+ pathToFileURL(resolve(modulePath)).href
37
+ )
38
+ const router = loaded[exportName]
39
+ if (!router || typeof router !== 'object') {
40
+ console.error(`orpc-fn: ${modulePath} has no router export "${exportName}"`)
41
+ return 2
42
+ }
43
+ const out = resolve(values.out)
44
+ const source = renderStreamManifest(
45
+ await createStreamManifestAsync(router as AnyRouter),
46
+ { format: out.endsWith('.json') ? 'json' : 'ts', name: values.name }
47
+ )
48
+ const current = await readFile(out, 'utf8').catch(() => undefined)
49
+ if (current === source) return 0
50
+ if (values.check) {
51
+ console.error(
52
+ `orpc-fn: ${values.out} is stale; rerun without --check to update it`
53
+ )
54
+ return 1
55
+ }
56
+ await writeFile(out, source)
57
+ console.log(`orpc-fn: wrote ${values.out}`)
58
+ return 0
59
+ }
60
+
61
+ // Exit explicitly: the imported router may hold open database or broker handles.
62
+ run(process.argv.slice(2)).then(
63
+ (code) => process.exit(code),
64
+ (error) => {
65
+ console.error(error)
66
+ process.exit(1)
67
+ }
68
+ )
package/src/create-fn.ts CHANGED
@@ -38,7 +38,7 @@ import {
38
38
  type CurrentContextOf,
39
39
  type BuilderLike,
40
40
  type FiniteExtras,
41
- type FiniteScopedExtras,
41
+ type ScopedExtrasGuard,
42
42
  type Fn,
43
43
  type FnCompletedEvent,
44
44
  type FnDefinition,
@@ -115,7 +115,7 @@ export type CreateFnOptions<
115
115
  signal: AbortSignal | undefined
116
116
  name: string
117
117
  procedure: K
118
- }) => MaybePromise<TScoped[K]>
118
+ }) => MaybePromise<TScoped[K] & ScopedExtrasGuard<NoInfer<TScoped[K]>>>
119
119
  }
120
120
  /**
121
121
  * Checks run before the handler; each key becomes a typed `fn()` option.
@@ -240,8 +240,7 @@ export function createFn<
240
240
  TLogger,
241
241
  TScoped
242
242
  > &
243
- FiniteExtras<NoInfer<TExtras>> &
244
- FiniteScopedExtras<NoInfer<TScoped>>
243
+ FiniteExtras<NoInfer<TExtras>>
245
244
  ): FnFactory<
246
245
  Definition<
247
246
  TProcedures,
package/src/index.ts CHANGED
@@ -22,7 +22,8 @@ export type {
22
22
  export {
23
23
  createRouter,
24
24
  createStreamManifest,
25
- createStreamManifestAsync
25
+ createStreamManifestAsync,
26
+ renderStreamManifest
26
27
  } from './router.js'
27
28
  export type {
28
29
  AnyFnContext,
@@ -66,10 +66,14 @@ export type FnLiveConfig<
66
66
  ChannelQueueOptions & {
67
67
  /** Subscribe route name; default `${name}.live`. */
68
68
  name?: string
69
- /** Validates parsed reducer state without reapplying an output transform. Required with a transformer. */
70
- stateSchema?: Schema<TOutput, TOutput>
69
+ /**
70
+ * Validates reducer state without reapplying an output transform. Required with a
71
+ * transformer. Its input may be looser than its output (e.g. branded IDs); it is
72
+ * checked at runtime.
73
+ */
74
+ stateSchema?: Schema<unknown, TOutput>
71
75
  /** Validates patch payloads. Required when a transformer returns fnLivePatch. */
72
- emitSchema?: Schema<TEmit, TEmit>
76
+ emitSchema?: Schema<unknown, TEmit>
73
77
  /** Optional access check before each update; idle streams keep their initial authorization. */
74
78
  reauthorize?: AuthFn<TInput, FnContext<TDef, TKey>>
75
79
  eventSchema: TEventSchema
@@ -122,10 +126,10 @@ export type FnLiveConfig<
122
126
  TEmit,
123
127
  TErrors
124
128
  >
125
- stateSchema: Schema<TOutput, TOutput>
129
+ stateSchema: Schema<unknown, TOutput>
126
130
  }
127
131
  ) &
128
- ([TEmit] extends [never] ? object : { emitSchema: Schema<TEmit, TEmit> })
132
+ ([TEmit] extends [never] ? object : { emitSchema: Schema<unknown, TEmit> })
129
133
 
130
134
  type FnLiveConfigTransformer<
131
135
  TDef extends FnDefinition,
@@ -204,7 +204,11 @@ export type CreatePublisher = <TEventSchema extends ZodType>(
204
204
  export type PubSubRuntimeOptions = {
205
205
  /** Queue depths, drops, successful reconnects and parse failures. */
206
206
  onMetric?: (event: PubSubMetric) => void
207
- transport: PubSubTransport
207
+ /**
208
+ * The broker, or a function creating it on first use so importing routes
209
+ * opens no connection: `transport: () => bunRedisTransport(getRedis())`.
210
+ */
211
+ transport: PubSubTransport | (() => PubSubTransport)
208
212
  /**
209
213
  * Called per event dropped from ingress, replay or delivery queues.
210
214
  * Delivery drops count events before subscriber filtering.
@@ -525,13 +529,19 @@ export function createLiveRuntime(options: LiveRuntimeOptions) {
525
529
  // the client's backoff reconnects to the new deployment.
526
530
  const activeSubscriberCleanups = new Set<() => void>()
527
531
 
532
+ let transport: PubSubTransport | undefined
528
533
  const requireTransport = () => {
529
534
  if (!options.pubsub) {
530
535
  throw new Error(
531
536
  'orpc-fn: live routes need a transport, pass createFn({ pubsub: { transport } })'
532
537
  )
533
538
  }
534
- return options.pubsub.transport
539
+ if (transport) return transport
540
+ // A lazy transport is never created during or after shutdown.
541
+ assertOpen()
542
+ const configured = options.pubsub.transport
543
+ transport = typeof configured === 'function' ? configured() : configured
544
+ return transport
535
545
  }
536
546
 
537
547
  /**
@@ -1241,8 +1251,7 @@ export function createLiveRuntime(options: LiveRuntimeOptions) {
1241
1251
  shutdownPromise = (async () => {
1242
1252
  await Promise.allSettled([...pendingOpens])
1243
1253
  await Promise.all([...releases])
1244
- if (options.pubsub?.ownsTransport)
1245
- await options.pubsub.transport.close?.()
1254
+ if (options.pubsub?.ownsTransport) await transport?.close?.()
1246
1255
  })()
1247
1256
  return shutdownPromise
1248
1257
  },
@@ -0,0 +1,102 @@
1
+ import {
2
+ type AnyRouter,
3
+ type InferRouterInitialContext,
4
+ call
5
+ } from '@orpc/server'
6
+ import { isObjectJsonSchema } from '../json-schema.js'
7
+ import { listTools, type McpTool, type ProcedureFilter } from '../mcp.js'
8
+
9
+ /** Text-only tool result: the shape both MCP SDK majors accept. */
10
+ export type TextToolResult = {
11
+ content: { type: 'text'; text: string }[]
12
+ isError?: boolean
13
+ }
14
+
15
+ export type RegisterMcpToolsOptions<R extends AnyRouter, TResult> = {
16
+ /** Tool exposure must be an explicit application choice. */
17
+ filter: ProcedureFilter
18
+ context: (request: {
19
+ name: string
20
+ signal: AbortSignal
21
+ }) => InferRouterInitialContext<R> | Promise<InferRouterInitialContext<R>>
22
+ readOnly?: ProcedureFilter
23
+ name?: NonNullable<Parameters<typeof listTools>[1]>['name']
24
+ formatResult?: (value: unknown, tool: McpTool) => TResult | Promise<TResult>
25
+ }
26
+
27
+ /** Default protocol result: JSON/text. Provide formatResult for other MCP content. */
28
+ export function formatMcpResult(value: unknown): TextToolResult {
29
+ const text =
30
+ typeof value === 'string'
31
+ ? value
32
+ : (JSON.stringify(value, (_key, item) =>
33
+ typeof item === 'bigint' ? item.toString() : item
34
+ ) ?? 'null')
35
+ return { content: [{ type: 'text', text }] }
36
+ }
37
+
38
+ const errorResult = (text: string): TextToolResult => ({
39
+ content: [{ type: 'text', text }],
40
+ isError: true
41
+ })
42
+
43
+ /** @internal SDK-independent `tools/list` and `tools/call` behind both adapters. */
44
+ export function createToolHandlers<R extends AnyRouter, TResult>(
45
+ router: R,
46
+ options: RegisterMcpToolsOptions<R, TResult>
47
+ ) {
48
+ const definitions = listTools(router, {
49
+ filter: options.filter,
50
+ ...(options.readOnly ? { readOnly: options.readOnly } : {}),
51
+ ...(options.name ? { name: options.name } : {})
52
+ })
53
+ const tools = new Map(definitions.map((tool) => [tool.name, tool]))
54
+ const descriptions = definitions.map((tool) => {
55
+ const inputSchema = tool.config.inputSchema?.[
56
+ '~standard'
57
+ ].jsonSchema.input() ?? { type: 'object', properties: {} }
58
+ if (!isObjectJsonSchema(inputSchema))
59
+ throw new TypeError(
60
+ `orpc-fn: MCP tool ${tool.name} needs an object input`
61
+ )
62
+ // MCP requires a top-level object; object unions retain their constraints.
63
+ return {
64
+ name: tool.name,
65
+ ...tool.config,
66
+ inputSchema: { ...inputSchema, type: 'object' as const }
67
+ }
68
+ })
69
+ return {
70
+ names: [...tools.keys()] as readonly string[],
71
+ list: () => ({ tools: descriptions }),
72
+ call: async (
73
+ name: string,
74
+ args: Record<string, unknown> | undefined,
75
+ signal: AbortSignal
76
+ ): Promise<TResult | TextToolResult> => {
77
+ const tool = tools.get(name)
78
+ if (!tool) return errorResult(`Unknown tool: ${name}`)
79
+ try {
80
+ signal.throwIfAborted()
81
+ const validation = await tool.config.inputSchema?.[
82
+ '~standard'
83
+ ].validate(args ?? {})
84
+ if (validation?.issues)
85
+ return errorResult(
86
+ validation.issues.map((issue) => issue.message).join('; ')
87
+ )
88
+ const context = await options.context({ name: tool.name, signal })
89
+ const result = await call(tool.procedure, validation?.value, {
90
+ context,
91
+ signal
92
+ })
93
+ return await (options.formatResult ?? formatMcpResult)(result, tool)
94
+ } catch (error) {
95
+ if (signal.aborted) throw error
96
+ return errorResult(
97
+ error instanceof Error ? error.message : String(error)
98
+ )
99
+ }
100
+ }
101
+ }
102
+ }
package/src/mcp/sdk.ts CHANGED
@@ -4,115 +4,33 @@ import {
4
4
  ListToolsRequestSchema,
5
5
  type CallToolResult
6
6
  } from '@modelcontextprotocol/sdk/types.js'
7
+ import type { AnyRouter } from '@orpc/server'
7
8
  import {
8
- type AnyRouter,
9
- type InferRouterInitialContext,
10
- call
11
- } from '@orpc/server'
12
- import { isObjectJsonSchema } from '../json-schema.js'
13
- import { listTools, type McpTool, type ProcedureFilter } from '../mcp.js'
9
+ createToolHandlers,
10
+ formatMcpResult,
11
+ type RegisterMcpToolsOptions as CoreOptions
12
+ } from './core.js'
14
13
 
15
- export type RegisterMcpToolsOptions<R extends AnyRouter> = {
16
- /** Tool exposure must be an explicit application choice. */
17
- filter: ProcedureFilter
18
- context: (request: {
19
- name: string
20
- signal: AbortSignal
21
- }) => InferRouterInitialContext<R> | Promise<InferRouterInitialContext<R>>
22
- readOnly?: ProcedureFilter
23
- name?: NonNullable<Parameters<typeof listTools>[1]>['name']
24
- formatResult?: (
25
- value: unknown,
26
- tool: McpTool
27
- ) => CallToolResult | Promise<CallToolResult>
28
- }
14
+ export { formatMcpResult }
29
15
 
30
- /** Default protocol result: JSON/text. Provide formatResult for other MCP content. */
31
- export function formatMcpResult(value: unknown): CallToolResult {
32
- const text =
33
- typeof value === 'string'
34
- ? value
35
- : (JSON.stringify(value, (_key, item) =>
36
- typeof item === 'bigint' ? item.toString() : item
37
- ) ?? 'null')
38
- return { content: [{ type: 'text', text }] }
39
- }
16
+ export type RegisterMcpToolsOptions<R extends AnyRouter> = CoreOptions<
17
+ R,
18
+ CallToolResult
19
+ >
40
20
 
41
- /** Register on the SDK's low-level Server, or on McpServer.server. */
21
+ /**
22
+ * Register on the SDK v1 low-level Server, or on McpServer.server. For SDK v2
23
+ * (`@modelcontextprotocol/server`) use `orpc-fn/mcp/server`.
24
+ */
42
25
  export function registerMcpTools<R extends AnyRouter>(
43
26
  server: Pick<Server, 'setRequestHandler'>,
44
27
  router: R,
45
28
  options: RegisterMcpToolsOptions<R>
46
29
  ): readonly string[] {
47
- const definitions = listTools(router, {
48
- filter: options.filter,
49
- ...(options.readOnly ? { readOnly: options.readOnly } : {}),
50
- ...(options.name ? { name: options.name } : {})
51
- })
52
- const tools = new Map(definitions.map((tool) => [tool.name, tool]))
53
- const descriptions = definitions.map((tool) => {
54
- const inputSchema = tool.config.inputSchema?.[
55
- '~standard'
56
- ].jsonSchema.input() ?? { type: 'object', properties: {} }
57
- if (!isObjectJsonSchema(inputSchema))
58
- throw new TypeError(
59
- `orpc-fn: MCP tool ${tool.name} needs an object input`
60
- )
61
- // MCP requires a top-level object; object unions retain their constraints.
62
- return {
63
- name: tool.name,
64
- ...tool.config,
65
- inputSchema: { ...inputSchema, type: 'object' as const }
66
- }
67
- })
68
- server.setRequestHandler(ListToolsRequestSchema, () => ({
69
- tools: descriptions
70
- }))
71
- server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
72
- const tool = tools.get(request.params.name)
73
- if (!tool)
74
- return {
75
- content: [
76
- { type: 'text', text: `Unknown tool: ${request.params.name}` }
77
- ],
78
- isError: true
79
- }
80
- try {
81
- extra.signal.throwIfAborted()
82
- const validation = await tool.config.inputSchema?.['~standard'].validate(
83
- request.params.arguments ?? {}
84
- )
85
- if (validation?.issues)
86
- return {
87
- content: [
88
- {
89
- type: 'text',
90
- text: validation.issues.map((issue) => issue.message).join('; ')
91
- }
92
- ],
93
- isError: true
94
- }
95
- const context = await options.context({
96
- name: tool.name,
97
- signal: extra.signal
98
- })
99
- const result = await call(tool.procedure, validation?.value, {
100
- context,
101
- signal: extra.signal
102
- })
103
- return await (options.formatResult ?? formatMcpResult)(result, tool)
104
- } catch (error) {
105
- if (extra.signal.aborted) throw error
106
- return {
107
- content: [
108
- {
109
- type: 'text',
110
- text: error instanceof Error ? error.message : String(error)
111
- }
112
- ],
113
- isError: true
114
- }
115
- }
116
- })
117
- return [...tools.keys()]
30
+ const handlers = createToolHandlers(router, options)
31
+ server.setRequestHandler(ListToolsRequestSchema, handlers.list)
32
+ server.setRequestHandler(CallToolRequestSchema, (request, extra) =>
33
+ handlers.call(request.params.name, request.params.arguments, extra.signal)
34
+ )
35
+ return handlers.names
118
36
  }
@@ -0,0 +1,35 @@
1
+ import type { CallToolResult, Server } from '@modelcontextprotocol/server'
2
+ import type { AnyRouter } from '@orpc/server'
3
+ import {
4
+ createToolHandlers,
5
+ formatMcpResult,
6
+ type RegisterMcpToolsOptions as CoreOptions
7
+ } from './core.js'
8
+
9
+ export { formatMcpResult }
10
+
11
+ export type RegisterMcpToolsOptions<R extends AnyRouter> = CoreOptions<
12
+ R,
13
+ CallToolResult
14
+ >
15
+
16
+ /**
17
+ * Register on the SDK v2 (`@modelcontextprotocol/server`) low-level Server, or
18
+ * on McpServer.server. Declare `capabilities: { tools: {} }` on the Server.
19
+ */
20
+ export function registerMcpTools<R extends AnyRouter>(
21
+ server: Pick<Server, 'setRequestHandler'>,
22
+ router: R,
23
+ options: RegisterMcpToolsOptions<R>
24
+ ): readonly string[] {
25
+ const handlers = createToolHandlers(router, options)
26
+ server.setRequestHandler('tools/list', handlers.list)
27
+ server.setRequestHandler('tools/call', (request, ctx) =>
28
+ handlers.call(
29
+ request.params.name,
30
+ request.params.arguments,
31
+ ctx.mcpReq.signal
32
+ )
33
+ )
34
+ return handlers.names
35
+ }
package/src/router.ts CHANGED
@@ -51,3 +51,22 @@ export async function createStreamManifestAsync(
51
51
  )
52
52
  return paths
53
53
  }
54
+
55
+ /**
56
+ * Deterministic module source for a stream manifest: paths sorted, `json`
57
+ * as an array, `ts` as `export const <name>`. Used by `orpc-fn stream-manifest`.
58
+ */
59
+ export function renderStreamManifest(
60
+ paths: readonly (readonly string[])[],
61
+ options: { format: 'json' | 'ts'; name?: string | undefined }
62
+ ): string {
63
+ const lines = paths
64
+ .map(
65
+ (path) =>
66
+ `\t[${path.map((segment) => JSON.stringify(segment)).join(', ')}]`
67
+ )
68
+ .sort()
69
+ const array = lines.length ? `[\n${lines.join(',\n')}\n]` : '[]'
70
+ if (options.format === 'json') return `${array}\n`
71
+ return `// Generated by \`orpc-fn stream-manifest\`. Do not edit.\nexport const ${options.name ?? 'streamPaths'}: readonly (readonly string[])[] = ${array}\n`
72
+ }
package/src/types.ts CHANGED
@@ -140,6 +140,17 @@ export type RejectKeys<T, K extends PropertyKey, TMessage extends string> = {
140
140
  [P in Extract<keyof T, K>]: TMessage
141
141
  }
142
142
 
143
+ /** Params every handler receives; extras may not reuse these names. */
144
+ type HandlerParamKey =
145
+ | 'input'
146
+ | 'context'
147
+ | 'call'
148
+ | 'signal'
149
+ | 'span'
150
+ | 'logger'
151
+ | 'errors'
152
+ | 'lastEventId'
153
+
143
154
  /**
144
155
  * Handler extras must have known keys: an index signature would claim every
145
156
  * handler param. Intersected into `createFn`'s options to reject it.
@@ -149,17 +160,7 @@ export type FiniteExtras<T> = T extends unknown
149
160
  ? { extras: 'orpc-fn: extras need known keys, not an index signature' }
150
161
  : number extends keyof T
151
162
  ? { extras: 'orpc-fn: extras need known keys, not an index signature' }
152
- : Extract<
153
- keyof T,
154
- | 'input'
155
- | 'context'
156
- | 'call'
157
- | 'signal'
158
- | 'span'
159
- | 'logger'
160
- | 'errors'
161
- | 'lastEventId'
162
- > extends never
163
+ : Extract<keyof T, HandlerParamKey> extends never
163
164
  ? unknown
164
165
  : { extras: 'orpc-fn: extras cannot shadow handler parameters' }
165
166
  : never
@@ -272,14 +273,7 @@ export type HandlerParams<
272
273
  : object
273
274
  : object
274
275
  >,
275
- | 'input'
276
- | 'context'
277
- | 'call'
278
- | 'signal'
279
- | 'span'
280
- | 'logger'
281
- | 'errors'
282
- | 'lastEventId'
276
+ HandlerParamKey
283
277
  > & {
284
278
  input: TInput
285
279
  context: FnContext<TDef, TKey>
@@ -506,26 +500,14 @@ export type CompletedHookEvent<
506
500
  )
507
501
  }[keyof TProcedures & string]
508
502
 
509
- export type FiniteScopedExtras<T> = {
510
- [K in keyof T]: string extends keyof T[K]
511
- ? K
512
- : number extends keyof T[K]
513
- ? K
514
- : Extract<
515
- keyof T[K],
516
- | 'input'
517
- | 'context'
518
- | 'call'
519
- | 'signal'
520
- | 'span'
521
- | 'logger'
522
- | 'errors'
523
- | 'lastEventId'
524
- > extends never
525
- ? never
526
- : K
527
- }[keyof T] extends never
528
- ? unknown
529
- : {
530
- extrasByProcedure: 'orpc-fn: scoped extras need finite keys and cannot shadow handler parameters'
531
- }
503
+ /**
504
+ * Intersected into each scoped extras function's return type: rejects index
505
+ * signatures and handler-parameter names at the offending property. A
506
+ * top-level conditional over the whole map broke inference when zero-argument
507
+ * and context-reading functions were mixed.
508
+ */
509
+ export type ScopedExtrasGuard<T> = (string extends keyof T
510
+ ? { 'orpc-fn: scoped extras need finite keys': never }
511
+ : number extends keyof T
512
+ ? { 'orpc-fn: scoped extras need finite keys': never }
513
+ : unknown) & { [K in HandlerParamKey]?: never }