Skip to content

Commit e042fae

Browse files
fix(core): enforce SpecT result type on setRequestHandler (no loose-overload fallthrough)
The SpecT-typed overload's P constraint (StandardSchemaV1<SpecParams>) was too tight to ever match real schemas, so calls always fell through to the loose (method: string, schema, h: => Result) overload, silently accepting any return type. Loosen overload 1's P to bare StandardSchemaV1 (params type still inferred from the schema; only the result type is SpecT-constrained) and never-guard the loose overload's method param against SpecRequests<SpecT> so spec-method calls that don't satisfy the typed overload error instead of falling through. Same guard on setNotificationHandler for symmetry. JSDoc updated; type tests added.
1 parent 2b8e43b commit e042fae

File tree

2 files changed

+22
-11
lines changed

2 files changed

+22
-11
lines changed

packages/core/src/shared/protocol.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1085,11 +1085,11 @@ export abstract class Protocol<ContextT extends BaseContext = BaseContext, SpecT
10851085
* no-params methods use `z.object({})`. `paramsSchema` may be any Standard Schema (Zod,
10861086
* Valibot, ArkType, etc.). The handler's `params` type is inferred from the passed
10871087
* `paramsSchema`; when `method` is listed in this instance's {@linkcode ProtocolSpec},
1088-
* `paramsSchema`'s input and the handler's result type are constrained by `SpecT`.
1088+
* the handler's result type is constrained to `SpecT`'s declared result.
10891089
* - **Zod schema** — `setRequestHandler(RequestZodSchema, (request, ctx) => …)`. The method
10901090
* name is read from the schema's `method` literal; the handler receives the parsed request.
10911091
*/
1092-
setRequestHandler<K extends SpecRequests<SpecT>, P extends StandardSchemaV1<_Requests<SpecT>[K]['params']>>(
1092+
setRequestHandler<K extends SpecRequests<SpecT>, P extends StandardSchemaV1>(
10931093
method: K,
10941094
paramsSchema: P,
10951095
handler: (
@@ -1101,8 +1101,8 @@ export abstract class Protocol<ContextT extends BaseContext = BaseContext, SpecT
11011101
method: M,
11021102
handler: (request: RequestTypeMap[M], ctx: ContextT) => Result | Promise<Result>
11031103
): void;
1104-
setRequestHandler<P extends StandardSchemaV1>(
1105-
method: string,
1104+
setRequestHandler<M extends string, P extends StandardSchemaV1>(
1105+
method: M extends SpecRequests<SpecT> ? never : M,
11061106
paramsSchema: P,
11071107
handler: (params: StandardSchemaV1.InferOutput<P>, ctx: ContextT) => Result | Promise<Result>
11081108
): void;
@@ -1194,12 +1194,10 @@ export abstract class Protocol<ContextT extends BaseContext = BaseContext, SpecT
11941194
*
11951195
* Mirrors {@linkcode setRequestHandler}: a two-arg spec-method form (handler receives the full
11961196
* notification object), a three-arg form with a `paramsSchema` (handler receives validated
1197-
* `params`), and a Zod-schema form (method read from the schema's `method` literal). When the
1198-
* three-arg form's `method` is listed in this instance's {@linkcode ProtocolSpec},
1199-
* `paramsSchema`'s input is constrained by `SpecT`; the handler's `params` type is always
1200-
* inferred from the passed schema.
1197+
* `params`), and a Zod-schema form (method read from the schema's `method` literal). The
1198+
* handler's `params` type is always inferred from the passed schema.
12011199
*/
1202-
setNotificationHandler<K extends SpecNotifications<SpecT>, P extends StandardSchemaV1<_Notifications<SpecT>[K]['params']>>(
1200+
setNotificationHandler<K extends SpecNotifications<SpecT>, P extends StandardSchemaV1>(
12031201
method: K,
12041202
paramsSchema: P,
12051203
handler: (params: StandardSchemaV1.InferOutput<P>) => void | Promise<void>
@@ -1208,8 +1206,8 @@ export abstract class Protocol<ContextT extends BaseContext = BaseContext, SpecT
12081206
method: M,
12091207
handler: (notification: NotificationTypeMap[M]) => void | Promise<void>
12101208
): void;
1211-
setNotificationHandler<P extends StandardSchemaV1>(
1212-
method: string,
1209+
setNotificationHandler<M extends string, P extends StandardSchemaV1>(
1210+
method: M extends SpecNotifications<SpecT> ? never : M,
12131211
paramsSchema: P,
12141212
handler: (params: StandardSchemaV1.InferOutput<P>) => void | Promise<void>
12151213
): void;

packages/core/test/shared/protocolSpec.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,17 @@ describe('ProtocolSpec typing', () => {
6565
return { ok: true };
6666
});
6767
});
68+
69+
it('typed-SpecT setRequestHandler enforces result type (no fallthrough to loose string overload)', () => {
70+
const p = new TestProtocol<AppSpec>();
71+
// @ts-expect-error -- result must be { opened: boolean }; string overload is `never`-guarded for spec methods
72+
p.setRequestHandler('ui/open-link', z.object({ url: z.string() }), () => ({ ok: 'wrong-type' }));
73+
// @ts-expect-error -- empty object doesn't satisfy { opened: boolean }
74+
p.setRequestHandler('ui/open-link', z.object({ url: z.string() }), () => ({}));
75+
// non-spec methods still allow loose Result
76+
p.setRequestHandler('not/in-spec', z.object({}), () => ({ anything: 1 }));
77+
// notifications: spec and non-spec both allow any schema and return void
78+
p.setNotificationHandler('ui/size-changed', z.object({ width: z.number(), height: z.number() }), () => {});
79+
p.setNotificationHandler('not/in-spec', z.object({ x: z.number() }), () => {});
80+
});
6881
});

0 commit comments

Comments
 (0)