refactor(src): split oversized modules

This commit is contained in:
Peter Steinberger
2026-01-14 01:08:15 +00:00
parent b2179de839
commit bcbfb357be
675 changed files with 91476 additions and 73453 deletions

View File

@@ -0,0 +1,13 @@
import { Buffer } from "node:buffer";
const CLOSE_REASON_MAX_BYTES = 120;
export function truncateCloseReason(
reason: string,
maxBytes = CLOSE_REASON_MAX_BYTES,
): string {
if (!reason) return "invalid handshake";
const buf = Buffer.from(reason);
if (buf.length <= maxBytes) return reason;
return buf.subarray(0, maxBytes).toString();
}

View File

@@ -0,0 +1,72 @@
import {
getHealthSnapshot,
type HealthSummary,
} from "../../commands/health.js";
import {
CONFIG_PATH_CLAWDBOT,
STATE_DIR_CLAWDBOT,
} from "../../config/config.js";
import { listSystemPresence } from "../../infra/system-presence.js";
import type { Snapshot } from "../protocol/index.js";
let presenceVersion = 1;
let healthVersion = 1;
let healthCache: HealthSummary | null = null;
let healthRefresh: Promise<HealthSummary> | null = null;
let broadcastHealthUpdate: ((snap: HealthSummary) => void) | null = null;
export function buildGatewaySnapshot(): Snapshot {
const presence = listSystemPresence();
const uptimeMs = Math.round(process.uptime() * 1000);
// Health is async; caller should await getHealthSnapshot and replace later if needed.
const emptyHealth: unknown = {};
return {
presence,
health: emptyHealth,
stateVersion: { presence: presenceVersion, health: healthVersion },
uptimeMs,
// Surface resolved paths so UIs can display the true config location.
configPath: CONFIG_PATH_CLAWDBOT,
stateDir: STATE_DIR_CLAWDBOT,
};
}
export function getHealthCache(): HealthSummary | null {
return healthCache;
}
export function getHealthVersion(): number {
return healthVersion;
}
export function incrementPresenceVersion(): number {
presenceVersion += 1;
return presenceVersion;
}
export function getPresenceVersion(): number {
return presenceVersion;
}
export function setBroadcastHealthUpdate(
fn: ((snap: HealthSummary) => void) | null,
) {
broadcastHealthUpdate = fn;
}
export async function refreshGatewayHealthSnapshot(opts?: { probe?: boolean }) {
if (!healthRefresh) {
healthRefresh = (async () => {
const snap = await getHealthSnapshot({ probe: opts?.probe });
healthCache = snap;
healthVersion += 1;
if (broadcastHealthUpdate) {
broadcastHealthUpdate(snap);
}
return snap;
})().finally(() => {
healthRefresh = null;
});
}
return healthRefresh;
}

122
src/gateway/server/hooks.ts Normal file
View File

@@ -0,0 +1,122 @@
import { randomUUID } from "node:crypto";
import type { CliDeps } from "../../cli/deps.js";
import { loadConfig } from "../../config/config.js";
import { resolveMainSessionKeyFromConfig } from "../../config/sessions.js";
import { runCronIsolatedAgentTurn } from "../../cron/isolated-agent.js";
import type { CronJob } from "../../cron/types.js";
import { requestHeartbeatNow } from "../../infra/heartbeat-wake.js";
import { enqueueSystemEvent } from "../../infra/system-events.js";
import type { createSubsystemLogger } from "../../logging.js";
import type { HookMessageChannel, HooksConfigResolved } from "../hooks.js";
import { createHooksRequestHandler } from "../server-http.js";
type SubsystemLogger = ReturnType<typeof createSubsystemLogger>;
export function createGatewayHooksRequestHandler(params: {
deps: CliDeps;
getHooksConfig: () => HooksConfigResolved | null;
bindHost: string;
port: number;
logHooks: SubsystemLogger;
}) {
const { deps, getHooksConfig, bindHost, port, logHooks } = params;
const dispatchWakeHook = (value: {
text: string;
mode: "now" | "next-heartbeat";
}) => {
const sessionKey = resolveMainSessionKeyFromConfig();
enqueueSystemEvent(value.text, { sessionKey });
if (value.mode === "now") {
requestHeartbeatNow({ reason: "hook:wake" });
}
};
const dispatchAgentHook = (value: {
message: string;
name: string;
wakeMode: "now" | "next-heartbeat";
sessionKey: string;
deliver: boolean;
channel: HookMessageChannel;
to?: string;
model?: string;
thinking?: string;
timeoutSeconds?: number;
}) => {
const sessionKey = value.sessionKey.trim()
? value.sessionKey.trim()
: `hook:${randomUUID()}`;
const mainSessionKey = resolveMainSessionKeyFromConfig();
const jobId = randomUUID();
const now = Date.now();
const job: CronJob = {
id: jobId,
name: value.name,
enabled: true,
createdAtMs: now,
updatedAtMs: now,
schedule: { kind: "at", atMs: now },
sessionTarget: "isolated",
wakeMode: value.wakeMode,
payload: {
kind: "agentTurn",
message: value.message,
model: value.model,
thinking: value.thinking,
timeoutSeconds: value.timeoutSeconds,
deliver: value.deliver,
channel: value.channel,
to: value.to,
},
state: { nextRunAtMs: now },
};
const runId = randomUUID();
void (async () => {
try {
const cfg = loadConfig();
const result = await runCronIsolatedAgentTurn({
cfg,
deps,
job,
message: value.message,
sessionKey,
lane: "cron",
});
const summary =
result.summary?.trim() || result.error?.trim() || result.status;
const prefix =
result.status === "ok"
? `Hook ${value.name}`
: `Hook ${value.name} (${result.status})`;
enqueueSystemEvent(`${prefix}: ${summary}`.trim(), {
sessionKey: mainSessionKey,
});
if (value.wakeMode === "now") {
requestHeartbeatNow({ reason: `hook:${jobId}` });
}
} catch (err) {
logHooks.warn(`hook agent failed: ${String(err)}`);
enqueueSystemEvent(`Hook ${value.name} (error): ${String(err)}`, {
sessionKey: mainSessionKey,
});
if (value.wakeMode === "now") {
requestHeartbeatNow({ reason: `hook:${jobId}:error` });
}
}
})();
return runId;
};
return createHooksRequestHandler({
getHooksConfig,
bindHost,
port,
logHooks,
dispatchAgentHook,
dispatchWakeHook,
});
}

View File

@@ -0,0 +1,38 @@
import type { Server as HttpServer } from "node:http";
import { GatewayLockError } from "../../infra/gateway-lock.js";
export async function listenGatewayHttpServer(params: {
httpServer: HttpServer;
bindHost: string;
port: number;
}) {
const { httpServer, bindHost, port } = params;
try {
await new Promise<void>((resolve, reject) => {
const onError = (err: NodeJS.ErrnoException) => {
httpServer.off("listening", onListening);
reject(err);
};
const onListening = () => {
httpServer.off("error", onError);
resolve();
};
httpServer.once("error", onError);
httpServer.once("listening", onListening);
httpServer.listen(port, bindHost);
});
} catch (err) {
const code = (err as NodeJS.ErrnoException).code;
if (code === "EADDRINUSE") {
throw new GatewayLockError(
`another gateway instance is already listening on ws://${bindHost}:${port}`,
err,
);
}
throw new GatewayLockError(
`failed to bind gateway socket on ws://${bindHost}:${port}: ${String(err)}`,
err,
);
}
}

View File

@@ -0,0 +1,270 @@
import { randomUUID } from "node:crypto";
import type { WebSocket, WebSocketServer } from "ws";
import { resolveCanvasHostUrl } from "../../infra/canvas-host-url.js";
import {
listSystemPresence,
upsertPresence,
} from "../../infra/system-presence.js";
import type { createSubsystemLogger } from "../../logging.js";
import { isWebchatClient } from "../../utils/message-channel.js";
import type { ResolvedGatewayAuth } from "../auth.js";
import { isLoopbackAddress } from "../net.js";
import { HANDSHAKE_TIMEOUT_MS } from "../server-constants.js";
import type {
GatewayRequestContext,
GatewayRequestHandlers,
} from "../server-methods/types.js";
import { formatError } from "../server-utils.js";
import { logWs } from "../ws-log.js";
import {
getHealthVersion,
getPresenceVersion,
incrementPresenceVersion,
} from "./health-state.js";
import { attachGatewayWsMessageHandler } from "./ws-connection/message-handler.js";
import type { GatewayWsClient } from "./ws-types.js";
type SubsystemLogger = ReturnType<typeof createSubsystemLogger>;
export function attachGatewayWsConnectionHandler(params: {
wss: WebSocketServer;
clients: Set<GatewayWsClient>;
port: number;
bridgeHost?: string;
canvasHostEnabled: boolean;
canvasHostServerPort?: number;
resolvedAuth: ResolvedGatewayAuth;
gatewayMethods: string[];
events: string[];
logGateway: SubsystemLogger;
logHealth: SubsystemLogger;
logWsControl: SubsystemLogger;
extraHandlers: GatewayRequestHandlers;
broadcast: (
event: string,
payload: unknown,
opts?: {
dropIfSlow?: boolean;
stateVersion?: { presence?: number; health?: number };
},
) => void;
buildRequestContext: () => GatewayRequestContext;
}) {
const {
wss,
clients,
port,
bridgeHost,
canvasHostEnabled,
canvasHostServerPort,
resolvedAuth,
gatewayMethods,
events,
logGateway,
logHealth,
logWsControl,
extraHandlers,
broadcast,
buildRequestContext,
} = params;
wss.on("connection", (socket, upgradeReq) => {
let client: GatewayWsClient | null = null;
let closed = false;
const openedAt = Date.now();
const connId = randomUUID();
const remoteAddr = (
socket as WebSocket & { _socket?: { remoteAddress?: string } }
)._socket?.remoteAddress;
const headerValue = (value: string | string[] | undefined) =>
Array.isArray(value) ? value[0] : value;
const requestHost = headerValue(upgradeReq.headers.host);
const requestOrigin = headerValue(upgradeReq.headers.origin);
const requestUserAgent = headerValue(upgradeReq.headers["user-agent"]);
const forwardedFor = headerValue(upgradeReq.headers["x-forwarded-for"]);
const canvasHostPortForWs =
canvasHostServerPort ?? (canvasHostEnabled ? port : undefined);
const canvasHostOverride =
bridgeHost && bridgeHost !== "0.0.0.0" && bridgeHost !== "::"
? bridgeHost
: undefined;
const canvasHostUrl = resolveCanvasHostUrl({
canvasPort: canvasHostPortForWs,
hostOverride: canvasHostServerPort ? canvasHostOverride : undefined,
requestHost: upgradeReq.headers.host,
forwardedProto: upgradeReq.headers["x-forwarded-proto"],
localAddress: upgradeReq.socket?.localAddress,
});
logWs("in", "open", { connId, remoteAddr });
let handshakeState: "pending" | "connected" | "failed" = "pending";
let closeCause: string | undefined;
let closeMeta: Record<string, unknown> = {};
let lastFrameType: string | undefined;
let lastFrameMethod: string | undefined;
let lastFrameId: string | undefined;
const setCloseCause = (cause: string, meta?: Record<string, unknown>) => {
if (!closeCause) closeCause = cause;
if (meta && Object.keys(meta).length > 0) {
closeMeta = { ...closeMeta, ...meta };
}
};
const setLastFrameMeta = (meta: {
type?: string;
method?: string;
id?: string;
}) => {
if (meta.type || meta.method || meta.id) {
lastFrameType = meta.type ?? lastFrameType;
lastFrameMethod = meta.method ?? lastFrameMethod;
lastFrameId = meta.id ?? lastFrameId;
}
};
const send = (obj: unknown) => {
try {
socket.send(JSON.stringify(obj));
} catch {
/* ignore */
}
};
const close = (code = 1000, reason?: string) => {
if (closed) return;
closed = true;
clearTimeout(handshakeTimer);
if (client) clients.delete(client);
try {
socket.close(code, reason);
} catch {
/* ignore */
}
};
socket.once("error", (err) => {
logWsControl.warn(
`error conn=${connId} remote=${remoteAddr ?? "?"}: ${formatError(err)}`,
);
close();
});
const isNoisySwiftPmHelperClose = (
userAgent: string | undefined,
remote: string | undefined,
) =>
Boolean(
userAgent?.toLowerCase().includes("swiftpm-testing-helper") &&
isLoopbackAddress(remote),
);
socket.once("close", (code, reason) => {
const durationMs = Date.now() - openedAt;
const closeContext = {
cause: closeCause,
handshake: handshakeState,
durationMs,
lastFrameType,
lastFrameMethod,
lastFrameId,
host: requestHost,
origin: requestOrigin,
userAgent: requestUserAgent,
forwardedFor,
...closeMeta,
};
if (!client) {
const logFn = isNoisySwiftPmHelperClose(requestUserAgent, remoteAddr)
? logWsControl.debug
: logWsControl.warn;
logFn(
`closed before connect conn=${connId} remote=${remoteAddr ?? "?"} fwd=${forwardedFor ?? "n/a"} origin=${requestOrigin ?? "n/a"} host=${requestHost ?? "n/a"} ua=${requestUserAgent ?? "n/a"} code=${code ?? "n/a"} reason=${reason?.toString() || "n/a"}`,
closeContext,
);
}
if (client && isWebchatClient(client.connect.client)) {
logWsControl.info(
`webchat disconnected code=${code} reason=${reason?.toString() || "n/a"} conn=${connId}`,
);
}
if (client?.presenceKey) {
upsertPresence(client.presenceKey, { reason: "disconnect" });
incrementPresenceVersion();
broadcast(
"presence",
{ presence: listSystemPresence() },
{
dropIfSlow: true,
stateVersion: {
presence: getPresenceVersion(),
health: getHealthVersion(),
},
},
);
}
logWs("out", "close", {
connId,
code,
reason: reason?.toString(),
durationMs,
cause: closeCause,
handshake: handshakeState,
lastFrameType,
lastFrameMethod,
lastFrameId,
});
close();
});
const handshakeTimer = setTimeout(() => {
if (!client) {
handshakeState = "failed";
setCloseCause("handshake-timeout", {
handshakeMs: Date.now() - openedAt,
});
logWsControl.warn(
`handshake timeout conn=${connId} remote=${remoteAddr ?? "?"}`,
);
close();
}
}, HANDSHAKE_TIMEOUT_MS);
attachGatewayWsMessageHandler({
socket,
upgradeReq,
connId,
remoteAddr,
forwardedFor,
requestHost,
requestOrigin,
requestUserAgent,
canvasHostUrl,
resolvedAuth,
gatewayMethods,
events,
extraHandlers,
buildRequestContext,
send,
close,
isClosed: () => closed,
clearHandshakeTimer: () => clearTimeout(handshakeTimer),
getClient: () => client,
setClient: (next) => {
client = next;
clients.add(next);
},
setHandshakeState: (next) => {
handshakeState = next;
},
setCloseCause,
setLastFrameMeta,
logGateway,
logHealth,
logWsControl,
});
});
}

View File

@@ -0,0 +1,412 @@
import type { IncomingMessage } from "node:http";
import os from "node:os";
import type { WebSocket } from "ws";
import { upsertPresence } from "../../../infra/system-presence.js";
import { rawDataToString } from "../../../infra/ws.js";
import type { createSubsystemLogger } from "../../../logging.js";
import {
isGatewayCliClient,
isWebchatClient,
} from "../../../utils/message-channel.js";
import type { ResolvedGatewayAuth } from "../../auth.js";
import { authorizeGatewayConnect } from "../../auth.js";
import { isLoopbackAddress } from "../../net.js";
import {
type ConnectParams,
ErrorCodes,
type ErrorShape,
errorShape,
formatValidationErrors,
PROTOCOL_VERSION,
type RequestFrame,
validateConnectParams,
validateRequestFrame,
} from "../../protocol/index.js";
import {
MAX_BUFFERED_BYTES,
MAX_PAYLOAD_BYTES,
TICK_INTERVAL_MS,
} from "../../server-constants.js";
import type {
GatewayRequestContext,
GatewayRequestHandlers,
} from "../../server-methods/types.js";
import { handleGatewayRequest } from "../../server-methods.js";
import { formatError } from "../../server-utils.js";
import { formatForLog, logWs } from "../../ws-log.js";
import { truncateCloseReason } from "../close-reason.js";
import {
buildGatewaySnapshot,
getHealthCache,
getHealthVersion,
incrementPresenceVersion,
refreshGatewayHealthSnapshot,
} from "../health-state.js";
import type { GatewayWsClient } from "../ws-types.js";
type SubsystemLogger = ReturnType<typeof createSubsystemLogger>;
export function attachGatewayWsMessageHandler(params: {
socket: WebSocket;
upgradeReq: IncomingMessage;
connId: string;
remoteAddr?: string;
forwardedFor?: string;
requestHost?: string;
requestOrigin?: string;
requestUserAgent?: string;
canvasHostUrl?: string;
resolvedAuth: ResolvedGatewayAuth;
gatewayMethods: string[];
events: string[];
extraHandlers: GatewayRequestHandlers;
buildRequestContext: () => GatewayRequestContext;
send: (obj: unknown) => void;
close: (code?: number, reason?: string) => void;
isClosed: () => boolean;
clearHandshakeTimer: () => void;
getClient: () => GatewayWsClient | null;
setClient: (next: GatewayWsClient) => void;
setHandshakeState: (state: "pending" | "connected" | "failed") => void;
setCloseCause: (cause: string, meta?: Record<string, unknown>) => void;
setLastFrameMeta: (meta: {
type?: string;
method?: string;
id?: string;
}) => void;
logGateway: SubsystemLogger;
logHealth: SubsystemLogger;
logWsControl: SubsystemLogger;
}) {
const {
socket,
upgradeReq,
connId,
remoteAddr,
forwardedFor,
requestHost,
requestOrigin,
requestUserAgent,
canvasHostUrl,
resolvedAuth,
gatewayMethods,
events,
extraHandlers,
buildRequestContext,
send,
close,
isClosed,
clearHandshakeTimer,
getClient,
setClient,
setHandshakeState,
setCloseCause,
setLastFrameMeta,
logGateway,
logHealth,
logWsControl,
} = params;
const isWebchatConnect = (p: ConnectParams | null | undefined) =>
isWebchatClient(p?.client);
socket.on("message", async (data) => {
if (isClosed()) return;
const text = rawDataToString(data);
try {
const parsed = JSON.parse(text);
const frameType =
parsed && typeof parsed === "object" && "type" in parsed
? typeof (parsed as { type?: unknown }).type === "string"
? String((parsed as { type?: unknown }).type)
: undefined
: undefined;
const frameMethod =
parsed && typeof parsed === "object" && "method" in parsed
? typeof (parsed as { method?: unknown }).method === "string"
? String((parsed as { method?: unknown }).method)
: undefined
: undefined;
const frameId =
parsed && typeof parsed === "object" && "id" in parsed
? typeof (parsed as { id?: unknown }).id === "string"
? String((parsed as { id?: unknown }).id)
: undefined
: undefined;
if (frameType || frameMethod || frameId) {
setLastFrameMeta({ type: frameType, method: frameMethod, id: frameId });
}
const client = getClient();
if (!client) {
// Handshake must be a normal request:
// { type:"req", method:"connect", params: ConnectParams }.
const isRequestFrame = validateRequestFrame(parsed);
if (
!isRequestFrame ||
(parsed as RequestFrame).method !== "connect" ||
!validateConnectParams((parsed as RequestFrame).params)
) {
const handshakeError = isRequestFrame
? (parsed as RequestFrame).method === "connect"
? `invalid connect params: ${formatValidationErrors(validateConnectParams.errors)}`
: "invalid handshake: first request must be connect"
: "invalid request frame";
setHandshakeState("failed");
setCloseCause("invalid-handshake", {
frameType,
frameMethod,
frameId,
handshakeError,
});
if (isRequestFrame) {
const req = parsed as RequestFrame;
send({
type: "res",
id: req.id,
ok: false,
error: errorShape(ErrorCodes.INVALID_REQUEST, handshakeError),
});
} else {
logWsControl.warn(
`invalid handshake conn=${connId} remote=${remoteAddr ?? "?"} fwd=${forwardedFor ?? "n/a"} origin=${requestOrigin ?? "n/a"} host=${requestHost ?? "n/a"} ua=${requestUserAgent ?? "n/a"}`,
);
}
const closeReason = truncateCloseReason(
handshakeError || "invalid handshake",
);
if (isRequestFrame) {
queueMicrotask(() => close(1008, closeReason));
} else {
close(1008, closeReason);
}
return;
}
const frame = parsed as RequestFrame;
const connectParams = frame.params as ConnectParams;
const clientLabel =
connectParams.client.displayName ?? connectParams.client.id;
// protocol negotiation
const { minProtocol, maxProtocol } = connectParams;
if (maxProtocol < PROTOCOL_VERSION || minProtocol > PROTOCOL_VERSION) {
setHandshakeState("failed");
logWsControl.warn(
`protocol mismatch conn=${connId} remote=${remoteAddr ?? "?"} client=${clientLabel} ${connectParams.client.mode} v${connectParams.client.version}`,
);
setCloseCause("protocol-mismatch", {
minProtocol,
maxProtocol,
expectedProtocol: PROTOCOL_VERSION,
client: connectParams.client.id,
clientDisplayName: connectParams.client.displayName,
mode: connectParams.client.mode,
version: connectParams.client.version,
});
send({
type: "res",
id: frame.id,
ok: false,
error: errorShape(ErrorCodes.INVALID_REQUEST, "protocol mismatch", {
details: { expectedProtocol: PROTOCOL_VERSION },
}),
});
close(1002, "protocol mismatch");
return;
}
const authResult = await authorizeGatewayConnect({
auth: resolvedAuth,
connectAuth: connectParams.auth,
req: upgradeReq,
});
if (!authResult.ok) {
setHandshakeState("failed");
logWsControl.warn(
`unauthorized conn=${connId} remote=${remoteAddr ?? "?"} client=${clientLabel} ${connectParams.client.mode} v${connectParams.client.version}`,
);
const authProvided = connectParams.auth?.token
? "token"
: connectParams.auth?.password
? "password"
: "none";
setCloseCause("unauthorized", {
authMode: resolvedAuth.mode,
authProvided,
authReason: authResult.reason,
allowTailscale: resolvedAuth.allowTailscale,
client: connectParams.client.id,
clientDisplayName: connectParams.client.displayName,
mode: connectParams.client.mode,
version: connectParams.client.version,
});
send({
type: "res",
id: frame.id,
ok: false,
error: errorShape(ErrorCodes.INVALID_REQUEST, "unauthorized"),
});
close(1008, "unauthorized");
return;
}
const authMethod = authResult.method ?? "none";
const shouldTrackPresence = !isGatewayCliClient(connectParams.client);
const clientId = connectParams.client.id;
const instanceId = connectParams.client.instanceId;
const presenceKey = shouldTrackPresence
? (instanceId ?? connId)
: undefined;
logWs("in", "connect", {
connId,
client: connectParams.client.id,
clientDisplayName: connectParams.client.displayName,
version: connectParams.client.version,
mode: connectParams.client.mode,
clientId,
platform: connectParams.client.platform,
auth: authMethod,
});
if (isWebchatConnect(connectParams)) {
logWsControl.info(
`webchat connected conn=${connId} remote=${remoteAddr ?? "?"} client=${clientLabel} ${connectParams.client.mode} v${connectParams.client.version}`,
);
}
if (presenceKey) {
upsertPresence(presenceKey, {
host:
connectParams.client.displayName ??
connectParams.client.id ??
os.hostname(),
ip: isLoopbackAddress(remoteAddr) ? undefined : remoteAddr,
version: connectParams.client.version,
platform: connectParams.client.platform,
deviceFamily: connectParams.client.deviceFamily,
modelIdentifier: connectParams.client.modelIdentifier,
mode: connectParams.client.mode,
instanceId,
reason: "connect",
});
incrementPresenceVersion();
}
const snapshot = buildGatewaySnapshot();
const cachedHealth = getHealthCache();
if (cachedHealth) {
snapshot.health = cachedHealth;
snapshot.stateVersion.health = getHealthVersion();
}
const helloOk = {
type: "hello-ok",
protocol: PROTOCOL_VERSION,
server: {
version:
process.env.CLAWDBOT_VERSION ??
process.env.npm_package_version ??
"dev",
commit: process.env.GIT_COMMIT,
host: os.hostname(),
connId,
},
features: { methods: gatewayMethods, events },
snapshot,
canvasHostUrl,
policy: {
maxPayload: MAX_PAYLOAD_BYTES,
maxBufferedBytes: MAX_BUFFERED_BYTES,
tickIntervalMs: TICK_INTERVAL_MS,
},
};
clearHandshakeTimer();
const nextClient: GatewayWsClient = {
socket,
connect: connectParams,
connId,
presenceKey,
};
setClient(nextClient);
setHandshakeState("connected");
logWs("out", "hello-ok", {
connId,
methods: gatewayMethods.length,
events: events.length,
presence: snapshot.presence.length,
stateVersion: snapshot.stateVersion.presence,
});
send({ type: "res", id: frame.id, ok: true, payload: helloOk });
void refreshGatewayHealthSnapshot({ probe: true }).catch((err) =>
logHealth.error(
`post-connect health refresh failed: ${formatError(err)}`,
),
);
return;
}
// After handshake, accept only req frames
if (!validateRequestFrame(parsed)) {
send({
type: "res",
id: (parsed as { id?: unknown })?.id ?? "invalid",
ok: false,
error: errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid request frame: ${formatValidationErrors(validateRequestFrame.errors)}`,
),
});
return;
}
const req = parsed as RequestFrame;
logWs("in", "req", { connId, id: req.id, method: req.method });
const respond = (
ok: boolean,
payload?: unknown,
error?: ErrorShape,
meta?: Record<string, unknown>,
) => {
send({ type: "res", id: req.id, ok, payload, error });
logWs("out", "res", {
connId,
id: req.id,
ok,
method: req.method,
errorCode: error?.code,
errorMessage: error?.message,
...meta,
});
};
void (async () => {
await handleGatewayRequest({
req,
respond,
client,
isWebchatConnect,
extraHandlers,
context: buildRequestContext(),
});
})().catch((err) => {
logGateway.error(`request handler failed: ${formatForLog(err)}`);
respond(
false,
undefined,
errorShape(ErrorCodes.UNAVAILABLE, formatForLog(err)),
);
});
} catch (err) {
logGateway.error(`parse/handle error: ${String(err)}`);
logWs("out", "parse-error", { connId, error: formatForLog(err) });
if (!getClient()) {
close();
}
}
});
}

View File

@@ -0,0 +1,10 @@
import type { WebSocket } from "ws";
import type { ConnectParams } from "../protocol/index.js";
export type GatewayWsClient = {
socket: WebSocket;
connect: ConnectParams;
connId: string;
presenceKey?: string;
};