Implementing Request-Response Pattern
1. Understanding Request-Response over WebSocket
WebSocket is message-oriented, not request/response. Build it at app level using correlation IDs and a pending-promise registry.
| Element | Purpose |
| Request ID | Match response to caller |
| Pending map | id → {resolve, reject, timer} |
| Timeout | Reject stalled calls |
| Cancellation | AbortController |
2. Generating Unique Message IDs
| Source | Notes |
crypto.randomUUID() | v4 UUID, 36 chars |
| ULID | Sortable, 26 chars |
| Snowflake | 64-bit time+seq |
| Monotonic counter | Per-connection, smallest |
3. Tracking Pending Requests
Example: Promise-based RPC
const pending = new Map();
function request(method, params, { timeout = 10000 } = {}) {
const id = crypto.randomUUID();
return new Promise((resolve, reject) => {
const t = setTimeout(() => {
pending.delete(id); reject(new Error("timeout"));
}, timeout);
pending.set(id, { resolve, reject, t });
ws.send(JSON.stringify({ "type": "rpc.req", "id": id, "method": method, "params": params }));
});
}
ws.addEventListener("message", (e) => {
const m = JSON.parse(e.data);
const p = pending.get(m.id);
if (!p) return;
clearTimeout(p.t); pending.delete(m.id);
m.error ? p.reject(m.error) : p.resolve(m.result);
});
4. Implementing Timeout Handling
| Strategy | Detail |
| Per-request timer | setTimeout + clearTimeout |
| Bulk reaper | Single interval scans for expired |
| Server timeout | Return error message with id |
5. Matching Responses to Requests
| Field | Detail |
id | Same as request |
type | rpc.res / rpc.err |
result | Success payload |
error | {code, message} |
6. Handling Request Errors
| Source | Action |
| App error | Reject with code+message |
| Timeout | Reject with TimeoutError |
| Connection lost | Reject all pending with ConnectionClosed |
| Abort | Reject with AbortError |
7. Implementing Callbacks
Example: Callback variant
function call(method, params, cb) {
const id = crypto.randomUUID();
pending.set(id, { cb });
ws.send(JSON.stringify({ "type":"rpc.req", "id":id, "method":method, "params":params }));
}
| Style | Notes |
| Node-style cb | cb(err, result) |
| Promise | Preferred, supports await |
| Observable | For streaming responses |
8. Using Promises for Requests
| Benefit | Detail |
| async/await | Clean syntax |
| Composition | Promise.all for parallel |
| Error propagation | Try/catch boundaries |
9. Canceling Pending Requests
Example: AbortSignal integration
function request(method, params, { signal } = {}) {
const id = crypto.randomUUID();
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject });
signal?.addEventListener("abort", () => {
pending.delete(id);
ws.send(JSON.stringify({ "type":"rpc.cancel", "id": id }));
reject(new DOMException("aborted","AbortError"));
});
ws.send(JSON.stringify({ "type":"rpc.req", "id":id, "method":method, "params":params }));
});
}
10. Implementing RPC Pattern
| Spec | Detail |
| JSON-RPC 2.0 | Standard envelope, error codes |
| gRPC-Web | HTTP/2 based, not WS |
| Custom | Lean for one-app systems |
Example: JSON-RPC 2.0 envelope
{ "jsonrpc": "2.0", "id": 1, "method": "user.get", "params": { "id": 42 } }
{ "jsonrpc": "2.0", "id": 1, "result": { "id": 42, "name": "Ada" } }
{ "jsonrpc": "2.0", "id": 1, "error": { "code": -32601, "message": "method not found" } }