feat(backend,frontend): migrate data APIs from REST to native tRPC over WebSocket
Replace REST module routers with a single typed tRPC appRouter served over
/trpc (HTTP + WebSocket), and rewire the frontend to call it via
@trpc/client wsLink (browser) and httpLink (RSC data layer). Existing
/api/health + /api/metrics stay as plain Express for infra scraping.
Notable fixes surfaced by the live smoke test:
- Express 5 / path-to-regexp v8 rejects the /trpc/* wildcard route; use a
prefix middleware that computes opts.path from the URL instead.
- nodeHTTPRequestHandler treats opts.path as the literal procedure path, so
it is derived per-request from req.url.
- Two ws servers on one http.Server (the /ws voice socket + /trpc) collided
and returned 400 on upgrade; both now use noServer + a manually routed
server.on('upgrade') keyed by path.
Verified: BE tsc+biome+40 vitest green; FE tsc+biome green; live
HTTP and WebSocket calls returned real prod data.
Co-Authored-By: Claude Opus 4.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
d8552a9fb8
commit
2fa1827f17
@@ -1 +0,0 @@
|
||||
export { createVoiceRouter } from "./voice.routes.js";
|
||||
@@ -1,45 +0,0 @@
|
||||
import type { Request, Response } from "express";
|
||||
import { createChildLogger } from "@/shared/logger/index";
|
||||
import { asyncHandler } from "../../shared/middlewares/index.js";
|
||||
import { publishCommandNoReply } from "../../shared/redis/index.js";
|
||||
import type { ConnectVoiceInput, VoiceCommandInput } from "./voice.schema.js";
|
||||
import {
|
||||
connectVoice,
|
||||
disconnectVoice,
|
||||
getVoiceStatus,
|
||||
} from "./voice.service.js";
|
||||
|
||||
const logger = createChildLogger("voice.controller");
|
||||
|
||||
export const handleGetVoiceStatus = asyncHandler(
|
||||
async (_req: Request, res: Response) => {
|
||||
const status = await getVoiceStatus();
|
||||
res.json(status);
|
||||
},
|
||||
);
|
||||
|
||||
export const handleConnectVoice = asyncHandler(
|
||||
async (req: Request, res: Response) => {
|
||||
const { guildId, channelId } = req.body as ConnectVoiceInput;
|
||||
logger.debug({ guildId, channelId }, "Connecting to voice channel");
|
||||
const status = await connectVoice(guildId, channelId);
|
||||
res.json(status);
|
||||
},
|
||||
);
|
||||
|
||||
export const handleDisconnectVoice = asyncHandler(
|
||||
async (_req: Request, res: Response) => {
|
||||
logger.debug("Disconnecting from voice");
|
||||
const status = await disconnectVoice();
|
||||
res.json(status);
|
||||
},
|
||||
);
|
||||
|
||||
export const handleVoiceCommand = asyncHandler(
|
||||
async (req: Request, res: Response) => {
|
||||
const { command } = req.body as VoiceCommandInput;
|
||||
logger.debug({ command }, "Publishing voice command");
|
||||
await publishCommandNoReply(command);
|
||||
res.json({ success: true, command });
|
||||
},
|
||||
);
|
||||
@@ -1,80 +0,0 @@
|
||||
import type { Request, Response, Router } from "express";
|
||||
import express from "express";
|
||||
import { createChildLogger } from "@/shared/logger/index";
|
||||
import { asyncHandler, validateBody } from "../../shared/middlewares/index.js";
|
||||
import {
|
||||
handleConnectVoice,
|
||||
handleDisconnectVoice,
|
||||
handleGetVoiceStatus,
|
||||
handleVoiceCommand,
|
||||
} from "./voice.controller.js";
|
||||
import { connectVoiceSchema, voiceCommandSchema } from "./voice.schema.js";
|
||||
import {
|
||||
getGuilds,
|
||||
getTextChannels,
|
||||
getVoiceChannels,
|
||||
} from "./voice.service.js";
|
||||
|
||||
const logger = createChildLogger("voice.routes");
|
||||
|
||||
export function createVoiceRouter(): Router {
|
||||
const router = express.Router();
|
||||
|
||||
// ── Guilds ──────────────────────────────────────────────────────────────
|
||||
|
||||
// GET /api/guilds
|
||||
router.get(
|
||||
"/guilds",
|
||||
asyncHandler(async (_req: Request, res: Response) => {
|
||||
logger.debug("Fetching guilds");
|
||||
const guilds = await getGuilds();
|
||||
res.json(guilds);
|
||||
}),
|
||||
);
|
||||
|
||||
// GET /api/guilds/:guildId/channels
|
||||
router.get(
|
||||
"/guilds/:guildId/channels",
|
||||
asyncHandler(async (req: Request, res: Response) => {
|
||||
const guildId = req.params.guildId as string;
|
||||
logger.debug({ guildId }, "Fetching text channels");
|
||||
const channels = await getTextChannels(guildId);
|
||||
res.json(channels);
|
||||
}),
|
||||
);
|
||||
|
||||
// GET /api/guilds/:guildId/voice-channels
|
||||
router.get(
|
||||
"/guilds/:guildId/voice-channels",
|
||||
asyncHandler(async (req: Request, res: Response) => {
|
||||
const guildId = req.params.guildId as string;
|
||||
logger.debug({ guildId }, "Fetching voice channels");
|
||||
const channels = await getVoiceChannels(guildId);
|
||||
res.json(channels);
|
||||
}),
|
||||
);
|
||||
|
||||
// ── Voice connection ────────────────────────────────────────────────────
|
||||
|
||||
// GET /api/voice/status
|
||||
router.get("/voice/status", handleGetVoiceStatus);
|
||||
|
||||
// POST /api/voice/connect
|
||||
router.post(
|
||||
"/voice/connect",
|
||||
validateBody(connectVoiceSchema),
|
||||
handleConnectVoice,
|
||||
);
|
||||
|
||||
// POST /api/voice/disconnect
|
||||
router.post("/voice/disconnect", handleDisconnectVoice);
|
||||
|
||||
// POST /api/voice/command — send arbitrary voice command (transmit start/stop)
|
||||
router.post(
|
||||
"/voice/command",
|
||||
validateBody(voiceCommandSchema),
|
||||
handleVoiceCommand,
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
Reference in New Issue
Block a user