Skip to content

Concepts: the pipeline

Eugene Lazutkin edited this page Jul 3, 2026 · 2 revisions

Concepts: the pipeline & extensibility

Every request flows through one pipeline. Knowing the order is how you know where to hook in — the whole point of the library is to make that setup cheap so calls stay trivial.

The request lifecycle

  1. Normalize — merge url / data / options into one options object.
  2. Prepare — build the URL (keys & URLs), headers, and body (encodeBody).
  3. Request inspectors — matching io.inspect.request(fn, match?) entries may rewrite the prepared request.
  4. Key — the canonical request key (io.makeKey) for dedup / cache lookups.
  5. Deduptrack, in the core (not a service): an identical in-flight GET is joined instead of re-run.
  6. Services onion — each attached service (by priority) may short-circuit (cache hit, mock) or pass through, wrapping the transport.
  7. Transportfetch by default (fetch transport).
  8. Decode — parse the body by content-type (or as forced by the decode option), or hand back the raw stream for {stream: true}.
  9. Envelope — build the response envelope; run matching response inspectors (io.inspect.response(fn, match?)); throw BadStatus on a non-2xx (unless ignoreBadStatus).

The seams

Everything extensible threads through this one flow:

Seam Register with Runs at
Request inspector io.inspect.request(fn, match?) 3 — rewrite the outgoing request
Response inspector io.inspect.response(fn, match?) 9 — post-process / replace the envelope
Data processor io.registerData(p) 2 — encode a request body by type
MIME processor io.registerMime(p) 8 — decode a response by content-type
Service io.attach(service) / io.detach(name) 6 — short-circuit or wrap the transport
Transport io.registerTransport(name, fn) 7 — swap the network layer

Inspectors are scoped: the optional match is a URL prefix string, a RegExp, or a predicate (url) => boolean, and the inspector runs only for matching request URLs — the registries (io.requestInspectors / io.responseInspectors) hold {fn, match} entries.

Services carry a priority (higher wraps outermost), so cache, retry, and mock compose as one onion. Participation is decided per request: a per-request flag (cache: false, retry: …, mock: false, track: true) wins when present; where a facility ships a default set (io.cache.theDefault, io.track.theDefault — a boolean or a predicate (options) => boolean, shipped as options => !options.transport), it governs the rest.

Events

The pipeline reports through events (io.on / io.off / io.emit): 'request' (request, ctx) when a run starts, 'success' (envelope, ctx), 'failure' (error, ctx), 'retry' ({attempt, response, error}, ctx) per retry attempt, and a one-time 'ready' after the code-forward drain. io.inFlight counts active runs. A cache hit still runs the pipeline, so it emits request / success.

See also

Clone this wiki locally