A <Client> can be understood as a state machine. As requests are dispatched,
processed, and drained, the client moves through a well-defined set of states
until it is eventually destroyed. This document describes those states and the
transitions between them.
The <Client> class is not literally implemented as a state machine, so the
actual execution may deviate slightly from what is described here. Treat this
guide as a conceptual model for reasoning about a client's behavior rather than
as a formal specification.
- A
<Client>begins in the idle state with no socket connection and no queued requests.- The connect transition moves the
<Client>to the pending state, where requests can be queued before they are processed. - Calling
client.close()orclient.destroy()moves the<Client>to the destroyed state. Because there are no queued requests in this state, close transitions straight to destroyed.
- The connect transition moves the
- The pending state indicates that the underlying socket connection has been established and that requests are queueing.
- The processing state initializes to the processing.running sub-state.
- If the current request body requires draining, needDrain moves the
<Client>into the processing.busy sub-state, which returns to processing.running once drainComplete fires. - After all queued requests complete, keepalive moves the
<Client>back to the pending state. If no requests are queued before the socket times out, the<Client>transitions to idle. - If close is fired while the
<Client>still has queued requests, the<Client>transitions to processing.closing, where it completes all outstanding requests before firing done. - The done transition moves the
<Client>gracefully to destroyed. - At any time, destroy moves the
<Client>from processing to destroyed, aborting any queued requests.
- If the current request body requires draining, needDrain moves the
- The destroyed state is final; the
<Client>is no longer functional.
A state diagram representing a <Client> instance:
stateDiagram-v2
[*] --> idle
idle --> pending : connect
idle --> destroyed : destroy/close
pending --> idle : timeout
pending --> destroyed : destroy
state close_fork <<fork>>
pending --> close_fork : close
close_fork --> processing
close_fork --> destroyed
pending --> processing : process
processing --> pending : keepalive
processing --> destroyed : done
processing --> destroyed : destroy
destroyed --> [*]
state processing {
[*] --> running
running --> closing : close
running --> busy : needDrain
busy --> running : drainComplete
running --> [*] : keepalive
closing --> [*] : done
}idle is the initial state of a <Client> instance. Although an origin is
required to construct a <Client>, the underlying socket connection is not
established until a request is queued through client.dispatch(). Calling
client.dispatch() directly, or through one of its higher-level wrappers
(client.connect(), client.pipeline(), client.request(),
client.stream(), or client.upgrade()), moves the <Client> from idle to pending, and then usually directly on to processing.
Calling client.close() or client.destroy() in this state moves the
<Client> straight to destroyed, since there are no queued requests.
pending signifies a connected but non-processing <Client>. On entering this
state, the <Client> establishes a socket connection and emits the 'connect' event, signalling that a connection to the origin supplied at
construction time has been established. The internal queue starts empty, and
requests can begin queueing.
Calling client.close() with queued requests moves the <Client> to the processing state; without queued requests it moves to destroyed.
Calling client.destroy() moves the <Client> directly to destroyed,
regardless of any queued requests.
processing is itself a sub-state machine that initializes to the
processing.running sub-state. client.dispatch(), client.close(),
and client.destroy() can all be called while the <Client> is in this
state. client.dispatch() queues additional requests while existing ones
continue to be processed, client.close() moves the <Client> to the processing.closing sub-state, and client.destroy() moves it to
destroyed.
In processing.running, queued requests are processed in FIFO order. If a
request body requires draining, needDrain moves the <Client> to the processing.busy sub-state. The close transition moves the <Client> to the processing.closing sub-state. When all queued requests have completed and
neither client.close() nor client.destroy() has been called, the
keepalive transition moves the <Client> back to the pending state. There
the <Client> waits for the socket connection to time out; once it does, the
<Client> returns to the idle state.
This sub-state is entered only when a request body is a <stream.Readable> that
requires draining. The <Client> cannot process additional requests while in
this state and waits until the current request body has been fully drained
before returning to processing.running.
This sub-state is entered only when a <Client> has queued requests and client.close() is called. The <Client> continues to process requests as
usual, except that no additional requests may be queued. Once all queued
requests have been processed, done fires and the <Client> moves gracefully to destroyed without an error.
destroyed is the final state of a <Client> instance. Once in this state, the
<Client> is no longer functional, and calling any further <Client> method
rejects or fails with a <ClientDestroyedError>.