fix: migrate WebSocket to Hono createBunWebSocket (single Bun.serve)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,9 +3,7 @@ import { Hono } from "hono";
|
|||||||
import { cors } from "hono/cors";
|
import { cors } from "hono/cors";
|
||||||
import { logger } from "hono/logger";
|
import { logger } from "hono/logger";
|
||||||
import { registerRoutes } from "./routes";
|
import { registerRoutes } from "./routes";
|
||||||
// import { shoppingWsHandlers } from "./ws/shopping-ws";
|
import { injectWebSocket } from "./ws/shopping-ws";
|
||||||
// import { db, eq } from "@haushaltsApp/db";
|
|
||||||
// import { session as sessionTable } from "@haushaltsApp/db/schema";
|
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
|
|
||||||
@@ -22,45 +20,13 @@ app.use(
|
|||||||
|
|
||||||
registerRoutes(app);
|
registerRoutes(app);
|
||||||
|
|
||||||
// When running under Bun directly (not imported as a module for tests),
|
if (typeof Bun !== "undefined" && !process.env.BUN_TEST) {
|
||||||
// start Bun.serve with WebSocket support.
|
const server = Bun.serve({
|
||||||
// if (typeof Bun !== "undefined" && !process.env.BUN_TEST) {
|
port: Number(process.env.PORT ?? 3000),
|
||||||
// Bun.serve({
|
hostname: "0.0.0.0",
|
||||||
// port: Number(process.env.PORT ?? 3000),
|
fetch: app.fetch,
|
||||||
// hostname: "0.0.0.0",
|
});
|
||||||
// websocket: shoppingWsHandlers,
|
injectWebSocket(server);
|
||||||
// async fetch(req: Request, server) {
|
}
|
||||||
// const url = new URL(req.url);
|
|
||||||
// if (url.pathname === "/api/shopping-lists/ws") {
|
|
||||||
// const token = url.searchParams.get("token") ?? "";
|
|
||||||
// const householdId = url.searchParams.get("householdId") ?? "";
|
|
||||||
|
|
||||||
// if (!householdId) {
|
|
||||||
// return new Response("Missing householdId", { status: 400 });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const rawToken = token.includes(".") ? token.split(".")[0] : token;
|
|
||||||
// if (!rawToken) return new Response("Unauthorized", { status: 401 });
|
|
||||||
|
|
||||||
// const sessionRow = await db.query.session.findFirst({
|
|
||||||
// where: eq(sessionTable.token, rawToken),
|
|
||||||
// with: { user: true },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (!sessionRow?.user || sessionRow.expiresAt < new Date()) {
|
|
||||||
// return new Response("Unauthorized", { status: 401 });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const upgraded = server.upgrade(req, {
|
|
||||||
// data: { householdId, userId: sessionRow.user.id },
|
|
||||||
// });
|
|
||||||
// if (upgraded) return undefined as unknown as Response;
|
|
||||||
// return new Response("WebSocket upgrade failed", { status: 400 });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return app.fetch(req);
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { db, eq, and, isNotNull } from "@haushaltsApp/db";
|
import { db, eq, and, isNotNull } from "@haushaltsApp/db";
|
||||||
import { shoppingItems } from "@haushaltsApp/db/schema";
|
import { shoppingItems, session as sessionTable } from "@haushaltsApp/db/schema";
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import type { ShoppingServerEvent } from "@haushaltsApp/shared/schemas/shopping.schema";
|
import type { ShoppingServerEvent } from "@haushaltsApp/shared/schemas/shopping.schema";
|
||||||
import { addShoppingItemSchema } from "@haushaltsApp/shared/schemas/shopping.schema";
|
import { addShoppingItemSchema } from "@haushaltsApp/shared/schemas/shopping.schema";
|
||||||
import { authMiddleware, requireAuth, type AuthVariables } from "../middleware/auth.middleware";
|
import { authMiddleware, requireAuth, type AuthVariables } from "../middleware/auth.middleware";
|
||||||
import { tenantMiddleware, requireHousehold, type TenantVariables } from "../middleware/tenant.middleware";
|
import { tenantMiddleware, requireHousehold, type TenantVariables } from "../middleware/tenant.middleware";
|
||||||
import { broadcast } from "../ws/shopping-ws";
|
import { broadcast, upgradeWebSocket, createShoppingWsHandler } from "../ws/shopping-ws";
|
||||||
import {
|
import {
|
||||||
getShoppingItems,
|
getShoppingItems,
|
||||||
addShoppingItem,
|
addShoppingItem,
|
||||||
@@ -81,3 +81,28 @@ shoppingRoutes.delete("/:id", async (c) => {
|
|||||||
broadcast(householdId, { type: "item:deleted", itemId: id } satisfies ShoppingServerEvent);
|
broadcast(householdId, { type: "item:deleted", itemId: id } satisfies ShoppingServerEvent);
|
||||||
return c.json({ ok: true });
|
return c.json({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// GET /api/shopping/ws — WebSocket upgrade
|
||||||
|
shoppingRoutes.get(
|
||||||
|
"/ws",
|
||||||
|
upgradeWebSocket(async (c) => {
|
||||||
|
const token = c.req.query("token") ?? "";
|
||||||
|
const householdId = c.req.query("householdId") ?? "";
|
||||||
|
|
||||||
|
if (!householdId || !token) {
|
||||||
|
return { onOpen(_e, ws) { ws.close(4001, "Unauthorized"); } };
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawToken = token.includes(".") ? token.split(".")[0] : token;
|
||||||
|
const sessionRow = await db.query.session.findFirst({
|
||||||
|
where: eq(sessionTable.token, rawToken),
|
||||||
|
with: { user: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!sessionRow?.user || sessionRow.expiresAt < new Date()) {
|
||||||
|
return { onOpen(_e, ws) { ws.close(4001, "Unauthorized"); } };
|
||||||
|
}
|
||||||
|
|
||||||
|
return createShoppingWsHandler(householdId, sessionRow.user.id);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { db, eq, and, isNotNull } from "@haushaltsApp/db";
|
import { db, eq, and, isNotNull } from "@haushaltsApp/db";
|
||||||
import { shoppingItems } from "@haushaltsApp/db/schema";
|
import { shoppingItems } from "@haushaltsApp/db/schema";
|
||||||
import type { ServerWebSocket } from "bun";
|
import { createBunWebSocket } from "hono/bun";
|
||||||
|
import type { WSContext } from "hono/ws";
|
||||||
import type { ShoppingServerEvent, ShoppingClientCommand } from "@haushaltsApp/shared/schemas/shopping.schema";
|
import type { ShoppingServerEvent, ShoppingClientCommand } from "@haushaltsApp/shared/schemas/shopping.schema";
|
||||||
import {
|
import {
|
||||||
getShoppingItems,
|
getShoppingItems,
|
||||||
@@ -12,16 +13,18 @@ import {
|
|||||||
|
|
||||||
type WsData = { householdId: string; userId: string };
|
type WsData = { householdId: string; userId: string };
|
||||||
|
|
||||||
|
export const { upgradeWebSocket, injectWebSocket } = createBunWebSocket<WsData>();
|
||||||
|
|
||||||
// ── Room Management ────────────────────────────────────────────────────────────
|
// ── Room Management ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const rooms = new Map<string, Set<ServerWebSocket<WsData>>>();
|
const rooms = new Map<string, Set<WSContext<WsData>>>();
|
||||||
|
|
||||||
function joinRoom(householdId: string, ws: ServerWebSocket<WsData>): void {
|
function joinRoom(householdId: string, ws: WSContext<WsData>): void {
|
||||||
if (!rooms.has(householdId)) rooms.set(householdId, new Set());
|
if (!rooms.has(householdId)) rooms.set(householdId, new Set());
|
||||||
rooms.get(householdId)!.add(ws);
|
rooms.get(householdId)!.add(ws);
|
||||||
}
|
}
|
||||||
|
|
||||||
function leaveRoom(householdId: string, ws: ServerWebSocket<WsData>): void {
|
function leaveRoom(householdId: string, ws: WSContext<WsData>): void {
|
||||||
const room = rooms.get(householdId);
|
const room = rooms.get(householdId);
|
||||||
if (!room) return;
|
if (!room) return;
|
||||||
room.delete(ws);
|
room.delete(ws);
|
||||||
@@ -31,7 +34,7 @@ function leaveRoom(householdId: string, ws: ServerWebSocket<WsData>): void {
|
|||||||
export function broadcast(
|
export function broadcast(
|
||||||
householdId: string,
|
householdId: string,
|
||||||
event: ShoppingServerEvent,
|
event: ShoppingServerEvent,
|
||||||
exclude?: ServerWebSocket<WsData>,
|
exclude?: WSContext<WsData>,
|
||||||
): void {
|
): void {
|
||||||
const sockets = rooms.get(householdId);
|
const sockets = rooms.get(householdId);
|
||||||
if (!sockets) return;
|
if (!sockets) return;
|
||||||
@@ -43,22 +46,21 @@ export function broadcast(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── WebSocket Handlers ─────────────────────────────────────────────────────────
|
// ── WebSocket Handler Factory ──────────────────────────────────────────────────
|
||||||
|
|
||||||
export const shoppingWsHandlers = {
|
export function createShoppingWsHandler(householdId: string, userId: string) {
|
||||||
async open(ws: ServerWebSocket<WsData>) {
|
return {
|
||||||
const { householdId } = ws.data;
|
async onOpen(_event: Event, ws: WSContext<WsData>) {
|
||||||
joinRoom(householdId, ws);
|
joinRoom(householdId, ws);
|
||||||
const items = await getShoppingItems(householdId);
|
const items = await getShoppingItems(householdId);
|
||||||
ws.send(JSON.stringify({ type: "sync", items } satisfies ShoppingServerEvent));
|
ws.send(JSON.stringify({ type: "sync", items } satisfies ShoppingServerEvent));
|
||||||
},
|
},
|
||||||
|
|
||||||
async message(ws: ServerWebSocket<WsData>, rawMessage: string | Buffer) {
|
async onMessage(event: MessageEvent, ws: WSContext<WsData>) {
|
||||||
const { householdId, userId } = ws.data;
|
|
||||||
let cmd: ShoppingClientCommand;
|
let cmd: ShoppingClientCommand;
|
||||||
try {
|
try {
|
||||||
cmd = JSON.parse(
|
cmd = JSON.parse(
|
||||||
typeof rawMessage === "string" ? rawMessage : rawMessage.toString(),
|
typeof event.data === "string" ? event.data : event.data.toString(),
|
||||||
) as ShoppingClientCommand;
|
) as ShoppingClientCommand;
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return;
|
||||||
@@ -93,7 +95,8 @@ export const shoppingWsHandlers = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
close(ws: ServerWebSocket<WsData>) {
|
onClose(_event: Event, ws: WSContext<WsData>) {
|
||||||
leaveRoom(ws.data.householdId, ws);
|
leaveRoom(householdId, ws);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user