In the previous chapter, Structured Message Protocol, we defined the "language" (NDJSON) that our CLI and the remote AI use to understand each other.
Now, we need to figure out the shipping method. How do we actually move those JSON messages across the internet?
While you might think "just use the internet," corporate networks, firewalls, and proxies often have strict rules. Some block standard persistent connections (WebSockets), while others might timeout idle connections.
Imagine you are a logistics manager trying to ship packages (messages) between your house (CLI) and a warehouse (Server).
The Transport Strategy pattern allows our application to switch between "The Highway" and "The Side Roads" automatically, without changing the messages inside the packages.
Transport InterfaceTo make the rest of the application not care how data is sent, we create a standard interface. Every shipping method must follow these rules:
This allows the core logic (from Chapter 1) to simply say transport.write(message) without worrying if it's using a WebSocket or an HTTP POST.
This is the default and preferred method. It opens a bidirectional pipe. Both the CLI and the Server can shout down the pipe at any time.
It uses the standard ws protocol. It creates a persistent connection that stays open as long as the session is active.
One specific problem with "Highways" is that if no cars drive on them for a while, security guards (proxies) might close the road.
To prevent this, WebSocketTransport.ts implements a "Ping/Pong" or "Keep Alive" system.
// transports/WebSocketTransport.ts (Simplified)
// Every few seconds...
this.pingInterval = setInterval(() => {
if (this.state === 'connected') {
// 1. Check if the server replied to our last ping
if (!this.pongReceived) {
// If not, the connection is dead. Reconnect!
this.handleConnectionError();
return;
}
// 2. Send a new ping
this.pongReceived = false;
this.ws.ping();
}
}, 10000); // 10 seconds
Explanation: The CLI pokes the server every 10 seconds ("Are you there?"). If the server doesn't poke back, the CLI hangs up and tries to call again.
Some environments (like strict corporate VPNs) dislike WebSockets. For these cases, we use a Hybrid approach.
The SSETransport listens to a stream of text data. It has to be smart enough to assemble pieces of data into a full message.
// transports/SSETransport.ts (Simplified)
// Read the incoming stream chunk by chunk
while (true) {
const { done, value } = await reader.read();
if (done) break;
// Decode bytes to text
buffer += decoder.decode(value);
// Parse specific "data:" lines used by SSE
const { frames, remaining } = parseSSEFrames(buffer);
// Process each complete frame
frames.forEach(frame => this.handleSSEFrame(frame));
}
Explanation: The transport buffers incoming text. Once it sees a complete message (marked by newlines), it processes it.
Sending data via HTTP POST is slower than a WebSocket. To avoid clogging the network, we use a Batch Uploader. If the user sends 5 messages quickly, the HybridTransport might bundle them into a single HTTP POST.
The file transports/transportUtils.ts acts as the traffic controller. It looks at your configuration and decides which vehicle to use.
Here is how the system decides what to use. It relies heavily on environment variables ("Env Vars") or protocol checks.
// transports/transportUtils.ts (Simplified)
export function getTransportForUrl(url: URL): Transport {
// 1. Check if we are forced to use SSE (The Side Road)
if (process.env.CLAUDE_CODE_USE_CCR_V2) {
// Transform the URL to an SSE endpoint
const sseUrl = convertToSSEUrl(url);
return new SSETransport(sseUrl);
}
// 2. Otherwise, check if the URL supports WebSockets
if (url.protocol === 'ws:' || url.protocol === 'wss:') {
// Return the standard "Highway" transport
return new WebSocketTransport(url);
}
throw new Error('Unsupported protocol');
}
Explanation: The code checks process.env. If a special flag is set, it swaps the strategy. This is useful for debugging or specific deployment environments.
Both strategies implement Exponential Backoff. If the internet cuts out:
This prevents the CLI from "spamming" the server if the server is down, which could make the problem worse (a "Thundering Herd" problem).
// transports/WebSocketTransport.ts (Simplified)
private handleConnectionError() {
// Calculate delay: 1s * 2^(attempts)
const delay = Math.min(
1000 * Math.pow(2, this.reconnectAttempts),
30000 // Max 30 seconds
);
setTimeout(() => {
this.connect(); // Try again
}, delay);
}
In this chapter, we learned that Transport Strategies allow the CLI to adapt to different network environments without changing the core application logic.
Now that we have a reliable connection and a way to send structured messages, we need to handle the state of the remote AI. The AI needs to "remember" what we are doing, even if we disconnect and reconnect.
Next Chapter: CCR State Synchronization
Generated by Code IQ