Message Dispatch

Focus

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.

Handler Registry GatewayRequestHandlers — Record<string, GatewayRequestHandler>
Handler Signature (opts: GatewayRequestHandlerOptions) => Promise<void> | void
Validation validateRequestFrame() with 80+ protocol validators
Protocol Frames RequestFrame & ResponseFrame from protocol/

Table of Contents

Overview

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.

📍 Key insight: The dispatch system is essentially a type-safe RPC router. Each method name in the RequestFrame maps to exactly one GatewayRequestHandler, and the RespondFn callback ensures responses always match the expected ResponseFrame structure.

Core Types

GatewayRequestHandler

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>;

GatewayRequestHandlerOptions

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;
}

RespondFn

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;

Protocol Frames

TypeKey FieldsDescription
RequestFrameid, method, paramsIncoming RPC request from the client
ResponseFrameid, ok, payload, error, metaOutgoing response sent via RespondFn
GatewayClientid, socket, authAuthenticated WebSocket connection
GatewayRequestContextstartedAt, traceId, sessionPer-request metadata and tracing info

Dispatch Pipeline

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
⚠ Performance note: The 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.

Request Validation

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.

Validator Categories

CategoryCountExamples
Structural12Has id, method is string, params is object
Type Safety18Param types match schema, no extra fields
Range & Bounds15String lengths, numeric ranges, array sizes
Semantic22Valid enum values, consistent field combinations
Security8No script injection, path traversal checks
Protocol Version5Compatible version, supported features

Frame Parsing

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;
  }
}

Validation Gate

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 };
}

Handler Lookup

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];
}

Handler Invocation

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',
    });
  }
}

Dispatch Sequence

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 Handling

Error TypeResponse CodeDescription
JSON parse failurePARSE_ERRORMessage is not valid JSON
Missing required fieldINVALID_FRAMEFrame lacks id, method, or params
Validation failureVALIDATION_ERROROne of the 80+ validators rejected the frame
Unknown methodMETHOD_NOT_FOUNDNo handler registered for the given method
Handler exceptionINTERNAL_ERRORHandler threw an unhandled error
TimeoutTIMEOUTHandler did not call respond within the deadline
🚨 Critical: If a handler neither calls 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.

Handler Registration

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
  };
}

Request Context

Every handler receives a GatewayRequestContext object with per-request metadata used for tracing, logging, and session correlation.

FieldTypeDescription
traceIdstringUnique ID for distributed tracing
startedAtnumberHigh-resolution timestamp (ms)
sessionGatewaySessionRow | nullActive session if applicable
clientIpstringRemote IP for rate limiting
userAgentstringClient user-agent string

Directory Map

Source files for this topic
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