Skip to content

Commit e52f5ba

Browse files
committed
refactor: deduplicate RPC function definition types
1 parent a75a4c8 commit e52f5ba

2 files changed

Lines changed: 92 additions & 113 deletions

File tree

packages/devframe/src/rpc/types.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,20 @@ function schema<Input, Output = Input>(): StandardSchemaV1<Input, Output> {
2525
}
2626

2727
describe('rpcFunctionDefinitionToFunction', () => {
28+
it('requires args and returns schemas together', () => {
29+
// @ts-expect-error args and returns schemas must be provided together
30+
defineRpcFunction({
31+
name: 'missingReturns',
32+
args: [v.string()],
33+
})
34+
35+
// @ts-expect-error args and returns schemas must be provided together
36+
defineRpcFunction({
37+
name: 'missingArgs',
38+
returns: v.string(),
39+
})
40+
})
41+
2842
it('should infer types from generic parameters when no schemas', () => {
2943
const fn = defineRpcFunction({
3044
name: 'noSchema',

packages/devframe/src/rpc/types.ts

Lines changed: 78 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -162,28 +162,53 @@ export type RpcDump<ARGS extends any[] = any[], RETURN = any, CONTEXT = any>
162162
= | RpcDumpDefinition<ARGS, RETURN>
163163
| RpcDumpGetter<ARGS, RETURN, CONTEXT>
164164

165-
/**
166-
* Base function definition metadata.
167-
*/
168-
export interface RpcFunctionDefinitionBase {
165+
/** Shared fields for an RPC function definition. */
166+
export interface RpcFunctionDefinitionBase<
167+
NAME extends string = string,
168+
TYPE extends RpcFunctionType = RpcFunctionType,
169+
ARGS extends any[] = any[],
170+
RETURN = any,
171+
CONTEXT = any,
172+
> {
169173
/** Function name (unique identifier) */
170-
name: string
174+
name: NAME
171175
/** Function type (static, action, event, or query) */
172-
type?: RpcFunctionType
176+
type?: TYPE
177+
/** Whether the function results should be cached */
178+
cacheable?: boolean
173179
/**
174-
* Declares whether this function's args/return are JSON-serializable,
175-
* i.e. no `Map`, `Set`, `Date`, `BigInt`, class instances, circular
176-
* references, `undefined` leaves, `Symbol`, or `Function` values.
180+
* Selects the serialization format for arguments and return values.
177181
*
178-
* - `true`: args and return are encoded with strict `JSON.stringify`
179-
* on the wire and on disk. Misshapen values throw `DF0019` at the
180-
* sender, surfacing the bug *during the offending call* rather than
181-
* silently coercing to `{}` later. Inferred for `agent` exposure.
182-
* - `false` (default): payloads use `structured-clone-es`, which
183-
* round-trips Maps/Sets/cycles. Functions in this mode cannot be
184-
* exposed via the `agent` field; registration throws `DF0018`.
182+
* - `true`: uses strict JSON encoding. Inferred for agent-exposed functions.
183+
* - `false` (default): uses structured-clone encoding and supports values
184+
* such as `Map`, `Set`, `Date`, and cycles. Functions using this mode
185+
* cannot be agent-exposed.
185186
*/
186187
jsonSerializable?: boolean
188+
/**
189+
* Expose this function to agents (e.g. via the MCP adapter).
190+
* When omitted, the function is not agent-exposed (default-deny).
191+
*/
192+
agent?: RpcFunctionAgentOptions
193+
/** Setup function called with context to initialize handler and dump */
194+
setup?: (context: CONTEXT) => Thenable<RpcFunctionSetupResult<ARGS, RETURN>>
195+
/** Function implementation (required if setup doesn't provide one) */
196+
handler?: (...args: ARGS) => RETURN
197+
/** Dump definition (setup dump takes priority) */
198+
dump?: RpcDump<ARGS, RETURN, CONTEXT>
199+
/**
200+
* Sugar for "query in dev, single baked snapshot in build": when
201+
* `true` and no `dump` is provided, the build adapter runs the
202+
* handler once with no arguments and stores the result as both a
203+
* no-args record and the fallback so any call variant resolves
204+
* to the same snapshot. Only valid on `query` (or untyped)
205+
* functions; `static` already has equivalent default behavior.
206+
*/
207+
snapshot?: boolean
208+
/** Per-context setup-result cache, populated by `getRpcResolvedSetupResult`. @internal */
209+
__cache?: WeakMap<object, Thenable<RpcFunctionSetupResult<ARGS, RETURN>>>
210+
/** Single-slot fallback for primitive contexts. @internal */
211+
__promise?: Thenable<RpcFunctionSetupResult<ARGS, RETURN>>
187212
}
188213

189214
/**
@@ -192,13 +217,47 @@ export interface RpcFunctionDefinitionBase {
192217
*/
193218
export interface RpcDumpStore<T = any> {
194219
/** Function definitions keyed by name */
195-
definitions: Record<string, RpcFunctionDefinitionBase>
220+
definitions: Record<string, Pick<RpcFunctionDefinitionBase, 'name' | 'type'>>
196221
/** Records keyed by '<function-name>---<hash>' or '<function-name>---fallback' */
197222
records: Record<string, RpcDumpRecord | (() => Promise<RpcDumpRecord>)>
198223
/** @internal */
199224
_functions?: T
200225
}
201226

227+
interface RpcFunctionDefinitionWithoutSchemas<
228+
NAME extends string,
229+
TYPE extends RpcFunctionType,
230+
ARGS extends any[],
231+
RETURN,
232+
AS extends RpcArgsSchema | undefined,
233+
RS extends RpcReturnSchema | undefined,
234+
CONTEXT,
235+
> extends RpcFunctionDefinitionBase<NAME, TYPE, ARGS, RETURN, CONTEXT> {
236+
/** Standard Schema array validating (and typing) the arguments */
237+
args?: AS
238+
/** Standard Schema validating (and typing) the return value */
239+
returns?: RS
240+
}
241+
242+
interface RpcFunctionDefinitionWithSchemas<
243+
NAME extends string,
244+
TYPE extends RpcFunctionType,
245+
AS extends RpcArgsSchema | undefined,
246+
RS extends RpcReturnSchema | undefined,
247+
CONTEXT,
248+
> extends RpcFunctionDefinitionBase<
249+
NAME,
250+
TYPE,
251+
InferArgsType<AS>,
252+
Thenable<InferReturnType<RS>>,
253+
CONTEXT
254+
> {
255+
/** Standard Schema array validating (and typing) the arguments */
256+
args: AS
257+
/** Standard Schema validating (and typing) the resolved return value */
258+
returns: RS
259+
}
260+
202261
/**
203262
* Dump client options.
204263
*/
@@ -233,102 +292,8 @@ export type RpcFunctionDefinition<
233292
CONTEXT = undefined,
234293
>
235294
= [AS, RS] extends [undefined, undefined]
236-
? {
237-
/** Function name (unique identifier) */
238-
name: NAME
239-
/** Function type (static, action, event, or query) */
240-
type?: TYPE
241-
/** Whether the function results should be cached */
242-
cacheable?: boolean
243-
/** Standard Schema array validating (and typing) the arguments */
244-
args?: AS
245-
/** Standard Schema validating (and typing) the return value */
246-
returns?: RS
247-
/**
248-
* Declares whether this function's args/return are JSON-serializable
249-
* (no Map/Set/Date/BigInt/cycles/class instances/undefined/Symbol/Function).
250-
*
251-
* - `true`: wire and dump use strict `JSON.stringify`; misshapen
252-
* values throw `DF0019` at the call site. Inferred for `agent`.
253-
* - `false` (default): `structured-clone-es` round-trips fancy
254-
* types. Cannot be `agent`-exposed (registration throws `DF0018`).
255-
*/
256-
jsonSerializable?: boolean
257-
/**
258-
* Expose this function to agents (e.g. via the MCP adapter).
259-
* When omitted, the function is not agent-exposed (default-deny).
260-
*/
261-
agent?: RpcFunctionAgentOptions
262-
/** Setup function called with context to initialize handler and dump */
263-
setup?: (context: CONTEXT) => Thenable<RpcFunctionSetupResult<ARGS, RETURN>>
264-
/** Function implementation (required if setup doesn't provide one) */
265-
handler?: (...args: ARGS) => RETURN
266-
/** Dump definition (setup dump takes priority) */
267-
dump?: RpcDump<ARGS, RETURN, CONTEXT>
268-
/**
269-
* Sugar for "query in dev, single baked snapshot in build": when
270-
* `true` and no `dump` is provided, the build adapter runs the
271-
* handler once with no arguments and stores the result as both a
272-
* no-args record and the fallback so any call variant resolves
273-
* to the same snapshot. Only valid on `query` (or untyped)
274-
* functions; `static` already has equivalent default behavior.
275-
*/
276-
snapshot?: boolean
277-
/** Per-context setup-result cache, populated by `getRpcResolvedSetupResult`. @internal */
278-
__cache?: WeakMap<object, Thenable<RpcFunctionSetupResult<ARGS, RETURN>>>
279-
/** Single-slot fallback for primitive contexts. @internal */
280-
__promise?: Thenable<RpcFunctionSetupResult<ARGS, RETURN>>
281-
}
282-
: {
283-
/** Function name (unique identifier) */
284-
name: NAME
285-
/** Function type (static, action, event, or query) */
286-
type?: TYPE
287-
/** Whether the function results should be cached */
288-
cacheable?: boolean
289-
/** Standard Schema array validating (and typing) the arguments */
290-
args: AS
291-
/** Standard Schema validating (and typing) the return value */
292-
returns: RS
293-
/**
294-
* Declares whether this function's args/return are JSON-serializable
295-
* (no Map/Set/Date/BigInt/cycles/class instances/undefined/Symbol/Function).
296-
*
297-
* - `true`: wire and dump use strict `JSON.stringify`; misshapen
298-
* values throw `DF0019` at the call site. Inferred for `agent`.
299-
* - `false` (default): `structured-clone-es` round-trips fancy
300-
* types. Cannot be `agent`-exposed (registration throws `DF0018`).
301-
*/
302-
jsonSerializable?: boolean
303-
/**
304-
* Expose this function to agents (e.g. via the MCP adapter).
305-
* When omitted, the function is not agent-exposed (default-deny).
306-
*/
307-
agent?: RpcFunctionAgentOptions
308-
/** Setup function called with context to initialize handler and dump */
309-
setup?: (context: CONTEXT) => Thenable<RpcFunctionSetupResult<InferArgsType<AS>, Thenable<InferReturnType<RS>>>>
310-
/**
311-
* Function implementation (required if setup doesn't provide one).
312-
* The declared `returns` schema describes the *resolved* value:
313-
* async handlers return a promise of it (the runtime always awaits).
314-
*/
315-
handler?: (...args: InferArgsType<AS>) => Thenable<InferReturnType<RS>>
316-
/** Dump definition (setup dump takes priority) */
317-
dump?: RpcDump<InferArgsType<AS>, Thenable<InferReturnType<RS>>, CONTEXT>
318-
/**
319-
* Sugar for "query in dev, single baked snapshot in build": when
320-
* `true` and no `dump` is provided, the build adapter runs the
321-
* handler once with no arguments and stores the result as both a
322-
* no-args record and the fallback so any call variant resolves
323-
* to the same snapshot. Only valid on `query` (or untyped)
324-
* functions; `static` already has equivalent default behavior.
325-
*/
326-
snapshot?: boolean
327-
/** Per-context setup-result cache, populated by `getRpcResolvedSetupResult`. @internal */
328-
__cache?: WeakMap<object, Thenable<RpcFunctionSetupResult<InferArgsType<AS>, Thenable<InferReturnType<RS>>>>>
329-
/** Single-slot fallback for primitive contexts. @internal */
330-
__promise?: Thenable<RpcFunctionSetupResult<InferArgsType<AS>, Thenable<InferReturnType<RS>>>>
331-
}
295+
? RpcFunctionDefinitionWithoutSchemas<NAME, TYPE, ARGS, RETURN, AS, RS, CONTEXT>
296+
: RpcFunctionDefinitionWithSchemas<NAME, TYPE, AS, RS, CONTEXT>
332297

333298
export type RpcFunctionDefinitionToFunction<T extends RpcFunctionDefinitionAny>
334299
= T extends { args: infer AS, returns: infer RS }

0 commit comments

Comments
 (0)