orpc-fn 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -28
- package/dist/assemble-procedure.d.ts.map +1 -1
- package/dist/assemble-procedure.js +3 -1
- package/dist/assemble-procedure.js.map +1 -1
- package/dist/client.d.ts +13 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +21 -1
- package/dist/client.js.map +1 -1
- package/dist/create-fn.d.ts +63 -5
- package/dist/create-fn.d.ts.map +1 -1
- package/dist/create-fn.js +45 -16
- package/dist/create-fn.js.map +1 -1
- package/dist/expected-client-error.d.ts +6 -0
- package/dist/expected-client-error.d.ts.map +1 -1
- package/dist/expected-client-error.js +16 -0
- package/dist/expected-client-error.js.map +1 -1
- package/dist/hono.d.ts +44 -22
- package/dist/hono.d.ts.map +1 -1
- package/dist/hono.js +51 -20
- package/dist/hono.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/issues.d.ts +12 -0
- package/dist/issues.d.ts.map +1 -0
- package/dist/issues.js +22 -0
- package/dist/issues.js.map +1 -0
- package/dist/json-schema.d.ts +3 -6
- package/dist/json-schema.d.ts.map +1 -1
- package/dist/json-schema.js +12 -7
- package/dist/json-schema.js.map +1 -1
- package/dist/live/codec.d.ts +2 -0
- package/dist/live/codec.d.ts.map +1 -1
- package/dist/live/codec.js +21 -16
- package/dist/live/codec.js.map +1 -1
- package/dist/live/fn-live.d.ts +6 -19
- package/dist/live/fn-live.d.ts.map +1 -1
- package/dist/live/fn-live.js +15 -23
- package/dist/live/fn-live.js.map +1 -1
- package/dist/live/index.d.ts +2 -2
- package/dist/live/index.d.ts.map +1 -1
- package/dist/live/index.js +1 -1
- package/dist/live/index.js.map +1 -1
- package/dist/live/memory.d.ts +11 -1
- package/dist/live/memory.d.ts.map +1 -1
- package/dist/live/memory.js +14 -1
- package/dist/live/memory.js.map +1 -1
- package/dist/live/pub-sub.d.ts +50 -8
- package/dist/live/pub-sub.d.ts.map +1 -1
- package/dist/live/pub-sub.js +79 -26
- package/dist/live/pub-sub.js.map +1 -1
- package/dist/live/redis-bun.d.ts +6 -1
- package/dist/live/redis-bun.d.ts.map +1 -1
- package/dist/live/redis-bun.js +10 -1
- package/dist/live/redis-bun.js.map +1 -1
- package/dist/live/scoped-change-bus.d.ts +3 -1
- package/dist/live/scoped-change-bus.d.ts.map +1 -1
- package/dist/live/scoped-change-bus.js +3 -0
- package/dist/live/scoped-change-bus.js.map +1 -1
- package/dist/mastra.d.ts +8 -3
- package/dist/mastra.d.ts.map +1 -1
- package/dist/mastra.js +8 -1
- package/dist/mastra.js.map +1 -1
- package/dist/mcp/core.d.ts +31 -14
- package/dist/mcp/core.d.ts.map +1 -1
- package/dist/mcp/core.js +39 -20
- package/dist/mcp/core.js.map +1 -1
- package/dist/mcp/sdk.d.ts +6 -2
- package/dist/mcp/sdk.d.ts.map +1 -1
- package/dist/mcp/sdk.js +9 -3
- package/dist/mcp/sdk.js.map +1 -1
- package/dist/mcp/server.d.ts +6 -2
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +9 -3
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp.d.ts +3 -7
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +4 -6
- package/dist/mcp.js.map +1 -1
- package/dist/meta.d.ts +14 -2
- package/dist/meta.d.ts.map +1 -1
- package/dist/meta.js +6 -2
- package/dist/meta.js.map +1 -1
- package/dist/otel.d.ts +16 -4
- package/dist/otel.d.ts.map +1 -1
- package/dist/otel.js +40 -10
- package/dist/otel.js.map +1 -1
- package/dist/react.d.ts +6 -1
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +5 -1
- package/dist/react.js.map +1 -1
- package/dist/route-options.d.ts.map +1 -1
- package/dist/route-options.js +4 -0
- package/dist/route-options.js.map +1 -1
- package/dist/stream-call.d.ts +15 -0
- package/dist/stream-call.d.ts.map +1 -0
- package/dist/stream-call.js +13 -0
- package/dist/stream-call.js.map +1 -0
- package/dist/timing.d.ts +2 -0
- package/dist/timing.d.ts.map +1 -1
- package/dist/timing.js +2 -0
- package/dist/timing.js.map +1 -1
- package/dist/tool-errors.d.ts +24 -0
- package/dist/tool-errors.d.ts.map +1 -0
- package/dist/tool-errors.js +36 -0
- package/dist/tool-errors.js.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/vue.d.ts +47 -4
- package/dist/vue.d.ts.map +1 -1
- package/dist/vue.js +39 -9
- package/dist/vue.js.map +1 -1
- package/package.json +1 -1
- package/src/assemble-procedure.ts +3 -1
- package/src/client.ts +26 -2
- package/src/create-fn.ts +149 -19
- package/src/expected-client-error.ts +19 -0
- package/src/hono.ts +120 -41
- package/src/index.ts +3 -2
- package/src/issues.ts +35 -0
- package/src/json-schema.ts +16 -10
- package/src/live/codec.ts +21 -19
- package/src/live/fn-live.ts +18 -36
- package/src/live/index.ts +2 -2
- package/src/live/memory.ts +22 -1
- package/src/live/pub-sub.ts +142 -37
- package/src/live/redis-bun.ts +23 -2
- package/src/live/scoped-change-bus.ts +10 -1
- package/src/mastra.ts +10 -4
- package/src/mcp/core.ts +76 -29
- package/src/mcp/sdk.ts +27 -2
- package/src/mcp/server.ts +27 -2
- package/src/mcp.ts +5 -16
- package/src/meta.ts +28 -4
- package/src/otel.ts +41 -9
- package/src/react.ts +10 -3
- package/src/route-options.ts +8 -0
- package/src/stream-call.ts +15 -0
- package/src/timing.ts +3 -0
- package/src/tool-errors.ts +52 -0
- package/src/types.ts +2 -0
- package/src/vue.ts +92 -19
package/src/mcp/server.ts
CHANGED
|
@@ -3,16 +3,23 @@ import type { AnyRouter } from '@orpc/server'
|
|
|
3
3
|
import {
|
|
4
4
|
createToolHandlers,
|
|
5
5
|
formatMcpResult,
|
|
6
|
+
type McpCallOptions,
|
|
7
|
+
type PreparedMcpTools,
|
|
8
|
+
prepareMcpTools,
|
|
6
9
|
type RegisterMcpToolsOptions as CoreOptions
|
|
7
10
|
} from './core.js'
|
|
8
11
|
|
|
9
|
-
export { formatMcpResult }
|
|
12
|
+
export { formatMcpResult, prepareMcpTools, type PreparedMcpTools }
|
|
10
13
|
|
|
11
14
|
export type RegisterMcpToolsOptions<R extends AnyRouter> = CoreOptions<
|
|
12
15
|
R,
|
|
13
16
|
CallToolResult
|
|
14
17
|
>
|
|
15
18
|
|
|
19
|
+
/** Options for registering tools from {@link prepareMcpTools}. */
|
|
20
|
+
export type RegisterPreparedMcpToolsOptions<R extends AnyRouter> =
|
|
21
|
+
McpCallOptions<R, CallToolResult>
|
|
22
|
+
|
|
16
23
|
/**
|
|
17
24
|
* Register on the SDK v2 (`@modelcontextprotocol/server`) low-level Server, or
|
|
18
25
|
* on McpServer.server. Declare `capabilities: { tools: {} }` on the Server.
|
|
@@ -22,7 +29,25 @@ export function registerMcpTools<R extends AnyRouter>(
|
|
|
22
29
|
router: R,
|
|
23
30
|
options: RegisterMcpToolsOptions<R>
|
|
24
31
|
): readonly string[] {
|
|
25
|
-
|
|
32
|
+
return install(
|
|
33
|
+
server,
|
|
34
|
+
createToolHandlers(prepareMcpTools(router, options), options)
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Register tools listed once with {@link prepareMcpTools}, e.g. on a server built per request. */
|
|
39
|
+
export function registerPreparedMcpTools<R extends AnyRouter>(
|
|
40
|
+
server: Pick<Server, 'setRequestHandler'>,
|
|
41
|
+
tools: PreparedMcpTools<R>,
|
|
42
|
+
options: RegisterPreparedMcpToolsOptions<R>
|
|
43
|
+
): readonly string[] {
|
|
44
|
+
return install(server, createToolHandlers(tools, options))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function install(
|
|
48
|
+
server: Pick<Server, 'setRequestHandler'>,
|
|
49
|
+
handlers: ReturnType<typeof createToolHandlers<AnyRouter, CallToolResult>>
|
|
50
|
+
): readonly string[] {
|
|
26
51
|
server.setRequestHandler('tools/list', handlers.list)
|
|
27
52
|
server.setRequestHandler('tools/call', (request, ctx) =>
|
|
28
53
|
handlers.call(
|
package/src/mcp.ts
CHANGED
|
@@ -6,16 +6,11 @@ import {
|
|
|
6
6
|
} from '@orpc/server'
|
|
7
7
|
import { procedureDefinition } from './compatibility.js'
|
|
8
8
|
import { rawInputSchema, type ToolSchema } from './json-schema.js'
|
|
9
|
-
import { readFnMeta, toToolName } from './meta.js'
|
|
9
|
+
import { hasTag, type ProcedureFilter, readFnMeta, toToolName } from './meta.js'
|
|
10
10
|
|
|
11
|
-
export { toToolName }
|
|
11
|
+
export { hasTag, toToolName }
|
|
12
12
|
export { rawInputSchema as toolInputSchema }
|
|
13
|
-
export type { ToolSchema }
|
|
14
|
-
|
|
15
|
-
/** Picks procedures; also fits oRPC's `OpenAPIHandler({ filter })`. */
|
|
16
|
-
export type ProcedureFilter = (
|
|
17
|
-
options: TraverseContractProcedureCallbackOptions
|
|
18
|
-
) => boolean
|
|
13
|
+
export type { ProcedureFilter, ToolSchema }
|
|
19
14
|
|
|
20
15
|
export type McpTool = {
|
|
21
16
|
/** Tool name: the sanitized `fn` name (`user.me` becomes `user-me`). */
|
|
@@ -32,12 +27,6 @@ export type McpTool = {
|
|
|
32
27
|
}
|
|
33
28
|
}
|
|
34
29
|
|
|
35
|
-
/** Procedures whose route has `tag`: `listTools(router, { filter: hasTag('external') })`. */
|
|
36
|
-
export const hasTag =
|
|
37
|
-
(tag: string): ProcedureFilter =>
|
|
38
|
-
({ contract }) =>
|
|
39
|
-
procedureDefinition(contract).route?.tags?.includes(tag) ?? false
|
|
40
|
-
|
|
41
30
|
const isGet: ProcedureFilter = ({ contract }) =>
|
|
42
31
|
procedureDefinition(contract).route?.method === 'GET'
|
|
43
32
|
|
|
@@ -80,8 +69,8 @@ export function listTools(
|
|
|
80
69
|
`orpc-fn: MCP tool name collision: ${name}; provide a name override`
|
|
81
70
|
)
|
|
82
71
|
names.set(name, procedure)
|
|
83
|
-
const { inputSchema
|
|
84
|
-
const description =
|
|
72
|
+
const { inputSchema } = procedureDefinition(procedure)
|
|
73
|
+
const description = meta.description || undefined
|
|
85
74
|
tools.push({
|
|
86
75
|
name,
|
|
87
76
|
procedure,
|
package/src/meta.ts
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import { getEventIteratorSchemaDetails, type AnySchema } from '@orpc/contract'
|
|
2
2
|
import { type LibraryMeta, procedureDefinition } from './compatibility.js'
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
AnyProcedure,
|
|
5
|
+
TraverseContractProcedureCallbackOptions
|
|
6
|
+
} from '@orpc/server'
|
|
7
|
+
|
|
8
|
+
/** Picks procedures; also fits oRPC's `OpenAPIHandler({ filter })`. */
|
|
9
|
+
export type ProcedureFilter = (
|
|
10
|
+
options: TraverseContractProcedureCallbackOptions
|
|
11
|
+
) => boolean
|
|
12
|
+
|
|
13
|
+
/** Procedures whose route has `tag`: `listTools(router, { filter: hasTag('external') })`. */
|
|
14
|
+
export const hasTag =
|
|
15
|
+
(tag: string): ProcedureFilter =>
|
|
16
|
+
({ contract }) =>
|
|
17
|
+
procedureDefinition(contract).route?.tags?.includes(tag) ?? false
|
|
4
18
|
|
|
5
19
|
/**
|
|
6
20
|
* Key under which `fn()` stores its metadata in oRPC's procedure meta
|
|
@@ -18,6 +32,10 @@ export type StoredFnMeta<
|
|
|
18
32
|
procedure: TKey
|
|
19
33
|
meta: TMeta
|
|
20
34
|
stream: boolean
|
|
35
|
+
/** Guard values set on the route. */
|
|
36
|
+
guards?: Record<string, unknown>
|
|
37
|
+
/** Guards decided per call through `guardResolvers`. */
|
|
38
|
+
dynamicGuards?: readonly string[]
|
|
21
39
|
}
|
|
22
40
|
|
|
23
41
|
export type FnMeta<
|
|
@@ -30,7 +48,7 @@ export type FnMeta<
|
|
|
30
48
|
procedure: TKey | undefined
|
|
31
49
|
/** The app's typed meta (`createFn({ meta })`) declared on this route. */
|
|
32
50
|
meta: Partial<TMeta>
|
|
33
|
-
/** Route
|
|
51
|
+
/** Route description, falling back to summary, falling back to ''. */
|
|
34
52
|
description: string
|
|
35
53
|
summary: string | undefined
|
|
36
54
|
method: string | undefined
|
|
@@ -39,6 +57,10 @@ export type FnMeta<
|
|
|
39
57
|
tags: readonly string[]
|
|
40
58
|
inputSchema: AnySchema | undefined
|
|
41
59
|
outputSchema: AnySchema | undefined
|
|
60
|
+
/** Guard values set on the route (`permission: 'deal.read'`). */
|
|
61
|
+
guards: Record<string, unknown>
|
|
62
|
+
/** Guards the route decides per call through `guardResolvers`. */
|
|
63
|
+
dynamicGuards: readonly string[]
|
|
42
64
|
}
|
|
43
65
|
|
|
44
66
|
/** Read what `fn()` (and oRPC) recorded on a procedure. */
|
|
@@ -69,7 +91,7 @@ export function readFnMeta<T extends AnyProcedure>(
|
|
|
69
91
|
name: stored?.name,
|
|
70
92
|
procedure: stored?.procedure as KeyFrom<T> | undefined,
|
|
71
93
|
meta: (stored?.meta ?? {}) as Partial<MetaFrom<T>>,
|
|
72
|
-
description: route.
|
|
94
|
+
description: route.description ?? route.summary ?? '',
|
|
73
95
|
summary: route.summary,
|
|
74
96
|
method: route.method,
|
|
75
97
|
path: route.path,
|
|
@@ -78,7 +100,9 @@ export function readFnMeta<T extends AnyProcedure>(
|
|
|
78
100
|
stored?.stream === true ||
|
|
79
101
|
getEventIteratorSchemaDetails(def.outputSchema) !== undefined,
|
|
80
102
|
inputSchema: def.inputSchema,
|
|
81
|
-
outputSchema: def.outputSchema
|
|
103
|
+
outputSchema: def.outputSchema,
|
|
104
|
+
guards: stored?.guards ?? {},
|
|
105
|
+
dynamicGuards: stored?.dynamicGuards ?? []
|
|
82
106
|
}
|
|
83
107
|
}
|
|
84
108
|
|
package/src/otel.ts
CHANGED
|
@@ -83,7 +83,15 @@ export type Tracing = {
|
|
|
83
83
|
end(span: SpanLike | undefined, error?: unknown): void
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
export function createTracing(
|
|
86
|
+
export function createTracing(
|
|
87
|
+
otel: OtelApiLike | undefined,
|
|
88
|
+
/**
|
|
89
|
+
* Errors the caller drives (4xx, aborts). Their span keeps an unset status
|
|
90
|
+
* and no exception event, as OpenTelemetry asks of server spans, so the
|
|
91
|
+
* caller's text stays out of the trace; `fn.error_code` still names them.
|
|
92
|
+
*/
|
|
93
|
+
isExpectedError: (error: unknown) => boolean = () => false
|
|
94
|
+
): Tracing {
|
|
87
95
|
let tracer: TracerLike<SpanLike> | undefined
|
|
88
96
|
try {
|
|
89
97
|
tracer = otel?.trace.getTracer('orpc-fn')
|
|
@@ -96,11 +104,21 @@ export function createTracing(otel: OtelApiLike | undefined): Tracing {
|
|
|
96
104
|
if (error.length === 0) {
|
|
97
105
|
span.setStatus({ code: otel.SpanStatusCode.OK })
|
|
98
106
|
} else {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
107
|
+
const code = errorCodeOf(error[0])
|
|
108
|
+
if (code !== undefined) span.setAttribute('fn.error_code', code)
|
|
109
|
+
let expected = false
|
|
110
|
+
try {
|
|
111
|
+
expected = isExpectedError(error[0])
|
|
112
|
+
} catch {
|
|
113
|
+
/* A failing classifier treats the error as unexpected. */
|
|
114
|
+
}
|
|
115
|
+
if (!expected) {
|
|
116
|
+
span.setStatus({
|
|
117
|
+
code: otel.SpanStatusCode.ERROR,
|
|
118
|
+
message: errorMessageOf(error[0])
|
|
119
|
+
})
|
|
120
|
+
if (error[0] instanceof Error) span.recordException(error[0])
|
|
121
|
+
}
|
|
104
122
|
}
|
|
105
123
|
span.end()
|
|
106
124
|
}
|
|
@@ -195,11 +213,25 @@ export function errorMessageOf(error: unknown): string {
|
|
|
195
213
|
return error instanceof Error ? error.message : String(error)
|
|
196
214
|
}
|
|
197
215
|
|
|
198
|
-
/**
|
|
199
|
-
export function
|
|
216
|
+
/** An error's string `code` (`ORPCError`'s `NOT_FOUND`, Node's `ECONNRESET`). */
|
|
217
|
+
export function errorCodeOf(error: unknown): string | undefined {
|
|
218
|
+
const code =
|
|
219
|
+
typeof error === 'object' && error !== null
|
|
220
|
+
? (error as { code?: unknown }).code
|
|
221
|
+
: undefined
|
|
222
|
+
return typeof code === 'string' ? code : undefined
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Error fields for a log line, named like the `fn.completed` ones. Without
|
|
227
|
+
* `message`, the error's text (written for the caller) is left out.
|
|
228
|
+
*/
|
|
229
|
+
export function errorAttributes(error: unknown, message = true) {
|
|
230
|
+
const code = errorCodeOf(error)
|
|
200
231
|
return {
|
|
201
232
|
error_type: error instanceof Error ? error.name : typeof error,
|
|
202
|
-
|
|
233
|
+
...(code === undefined ? {} : { error_code: code }),
|
|
234
|
+
...(message ? { error_message: errorMessageOf(error) } : {})
|
|
203
235
|
}
|
|
204
236
|
}
|
|
205
237
|
|
package/src/react.ts
CHANGED
|
@@ -18,13 +18,17 @@ import {
|
|
|
18
18
|
type SubscriptionRunner,
|
|
19
19
|
seedQueryFn
|
|
20
20
|
} from './realtime.js'
|
|
21
|
+
import { streamCall } from './stream-call.js'
|
|
21
22
|
|
|
22
23
|
export type { InvalidationDomain, LiveConnectionStatus } from './realtime.js'
|
|
23
24
|
|
|
24
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* An oRPC client procedure fits: `client.order.watch`. It gets `streamCall`
|
|
27
|
+
* as its client context, which keeps it out of `createRpcLink`'s batches.
|
|
28
|
+
*/
|
|
25
29
|
export type SubscribeFn<TInput, TEvent> = (
|
|
26
30
|
input: TInput,
|
|
27
|
-
options: { signal: AbortSignal }
|
|
31
|
+
options: { signal: AbortSignal; context: typeof streamCall }
|
|
28
32
|
) => Promise<AsyncIterable<TEvent>>
|
|
29
33
|
|
|
30
34
|
export type UseOrpcSubscriptionOptions<TInput, TEvent> = {
|
|
@@ -78,7 +82,10 @@ export function useOrpcSubscription<TInput, TEvent>(
|
|
|
78
82
|
runnerRef.current = createSubscriptionRunner<TEvent>({
|
|
79
83
|
open: (signal) => {
|
|
80
84
|
openedInput = latest.current.input
|
|
81
|
-
return latest.current.subscribe(openedInput, {
|
|
85
|
+
return latest.current.subscribe(openedInput, {
|
|
86
|
+
signal,
|
|
87
|
+
context: streamCall
|
|
88
|
+
})
|
|
82
89
|
},
|
|
83
90
|
canConnect: () => latest.current.shouldRun,
|
|
84
91
|
shouldReconnect: () => latest.current.shouldRun,
|
package/src/route-options.ts
CHANGED
|
@@ -72,6 +72,14 @@ export function normalizeRoute(
|
|
|
72
72
|
throw new TypeError(
|
|
73
73
|
'orpc-fn: an event iterator schema cannot declare stream: false'
|
|
74
74
|
)
|
|
75
|
+
if (
|
|
76
|
+
stream !== true &&
|
|
77
|
+
!iterator &&
|
|
78
|
+
handler.constructor?.name === 'AsyncGeneratorFunction'
|
|
79
|
+
)
|
|
80
|
+
throw new TypeError(
|
|
81
|
+
`orpc-fn: ${name} has an async generator handler; declare stream: true`
|
|
82
|
+
)
|
|
75
83
|
for (const [guard, resolver] of Object.entries(guardResolvers)) {
|
|
76
84
|
if (!isGuard(guard))
|
|
77
85
|
throw new TypeError(`orpc-fn: unknown guard resolver "${guard}"`)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-context key the `orpc-fn/vue` and `orpc-fn/react` stream helpers set,
|
|
3
|
+
* so `createRpcLink` keeps the call out of a batch without a stream manifest.
|
|
4
|
+
* Shared across package copies.
|
|
5
|
+
*/
|
|
6
|
+
export const STREAM_CALL: unique symbol = Symbol.for('orpc-fn.streamCall')
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Pass as the client context of a stream opened by hand:
|
|
10
|
+
* `client.metrics.run(input, { signal, context: streamCall })`.
|
|
11
|
+
*/
|
|
12
|
+
export const streamCall = { [STREAM_CALL]: true } as const
|
|
13
|
+
|
|
14
|
+
export const isStreamCall = (context: object) =>
|
|
15
|
+
(context as { [STREAM_CALL]?: unknown })[STREAM_CALL] === true
|
package/src/timing.ts
CHANGED
|
@@ -3,3 +3,6 @@ export const REQUEST_TIMING = Symbol('orpc-fn.requestTiming')
|
|
|
3
3
|
export type InvocationTiming = { procedureMs: number; calls: number }
|
|
4
4
|
|
|
5
5
|
export const RAW_INPUT = Symbol('orpc-fn.rawInput')
|
|
6
|
+
|
|
7
|
+
/** Extras a `createTestCall` replaces, carried through nested calls. */
|
|
8
|
+
export const EXTRAS_OVERRIDE = Symbol('orpc-fn.extrasOverride')
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { isExpectedClientError } from './expected-client-error.js'
|
|
2
|
+
import { formatIssue, issuesOf } from './issues.js'
|
|
3
|
+
import { errorCodeOf, errorMessageOf } from './otel.js'
|
|
4
|
+
|
|
5
|
+
/** How a tool adapter turns a failed call into text for the model. */
|
|
6
|
+
export type ToolErrorOptions = {
|
|
7
|
+
/**
|
|
8
|
+
* Errors the model may read. Default: `isExpectedClientError` (4xx
|
|
9
|
+
* `ORPCError`, aborts, and whatever the factory's `isExpectedError` accepted).
|
|
10
|
+
*/
|
|
11
|
+
isExpectedError?: (error: unknown) => boolean
|
|
12
|
+
/** Unexpected errors, whose text the model does not see. Report them here. */
|
|
13
|
+
onError?: (error: unknown, info: { tool: string }) => void
|
|
14
|
+
/** Replace the text the model reads. */
|
|
15
|
+
formatError?: (
|
|
16
|
+
error: unknown,
|
|
17
|
+
info: { tool: string; expected: boolean }
|
|
18
|
+
) => string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* An expected error reads `CODE: message`, then one `path: message` line per
|
|
23
|
+
* validation issue, so the model can fix its arguments. Anything else reads
|
|
24
|
+
* as a fixed sentence: its text may hold internals and goes to `onError`.
|
|
25
|
+
*/
|
|
26
|
+
export function toolErrorText(
|
|
27
|
+
error: unknown,
|
|
28
|
+
tool: string,
|
|
29
|
+
options: ToolErrorOptions
|
|
30
|
+
): string {
|
|
31
|
+
let expected = false
|
|
32
|
+
try {
|
|
33
|
+
expected = (options.isExpectedError ?? isExpectedClientError)(error)
|
|
34
|
+
} catch {
|
|
35
|
+
/* A failing classifier hides the error. */
|
|
36
|
+
}
|
|
37
|
+
if (!expected) {
|
|
38
|
+
try {
|
|
39
|
+
options.onError?.(error, { tool })
|
|
40
|
+
} catch {
|
|
41
|
+
/* Reporting cannot change the tool's answer. */
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (options.formatError) return options.formatError(error, { tool, expected })
|
|
45
|
+
if (!expected) return `${tool} failed unexpectedly. The error was reported.`
|
|
46
|
+
const code = errorCodeOf(error)
|
|
47
|
+
const message = errorMessageOf(error)
|
|
48
|
+
return [
|
|
49
|
+
code ? `${code}: ${message}` : message,
|
|
50
|
+
...issuesOf(error).map(formatIssue)
|
|
51
|
+
].join('\n')
|
|
52
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -122,6 +122,8 @@ export type BuildProcedure = (options: {
|
|
|
122
122
|
procedure: string | undefined
|
|
123
123
|
route: Record<string, unknown>
|
|
124
124
|
meta: Record<string, unknown>
|
|
125
|
+
guards?: Record<string, unknown>
|
|
126
|
+
dynamicGuards?: readonly string[]
|
|
125
127
|
input?: AnySchema | undefined
|
|
126
128
|
output?: AnySchema | undefined
|
|
127
129
|
stream?: boolean
|
package/src/vue.ts
CHANGED
|
@@ -30,12 +30,13 @@ import {
|
|
|
30
30
|
type MaybeRefOrGetter,
|
|
31
31
|
onScopeDispose,
|
|
32
32
|
type Ref,
|
|
33
|
+
reactive,
|
|
33
34
|
ref,
|
|
34
35
|
shallowRef,
|
|
35
36
|
toValue,
|
|
36
37
|
watch
|
|
37
38
|
} from 'vue'
|
|
38
|
-
import { userFacingErrorMessage } from './client.js'
|
|
39
|
+
import { streamCall, userFacingErrorMessage } from './client.js'
|
|
39
40
|
import {
|
|
40
41
|
createInvalidationCoordinator,
|
|
41
42
|
createStreamScope,
|
|
@@ -93,17 +94,23 @@ const missingOrpc = () =>
|
|
|
93
94
|
'orpc-fn/vue: no oRPC client provided. Call provideOrpc(app, { client, utils }) or pass `orpc` to the composable.'
|
|
94
95
|
)
|
|
95
96
|
|
|
96
|
-
|
|
97
|
-
export function useOrpc(): {
|
|
97
|
+
type RegisteredOrpc = {
|
|
98
98
|
client: RegisteredOrpcClient
|
|
99
99
|
utils: RegisteredOrpcUtils
|
|
100
|
-
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The client and utils from {@link provideOrpc}. Throws when none was
|
|
104
|
+
* provided, unless `optional`, which returns null instead.
|
|
105
|
+
*/
|
|
106
|
+
export function useOrpc(options: { optional: true }): RegisteredOrpc | null
|
|
107
|
+
export function useOrpc(options?: { optional?: false }): RegisteredOrpc
|
|
108
|
+
export function useOrpc(options?: {
|
|
109
|
+
optional?: boolean
|
|
110
|
+
}): RegisteredOrpc | null {
|
|
101
111
|
const provided = hasInjectionContext() ? inject(orpcKey, null) : null
|
|
102
|
-
if (!provided) throw missingOrpc()
|
|
103
|
-
return provided as
|
|
104
|
-
client: RegisteredOrpcClient
|
|
105
|
-
utils: RegisteredOrpcUtils
|
|
106
|
-
}
|
|
112
|
+
if (!provided && !options?.optional) throw missingOrpc()
|
|
113
|
+
return provided as RegisteredOrpc | null
|
|
107
114
|
}
|
|
108
115
|
|
|
109
116
|
/** Resolve the provided utils now; fail only when they are needed and missing. */
|
|
@@ -123,7 +130,7 @@ let documentVisible: Ref<boolean> | null = null
|
|
|
123
130
|
* Whether the page is visible, from TanStack's `focusManager` (which follows
|
|
124
131
|
* `visibilitychange` in browsers). Live streams pause while it is false.
|
|
125
132
|
*/
|
|
126
|
-
function useDocumentVisible(): Ref<boolean> {
|
|
133
|
+
export function useDocumentVisible(): Ref<boolean> {
|
|
127
134
|
if (!documentVisible) {
|
|
128
135
|
const visible = ref(focusManager.isFocused())
|
|
129
136
|
focusManager.subscribe((focused) => {
|
|
@@ -142,13 +149,15 @@ function resolveQueryEnabled(enabled: unknown): boolean {
|
|
|
142
149
|
|
|
143
150
|
/**
|
|
144
151
|
* Picks the stream to open from the utils. A procedure's `call` fits as is:
|
|
145
|
-
* `(o) => o.order.watch.call`. The composable passes the input
|
|
152
|
+
* `(o) => o.order.watch.call`. The composable passes the input, an abort
|
|
153
|
+
* signal and `streamCall` as the client context, which keeps the call out of
|
|
154
|
+
* `createRpcLink`'s batches.
|
|
146
155
|
*/
|
|
147
156
|
export type OrpcSubscribeFn<TInput, TEvent, TOrpc = RegisteredOrpcUtils> = (
|
|
148
157
|
orpc: TOrpc
|
|
149
158
|
) => (
|
|
150
159
|
input: TInput,
|
|
151
|
-
options: { signal: AbortSignal }
|
|
160
|
+
options: { signal: AbortSignal; context: typeof streamCall }
|
|
152
161
|
) => Promise<AsyncIterable<TEvent>>
|
|
153
162
|
|
|
154
163
|
export interface UseSubscriptionOptions<TEvent, TOrpc = RegisteredOrpcUtils> {
|
|
@@ -226,7 +235,8 @@ export function useOrpcSubscription<
|
|
|
226
235
|
const error = ref<Error | null>(null)
|
|
227
236
|
|
|
228
237
|
const runner = createSubscriptionRunner<TEvent>({
|
|
229
|
-
open: (signal) =>
|
|
238
|
+
open: (signal) =>
|
|
239
|
+
subscribeFn(getUtils())(toValue(input), { signal, context: streamCall }),
|
|
230
240
|
canConnect: () => isStreamAllowed.value,
|
|
231
241
|
shouldReconnect: () => autoReconnectOnClose && shouldAutoConnect.value,
|
|
232
242
|
onStatus: (next) => {
|
|
@@ -393,7 +403,9 @@ function useBoundApiClient<TOrpc>(orpc: MaybeRefOrGetter<TOrpc> | undefined) {
|
|
|
393
403
|
|
|
394
404
|
/**
|
|
395
405
|
* With `initialInput`, also returns `inputData`, a ref that `mutate()` and
|
|
396
|
-
* `mutateAsync()` use when called without variables (form state).
|
|
406
|
+
* `mutateAsync()` use when called without variables (form state). With
|
|
407
|
+
* `invalidates`, a success invalidates those keys, awaited before the
|
|
408
|
+
* options' own `onSuccess`: `{ invalidates: (o) => [o.order.key()] }`.
|
|
397
409
|
*/
|
|
398
410
|
// The input type comes from the route alone; `initialInput` is only checked
|
|
399
411
|
// against it. `unknown`, not `void`, lets `initialInput` pass the first
|
|
@@ -407,7 +419,10 @@ function useBoundApiClient<TOrpc>(orpc: MaybeRefOrGetter<TOrpc> | undefined) {
|
|
|
407
419
|
optionsFn: (
|
|
408
420
|
orpc: TOrpc
|
|
409
421
|
) => MutationObserverOptions<TData, TError, TVariables, TContext>,
|
|
410
|
-
config: {
|
|
422
|
+
config: {
|
|
423
|
+
initialInput: NoInfer<TVariables>
|
|
424
|
+
invalidates?: (orpc: TOrpc) => readonly QueryKey[]
|
|
425
|
+
}
|
|
411
426
|
): UseMutationReturnType<TData, TError, TVariables, TContext> & {
|
|
412
427
|
inputData: Ref<TVariables>
|
|
413
428
|
mutate: (variables?: TVariables) => void
|
|
@@ -421,17 +436,41 @@ function useBoundApiClient<TOrpc>(orpc: MaybeRefOrGetter<TOrpc> | undefined) {
|
|
|
421
436
|
>(
|
|
422
437
|
optionsFn: (
|
|
423
438
|
orpc: TOrpc
|
|
424
|
-
) => MutationObserverOptions<TData, TError, TVariables, TContext
|
|
439
|
+
) => MutationObserverOptions<TData, TError, TVariables, TContext>,
|
|
440
|
+
config?: { invalidates?: (orpc: TOrpc) => readonly QueryKey[] }
|
|
425
441
|
): UseMutationReturnType<TData, TError, TVariables, TContext>
|
|
426
442
|
function useMutation<TData, TError, TVariables, TContext>(
|
|
427
443
|
optionsFn: (
|
|
428
444
|
orpc: TOrpc
|
|
429
445
|
) => MutationObserverOptions<TData, TError, TVariables, TContext>,
|
|
430
|
-
config?: {
|
|
446
|
+
config?: {
|
|
447
|
+
initialInput?: TVariables
|
|
448
|
+
invalidates?: (orpc: TOrpc) => readonly QueryKey[]
|
|
449
|
+
}
|
|
431
450
|
) {
|
|
432
|
-
const
|
|
451
|
+
const invalidates = config?.invalidates
|
|
452
|
+
const mutation = useVueMutation(
|
|
453
|
+
computed(() => {
|
|
454
|
+
const options = optionsFn(getO())
|
|
455
|
+
if (!invalidates) return options
|
|
456
|
+
return {
|
|
457
|
+
...options,
|
|
458
|
+
onSuccess: async (
|
|
459
|
+
...args: Parameters<NonNullable<typeof options.onSuccess>>
|
|
460
|
+
) => {
|
|
461
|
+
await Promise.all(
|
|
462
|
+
invalidates(getO()).map((queryKey) =>
|
|
463
|
+
queryClient.invalidateQueries({ queryKey })
|
|
464
|
+
)
|
|
465
|
+
)
|
|
466
|
+
return options.onSuccess?.(...args)
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
})
|
|
470
|
+
)
|
|
433
471
|
if (config?.initialInput === undefined) return mutation
|
|
434
|
-
const
|
|
472
|
+
const initialInput = config.initialInput
|
|
473
|
+
const inputData = ref(initialInput) as Ref<TVariables>
|
|
435
474
|
return {
|
|
436
475
|
...mutation,
|
|
437
476
|
inputData,
|
|
@@ -796,3 +835,37 @@ export function useQueryErrorMessage(
|
|
|
796
835
|
return err ? userFacingErrorMessage(err, toValue(fallback)) : null
|
|
797
836
|
})
|
|
798
837
|
}
|
|
838
|
+
|
|
839
|
+
/** The parts of a query `useQueryState` reads. */
|
|
840
|
+
export type QueryStateSource = {
|
|
841
|
+
status: Ref<'pending' | 'error' | 'success'>
|
|
842
|
+
error: Ref<unknown>
|
|
843
|
+
isFetching: Ref<boolean>
|
|
844
|
+
failureCount: Ref<number>
|
|
845
|
+
refetch: () => Promise<unknown>
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* A query as the props a list or loading component takes, in one reactive
|
|
850
|
+
* object: `<ListStates v-bind="useQueryState(query, { fallback })" />`.
|
|
851
|
+
* `errorMessage` follows {@link useQueryErrorMessage}.
|
|
852
|
+
*/
|
|
853
|
+
export function useQueryState(
|
|
854
|
+
query: QueryStateSource,
|
|
855
|
+
options: { fallback: MaybeRefOrGetter<string> }
|
|
856
|
+
) {
|
|
857
|
+
const errorMessage = useQueryErrorMessage(query.error, options.fallback)
|
|
858
|
+
return reactive({
|
|
859
|
+
status: computed(() => query.status.value),
|
|
860
|
+
error: computed(() => query.error.value),
|
|
861
|
+
errorMessage,
|
|
862
|
+
isRetrying: computed(
|
|
863
|
+
() =>
|
|
864
|
+
query.isFetching.value &&
|
|
865
|
+
(query.status.value === 'error' || query.failureCount.value > 0)
|
|
866
|
+
),
|
|
867
|
+
retry: () => {
|
|
868
|
+
void query.refetch()
|
|
869
|
+
}
|
|
870
|
+
})
|
|
871
|
+
}
|