GatewayRequestHandlers — Record<string, GatewayRequestHandler>RPC Request Routing & Handler Resolution
The message dispatch subsystem receives incoming RPC request frames, validates their structure against 80+ protocol validators, resolves the matching handler from the handler registry, and invokes it with a fully-typed options object. Every request flows through a deterministic pipeline of validation, lookup, execution, and response.
GatewayRequestHandlers — Record<string, GatewayRequestHandler>(opts: GatewayRequestHandlerOptions) => Promise<void> | voidvalidateRequestFrame() with 80+ protocol validatorsRequestFrame & ResponseFrame from protocol/When a client sends an RPC message over the WebSocket connection, the Gateway must determine which handler should process the request and ensure the message conforms to the expected protocol schema. The dispatch pipeline is the central routing mechanism that connects raw protocol frames to strongly-typed handler functions.
The pipeline is intentionally synchronous in its lookup phase — handler resolution is a simple dictionary lookup — but handlers themselves may be async. This design keeps the hot path fast while allowing individual handlers to perform I/O operations like database queries or external API calls.
RequestFrame maps to exactly one GatewayRequestHandler, and the RespondFn callback ensures responses always match the expected ResponseFrame structure.
Each handler is a function that receives a fully-populated options object and either returns void synchronously or returns a Promise for async operations.
/**
* A single RPC handler function. Receives the parsed request,
* the originating client, and a respond callback.
*/
type GatewayRequestHandler = (
opts: GatewayRequestHandlerOptions
) => Promise<void> | void;
/**
* The handler registry maps method names to their handlers.
* Used by the dispatch pipeline to resolve incoming requests.
*/
type GatewayRequestHandlers = Record<string, GatewayRequestHandler>;
The options object bundles everything a handler needs to process a request and send a response.
interface GatewayRequestHandlerOptions {
/** The validated and parsed incoming request frame */
req: RequestFrame;
/** The connected client that sent this request */
client: GatewayClient;
/**
* Callback to send a response back to the client.
* @param ok - Whether the request succeeded
* @param payload - Optional success payload
* @param error - Optional error details
* @param meta - Optional response metadata
*/
respond: RespondFn;
/** Contextual information about the current request */
context: GatewayRequestContext;
}
The respond function is the sole mechanism for handlers to send data back to the client. It enforces a consistent response envelope.
/**
* Sends a typed response back to the requesting client.
* Wraps the payload in a ResponseFrame automatically.
*/
type RespondFn = (
ok: boolean,
payload?: unknown,
error?: string | { code: string; message: string },
meta?: Record<string, unknown>
) => void;
| Type | Key Fields | Description |
|---|---|---|
RequestFrame | id, method, params | Incoming RPC request from the client |
ResponseFrame | id, ok, payload, error, meta | Outgoing response sent via RespondFn |
GatewayClient | id, socket, auth | Authenticated WebSocket connection |
GatewayRequestContext | startedAt, traceId, session | Per-request metadata and tracing info |
Every incoming message traverses five stages before reaching a handler. The pipeline is designed to fail fast — invalid frames are rejected before any handler code executes.
flowchart TD
A[WebSocket Message Received] --> B[Parse JSON Frame]
B --> C{Valid JSON?}
C -- No --> D[Send Parse Error Response]
C -- Yes --> E[validateRequestFrame]
E --> F{Frame Valid?}
F -- No --> G[Send Validation Error Response]
F -- Yes --> H[Extract method from RequestFrame]
H --> I{Handler Exists?}
I -- No --> J[Send Method Not Found Response]
I -- Yes --> K[Build GatewayRequestHandlerOptions]
K --> L[Invoke Handler with opts]
L --> M{Handler Throws?}
M -- Yes --> N[Catch Error and Send Error Response]
M -- No --> O[Handler calls respond fn]
O --> P[Build ResponseFrame]
P --> Q[Send to Client via WebSocket]
D --> R[Log and Metrics]
G --> R
J --> R
N --> R
Q --> R
validateRequestFrame() step runs 80+ validators in sequence. While each individual check is O(1), the aggregate cost can be significant for high-throughput scenarios. The validators are ordered by frequency of failure to short-circuit as early as possible.
The validateRequestFrame() function ensures every incoming frame conforms to the protocol specification before any handler logic executes. This prevents handlers from needing to perform their own input validation.
| Category | Count | Examples |
|---|---|---|
| Structural | 12 | Has id, method is string, params is object |
| Type Safety | 18 | Param types match schema, no extra fields |
| Range & Bounds | 15 | String lengths, numeric ranges, array sizes |
| Semantic | 22 | Valid enum values, consistent field combinations |
| Security | 8 | No script injection, path traversal checks |
| Protocol Version | 5 | Compatible version, supported features |
The raw WebSocket message is parsed from JSON. If parsing fails, an error response is sent immediately without entering the dispatch pipeline.
function parseIncomingFrame(raw: string): RequestFrame | null {
try {
const parsed = JSON.parse(raw);
if (typeof parsed !== 'object' || parsed === null) return null;
if (typeof parsed.id !== 'string') return null;
if (typeof parsed.method !== 'string') return null;
return parsed as RequestFrame;
} catch {
return null;
}
}
The parsed frame is run through validateRequestFrame(). Each validator returns a result object; the first failure short-circuits the chain.
function validateRequestFrame(frame: RequestFrame): ValidationResult {
for (const validator of PROTOCOL_VALIDATORS) {
const result = validator(frame);
if (!result.ok) {
return {
ok: false,
code: result.code,
message: result.message,
validator: validator.name,
};
}
}
return { ok: true };
}
The dispatch system performs a direct key lookup on the GatewayRequestHandlers registry. This is O(1) and never throws.
function resolveHandler(
handlers: GatewayRequestHandlers,
method: string
): GatewayRequestHandler | undefined {
return handlers[method];
}
The resolved handler is called with a fully-constructed options object. Both sync and async handlers are supported — the dispatch loop awaits the result in either case.
async function dispatchToHandler(
handler: GatewayRequestHandler,
opts: GatewayRequestHandlerOptions
): Promise<void> {
try {
await handler(opts);
} catch (err) {
opts.respond(false, undefined, {
code: 'INTERNAL_ERROR',
message: err instanceof Error ? err.message : 'Unknown error',
});
}
}
The following sequence diagram shows the complete message lifecycle from client to handler and back.
sequenceDiagram
participant C as Client
participant WS as WebSocket Layer
participant DP as Dispatch Pipeline
participant V as validateRequestFrame
participant R as Handler Registry
participant H as Request Handler
participant RS as RespondFn
C->>WS: Send JSON RPC message
WS->>DP: onMessage(raw)
DP->>DP: JSON.parse(raw)
alt parse failure
DP-->>C: {ok: false, error: "PARSE_ERROR"}
end
DP->>V: validateRequestFrame(frame)
V->>V: Run 80+ validators
alt validation failure
V-->>DP: {ok: false, code, message}
DP-->>C: {ok: false, error: validationError}
end
V-->>DP: {ok: true}
DP->>R: lookup(frame.method)
alt handler not found
R-->>DP: undefined
DP-->>C: {ok: false, error: "METHOD_NOT_FOUND"}
end
R-->>DP: handler
DP->>H: handler({req, client, respond, context})
H->>RS: respond(true, payload)
RS->>WS: Build ResponseFrame
WS-->>C: Send JSON response
| Error Type | Response Code | Description |
|---|---|---|
| JSON parse failure | PARSE_ERROR | Message is not valid JSON |
| Missing required field | INVALID_FRAME | Frame lacks id, method, or params |
| Validation failure | VALIDATION_ERROR | One of the 80+ validators rejected the frame |
| Unknown method | METHOD_NOT_FOUND | No handler registered for the given method |
| Handler exception | INTERNAL_ERROR | Handler threw an unhandled error |
| Timeout | TIMEOUT | Handler did not call respond within the deadline |
respond() nor throws, the dispatch pipeline will detect the orphaned request after a configurable timeout (default 30s) and send a TIMEOUT error response. This prevents resource leaks and stuck clients.
Handlers are registered at Gateway startup by building the GatewayRequestHandlers map. Each entry maps a method name string to its handler function.
function buildHandlerRegistry(): GatewayRequestHandlers {
return {
'session.create': handleSessionCreate,
'session.list': handleSessionList,
'session.patch': handleSessionPatch,
'session.delete': handleSessionDelete,
'message.send': handleMessageSend,
'message.stream': handleMessageStream,
'assistant.resolve': handleAssistantResolve,
'config.get': handleConfigGet,
'config.set': handleConfigSet,
'health.ping': handleHealthPing,
// ... 70+ more handlers
};
}
Every handler receives a GatewayRequestContext object with per-request metadata used for tracing, logging, and session correlation.
| Field | Type | Description |
|---|---|---|
traceId | string | Unique ID for distributed tracing |
startedAt | number | High-resolution timestamp (ms) |
session | GatewaySessionRow | null | Active session if applicable |
clientIp | string | Remote IP for rate limiting |
userAgent | string | Client user-agent string |
src/gateway/
├── dispatch.ts ← dispatch pipeline, handler invocation
├── handlers/
│ ├── index.ts ← buildHandlerRegistry()
│ ├── session.ts ← session.* handlers
│ ├── message.ts ← message.* handlers
│ ├── assistant.ts ← assistant.* handlers
│ └── config.ts ← config.* handlers
├── protocol/
│ ├── index.ts ← RequestFrame, ResponseFrame
│ ├── validators.ts ← validateRequestFrame(), 80+ validators
│ └── types.ts ← GatewayRequestHandlerOptions, RespondFn
└── client.ts ← GatewayClient, GatewayRequestContext