@@ -617,21 +617,29 @@ struct Stream::Impl {
617617 ASSIGN_OR_RETURN_UNWRAP (&stream, args.This ());
618618 DCHECK (args[0 ]->IsArrayBufferView ());
619619
620- // A datagram can only be associated with a stream that has a real id.
621- // A pending stream has no id yet, so there is nothing to bind to.
622- if (stream->is_pending ()) {
623- return args.GetReturnValue ().Set (BigInt::New (env->isolate (), 0 ));
624- }
625-
626620 Store store;
627621 if (!Store::From (args[0 ].As <ArrayBufferView>()).To (&store)) {
628622 return ;
629623 }
630624
625+ // Mint the id up front. It is exposed (returned non-zero to JS) only
626+ // if the datagram is committed - queued now, or buffered for a pending
627+ // stream. Sync rejection returns 0 and discards (no subsequent status).
628+ datagram_id id = stream->session ().ReserveDatagramId ();
629+
630+ // A pending stream has no id yet, so the datagram cannot be framed and
631+ // bound to a Quarter Stream ID. Buffer it and flush when the stream opens.
632+ // If it cannot be buffered (queue full) the id is discarded.
633+ if (stream->is_pending ()) {
634+ bool buffered = stream->EnqueuePendingDatagram (id, std::move (store));
635+ return args.GetReturnValue ().Set (
636+ BigInt::New (env->isolate (), buffered ? id : 0 ));
637+ }
638+
631639 Session::SendPendingDataScope send_scope (&stream->session ());
632- datagram_id id =
633- stream-> session (). application (). SendDatagram (stream , std::move (store));
634- args.GetReturnValue ().Set (BigInt::New (env->isolate (), id ));
640+ datagram_id result = stream-> session (). application (). SendDatagram (
641+ stream, std::move (store), id );
642+ args.GetReturnValue ().Set (BigInt::New (env->isolate (), result ));
635643 }
636644};
637645
@@ -1313,6 +1321,21 @@ void Stream::NotifyStreamOpened(stream_id id) {
13131321 headers->flags );
13141322 }
13151323 }
1324+ if (!pending_datagram_queue_.empty ()) {
1325+ // Like the headers flush above, this runs inside an ngtcp2 callback, so
1326+ // the queued datagrams ride the session's deferred flush; no send scope.
1327+ std::deque<PendingDatagram> queue;
1328+ pending_datagram_queue_.swap (queue);
1329+ pending_datagram_bytes_ = 0 ;
1330+ for (auto & dgram : queue) {
1331+ // The id was already returned to JS. If the send is rejected now (e.g.
1332+ // the framed size exceeds the peer's max) we report ABANDONED.
1333+ if (session ().application ().SendDatagram (
1334+ this , std::move (dgram.data ), dgram.id ) == 0 ) {
1335+ session ().DatagramStatus (dgram.id , DatagramStatus::ABANDONED );
1336+ }
1337+ }
1338+ }
13161339 // If the stream is not a local undirectional stream and is_readable is
13171340 // false, then we should shutdown the streams readable side now.
13181341 if (!is_local_unidirectional () && !is_readable ()) {
@@ -1350,6 +1373,23 @@ void Stream::EnqueuePendingHeaders(HeadersKind kind,
13501373 kind, Global<Array>(env ()->isolate (), headers), flags));
13511374}
13521375
1376+ bool Stream::EnqueuePendingDatagram (datagram_id id, Store&& store) {
1377+ // Bound the pending queue by the stream's outbound high water mark. When
1378+ // full, we refuse to send the datagram (its id is discarded by the caller).
1379+ // A high water mark of 0 means unbounded, matching the stream-write path.
1380+ uint64_t hwm = state ()->high_water_mark ;
1381+ size_t incoming = store.length ();
1382+ if (hwm > 0 && !pending_datagram_queue_.empty () &&
1383+ pending_datagram_bytes_ + incoming > hwm) {
1384+ Debug (this , " Pending datagram buffer full, refusing datagram %" PRIu64, id);
1385+ return false ;
1386+ }
1387+ Debug (this , " Buffering datagram %" PRIu64 " for pending stream" , id);
1388+ pending_datagram_bytes_ += incoming;
1389+ pending_datagram_queue_.push_back ({id, std::move (store)});
1390+ return true ;
1391+ }
1392+
13531393bool Stream::is_pending () const {
13541394 return state ()->pending ;
13551395}
@@ -1683,6 +1723,19 @@ void Stream::Destroy(QuicError error) {
16831723 }
16841724 state ()->pending = 0 ;
16851725
1726+ // Datagrams still buffered for a pending stream had their ids returned to
1727+ // JS but will never reach the wire, so abandon them here to close each
1728+ // handle. If the session is already gone there is nobody to notify.
1729+ if (!pending_datagram_queue_.empty ()) {
1730+ if (!session_->is_destroyed ()) {
1731+ for (auto & dgram : pending_datagram_queue_) {
1732+ session_->DatagramStatus (dgram.id , DatagramStatus::ABANDONED );
1733+ }
1734+ }
1735+ pending_datagram_queue_.clear ();
1736+ pending_datagram_bytes_ = 0 ;
1737+ }
1738+
16861739 maybe_pending_stream_.reset ();
16871740
16881741 // End the writable before marking as destroyed.
0 commit comments