Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
7 changes: 6 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,12 @@ export {

// Route

export { type RouteDefinition, defineRoute } from "./utils/route.ts";
export {
type RouteDefinition,
type WebSocketRouteDefinition,
defineRoute,
defineWebSocketRoute,
} from "./utils/route.ts";

// Request

Expand Down
55 changes: 55 additions & 0 deletions src/utils/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import type { H3RouteMeta, HTTPMethod } from "../types/h3.ts";
import type { EventHandler, Middleware } from "../types/handler.ts";
import type { H3Plugin, H3 } from "../types/h3.ts";
import type { StandardSchemaV1 } from "./internal/standard-schema.ts";
import type { Hooks as WSHooks } from "crossws";
import { defineValidatedHandler } from "../handler.ts";
import { defineWebSocketHandler } from "./ws.ts";

/**
* Route definition options
Expand Down Expand Up @@ -69,3 +71,56 @@ export function defineRoute(def: RouteDefinition): H3Plugin {
h3.on(def.method, def.route, handler);
};
}

/**
* WebSocket route definition options
*/
export interface WebSocketRouteDefinition {
/**
* Route pattern, e.g. '/api/ws'
*/
route: string;

/**
* WebSocket hooks
*/
websocket: Partial<WSHooks>;

// TODO: Support middleware when implemented in defineWebSocketHandler
// middleware?: Middleware[];

// TODO: Support metadata when implemented in WebSocket routing
// meta?: Record<string, unknown>;
}

/**
* Define a WebSocket route as a plugin that can be registered with app.register()
*
* @example
* ```js
* const wsRoute = defineWebSocketRoute({
* route: '/api/ws',
* websocket: {
* open: (peer) => {
* console.log('WebSocket connected:', peer.id);
* peer.send('Welcome!');
* },
* message: (peer, message) => {
* console.log('Received:', message);
* peer.send(`Echo: ${message}`);
* },
* close: (peer) => {
* console.log('WebSocket closed:', peer.id);
* }
* }
* });
*
* app.register(wsRoute);
* ```
*/
export function defineWebSocketRoute(def: WebSocketRouteDefinition): H3Plugin {
const handler = defineWebSocketHandler(def.websocket);
return (h3: H3) => {
h3.on("GET", def.route, handler);
};
}
102 changes: 101 additions & 1 deletion test/route.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from "vitest";
import { defineRoute } from "../src/utils/route.ts";
import { defineRoute, defineWebSocketRoute } from "../src/utils/route.ts";
import { H3 } from "../src/h3.ts";
import { z } from "zod";

Expand Down Expand Up @@ -76,3 +76,103 @@ describe("defineRoute", () => {
});
});
});

describe("defineWebSocketRoute", () => {
it("should create a plugin that registers WebSocket route", async () => {
const app = new H3();
const wsRoute = defineWebSocketRoute({
route: "/ws",
websocket: {
open: () => {},
message: () => {},
},
});
app.register(wsRoute);

const route = app["~routes"].find((r) => r.route === "/ws");
expect(route).toBeDefined();
expect(route?.method).toBe("GET");
});

it("should use GET method for WebSocket routes", async () => {
const app = new H3();
const wsRoute = defineWebSocketRoute({
route: "/ws",
websocket: {},
});
app.register(wsRoute);

const route = app["~routes"].find((r) => r.route === "/ws");
expect(route?.method).toBe("GET");
});

it("should test WebSocket upgrade response", async () => {
const app = new H3();
const wsRoute = defineWebSocketRoute({
route: "/ws",
websocket: {
open: (peer) => {
peer.send("Welcome!");
},
},
});
app.register(wsRoute);

const res = await app.request("/ws");
expect(res.status).toBe(426);
expect(await res.text()).toContain("WebSocket upgrade is required");
expect((res as any).crossws).toBeDefined();
});

it("should work with different route patterns", async () => {
const app = new H3();
const patterns = ["/api/ws", "/ws/:id", "/chat/:room/:user"];

for (const pattern of patterns) {
const wsRoute = defineWebSocketRoute({
route: pattern,
websocket: {},
});
app.register(wsRoute);

const route = app["~routes"].find((r) => r.route === pattern);
expect(route).toBeDefined();
expect(route?.route).toBe(pattern);
}
});

it("should be compatible with existing WebSocket handler methods", async () => {
const app = new H3();
const hooks = {
open: () => {},
message: () => {},
close: () => {},
};

// Test compatibility with defineWebSocketRoute
const wsRoute = defineWebSocketRoute({
route: "/new-ws",
websocket: hooks,
});
app.register(wsRoute);

// Test compatibility with traditional approach
const { defineWebSocketHandler } = await import("../src/index.ts");
app.on("GET", "/old-ws", defineWebSocketHandler(hooks));

// Both routes should be registered
const newRoute = app["~routes"].find((r) => r.route === "/new-ws");
const oldRoute = app["~routes"].find((r) => r.route === "/old-ws");

expect(newRoute).toBeDefined();
expect(oldRoute).toBeDefined();

// Both should return similar responses
const newRes = await app.request("/new-ws");
const oldRes = await app.request("/old-ws");

expect(newRes.status).toBe(oldRes.status);
expect((newRes as any).crossws).toBeDefined();
expect((oldRes as any).crossws).toBeDefined();
});
});
1 change: 1 addition & 0 deletions test/unit/package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ describe("h3 package", () => {
"defineValidatedHandler",
"defineWebSocket",
"defineWebSocketHandler",
"defineWebSocketRoute",
"deleteChunkedCookie",
"deleteCookie",
"dynamicEventHandler",
Expand Down