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.

ElementPurpose
Request IDMatch response to caller
Pending mapid → {resolve, reject, timer}
TimeoutReject stalled calls
CancellationAbortController

2. Generating Unique Message IDs

SourceNotes
crypto.randomUUID()v4 UUID, 36 chars
ULIDSortable, 26 chars
Snowflake64-bit time+seq
Monotonic counterPer-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

StrategyDetail
Per-request timersetTimeout + clearTimeout
Bulk reaperSingle interval scans for expired
Server timeoutReturn error message with id

5. Matching Responses to Requests

FieldDetail
idSame as request
typerpc.res / rpc.err
resultSuccess payload
error{code, message}

6. Handling Request Errors

SourceAction
App errorReject with code+message
TimeoutReject with TimeoutError
Connection lostReject all pending with ConnectionClosed
AbortReject 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 }));
}
StyleNotes
Node-style cbcb(err, result)
PromisePreferred, supports await
ObservableFor streaming responses

8. Using Promises for Requests

BenefitDetail
async/awaitClean syntax
CompositionPromise.all for parallel
Error propagationTry/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

SpecDetail
JSON-RPC 2.0Standard envelope, error codes
gRPC-WebHTTP/2 based, not WS
CustomLean 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" } }