@fishenv/http 0.0.2 → 0.0.3
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/dist/contract/builder.d.ts +9 -4
- package/dist/contract/builder.js +2 -0
- package/dist/contract/index.d.ts +3 -1
- package/dist/contract/index.js +1 -0
- package/dist/contract/types.d.ts +9 -1
- package/dist/contract/ws.d.ts +178 -0
- package/dist/contract/ws.js +164 -0
- package/dist/core/compose.js +5 -0
- package/dist/core/error.d.ts +21 -0
- package/dist/core/error.js +50 -7
- package/dist/core/index.d.ts +7 -3
- package/dist/core/index.js +5 -1
- package/dist/core/matcher.js +2 -1
- package/dist/core/route-builder.d.ts +4 -4
- package/dist/core/router.d.ts +16 -2
- package/dist/core/router.js +45 -6
- package/dist/core/transport.d.ts +39 -0
- package/dist/core/transport.js +68 -0
- package/dist/core/types.d.ts +22 -3
- package/dist/core/upgrade.d.ts +85 -0
- package/dist/core/upgrade.js +110 -0
- package/dist/core/utils/url.d.ts +4 -1
- package/dist/core/utils/url.js +14 -3
- package/dist/sse/hub.js +15 -33
- package/dist/sse/types.d.ts +3 -5
- package/dist/static/index.d.ts +27 -0
- package/dist/static/index.js +25 -0
- package/dist/static/negotiate.d.ts +23 -0
- package/dist/static/negotiate.js +85 -0
- package/dist/static/options.d.ts +44 -0
- package/dist/static/options.js +190 -0
- package/dist/static/resolve.d.ts +56 -0
- package/dist/static/resolve.js +113 -0
- package/dist/static/response.d.ts +53 -0
- package/dist/static/response.js +146 -0
- package/dist/static/router.d.ts +59 -0
- package/dist/static/router.js +217 -0
- package/dist/static/rules.d.ts +21 -0
- package/dist/static/rules.js +49 -0
- package/dist/static/types.d.ts +76 -0
- package/dist/static/types.js +0 -0
- package/dist/ws/channels/channel.d.ts +175 -0
- package/dist/ws/channels/channel.js +125 -0
- package/dist/ws/channels/handler.d.ts +33 -0
- package/dist/ws/channels/handler.js +486 -0
- package/dist/ws/channels/protocol.d.ts +96 -0
- package/dist/ws/channels/protocol.js +94 -0
- package/dist/ws/channels/target.d.ts +36 -0
- package/dist/ws/channels/target.js +56 -0
- package/dist/ws/connection.d.ts +56 -0
- package/dist/ws/connection.js +172 -0
- package/dist/ws/helpers.d.ts +17 -0
- package/dist/ws/helpers.js +48 -0
- package/dist/ws/hub.d.ts +172 -0
- package/dist/ws/hub.js +489 -0
- package/dist/ws/index.d.ts +65 -0
- package/dist/ws/index.js +66 -0
- package/dist/ws/lifecycle.d.ts +21 -0
- package/dist/ws/lifecycle.js +97 -0
- package/dist/ws/limits.d.ts +32 -0
- package/dist/ws/limits.js +67 -0
- package/dist/ws/message.d.ts +19 -0
- package/dist/ws/message.js +58 -0
- package/dist/ws/origin.d.ts +40 -0
- package/dist/ws/origin.js +152 -0
- package/dist/ws/presence.d.ts +96 -0
- package/dist/ws/presence.js +420 -0
- package/dist/ws/route-builder.d.ts +136 -0
- package/dist/ws/route-builder.js +265 -0
- package/dist/ws/router.d.ts +21 -0
- package/dist/ws/router.js +36 -0
- package/dist/ws/tickets.d.ts +82 -0
- package/dist/ws/tickets.js +161 -0
- package/dist/ws/typed.d.ts +54 -0
- package/dist/ws/typed.js +104 -0
- package/dist/ws/types.d.ts +69 -0
- package/dist/ws/types.js +70 -0
- package/package.json +14 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnySchema, BasePathParams, HttpMethod, InferOutput, InputKind, InputOptions, MergeParam, RouteMeta } from "./types.ts";
|
|
1
|
+
import type { AnySchema, BasePathParams, ErrorTypes, HttpMethod, InferOutput, InputKind, InputOptions, MergeParam, RouteMeta } from "./types.ts";
|
|
2
2
|
/**
|
|
3
3
|
* Plain-data snapshot of a contract endpoint.
|
|
4
4
|
*
|
|
@@ -14,7 +14,7 @@ export interface ContractDefinition {
|
|
|
14
14
|
inputKind: InputKind;
|
|
15
15
|
inputOptions: InputOptions<InputKind>;
|
|
16
16
|
outputSchema?: AnySchema;
|
|
17
|
-
errorTypes?:
|
|
17
|
+
errorTypes?: ErrorTypes;
|
|
18
18
|
meta?: RouteMeta;
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
@@ -30,7 +30,7 @@ export declare class ContractBuilder<M extends HttpMethod, P extends string, K e
|
|
|
30
30
|
#private;
|
|
31
31
|
readonly method: M;
|
|
32
32
|
readonly path: P;
|
|
33
|
-
constructor(method: M, path: P, paramSchemas?: Map<string, AnySchema>, inputKind?: InputKind, inputOptions?: InputOptions<InputKind>, outputSchema?: AnySchema, errorTypes?:
|
|
33
|
+
constructor(method: M, path: P, paramSchemas?: Map<string, AnySchema>, inputKind?: InputKind, inputOptions?: InputOptions<InputKind>, outputSchema?: AnySchema, errorTypes?: ErrorTypes, meta?: RouteMeta);
|
|
34
34
|
/** Set contract metadata (title, tags, etc.) — returns new builder */
|
|
35
35
|
meta(data: RouteMeta): ContractBuilder<M, P, K, O, Params, Output>;
|
|
36
36
|
/** Narrow/coerce a path param with a Valibot schema — returns new builder */
|
|
@@ -38,7 +38,7 @@ export declare class ContractBuilder<M extends HttpMethod, P extends string, K e
|
|
|
38
38
|
/** Set input kind and options — returns new builder */
|
|
39
39
|
input<NK extends InputKind, NO extends InputOptions<NK>>(kind: NK, options?: NO): ContractBuilder<M, P, NK, NO extends InputOptions<NK> ? NO : InputOptions<NK>, Params, Output>;
|
|
40
40
|
/** Set output schema + optional error types — returns new builder */
|
|
41
|
-
output<S extends AnySchema>(schema: S, errors?:
|
|
41
|
+
output<S extends AnySchema>(schema: S, errors?: ErrorTypes): ContractBuilder<M, P, K, O, Params, InferOutput<S>>;
|
|
42
42
|
/**
|
|
43
43
|
* Runtime snapshot of this endpoint. Used by consumers such as
|
|
44
44
|
* `router.contract()`, OpenAPI generation, or a typed client — not meant
|
|
@@ -73,6 +73,11 @@ export type AnyContractBuilder = ContractBuilder<any, any, any, any, any, any>;
|
|
|
73
73
|
* ```
|
|
74
74
|
*/
|
|
75
75
|
export declare const c: {
|
|
76
|
+
ws: <P extends string>(path: P) => import("./ws.ts").WsContract<P, BasePathParams<P>, "raw", [], undefined, undefined>;
|
|
77
|
+
channel: <N extends string>(name: N) => import("./ws.ts").ChannelContract<N, BasePathParams<N>, {
|
|
78
|
+
payload: undefined;
|
|
79
|
+
reply: undefined;
|
|
80
|
+
}, Record<never, never>, Record<never, never>, undefined>;
|
|
76
81
|
get: <P extends string>(path: P) => ContractBuilder<"GET", P, "none", InputOptions<"none">, BasePathParams<P>, undefined>;
|
|
77
82
|
post: <P extends string>(path: P) => ContractBuilder<"POST", P, "none", InputOptions<"none">, BasePathParams<P>, undefined>;
|
|
78
83
|
put: <P extends string>(path: P) => ContractBuilder<"PUT", P, "none", InputOptions<"none">, BasePathParams<P>, undefined>;
|
package/dist/contract/builder.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { wsContractFactories } from "./ws.js";
|
|
1
2
|
/**
|
|
2
3
|
* Fluent, immutable contract endpoint builder.
|
|
3
4
|
*
|
|
@@ -78,6 +79,7 @@ function endpoint(method) {
|
|
|
78
79
|
* ```
|
|
79
80
|
*/
|
|
80
81
|
export const c = {
|
|
82
|
+
...wsContractFactories,
|
|
81
83
|
get: endpoint("GET"),
|
|
82
84
|
post: endpoint("POST"),
|
|
83
85
|
put: endpoint("PUT"),
|
package/dist/contract/index.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { type AnyContractBuilder, ContractBuilder, type ContractDefinition, type ContractShape, c, } from "./builder.ts";
|
|
2
|
-
export type { AnySchema, BasePathParams, ExtractParams, HttpMethod, InferBody, InferCookies, InferHeaders, InferOutput, InferQuery, InputKind, InputOptions, MergeParam, RouteMeta, ValibotSchema, } from "./types.ts";
|
|
2
|
+
export type { AnySchema, BasePathParams, ErrorTypes, ExtractParams, HttpMethod, InferBody, InferCookies, InferHeaders, InferOutput, InferQuery, InputKind, InputOptions, MergeParam, RouteMeta, ValibotSchema, } from "./types.ts";
|
|
3
|
+
export type { AnyChannelContract, AnyWsContract, ChannelContractDefinition, ChannelEventDefinition, EventSchemas, InferChannel, InferSocket, JoinSchemas, SchemaInput, SchemaOutput, WsContractDefinition, WsContractKind, } from "./ws.ts";
|
|
4
|
+
export { CHANNELS_PROTOCOL, ChannelContract, WsContract } from "./ws.ts";
|
package/dist/contract/index.js
CHANGED
package/dist/contract/types.d.ts
CHANGED
|
@@ -25,7 +25,8 @@ export type InferBody<K extends InputKind, O extends InputOptions<K>> = K extend
|
|
|
25
25
|
export type InferHeaders<O extends InputOptions<InputKind>> = O["headers"] extends AnySchema ? InferOutput<O["headers"]> : Record<string, string>;
|
|
26
26
|
export type InferQuery<O extends InputOptions<InputKind>> = O["query"] extends AnySchema ? InferOutput<O["query"]> : Record<string, string | string[]>;
|
|
27
27
|
export type InferCookies<O extends InputOptions<InputKind>> = O["cookies"] extends AnySchema ? InferOutput<O["cookies"]> : Record<string, string>;
|
|
28
|
-
|
|
28
|
+
type ParamName<Param extends string> = Param extends `${infer Name}?` ? Name : Param extends `${infer Name}*` ? Name : Param;
|
|
29
|
+
export type ExtractParams<P extends string> = P extends `${string}:${infer Param}/${infer Rest}` ? ParamName<Param> | ExtractParams<`/${Rest}`> : P extends `${string}:${infer Param}` ? ParamName<Param> : never;
|
|
29
30
|
export type BasePathParams<P extends string> = {
|
|
30
31
|
[K in ExtractParams<P>]: string;
|
|
31
32
|
};
|
|
@@ -37,3 +38,10 @@ export interface RouteMeta {
|
|
|
37
38
|
deprecated?: boolean;
|
|
38
39
|
operationId?: string;
|
|
39
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Error classes a handler may throw — documentation and client typing only
|
|
43
|
+
* (`.output(schema, errors)`, WebSocket replies). `any[]` so that classes with
|
|
44
|
+
* typed constructor params (`ForbiddenError(message?: string)`) fit.
|
|
45
|
+
*/
|
|
46
|
+
export type ErrorTypes = (abstract new (...args: any[]) => Error)[];
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket contracts — portable data describing sockets and channels, shared
|
|
3
|
+
* by the server (`@fishenv/http/ws`) and typed clients.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* export const room = c
|
|
7
|
+
* .channel("rooms/:roomId")
|
|
8
|
+
* .param("roomId", v.pipe(v.string(), v.uuid()))
|
|
9
|
+
* .join(JoinPayload, JoinReply, [ForbiddenError])
|
|
10
|
+
* .clientEvent("message", MessageIn, MessageAck)
|
|
11
|
+
* .serverEvent("message", Message)
|
|
12
|
+
* .presence(PresenceMeta, { clientUpdates: true });
|
|
13
|
+
*
|
|
14
|
+
* export const socket = c.ws("/socket").channels(room);
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Like HTTP contracts, builders are immutable and expose a plain
|
|
18
|
+
* `_definition` snapshot; the compile-time shape is recovered with
|
|
19
|
+
* {@link InferChannel} / {@link InferSocket}.
|
|
20
|
+
*/
|
|
21
|
+
import type * as v from "valibot";
|
|
22
|
+
import type { AnySchema, BasePathParams, ErrorTypes, MergeParam, RouteMeta } from "./types.ts";
|
|
23
|
+
/** The channel protocol's subprotocol name. */
|
|
24
|
+
export declare const CHANNELS_PROTOCOL = "fishenv.channels.v1";
|
|
25
|
+
type Maybe<S> = S extends AnySchema ? S : undefined;
|
|
26
|
+
/** What the sender passes in (before schema transforms). */
|
|
27
|
+
export type SchemaInput<S> = S extends AnySchema ? v.InferInput<S> : undefined;
|
|
28
|
+
/** What the receiver gets (after validation and transforms). */
|
|
29
|
+
export type SchemaOutput<S> = S extends AnySchema ? v.InferOutput<S> : undefined;
|
|
30
|
+
export interface EventSchemas {
|
|
31
|
+
payload: AnySchema;
|
|
32
|
+
reply: AnySchema | undefined;
|
|
33
|
+
}
|
|
34
|
+
export interface JoinSchemas {
|
|
35
|
+
payload: AnySchema | undefined;
|
|
36
|
+
reply: AnySchema | undefined;
|
|
37
|
+
}
|
|
38
|
+
export interface ChannelEventDefinition {
|
|
39
|
+
payload: AnySchema;
|
|
40
|
+
reply?: AnySchema;
|
|
41
|
+
errors?: ErrorTypes;
|
|
42
|
+
}
|
|
43
|
+
export interface ChannelContractDefinition {
|
|
44
|
+
/** Pattern using router path syntax, e.g. `rooms/:roomId`. */
|
|
45
|
+
name: string;
|
|
46
|
+
paramSchemas: Map<string, AnySchema>;
|
|
47
|
+
join: {
|
|
48
|
+
payload?: AnySchema;
|
|
49
|
+
reply?: AnySchema;
|
|
50
|
+
errors?: ErrorTypes;
|
|
51
|
+
};
|
|
52
|
+
clientEvents: Map<string, ChannelEventDefinition>;
|
|
53
|
+
serverEvents: Map<string, AnySchema>;
|
|
54
|
+
presence?: {
|
|
55
|
+
meta: AnySchema;
|
|
56
|
+
clientUpdates: boolean;
|
|
57
|
+
};
|
|
58
|
+
meta?: RouteMeta;
|
|
59
|
+
}
|
|
60
|
+
export declare class ChannelContract<N extends string, Params extends Record<string, unknown> = BasePathParams<N>, J extends JoinSchemas = {
|
|
61
|
+
payload: undefined;
|
|
62
|
+
reply: undefined;
|
|
63
|
+
}, CE extends Record<string, EventSchemas> = Record<never, never>, SE extends Record<string, AnySchema> = Record<never, never>, PM extends AnySchema | undefined = undefined> {
|
|
64
|
+
#private;
|
|
65
|
+
readonly name: N;
|
|
66
|
+
constructor(name: N, def?: ChannelContractDefinition);
|
|
67
|
+
meta(data: RouteMeta): ChannelContract<N, Params, J, CE, SE, PM>;
|
|
68
|
+
/** Narrow/coerce a channel-name param. */
|
|
69
|
+
param<Name extends string & keyof Params, S extends AnySchema>(name: Name, schema: S): ChannelContract<N, MergeParam<Params, Name, S>, J, CE, SE, PM>;
|
|
70
|
+
/** Join payload, join reply and documented errors. All optional. */
|
|
71
|
+
join<P extends AnySchema | undefined = undefined, R extends AnySchema | undefined = undefined>(payload?: P, reply?: R, errors?: ErrorTypes): ChannelContract<N, Params, {
|
|
72
|
+
payload: Maybe<P>;
|
|
73
|
+
reply: Maybe<R>;
|
|
74
|
+
}, CE, SE, PM>;
|
|
75
|
+
/** Client → server event. Without `reply` it is fire-and-forget. */
|
|
76
|
+
clientEvent<E extends string, P extends AnySchema, R extends AnySchema | undefined = undefined>(name: E, payload: P, reply?: R, errors?: ErrorTypes): ChannelContract<N, Params, J, CE & Record<E, {
|
|
77
|
+
payload: P;
|
|
78
|
+
reply: Maybe<R>;
|
|
79
|
+
}>, SE, PM>;
|
|
80
|
+
/** Server → client event. */
|
|
81
|
+
serverEvent<E extends string, P extends AnySchema>(name: E, payload: P): ChannelContract<N, Params, J, CE, SE & Record<E, P>, PM>;
|
|
82
|
+
/**
|
|
83
|
+
* Enable presence for this channel. `clientUpdates` lets clients change
|
|
84
|
+
* their own meta (rate-limited server-side).
|
|
85
|
+
*/
|
|
86
|
+
presence<M extends AnySchema>(meta: M, opts?: {
|
|
87
|
+
clientUpdates?: boolean;
|
|
88
|
+
}): ChannelContract<N, Params, J, CE, SE, M>;
|
|
89
|
+
get _definition(): ChannelContractDefinition;
|
|
90
|
+
}
|
|
91
|
+
export type AnyChannelContract = ChannelContract<any, any, any, any, any, any>;
|
|
92
|
+
/** Compile-time shape of a channel contract, from both sides' perspective. */
|
|
93
|
+
export type InferChannel<C> = C extends ChannelContract<infer N, infer Params, infer J, infer CE, infer SE, infer PM> ? {
|
|
94
|
+
name: N;
|
|
95
|
+
params: Params;
|
|
96
|
+
join: {
|
|
97
|
+
/** What the client sends / the server receives. */
|
|
98
|
+
payloadIn: SchemaInput<J["payload"]>;
|
|
99
|
+
payload: SchemaOutput<J["payload"]>;
|
|
100
|
+
/** What the server returns / the client receives. */
|
|
101
|
+
replyIn: SchemaInput<J["reply"]>;
|
|
102
|
+
reply: SchemaOutput<J["reply"]>;
|
|
103
|
+
};
|
|
104
|
+
clientEvents: {
|
|
105
|
+
[K in keyof CE]: {
|
|
106
|
+
payloadIn: SchemaInput<CE[K]["payload"]>;
|
|
107
|
+
payload: SchemaOutput<CE[K]["payload"]>;
|
|
108
|
+
replyIn: SchemaInput<CE[K]["reply"]>;
|
|
109
|
+
reply: SchemaOutput<CE[K]["reply"]>;
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
serverEvents: {
|
|
113
|
+
[K in keyof SE]: {
|
|
114
|
+
payloadIn: SchemaInput<SE[K]>;
|
|
115
|
+
payload: SchemaOutput<SE[K]>;
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
presenceMeta: PM extends AnySchema ? {
|
|
119
|
+
in: SchemaInput<PM>;
|
|
120
|
+
out: SchemaOutput<PM>;
|
|
121
|
+
} : never;
|
|
122
|
+
} : never;
|
|
123
|
+
export type WsContractKind = "raw" | "messages" | "channels";
|
|
124
|
+
export interface WsContractDefinition {
|
|
125
|
+
path: string;
|
|
126
|
+
paramSchemas: Map<string, AnySchema>;
|
|
127
|
+
kind: WsContractKind;
|
|
128
|
+
protocols?: string[];
|
|
129
|
+
channels: AnyChannelContract[];
|
|
130
|
+
messages?: {
|
|
131
|
+
client: AnySchema;
|
|
132
|
+
server: AnySchema;
|
|
133
|
+
};
|
|
134
|
+
meta?: RouteMeta;
|
|
135
|
+
}
|
|
136
|
+
export declare class WsContract<P extends string, Params extends Record<string, unknown> = BasePathParams<P>, K extends WsContractKind = "raw", Channels extends readonly AnyChannelContract[] = [], Client extends AnySchema | undefined = undefined, Server extends AnySchema | undefined = undefined> {
|
|
137
|
+
#private;
|
|
138
|
+
readonly path: P;
|
|
139
|
+
constructor(path: P, def?: WsContractDefinition);
|
|
140
|
+
meta(data: RouteMeta): WsContract<P, Params, K, Channels, Client, Server>;
|
|
141
|
+
param<Name extends string & keyof Params, S extends AnySchema>(name: Name, schema: S): WsContract<P, MergeParam<Params, Name, S>, K, Channels, Client, Server>;
|
|
142
|
+
/** Subprotocols for raw/message sockets (channels use their own). */
|
|
143
|
+
protocols(list: string[]): WsContract<P, Params, K, Channels, Client, Server>;
|
|
144
|
+
/** A typed socket exchanging one JSON message type in each direction. */
|
|
145
|
+
messages<CS extends AnySchema, SS extends AnySchema>(schemas: {
|
|
146
|
+
client: CS;
|
|
147
|
+
server: SS;
|
|
148
|
+
}): WsContract<P, Params, "messages", [], CS, SS>;
|
|
149
|
+
/** A socket speaking the channel protocol with exactly these channels. */
|
|
150
|
+
channels<const Cs extends readonly AnyChannelContract[]>(...channels: Cs): WsContract<P, Params, "channels", Cs, undefined, undefined>;
|
|
151
|
+
get _definition(): WsContractDefinition;
|
|
152
|
+
}
|
|
153
|
+
export type AnyWsContract = WsContract<any, any, any, any, any, any>;
|
|
154
|
+
export type InferSocket<C> = C extends WsContract<infer P, infer Params, infer K, infer Channels, infer Client, infer Server> ? {
|
|
155
|
+
path: P;
|
|
156
|
+
params: Params;
|
|
157
|
+
kind: K;
|
|
158
|
+
channels: {
|
|
159
|
+
[I in keyof Channels]: InferChannel<Channels[I]>;
|
|
160
|
+
};
|
|
161
|
+
channelNames: Channels[number]["name"];
|
|
162
|
+
/** Client → server message (raw `messages()` sockets). */
|
|
163
|
+
clientMessageIn: SchemaInput<Client>;
|
|
164
|
+
clientMessage: SchemaOutput<Client>;
|
|
165
|
+
/** Server → client message. */
|
|
166
|
+
serverMessageIn: SchemaInput<Server>;
|
|
167
|
+
serverMessage: SchemaOutput<Server>;
|
|
168
|
+
} : never;
|
|
169
|
+
export declare const wsContractFactories: {
|
|
170
|
+
/** A WebSocket endpoint. */
|
|
171
|
+
ws: <P extends string>(path: P) => WsContract<P, BasePathParams<P>, "raw", [], undefined, undefined>;
|
|
172
|
+
/** A channel, named with router path syntax (`rooms/:roomId`). */
|
|
173
|
+
channel: <N extends string>(name: N) => ChannelContract<N, BasePathParams<N>, {
|
|
174
|
+
payload: undefined;
|
|
175
|
+
reply: undefined;
|
|
176
|
+
}, Record<never, never>, Record<never, never>, undefined>;
|
|
177
|
+
};
|
|
178
|
+
export {};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket contracts — portable data describing sockets and channels, shared
|
|
3
|
+
* by the server (`@fishenv/http/ws`) and typed clients.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* export const room = c
|
|
7
|
+
* .channel("rooms/:roomId")
|
|
8
|
+
* .param("roomId", v.pipe(v.string(), v.uuid()))
|
|
9
|
+
* .join(JoinPayload, JoinReply, [ForbiddenError])
|
|
10
|
+
* .clientEvent("message", MessageIn, MessageAck)
|
|
11
|
+
* .serverEvent("message", Message)
|
|
12
|
+
* .presence(PresenceMeta, { clientUpdates: true });
|
|
13
|
+
*
|
|
14
|
+
* export const socket = c.ws("/socket").channels(room);
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Like HTTP contracts, builders are immutable and expose a plain
|
|
18
|
+
* `_definition` snapshot; the compile-time shape is recovered with
|
|
19
|
+
* {@link InferChannel} / {@link InferSocket}.
|
|
20
|
+
*/
|
|
21
|
+
/** The channel protocol's subprotocol name. */
|
|
22
|
+
export const CHANNELS_PROTOCOL = "fishenv.channels.v1";
|
|
23
|
+
export class ChannelContract {
|
|
24
|
+
name;
|
|
25
|
+
#def;
|
|
26
|
+
constructor(name, def) {
|
|
27
|
+
if (name.startsWith("/")) {
|
|
28
|
+
throw new Error(`Channel name "${name}" must not start with "/"`);
|
|
29
|
+
}
|
|
30
|
+
this.name = name;
|
|
31
|
+
this.#def = def ?? {
|
|
32
|
+
name,
|
|
33
|
+
paramSchemas: new Map(),
|
|
34
|
+
join: {},
|
|
35
|
+
clientEvents: new Map(),
|
|
36
|
+
serverEvents: new Map(),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
#next(patch) {
|
|
40
|
+
const def = this.#def;
|
|
41
|
+
return new ChannelContract(this.name, {
|
|
42
|
+
...def,
|
|
43
|
+
paramSchemas: new Map(def.paramSchemas),
|
|
44
|
+
join: { ...def.join },
|
|
45
|
+
clientEvents: new Map(def.clientEvents),
|
|
46
|
+
serverEvents: new Map(def.serverEvents),
|
|
47
|
+
presence: def.presence ? { ...def.presence } : undefined,
|
|
48
|
+
meta: def.meta ? { ...def.meta } : undefined,
|
|
49
|
+
...patch,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
meta(data) {
|
|
53
|
+
return this.#next({ meta: { ...this.#def.meta, ...data } });
|
|
54
|
+
}
|
|
55
|
+
/** Narrow/coerce a channel-name param. */
|
|
56
|
+
param(name, schema) {
|
|
57
|
+
const paramSchemas = new Map(this.#def.paramSchemas);
|
|
58
|
+
paramSchemas.set(name, schema);
|
|
59
|
+
return this.#next({ paramSchemas });
|
|
60
|
+
}
|
|
61
|
+
/** Join payload, join reply and documented errors. All optional. */
|
|
62
|
+
join(payload, reply, errors) {
|
|
63
|
+
return this.#next({ join: { payload, reply, errors } });
|
|
64
|
+
}
|
|
65
|
+
/** Client → server event. Without `reply` it is fire-and-forget. */
|
|
66
|
+
clientEvent(name, payload, reply, errors) {
|
|
67
|
+
if (this.#def.clientEvents.has(name)) {
|
|
68
|
+
throw new Error(`clientEvent "${name}" is already defined`);
|
|
69
|
+
}
|
|
70
|
+
const clientEvents = new Map(this.#def.clientEvents);
|
|
71
|
+
clientEvents.set(name, { payload, reply, errors });
|
|
72
|
+
return this.#next({ clientEvents });
|
|
73
|
+
}
|
|
74
|
+
/** Server → client event. */
|
|
75
|
+
serverEvent(name, payload) {
|
|
76
|
+
if (this.#def.serverEvents.has(name)) {
|
|
77
|
+
throw new Error(`serverEvent "${name}" is already defined`);
|
|
78
|
+
}
|
|
79
|
+
const serverEvents = new Map(this.#def.serverEvents);
|
|
80
|
+
serverEvents.set(name, payload);
|
|
81
|
+
return this.#next({ serverEvents });
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Enable presence for this channel. `clientUpdates` lets clients change
|
|
85
|
+
* their own meta (rate-limited server-side).
|
|
86
|
+
*/
|
|
87
|
+
presence(meta, opts = {}) {
|
|
88
|
+
return this.#next({
|
|
89
|
+
presence: { meta, clientUpdates: opts.clientUpdates ?? false },
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
get _definition() {
|
|
93
|
+
return this.#def;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export class WsContract {
|
|
97
|
+
path;
|
|
98
|
+
#def;
|
|
99
|
+
constructor(path, def) {
|
|
100
|
+
this.path = path;
|
|
101
|
+
this.#def = def ?? {
|
|
102
|
+
path,
|
|
103
|
+
paramSchemas: new Map(),
|
|
104
|
+
kind: "raw",
|
|
105
|
+
channels: [],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
#next(patch) {
|
|
109
|
+
const def = this.#def;
|
|
110
|
+
return new WsContract(this.path, {
|
|
111
|
+
...def,
|
|
112
|
+
paramSchemas: new Map(def.paramSchemas),
|
|
113
|
+
channels: [...def.channels],
|
|
114
|
+
protocols: def.protocols ? [...def.protocols] : undefined,
|
|
115
|
+
...patch,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
meta(data) {
|
|
119
|
+
return this.#next({ meta: { ...this.#def.meta, ...data } });
|
|
120
|
+
}
|
|
121
|
+
param(name, schema) {
|
|
122
|
+
const paramSchemas = new Map(this.#def.paramSchemas);
|
|
123
|
+
paramSchemas.set(name, schema);
|
|
124
|
+
return this.#next({ paramSchemas });
|
|
125
|
+
}
|
|
126
|
+
/** Subprotocols for raw/message sockets (channels use their own). */
|
|
127
|
+
protocols(list) {
|
|
128
|
+
return this.#next({ protocols: [...list] });
|
|
129
|
+
}
|
|
130
|
+
/** A typed socket exchanging one JSON message type in each direction. */
|
|
131
|
+
messages(schemas) {
|
|
132
|
+
if (this.#def.kind === "channels") {
|
|
133
|
+
throw new Error("messages() and channels() are mutually exclusive");
|
|
134
|
+
}
|
|
135
|
+
return this.#next({ kind: "messages", messages: { ...schemas } });
|
|
136
|
+
}
|
|
137
|
+
/** A socket speaking the channel protocol with exactly these channels. */
|
|
138
|
+
channels(...channels) {
|
|
139
|
+
if (this.#def.kind === "messages") {
|
|
140
|
+
throw new Error("messages() and channels() are mutually exclusive");
|
|
141
|
+
}
|
|
142
|
+
const names = new Set();
|
|
143
|
+
for (const ch of channels) {
|
|
144
|
+
if (names.has(ch.name)) {
|
|
145
|
+
throw new Error(`Channel "${ch.name}" is listed twice`);
|
|
146
|
+
}
|
|
147
|
+
names.add(ch.name);
|
|
148
|
+
}
|
|
149
|
+
return this.#next({
|
|
150
|
+
kind: "channels",
|
|
151
|
+
channels: [...channels],
|
|
152
|
+
protocols: [CHANNELS_PROTOCOL],
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
get _definition() {
|
|
156
|
+
return this.#def;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
export const wsContractFactories = {
|
|
160
|
+
/** A WebSocket endpoint. */
|
|
161
|
+
ws: (path) => new WsContract(path),
|
|
162
|
+
/** A channel, named with router path syntax (`rooms/:roomId`). */
|
|
163
|
+
channel: (name) => new ChannelContract(name),
|
|
164
|
+
};
|
package/dist/core/compose.js
CHANGED
|
@@ -50,6 +50,11 @@ export function coerceToResponse(raw, def) {
|
|
|
50
50
|
*/
|
|
51
51
|
export async function dispatch(req, definition, params, container, errorHandlers) {
|
|
52
52
|
try {
|
|
53
|
+
// 0. Guards run before any middleware
|
|
54
|
+
if (definition.guards) {
|
|
55
|
+
for (const guard of definition.guards)
|
|
56
|
+
await guard(req);
|
|
57
|
+
}
|
|
53
58
|
// 1. Build context via middleware chain
|
|
54
59
|
const ctx = await runMiddlewarePipeline(req, definition.middlewares, container);
|
|
55
60
|
// 2. Parse & validate input
|
package/dist/core/error.d.ts
CHANGED
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
import type * as v from "valibot";
|
|
2
2
|
import type { HttpMethod } from "./types.ts";
|
|
3
|
+
/** Stable machine-readable code for a status, e.g. `403` → `"forbidden"`. */
|
|
4
|
+
export declare function errorCodeForStatus(status: number): string;
|
|
5
|
+
/**
|
|
6
|
+
* Transport-neutral error payload. HTTP responses and WebSocket replies are
|
|
7
|
+
* both rendered from this, so they share one disclosure policy.
|
|
8
|
+
*/
|
|
9
|
+
export interface HttpErrorJSON {
|
|
10
|
+
status: number;
|
|
11
|
+
code: string;
|
|
12
|
+
message: string;
|
|
13
|
+
details?: Record<string, unknown>;
|
|
14
|
+
stack?: string;
|
|
15
|
+
}
|
|
3
16
|
export declare class HttpError extends Error {
|
|
4
17
|
readonly status: number;
|
|
5
18
|
constructor(status: number, message: string, cause?: unknown);
|
|
19
|
+
/** Machine-readable code. Override in subclasses for custom errors. */
|
|
20
|
+
get code(): string;
|
|
21
|
+
/** Extra public fields (e.g. validation issues). Never include secrets. */
|
|
22
|
+
protected details(): Record<string, unknown> | undefined;
|
|
23
|
+
/** The public payload; the stack is only included in development. */
|
|
24
|
+
toJSON(development?: boolean): HttpErrorJSON;
|
|
6
25
|
toResponse(development?: boolean): Response;
|
|
7
26
|
}
|
|
8
27
|
export declare class BadRequestError extends HttpError {
|
|
@@ -37,6 +56,8 @@ export declare class ValidationError extends HttpError {
|
|
|
37
56
|
readonly field: ValidationField;
|
|
38
57
|
readonly issues: v.BaseIssue<unknown>[];
|
|
39
58
|
constructor(field: ValidationField, issues: v.BaseIssue<unknown>[]);
|
|
59
|
+
get code(): string;
|
|
60
|
+
protected details(): Record<string, unknown>;
|
|
40
61
|
toResponse(): Response;
|
|
41
62
|
}
|
|
42
63
|
export interface ErrorHandlerOpts {
|
package/dist/core/error.js
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
const CODES = {
|
|
2
|
+
400: "bad_request",
|
|
3
|
+
401: "unauthorized",
|
|
4
|
+
403: "forbidden",
|
|
5
|
+
404: "not_found",
|
|
6
|
+
405: "method_not_allowed",
|
|
7
|
+
409: "conflict",
|
|
8
|
+
422: "unprocessable_entity",
|
|
9
|
+
429: "too_many_requests",
|
|
10
|
+
500: "internal",
|
|
11
|
+
};
|
|
12
|
+
/** Stable machine-readable code for a status, e.g. `403` → `"forbidden"`. */
|
|
13
|
+
export function errorCodeForStatus(status) {
|
|
14
|
+
return CODES[status] ?? `http_${status}`;
|
|
15
|
+
}
|
|
1
16
|
export class HttpError extends Error {
|
|
2
17
|
status;
|
|
3
18
|
constructor(status, message, cause) {
|
|
@@ -5,10 +20,33 @@ export class HttpError extends Error {
|
|
|
5
20
|
this.status = status;
|
|
6
21
|
this.name = "HttpError";
|
|
7
22
|
}
|
|
8
|
-
|
|
9
|
-
|
|
23
|
+
/** Machine-readable code. Override in subclasses for custom errors. */
|
|
24
|
+
get code() {
|
|
25
|
+
return errorCodeForStatus(this.status);
|
|
26
|
+
}
|
|
27
|
+
/** Extra public fields (e.g. validation issues). Never include secrets. */
|
|
28
|
+
details() {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
/** The public payload; the stack is only included in development. */
|
|
32
|
+
toJSON(development = false) {
|
|
33
|
+
const json = {
|
|
34
|
+
status: this.status,
|
|
35
|
+
code: this.code,
|
|
36
|
+
message: this.message,
|
|
37
|
+
};
|
|
38
|
+
const details = this.details();
|
|
39
|
+
if (details != null)
|
|
40
|
+
json.details = details;
|
|
10
41
|
if (development && this.stack)
|
|
11
|
-
|
|
42
|
+
json.stack = this.stack;
|
|
43
|
+
return json;
|
|
44
|
+
}
|
|
45
|
+
toResponse(development = false) {
|
|
46
|
+
const json = this.toJSON(development);
|
|
47
|
+
const body = { error: json.message };
|
|
48
|
+
if (json.stack)
|
|
49
|
+
body.stack = json.stack;
|
|
12
50
|
return Response.json(body, { status: this.status });
|
|
13
51
|
}
|
|
14
52
|
}
|
|
@@ -66,15 +104,20 @@ export class ValidationError extends HttpError {
|
|
|
66
104
|
this.field = field;
|
|
67
105
|
this.issues = issues;
|
|
68
106
|
}
|
|
69
|
-
|
|
70
|
-
return
|
|
71
|
-
|
|
107
|
+
get code() {
|
|
108
|
+
return "validation_error";
|
|
109
|
+
}
|
|
110
|
+
details() {
|
|
111
|
+
return {
|
|
72
112
|
field: this.field,
|
|
73
113
|
issues: this.issues.map((i) => ({
|
|
74
114
|
message: i.message,
|
|
75
115
|
path: i.path?.map((p) => p.key) ?? [],
|
|
76
116
|
})),
|
|
77
|
-
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
toResponse() {
|
|
120
|
+
return Response.json({ error: "ValidationError", ...this.details() }, { status: 400 });
|
|
78
121
|
}
|
|
79
122
|
}
|
|
80
123
|
export function defaultErrorHandler(err, _args, opts = {}) {
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { coerceToResponse, dispatch, runInterceptors, runMiddlewarePipeline, } from "./compose.ts";
|
|
2
2
|
export type { CompiledCors, CorsOptions } from "./cors.ts";
|
|
3
3
|
export { buildCors } from "./cors.ts";
|
|
4
|
-
export type { ValidationField } from "./error.ts";
|
|
5
|
-
export { BadRequestError, ConflictError, defaultErrorHandler, defaultMethodNotAllowedHandler, defaultNotFoundHandler, ForbiddenError, HttpError, InternalServerError, MethodNotAllowedError, NotFoundError, TooManyRequestsError, UnauthorizedError, UnprocessableEntityError, ValidationError, } from "./error.ts";
|
|
4
|
+
export type { HttpErrorJSON, ValidationField } from "./error.ts";
|
|
5
|
+
export { BadRequestError, ConflictError, defaultErrorHandler, defaultMethodNotAllowedHandler, defaultNotFoundHandler, errorCodeForStatus, ForbiddenError, HttpError, InternalServerError, MethodNotAllowedError, NotFoundError, TooManyRequestsError, UnauthorizedError, UnprocessableEntityError, ValidationError, } from "./error.ts";
|
|
6
6
|
export type { MatchedRoute, RouterMatcher } from "./matcher.ts";
|
|
7
7
|
export { Matcher } from "./matcher.ts";
|
|
8
8
|
export type { ParsedInput } from "./request.ts";
|
|
@@ -11,6 +11,10 @@ export type { RouterRef } from "./route-builder.ts";
|
|
|
11
11
|
export { createRouteBuilder, createRouteBuilderFromContract, FinishedRoute, RouteBuilder, } from "./route-builder.ts";
|
|
12
12
|
export type { CtxOf, HttpServer, Logger, ServeOptions } from "./router.ts";
|
|
13
13
|
export { Router, r, router, serve } from "./router.ts";
|
|
14
|
-
export type {
|
|
14
|
+
export type { Transport, TransportBridge, TransportEnvelope, WithoutOrigin, } from "./transport.ts";
|
|
15
|
+
export { createMemoryTransport, createTransportBridge } from "./transport.ts";
|
|
16
|
+
export type { AfterInterceptorFn, AnySchema, BasePathParams, BeforeInterceptorFn, BuildEnv, DIContainer, ErrorHandlerFn, ExtractParams, HandlerArgs, HandlerFn, HttpMethod, InferBody, InferCookies, InferHeaders, InferOutput, InferQuery, InputKind, InputOptions, InterceptorPair, InterceptorState, MergeCtx, MergeParam, MiddlewareFn, NotFoundHandler, ResolvedInput, RouteDefinition, RouteGuardFn, RouteMeta, ValibotSchema, } from "./types.ts";
|
|
17
|
+
export type { SocketData, SocketHandlers, UpgradedSocket, UpgradeIntent, WebSocketRuntime, WsServerOptions, } from "./upgrade.ts";
|
|
18
|
+
export { CLOSE_GOING_AWAY, CLOSE_INTERNAL_ERROR, completeUpgrade, createWebSocketRuntime, DEFAULT_BACKPRESSURE_LIMIT, DEFAULT_MAX_PAYLOAD_LENGTH, requestUpgrade, takeUpgrade, } from "./upgrade.ts";
|
|
15
19
|
export { buildRouteRegex, extractParamNames, mergePaths } from "./utils/url.ts";
|
|
16
20
|
export { validateField, validatePathParams } from "./validation.ts";
|
package/dist/core/index.js
CHANGED
|
@@ -4,7 +4,7 @@ export { coerceToResponse, dispatch, runInterceptors, runMiddlewarePipeline, } f
|
|
|
4
4
|
// CORS
|
|
5
5
|
export { buildCors } from "./cors.js";
|
|
6
6
|
// Errors
|
|
7
|
-
export { BadRequestError, ConflictError, defaultErrorHandler, defaultMethodNotAllowedHandler, defaultNotFoundHandler, ForbiddenError, HttpError, InternalServerError, MethodNotAllowedError, NotFoundError, TooManyRequestsError, UnauthorizedError, UnprocessableEntityError, ValidationError, } from "./error.js";
|
|
7
|
+
export { BadRequestError, ConflictError, defaultErrorHandler, defaultMethodNotAllowedHandler, defaultNotFoundHandler, errorCodeForStatus, ForbiddenError, HttpError, InternalServerError, MethodNotAllowedError, NotFoundError, TooManyRequestsError, UnauthorizedError, UnprocessableEntityError, ValidationError, } from "./error.js";
|
|
8
8
|
// Matcher
|
|
9
9
|
export { Matcher } from "./matcher.js";
|
|
10
10
|
// Request parsing
|
|
@@ -13,6 +13,10 @@ export { extractHeaders, extractQuery, parseBody, parseCookies, parseInput, } fr
|
|
|
13
13
|
export { createRouteBuilder, createRouteBuilderFromContract, FinishedRoute, RouteBuilder, } from "./route-builder.js";
|
|
14
14
|
// Router
|
|
15
15
|
export { Router, r, router, serve } from "./router.js";
|
|
16
|
+
// Transport (shared by SSE and WS hubs)
|
|
17
|
+
export { createMemoryTransport, createTransportBridge } from "./transport.js";
|
|
18
|
+
// WebSocket upgrade plumbing
|
|
19
|
+
export { CLOSE_GOING_AWAY, CLOSE_INTERNAL_ERROR, completeUpgrade, createWebSocketRuntime, DEFAULT_BACKPRESSURE_LIMIT, DEFAULT_MAX_PAYLOAD_LENGTH, requestUpgrade, takeUpgrade, } from "./upgrade.js";
|
|
16
20
|
// URL utilities
|
|
17
21
|
export { buildRouteRegex, extractParamNames, mergePaths } from "./utils/url.js";
|
|
18
22
|
// Validation
|
package/dist/core/matcher.js
CHANGED
|
@@ -4,8 +4,9 @@ function specificity(path) {
|
|
|
4
4
|
return path.split("/").reduce((score, seg) => {
|
|
5
5
|
if (!seg)
|
|
6
6
|
return score;
|
|
7
|
-
if (seg === "**")
|
|
7
|
+
if (seg === "**" || (seg.startsWith(":") && seg.endsWith("*"))) {
|
|
8
8
|
return score + 0;
|
|
9
|
+
}
|
|
9
10
|
if (seg.includes("*") || seg.includes("{"))
|
|
10
11
|
return score + 1;
|
|
11
12
|
if (seg.startsWith(":"))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnyContractBuilder, ContractShape } from "../contract/index.ts";
|
|
1
|
+
import type { AnyContractBuilder, ContractShape, ErrorTypes } from "../contract/index.ts";
|
|
2
2
|
import type { AfterInterceptorFn, AnySchema, BasePathParams, BeforeInterceptorFn, ErrorHandlerFn, HandlerArgs, HttpMethod, InferOutput, InputKind, InputOptions, InterceptorPair, MergeParam, MiddlewareFn, RouteDefinition, RouteMeta } from "./types.ts";
|
|
3
3
|
/**
|
|
4
4
|
* Internal interface — the Router class (step 06) will implement this.
|
|
@@ -26,7 +26,7 @@ export declare class FinishedRoute {
|
|
|
26
26
|
*/
|
|
27
27
|
export declare class RouteBuilder<RouterCtx extends Record<string, unknown>, RouteCtx extends Record<string, unknown>, K extends InputKind = "none", O extends InputOptions<K> = InputOptions<K>, Params extends Record<string, unknown> = Record<string, unknown>, Output = undefined> {
|
|
28
28
|
#private;
|
|
29
|
-
constructor(router: RouterRef, method: HttpMethod, path: string, middlewares?: MiddlewareFn<Record<string, unknown>, Record<string, unknown>>[], paramSchemas?: Map<string, AnySchema>, inputKind?: InputKind, inputOptions?: InputOptions<InputKind>, outputSchema?: AnySchema, errorTypes?:
|
|
29
|
+
constructor(router: RouterRef, method: HttpMethod, path: string, middlewares?: MiddlewareFn<Record<string, unknown>, Record<string, unknown>>[], paramSchemas?: Map<string, AnySchema>, inputKind?: InputKind, inputOptions?: InputOptions<InputKind>, outputSchema?: AnySchema, errorTypes?: ErrorTypes, meta?: RouteMeta, interceptors?: InterceptorPair<Record<string, unknown>>[]);
|
|
30
30
|
/** Set route metadata (title, tags, etc.) — mutates, returns this */
|
|
31
31
|
meta(data: RouteMeta): this;
|
|
32
32
|
/** Narrow/coerce a path param with a Valibot schema — returns new builder */
|
|
@@ -36,11 +36,11 @@ export declare class RouteBuilder<RouterCtx extends Record<string, unknown>, Rou
|
|
|
36
36
|
/** Set input kind and options — returns new builder */
|
|
37
37
|
input<NK extends InputKind, NO extends InputOptions<NK>>(kind: NK, options?: NO): RouteBuilder<RouterCtx, RouteCtx, NK, NO extends InputOptions<NK> ? NO : InputOptions<NK>, Params, Output>;
|
|
38
38
|
/** Set output schema + optional error types — returns new builder */
|
|
39
|
-
output<S extends AnySchema>(schema: S, errors?:
|
|
39
|
+
output<S extends AnySchema>(schema: S, errors?: ErrorTypes): RouteBuilder<RouterCtx, RouteCtx, K, O, Params, InferOutput<S>>;
|
|
40
40
|
/** Add a before/after interceptor pair — mutates, returns this */
|
|
41
41
|
intercept(before?: BeforeInterceptorFn<RouterCtx & RouteCtx>, after?: AfterInterceptorFn<RouterCtx & RouteCtx>): this;
|
|
42
42
|
/** Terminal — registers the route on the router and returns a FinishedRoute */
|
|
43
|
-
handle(fn: (args: HandlerArgs<RouterCtx & RouteCtx, K, O, Params>) => Output extends undefined ? Response | Promise<Response> : Response | Output | Promise<Response | Output>): FinishedRoute;
|
|
43
|
+
handle(fn: (args: HandlerArgs<RouterCtx & RouteCtx, K, O, Params>) => [Output] extends [undefined] ? Response | Promise<Response> : Response | Output | Promise<Response | Output>): FinishedRoute;
|
|
44
44
|
}
|
|
45
45
|
/**
|
|
46
46
|
* Factory — creates a RouteBuilder bound to a router reference.
|