current commit
260925.48 · doc,src,test: fix typos in code comments
#pragma once

#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

#include <aliased_struct.h>
#include <async_wrap.h>
#include <base_object.h>
#include <dataqueue/queue.h>
#include <env.h>
#include <memory_tracker.h>
#include <node_blob.h>
#include <node_bob.h>
#include <util.h>
#include "bindingdata.h"
#include "data.h"

#include <vector>

namespace node::quic {

class Session;
class Stream;

// Optional per-stream state owned by the negotiated application protocol.
// Stream deliberately treats this as opaque so protocol semantics do not
// become part of the transport stream abstraction.
class StreamApplicationState {
 public:
  virtual ~StreamApplicationState() = default;
};

// An elastic ring buffer used by Stream to coalesce received data before
// flushing it into the DataQueue. This avoids creating many small V8
// BackingStore allocations from per-QUIC-frame ngtcp2 callbacks. Data is
// memcpy'd into the ring buffer and flushed as a single right-sized
// BackingStore when the reader pulls or the buffer reaches its flush
// threshold. The buffer starts at kMinCapacity and can grow up to a
// configured maximum (typically the stream flow control window).
class RecvAccumulator final {
 public:
  static constexpr size_t kMinCapacity = 64 * 1024;        // 64 KB
  static constexpr size_t kFlushThreshold = kMinCapacity;  // flush at 64 KB

  explicit RecvAccumulator(size_t max_capacity);
  ~RecvAccumulator() = default;

  DISALLOW_COPY_AND_MOVE(RecvAccumulator)

  // Append data. Returns the number of bytes written (may be less than
  // len if the buffer is full). The caller should flush and retry with
  // the remaining bytes.
  size_t Write(const uint8_t* data, size_t len);

  // Flush accumulated data as a single DataQueue entry backed by a
  // right-sized V8 BackingStore. Returns nullptr if nothing is
  // accumulated. Resets the read/write cursors and shrinks the buffer
  // back toward kMinCapacity if it was expanded.
  std::unique_ptr<DataQueue::Entry> Flush(Environment* env);

  // Current number of bytes awaiting flush.
  size_t available() const { return len_; }

  // Number of bytes that can still be written before the buffer is full.
  size_t remaining() const { return buf_.size() - len_; }

  // True if accumulated data has reached the flush threshold.
  bool should_flush() const { return len_ >= kFlushThreshold; }

  // Grow the buffer capacity (doubles, up to max_capacity_).
  void Grow();

 private:
  std::vector<uint8_t> buf_;
  size_t max_capacity_;
  size_t read_pos_ = 0;
  size_t write_pos_ = 0;
  size_t len_ = 0;  // write_pos_ - read_pos_, accounting for wrap
};

using Ngtcp2Source = bob::SourceImpl<ngtcp2_vec>;

// When a request to open a stream is made before a Session is able to actually
// open a stream (either because the handshake is not yet sufficiently complete
// or concurrency limits are temporarily reached) then the request to open the
// stream is represented as a queued PendingStream.
//
// The PendingStream instance itself is owned by the stream created but a
// reference sits in a linked list in the session.
//
// The PendingStream request can be canceled by dropping the PendingStream
// instance before it can be fulfilled, at which point it is removed from the
// pending stream queue.
//
// Note that only locally initiated streams can be created in a pending state.
class PendingStream final {
 public:
  explicit PendingStream(Direction direction,
                         Stream* stream,
                         BaseObjectWeakPtr<Session> session);
  DISALLOW_COPY_AND_MOVE(PendingStream)
  ~PendingStream();

  // Called when the stream has been opened. Transitions the stream from a
  // pending state to an opened state.
  void fulfill(stream_id id);

  // Called when opening the stream fails or is canceled. Transitions the
  // stream into a closed/destroyed state.
  void reject(QuicError error = QuicError());

  inline Direction direction() const { return direction_; }

 private:
  Direction direction_;
  Stream* stream_;
  BaseObjectWeakPtr<Session> session_;
  bool waiting_ = true;

  ListNode<PendingStream> pending_stream_queue_;

 public:
  using PendingStreamQueue =
      ListHead<PendingStream, &PendingStream::pending_stream_queue_>;
};

// QUIC Stream's are simple data flows that may be:
//
// * Bidirectional (both sides can send) or Unidirectional (one side can send)
// * Server or Client Initiated
//
// The flow direction and origin of the stream are important in determining the
// write and read state (Open or Closed). Specifically:
//
// Bidirectional Stream States:
// +--------+--------------+----------+----------+
// |   ON   | Initiated By | Readable | Writable |
// +--------+--------------+----------+----------+
// | Server |   Server     |    Y     |    Y     |
// +--------+--------------+----------+----------+
// | Server |   Client     |    Y     |    Y     |
// +--------+--------------+----------+----------+
// | Client |   Server     |    Y     |    Y     |
// +--------+--------------+----------+----------+
// | Client |   Client     |    Y     |    Y     |
// +--------+--------------+----------+----------+
//
// Unidirectional Stream States
// +--------+--------------+----------+----------+
// |   ON   | Initiated By | Readable | Writable |
// +--------+--------------+----------+----------+
// | Server |   Server     |    N     |    Y     |
// +--------+--------------+----------+----------+
// | Server |   Client     |    Y     |    N     |
// +--------+--------------+----------+----------+
// | Client |   Server     |    Y     |    N     |
// +--------+--------------+----------+----------+
// | Client |   Client     |    N     |    Y     |
// +--------+--------------+----------+----------+
//
// All data sent via the Stream is buffered internally until either receipt is
// acknowledged from the peer or attempts to send are abandoned. The fact that
// data is buffered in memory makes it essential that the flow control for the
// session and the stream are properly handled. For now, we are largely relying
// on ngtcp2's default flow control mechanisms which generally should be doing
// the right thing. From the JavaScript side, the application pushes data into
// the stream's outbound queue and ngtcp2 pulls data from that queue as it is
// able. The stream outbound has a high watermark. The JS side can choose to
// continue writing data even after the high watermark is reached but this
// risks using up large amounts of memory if the session is slow to send data
// or the peer is slow to acknowledge receipt. The JavaScript side needs to
// be aware of this risk and pay proper attention to the backpressure signals.
//
// A Stream may be in a fully closed state (No longer readable nor writable)
// state but still have unacknowledged data in both the inbound and outbound
// queues.
//
// A Stream is gracefully closed when (a) both read and write states are closed,
// (b) all queued data has been acknowledged.
//
// The Stream may be forcefully closed immediately using destroy(err). This
// causes all queued outbound data to be cleared, pending JavaScript writes
// to be abandoned, the Stream to be immediately closed at the ngtcp2 level
// without waiting for any outstanding acknowledgements. Keep in mind, however,
// that the peer is not notified that the stream is destroyed and may attempt
// to continue sending data and acknowledgements until it is able to determine
// that the stream is gone. Any data that has already been received and is in
// the inbound queue is preserved and may be read by the application.
//
// Streams may be created in a pending state. This means that while the Stream
// object is created, it has not yet been opened in ngtcp2 and therefore has
// no official status yet. Certain operations can still be performed on the
// stream object such as providing data, adding headers, or destroying the
// stream.
//
// When a stream is created the data source for the stream must be given.
// If no data source is given, then the stream is assumed to not have any
// outbound data. If the stream was created as bidirectional, the outbound
// side will be closed. The data source can be fixed length or may support
// streaming. What this means practically is, when a stream is opened,
// you must already have a sense of whether that will provide data or
// not. When in doubt, specify a streaming data source, which can produce
// zero-length output.
class Stream final : public AsyncWrap,
                     public Ngtcp2Source,
                     public DataQueue::BackpressureListener {
 public:
  // Acquire a DataQueue from the given value if it is valid. The return
  // follows the typical V8 rules for Maybe types. If an error occurs,
  // the Maybe will be empty and an exception will be set on the isolate.
  static v8::Maybe<std::shared_ptr<DataQueue>> GetDataQueueFromSource(
      Environment* env, v8::Local<v8::Value> value);

  // The stream_user_data field is from ngtcp2 and will point to the
  // Stream instance associated with the stream_id.
  static Stream* From(void* stream_user_data);

  JS_CONSTRUCTOR(Stream);
  JS_BINDING_INIT_BOILERPLATE();

  // Creates a new non-pending stream. The directionality of the stream
  // is inferred from the stream id.
  static BaseObjectPtr<Stream> Create(
      Session* session,
      stream_id id,
      std::shared_ptr<DataQueue> source = nullptr);

  // Creates a new pending stream.
  static BaseObjectPtr<Stream> Create(
      Session* session,
      Direction direction,
      std::shared_ptr<DataQueue> source = nullptr);

  // The constructor is only public to be visible by MakeDetachedBaseObject.
  // Call Create to create new instances of Stream.
  Stream(BaseObjectWeakPtr<Session> session,
         v8::Local<v8::Object> obj,
         stream_id id,
         std::shared_ptr<DataQueue> source);

  // Creates the stream in a pending state. The constructor is only public
  // to be visible to MakeDetachedBaseObject. Call Create to create new
  // instances of Stream.
  Stream(BaseObjectWeakPtr<Session> session,
         v8::Local<v8::Object> obj,
         Direction direction,
         std::shared_ptr<DataQueue> source);
  DISALLOW_COPY_AND_MOVE(Stream)
  ~Stream() override;

  // While the stream is still pending, the id will be kMaxStreamId,
  // indicating the maximum possible stream id is kMaxStreamId - 1.
  stream_id id() const;

  // While the stream is still pending, the origin will be invalid.
  Side origin() const;

  Direction direction() const;

  Session& session() const;

  // Returns the most recent activity timestamp for this stream in
  // nanoseconds (uv_hrtime). Uses received_at if data has been received,
  // otherwise falls back to created_at. Returns 0 if neither is set.
  uint64_t last_activity_timestamp() const;

  // Records protocol-level receive activity that does not pass through
  // ReceiveData(), such as application framing metadata.
  void RecordReceivedActivity();

  // True if this stream was created in a pending state and is still waiting
  // to be created.
  bool is_pending() const;

  // True if the stream is already destroyed.
  bool is_destroyed() const;

  // True if we've completely sent all outbound data for this stream.
  // Importantly, this does not necessarily mean that we are completely
  // done with the outbound data. We may still be waiting on outbound
  // data to be acknowledged by the remote peer.
  bool is_eos() const;

  // Marks this stream as having received 0-RTT early data.
  void set_early();

  // True if this stream is still in a readable state.
  bool is_readable() const;

  // True if this stream is still in a writable state.
  bool is_writable() const;

  // True if an outbound data source has been configured.
  bool has_outbound() const;

  // True if a Blob::Reader has been created for the inbound data.
  bool has_reader() const;

  // Returns the Blob::Reader for the inbound data, or nullptr.
  Blob::Reader* reader() const;

  StreamApplicationState* application_state() const {
    return application_state_.get();
  }
  void set_application_state(
      std::unique_ptr<StreamApplicationState> application_state) {
    application_state_ = std::move(application_state);
  }

  StreamPriority priority() const { return priority_.priority; }
  StreamPriorityFlags priority_flags() const { return priority_.flags; }

  // Called by the session/application to indicate that the specified number
  // of bytes have been acknowledged by the peer.
  void Acknowledge(size_t datalen);

  // Called by the session/application to indicate that the specified number
  // of bytes have been transmitted to the peer.  This is an initial
  // indication occurring the first time data is sent. It does not indicate
  // that the data has been retransmitted due to loss or has been
  // acknowledged to have been received by the peer.
  void Commit(size_t datalen, bool fin = false);

  // Updates the write_desired_size state field based on current flow control
  // and outbound buffer state. Emits drain if transitioning from 0 to > 0.
  void UpdateWriteDesiredSize();

  void EndWritable();
  // The read side ended cleanly with a peer FIN: the content is complete.
  void FinishReadable();
  // The read side ended without a FIN (a reset, a local abort, or the session
  // being torn down) so the content is truncated.
  void TruncateReadable(
      std::optional<uint64_t> maybe_final_size = std::nullopt);
  void EntryRead(size_t amount) override;
  void BeforePull() override;

  // Pulls data from the internal outbound DataQueue configured for this stream.
  // This is called by the session/application when it is preparing to send
  // data to the peer. There is no guarantee that the requested amount of data
  // will actually be sent. The amount of data actually sent is indicated
  // by the datalen argument to the Commit() method.
  int DoPull(bob::Next<ngtcp2_vec> next,
             int options,
             ngtcp2_vec* data,
             size_t count,
             size_t max_count_hint) override;

  // Forcefully close the stream immediately. Data already queued in the
  // inbound is preserved but new data will not be accepted. All pending
  // writes are abandoned, and the stream is immediately closed at the ngtcp2
  // level without waiting for any outstanding acknowledgements.
  void Destroy(QuicError error = QuicError());

  struct ReceiveDataFlags final {
    // Identifies the final chunk of data that the peer will send for the
    // stream.
    bool fin = false;
    // Indicates that this chunk of data was received in a 0RTT packet before
    // the TLS handshake completed, suggesting that is is not as secure and
    // could be replayed by an attacker.
    bool early = false;
  };

  void ReceiveData(const uint8_t* data, size_t len, ReceiveDataFlags flags);
  void ReceiveStopSending(QuicError error);
  void ReceiveStreamReset(uint64_t final_size, QuicError error);

  // Sends a reset stream to the peer to tell it we will not be sending any
  // more data for this stream. This has the effect of shutting down the
  // writable side of the stream for this peer. Any data that is held in the
  // outbound queue will be dropped. The stream may still be readable.
  void DoStreamReset(error_code code);

  // Tells the peer to stop sending data for this stream. This has the effect
  // of shutting down the readable side of the stream for this peer. Any data
  // that has already been received is still readable.
  void SendStopSending(error_code code);

  // TODO(@jasnell): Implement MemoryInfo to track outbound_, inbound_,
  // reader_, and application_state_.
  SET_NO_MEMORY_INFO()
  SET_MEMORY_INFO_NAME(Stream)
  SET_SELF_SIZE(Stream)

  struct State;
  struct Stats;

  // Typed accessors for arena-allocated state/stats. These are defined
  // in streams.cc where State and Stats are complete types.
  inline State* state() { return static_cast<State*>(state_slot_.ptr); }
  inline const State* state() const {
    return static_cast<const State*>(state_slot_.ptr);
  }
  inline Stats* stats() { return static_cast<Stats*>(stats_slot_.ptr); }
  inline const Stats* stats() const {
    return static_cast<const Stats*>(stats_slot_.ptr);
  }

 private:
  struct Impl;

  class Outbound;

  // Flushes any data accumulated in the receive ring buffer into the
  // inbound DataQueue as a single right-sized entry.
  void FlushAccumulation();

  // Every byte ngtcp2 delivers is charged against both the stream-level and
  // the connection-level receive window until we hand the credit back.
  enum class CreditScope : uint8_t {
    // The stream is still readable, so the peer may usefully send more on it.
    STREAM_AND_CONNECTION,
    // The stream is finished, so only the session-wide window is extended.
    CONNECTION_ONLY,
  };

  // Returns `amount` bytes of inbound flow control credit to the peer.
  void ReturnFlowControlCredit(uint64_t amount, CreditScope scope);

  // Returns credit for bytes that have left our custody, either read by the
  // consumer or dropped before reaching one.
  void CreditConsumedBytes(uint64_t amount);

  // Common tail of FinishReadable()/TruncateReadable(): marks the read side
  // ended, caps it at the final size, and notifies the reader.
  void CapReadable(std::optional<uint64_t> maybe_final_size);

  // Gets a reader for the data received for this stream from the peer,
  BaseObjectPtr<Blob::Reader> get_reader();

  void set_outbound(std::shared_ptr<DataQueue> source);

  // Streaming outbound support
  void InitStreaming();
  void WriteStreamData(const v8::FunctionCallbackInfo<v8::Value>& args);
  void EndWriting();

  bool is_local_unidirectional() const;
  bool is_remote_unidirectional() const;
  void ReleaseArenaSlots();

  // JavaScript callouts

  // Notifies the JavaScript side that a previously pending stream
  // is now available.
  void EmitStreamAvailable();

  // Notifies the JavaScript side that the stream has been destroyed.
  void EmitClose(const QuicError& error);

  // Notifies the JavaScript side that the stream has been reset.
  void EmitReset(const QuicError& error);

  // Notifies the JavaScript side that the peer asked it to stop sending.
  void EmitStopSending(const QuicError& error);

  // Notifies the JavaScript side that sending data on the stream has been
  // blocked because of flow control restriction.
  void EmitBlocked();

  // Notifies the JavaScript side that the outbound buffer has capacity
  // for more data. Fires when write_desired_size transitions from 0 to > 0.
  void EmitDrain();

  void NotifyReadableEnded(error_code code);
  void NotifyWritableEnded(error_code code);

  // When a pending stream is finally opened, the NotifyStreamOpened method
  // will be called and the id will be assigned.
  void NotifyStreamOpened(stream_id id);
  ArenaSlotBase stats_slot_;
  ArenaSlotBase state_slot_;
  BaseObjectWeakPtr<Session> session_;
  std::unique_ptr<Outbound> outbound_;
  std::shared_ptr<DataQueue> inbound_;
  BaseObjectWeakPtr<Blob::Reader> reader_;
  std::unique_ptr<RecvAccumulator> recv_accumulator_;
  std::unique_ptr<StreamApplicationState> application_state_;

  // Bytes delivered to ReceiveData() that still hold inbound flow control
  // credit. Returned incrementally as the consumer reads them, and in bulk
  // when the stream is destroyed -- otherwise abandoning a stream with unread
  // data would permanently shrink the session's receive window. Data still
  // buffered inside nghttp3 is deliberately not counted: nghttp3 returns that
  // credit itself through its deferred_consume callback.
  uint64_t uncredited_bytes_ = 0;

  // If the stream cannot be opened yet, it will be created in a pending state.
  // Once the owning session is able to, it will complete opening of the stream
  // and the stream id will be assigned.
  std::optional<std::unique_ptr<PendingStream>> maybe_pending_stream_ =
      std::nullopt;
  error_code pending_close_read_code_ = 0;
  error_code pending_close_write_code_ = 0;

  struct StoredPriority {
    StreamPriority priority = StreamPriority::DEFAULT;
    StreamPriorityFlags flags = StreamPriorityFlags::NON_INCREMENTAL;
    bool pending = false;
  };
  StoredPriority priority_;

  friend struct Impl;
  friend class PendingStream;
  friend class DefaultApplication;

 public:
  // The Queue/Schedule here are part of the mechanism used to
  // determine which streams have data to send on the session. When a stream
  // potentially has data available, it will be scheduled in the Queue. Then,
  // when the Session::Application starts sending pending data, it will check
  // the queue to see if there are streams waiting. If there are, it will grab
  // one and check to see if there is data to send. When a stream does not have
  // data to send (such as when it is initially created or is using an async
  // source that is still waiting for data to be pushed) it will not appear in
  // the queue.
  ListNode<Stream> stream_queue_;
  using Queue = ListHead<Stream, &Stream::stream_queue_>;

  void Schedule(Queue* queue);
  void Unschedule();
};

}  // namespace node::quic

#endif  // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS