GatewayWsClient with canvas capability fieldsCapability Negotiation
Mobile Node Capability Registration
Capability negotiation is the process by which mobile nodes (Android/iOS clients) register their supported features with the Gateway during connection. The NodeRegistry tracks each node's capabilities, expiration times, and connection metadata to enable feature-aware routing.
NodeRegistry — tracks connected nodesConnectParams with version & capability flagscanvasCapabilityExpiresAtMs for time-limited capabilitiesTable of Contents
Overview
When a mobile node connects to the Gateway via WebSocket, it includes capability information in its ConnectParams. The Gateway parses these flags, validates the client version, and registers the node's capabilities in the NodeRegistry. This registry is then consulted by the routing layer to determine which nodes can handle specific request types (e.g., canvas rendering, media playback).
canvasCapabilityExpiresAtMs). The Gateway automatically prunes expired capabilities and notifies affected nodes to re-negotiate if they are still connected.
Capability Registration Flow
The following sequence diagram shows the full lifecycle of capability registration, from WebSocket handshake through periodic renewal.
sequenceDiagram
participant M as Mobile Node
participant WS as WebSocket Server
participant GW as Gateway
participant NR as NodeRegistry
participant CS as CronService
M->>WS: WebSocket connect
WS->>GW: onConnection(socket, request)
GW->>GW: Parse ConnectParams from query/headers
GW->>GW: Validate client version
alt version unsupported
GW-->>M: Close(4001, "unsupported-version")
end
GW->>NR: registerNode(connId, capabilities)
NR->>NR: Store GatewayWsClient entry
NR-->>GW: registration confirmed
GW-->>M: Connected with capabilities acknowledged
Note over CS: Every 30s
CS->>NR: pruneExpiredCapabilities()
NR->>NR: Check canvasCapabilityExpiresAtMs
alt capabilities expired
NR->>GW: onCapabilityExpired(connId)
GW-->>M: Notify capability expired
M->>GW: Re-send capabilities
GW->>NR: updateCapabilities(connId, newCaps)
end
GatewayWsClient Type
The GatewayWsClient type represents a connected WebSocket client with its associated capability metadata. Each client entry tracks the canvas capability status, the host URL for canvas rendering, and the expiration timestamp.
interface GatewayWsClient {
// Connection identity
connId: string;
clientIp: string;
socket: WebSocket;
connectedAtMs: number;
// Canvas capability fields
canvasCapability: CanvasCapability | null;
canvasHostUrl: string | null;
canvasCapabilityExpiresAtMs: number | null;
// Client metadata
clientVersion: string;
platform: "android" | "ios" | "desktop" | "web";
deviceId: string;
// State tracking
lastPingMs: number;
pendingMessages: number;
isAlive: boolean;
}
type CanvasCapability = {
supported: boolean;
version: number;
maxWidth: number;
maxHeight: number;
formats: ("png" | "webp" | "svg")[];
};
canvasCapability to be null initially.
ConnectParams Structure
The ConnectParams object is extracted from the WebSocket handshake request. It carries authentication credentials, client version information, and capability flags that the Gateway uses to build the GatewayWsClient entry.
Parse Connection Parameters
Connection parameters are extracted from a combination of URL query parameters and HTTP headers. The client version and capability flags are required fields; missing values cause the connection to be rejected.
interface ConnectParams {
// Authentication
connectAuth: ConnectAuth;
sessionId?: string;
// Client identification
clientVersion: string;
platform: "android" | "ios" | "desktop" | "web";
deviceId: string;
// Capability flags
capabilities: {
canvas: boolean;
canvasVersion?: number;
canvasHostUrl?: string;
canvasMaxWidth?: number;
canvasMaxHeight?: number;
canvasFormats?: string[];
canvasExpiresInMs?: number;
};
}
function parseConnectParams(
url: URL,
headers: Headers,
): ConnectParams | { error: string } {
const version = url.searchParams.get("v");
if (!version) {
return { error: "missing-client-version" };
}
const platform = url.searchParams.get("platform");
if (!platform || !["android", "ios", "desktop", "web"].includes(platform)) {
return { error: "invalid-platform" };
}
return {
connectAuth: extractAuth(headers),
clientVersion: version,
platform: platform as ConnectParams["platform"],
deviceId: url.searchParams.get("deviceId") ?? generateDeviceId(),
capabilities: {
canvas: url.searchParams.get("canvas") === "true",
canvasVersion: parseInt(url.searchParams.get("canvasVersion") ?? "0"),
canvasHostUrl: url.searchParams.get("canvasHostUrl") ?? undefined,
canvasMaxWidth: parseInt(url.searchParams.get("canvasMaxWidth") ?? "0"),
canvasMaxHeight: parseInt(url.searchParams.get("canvasMaxHeight") ?? "0"),
canvasFormats: url.searchParams.get("canvasFormats")?.split(","),
canvasExpiresInMs: parseInt(url.searchParams.get("canvasExpiresInMs") ?? "0"),
},
};
}
Validate Client Version
The Gateway maintains a minimum supported version. Clients below this version are disconnected with a 4001 close code and must update before reconnecting.
Register in NodeRegistry
Once validated, the client's capabilities are registered in the NodeRegistry. The registry maintains a map of connection IDs to GatewayWsClient entries and emits events when capabilities change.
class NodeRegistry {
private nodes: Map<string, GatewayWsClient> = new Map();
private listeners: Map<string, Set<CapabilityListener>> = new Map();
registerNode(connId: string, params: ConnectParams, socket: WebSocket, clientIp: string): void {
const client: GatewayWsClient = {
connId,
clientIp,
socket,
connectedAtMs: Date.now(),
canvasCapability: params.capabilities.canvas ? {
supported: true,
version: params.capabilities.canvasVersion ?? 1,
maxWidth: params.capabilities.canvasMaxWidth ?? 1920,
maxHeight: params.capabilities.canvasMaxHeight ?? 1080,
formats: (params.capabilities.canvasFormats ?? ["png"]) as CanvasCapability["formats"],
} : null,
canvasHostUrl: params.capabilities.canvasHostUrl ?? null,
canvasCapabilityExpiresAtMs: params.capabilities.canvasExpiresInMs
? Date.now() + params.capabilities.canvasExpiresInMs
: null,
clientVersion: params.clientVersion,
platform: params.platform,
deviceId: params.deviceId,
lastPingMs: Date.now(),
pendingMessages: 0,
isAlive: true,
};
this.nodes.set(connId, client);
this.emit("registered", connId, client);
}
getNode(connId: string): GatewayWsClient | undefined {
return this.nodes.get(connId);
}
getNodesWithCapability(cap: string): GatewayWsClient[] {
return Array.from(this.nodes.values()).filter(n => {
if (cap === "canvas") return n.canvasCapability?.supported === true;
return false;
});
}
removeNode(connId: string): void {
const node = this.nodes.get(connId);
if (node) {
this.nodes.delete(connId);
this.emit("removed", connId, node);
}
}
private emit(event: string, connId: string, client: GatewayWsClient): void {
const set = this.listeners.get(event);
if (set) set.forEach(fn => fn(connId, client));
}
}
type CapabilityListener = (connId: string, client: GatewayWsClient) => void;
Capability Expiration & Renewal
The CronService periodically checks canvasCapabilityExpiresAtMs for each registered node. When a capability expires, the node is notified via WebSocket and expected to re-send updated capability flags within a grace period. If the node does not renew, the capability is set to null.
Class Relationship Diagram
This diagram shows how the core types relate to each other in the capability negotiation subsystem.
classDiagram
class GatewayWsClient {
+string connId
+string clientIp
+WebSocket socket
+number connectedAtMs
+CanvasCapability canvasCapability
+string canvasHostUrl
+number canvasCapabilityExpiresAtMs
+string clientVersion
+string platform
+string deviceId
+boolean isAlive
}
class ConnectParams {
+ConnectAuth connectAuth
+string clientVersion
+string platform
+string deviceId
+CapabilityFlags capabilities
}
class CanvasCapability {
+boolean supported
+number version
+number maxWidth
+number maxHeight
+string[] formats
}
class NodeRegistry {
-Map nodes
-Map listeners
+registerNode()
+getNode()
+getNodesWithCapability()
+removeNode()
+pruneExpiredCapabilities()
}
class GatewayClient {
+Function connect
+string connId
+string clientIp
+CanvasCapability canvasCapability
}
class CronService {
+start()
+registerTask()
+stop()
}
ConnectParams --> GatewayWsClient : creates
GatewayWsClient --> CanvasCapability : has
NodeRegistry --> GatewayWsClient : manages
GatewayClient --> CanvasCapability : references
CronService --> NodeRegistry : prunes expired
GatewayClient Interface
The GatewayClient is a simplified view of a connected client exposed to the routing and request handling layers. It wraps the essential fields from GatewayWsClient without exposing the raw WebSocket or internal state.
| Field | Type | Description |
|---|---|---|
connect | Function | Reconnection handler for the client |
connId | string | Unique connection identifier |
clientIp | string | Client IP address (normalized) |
canvasCapability | CanvasCapability | null | Current canvas capability or null |
Capability Expiration & Renewal
Capabilities are not permanent. The canvasCapabilityExpiresAtMs field tracks when a capability grant expires. The CronService runs a pruning task every 30 seconds to detect and handle expirations.
function pruneExpiredCapabilities(registry: NodeRegistry): number {
const now = Date.now();
let pruned = 0;
for (const [connId, client] of registry.entries()) {
if (
client.canvasCapabilityExpiresAtMs !== null &&
client.canvasCapabilityExpiresAtMs < now
) {
// Mark canvas capability as expired
client.canvasCapability = null;
client.canvasCapabilityExpiresAtMs = null;
client.canvasHostUrl = null;
pruned++;
// Notify client to re-negotiate
if (client.isAlive && client.socket.readyState === WebSocket.OPEN) {
client.socket.send(JSON.stringify({
type: "capability-expired",
capability: "canvas",
message: "Canvas capability has expired. Please re-send capabilities.",
}));
}
}
}
return pruned;
}
// Registered during boot as a CronService task
cronService.registerTask("prune-capabilities", "*/30 * * * * *", () => {
const count = pruneExpiredCapabilities(nodeRegistry);
if (count > 0) {
logger.info(`Pruned ${count} expired capabilities`);
}
});
capability-expired message can immediately re-send their capability flags without disconnecting. The Gateway updates the NodeRegistry entry in place, preserving the connection and session state.
Edge Cases & Error Handling
| Scenario | Behavior | Impact |
|---|---|---|
| Missing client version | Connection rejected with error: "missing-client-version" | Client must include v query param |
| Unsupported platform | Connection rejected with error: "invalid-platform" | Only android, ios, desktop, web allowed |
| Canvas capability deferred (iOS) | canvasCapability set to null initially | Node not routed canvas requests until reported |
| Expired capability, node offline | Capability pruned; node marked stale | Requests rerouted to other capable nodes |
| Duplicate connId | Previous entry evicted, new entry stored | Old socket closed with 4002 code |
| Renewal after expiration grace | New expiration timestamp computed from current time | Seamless capability restoration |
canvasHostUrl field is validated against an allowlist of trusted canvas hosts. Untrusted URLs are rejected to prevent SSRF attacks through capability registration.
Configuration Reference
| Parameter | Type | Default | Description |
|---|---|---|---|
capability.minClientVersion | string | "1.0.0" | Minimum supported client version |
capability.canvasHostAllowlist | string[] | [] | Allowed canvas host URLs |
capability.expirationGraceMs | number | 60000 | Grace period before pruning (60s) |
capability.pruneIntervalSec | number | 30 | How often to check for expired capabilities |
capability.maxNodesPerDevice | number | 3 | Max concurrent connections per device ID |
Related Topics
- Boot Overview — full boot subsystem documentation
- Startup Sequence — how NodeRegistry is initialized during boot
- Health Checks — monitoring node health and capabilities
Directory Map
Source files for this topic
src/gateway/
├── node-registry.ts ← NodeRegistry class, registerNode(), pruneExpiredCapabilities()
├── ws-client.ts ← GatewayWsClient interface, CanvasCapability type
├── connect-params.ts ← ConnectParams interface, parseConnectParams()
├── gateway-client.ts ← GatewayClient simplified interface
├── cron.ts ← CronService, capability pruning task registration
└── protocol/
└── index.ts ← ConnectAuth, capability flag constants