Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/5.migration/0.index.md
15 changes: 11 additions & 4 deletions src/h3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ import { toResponse, kNotFound } from "./response.ts";
import { callMiddleware, normalizeMiddleware } from "./middleware.ts";

import type { ServerRequest } from "srvx";
import type { H3Config, H3CoreConfig, H3Plugin, MatchedRoute, RouterContext } from "./types/h3.ts";
import type {
H3Config,
H3CoreConfig,
H3Plugin,
H3Response,
MatchedRoute,
RouterContext,
} from "./types/h3.ts";
import type { H3EventContext } from "./types/context.ts";
import type {
EventHandler,
Expand Down Expand Up @@ -40,7 +47,7 @@ export class H3Core implements H3CoreType {
this.handler = this.handler.bind(this);
}

fetch(request: ServerRequest): Response | Promise<Response> {
fetch(request: ServerRequest): H3Response | Promise<H3Response> {
return this["~request"](request);
}

Expand All @@ -57,7 +64,7 @@ export class H3Core implements H3CoreType {
: routeHandler(event);
}

"~request"(request: ServerRequest, context?: H3EventContext): Response | Promise<Response> {
"~request"(request: ServerRequest, context?: H3EventContext): H3Response | Promise<H3Response> {
// Create a new event instance
const event = new H3Event(request, context, this as unknown as H3Type);

Expand Down Expand Up @@ -114,7 +121,7 @@ export const H3 = /* @__PURE__ */ (() => {
_req: ServerRequest | URL | string,
_init?: RequestInit,
context?: H3EventContext,
): Response | Promise<Response> {
): H3Response | Promise<H3Response> {
return this["~request"](toRequest(_req, _init), context);
}

Expand Down
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export type {
H3Config,
H3CoreConfig,
H3Plugin,
H3Response,
H3Route,
H3RouteMeta,
HTTPMethod,
Expand Down Expand Up @@ -190,10 +191,13 @@ export { type RequestFingerprintOptions, getRequestFingerprint } from "./utils/f

// WebSocket

// WebSocket

export {
type WebSocketHooks,
type WebSocketPeer,
type WebSocketMessage,
type WebSocketResponse,
defineWebSocketHandler,
defineWebSocket,
} from "./utils/ws.ts";
Expand Down
4 changes: 2 additions & 2 deletions src/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { FastResponse } from "srvx";
import { HTTPError } from "./error.ts";
import { isJSONSerializable } from "./utils/internal/object.ts";

import type { H3Config } from "./types/h3.ts";
import type { H3Config, H3Response } from "./types/h3.ts";
import { kEventRes, kEventResHeaders, type H3Event } from "./event.ts";

export const kNotFound: symbol = /* @__PURE__ */ Symbol.for("h3.notFound");
Expand All @@ -12,7 +12,7 @@ export function toResponse(
val: unknown,
event: H3Event,
config: H3Config = {},
): Response | Promise<Response> {
): H3Response | Promise<H3Response> {
if (typeof (val as PromiseLike<unknown>)?.then === "function") {
return ((val as Promise<unknown>).catch?.((error) => error) || Promise.resolve(val)).then(
(resolvedVal) => toResponse(resolvedVal, event, config),
Expand Down
19 changes: 15 additions & 4 deletions src/types/h3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { MaybePromise } from "./_utils.ts";
import type { FetchHandler, ServerRequest } from "srvx";
// import type { MatchedRoute, RouterContext } from "rou3";
import type { H3Event } from "../event.ts";
import type { Hooks as WebSocketHooks } from "crossws";

// Inlined from rou3 for type portability
export interface RouterContext {
Expand All @@ -21,7 +22,7 @@ export type MatchedRoute<T = any> = {

// https://www.rfc-editor.org/rfc/rfc7231#section-4.1
// prettier-ignore
export type HTTPMethod = "GET" | "HEAD" | "PATCH" | "POST" | "PUT" | "DELETE" | "CONNECT" | "OPTIONS" | "TRACE";
export type HTTPMethod = "GET" | "HEAD" | "PATCH" | "POST" | "PUT" | "DELETE" | "CONNECT" | "OPTIONS" | "TRACE";

export interface H3Config {
/**
Expand All @@ -43,6 +44,16 @@ export interface H3Config {

export type H3CoreConfig = Omit<H3Config, "plugins">;

/**
* Response type returned by H3 fetch methods.
*
* Extends standard Response with an optional `.crossws` property that is present
* when the response originates from a WebSocket handler defined via `defineWebSocketHandler`.
*/
export type H3Response = Response & {
crossws?: Partial<WebSocketHooks> | Promise<Partial<WebSocketHooks>>;
};

export type PreparedResponse = ResponseInit & { body?: BodyInit | null };

export interface H3RouteMeta {
Expand Down Expand Up @@ -103,15 +114,15 @@ export declare class H3Core {
*
* Returned value is a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) Promise.
*/
fetch(_request: ServerRequest): Response | Promise<Response>;
fetch(_request: ServerRequest): H3Response | Promise<H3Response>;

/**
* An h3 compatible event handler useful to compose multiple h3 app instances.
*/
handler(event: H3Event): unknown | Promise<unknown>;

/** @internal */
"~request"(request: ServerRequest, context?: H3EventContext): Response | Promise<Response>;
"~request"(request: ServerRequest, context?: H3EventContext): H3Response | Promise<H3Response>;

/** @internal */
"~findRoute"(_event: H3Event): MatchedRoute<H3Route> | void;
Expand All @@ -138,7 +149,7 @@ export declare class H3 extends H3Core {
request: ServerRequest | URL | string,
options?: RequestInit,
context?: H3EventContext,
): Response | Promise<Response>;
): H3Response | Promise<H3Response>;

/**
* Register a global middleware.
Expand Down
20 changes: 16 additions & 4 deletions src/utils/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,26 @@ import { defineHandler } from "../handler.ts";

import type { Hooks as WebSocketHooks } from "crossws";
import type { H3Event } from "../event.ts";
import type { EventHandler } from "../types/handler.ts";
import type { EventHandler, EventHandlerRequest } from "../types/handler.ts";

export type {
Hooks as WebSocketHooks,
Message as WebSocketMessage,
Peer as WebSocketPeer,
} from "crossws";

/**
* WebSocket response type with crossws hooks attached.
*
* This type represents a Response object augmented with WebSocket hooks.
* The `.crossws` property is used by CrossWS server plugins.
*
* @see https://h3.dev/guide/websocket
*/
export type WebSocketResponse = Response & {
crossws: Partial<WebSocketHooks> | Promise<Partial<WebSocketHooks>>;
};

/**
* Define WebSocket hooks.
*
Expand All @@ -28,8 +40,8 @@ export function defineWebSocketHandler(
hooks:
| Partial<WebSocketHooks>
| ((event: H3Event) => Partial<WebSocketHooks> | Promise<Partial<WebSocketHooks>>),
): EventHandler {
return defineHandler(function _webSocketHandler(event) {
): EventHandler<EventHandlerRequest, WebSocketResponse> {
return defineHandler<EventHandlerRequest, WebSocketResponse>(function _webSocketHandler(event) {
const crossws = typeof hooks === "function" ? hooks(event) : hooks;

return Object.assign(
Expand All @@ -39,6 +51,6 @@ export function defineWebSocketHandler(
{
crossws,
},
);
) as WebSocketResponse;
});
}
22 changes: 21 additions & 1 deletion test/unit/types.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { H3Event } from "../../src/index.ts";
import type { H3Event, WebSocketResponse, H3Response } from "../../src/index.ts";
import { describe, it, expectTypeOf } from "vitest";
import {
H3,
defineHandler,
defineWebSocketHandler,
getQuery,
readBody,
readValidatedBody,
Expand Down Expand Up @@ -123,4 +125,22 @@ describe("types", () => {
});
});
});

describe("defineWebSocketHandler", () => {
it("should return a handler with a typed crossws property", () => {
const handler = defineWebSocketHandler({ message: () => {} });
const result = handler({} as H3Event);
expectTypeOf(result).toHaveProperty("crossws");
expectTypeOf(result.crossws).toEqualTypeOf<WebSocketResponse["crossws"]>();
});
});

describe("app.fetch", () => {
it("should return H3Response with optional crossws", async () => {
const app = new H3();
const res = await app.fetch(new Request("http://localhost"));
expectTypeOf(res).toEqualTypeOf<H3Response>();
expectTypeOf(res.crossws).toEqualTypeOf<H3Response["crossws"]>();
});
});
});
4 changes: 2 additions & 2 deletions test/ws.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe("defineWebSocketHandler", () => {
expect(res).toBeInstanceOf(Response);
expect((res as Response).status).toBe(426);
// expect((res as Response).statusText).toBe("Upgrade Required");
expect((res as any).crossws).toEqual(hooks);
expect(res.crossws).toEqual(hooks);
});

it("should attach the provided hooks with function argument", () => {
Expand All @@ -26,6 +26,6 @@ describe("defineWebSocketHandler", () => {
expect(res).toBeInstanceOf(Response);
expect((res as Response).status).toBe(426);
// expect((res as Response).statusText).toBe("Upgrade Required");
expect((res as any).crossws).toEqual(hooks);
expect(res.crossws).toEqual(hooks);
});
});