Skip to content

Commit 6736ee2

Browse files
committed
quic: add promise to QuicStream for pending strms
before this PR, it was necessary to poll, if a stream can not be created immediately due to flow control. This PR adds a promise to QuicStream, that fulfills, when a stream is available and ready. Signed-off-by: Marten Richter <marten.richter@freenet.de>
1 parent ad67159 commit 6736ee2

10 files changed

Lines changed: 90 additions & 0 deletions

File tree

‎doc/api/quic.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1921,6 +1921,19 @@ Either `'application'` or `'transport'`. Indicates the namespace of
19211921
added: v23.8.0
19221922
-->
19231923

1924+
### `stream.ready`
1925+
1926+
<!-- YAML
1927+
added: REPLACEME
1928+
-->
1929+
1930+
* Type: {Promise}
1931+
1932+
A promise that is immediately fulfilled, if the stream fits within
1933+
flow control limits or fulfilled when the pending stream is created.
1934+
It rejects, if a pending stream is closed with an error before being
1935+
created.
1936+
19241937
### `stream.closed`
19251938

19261939
<!-- YAML

‎lib/internal/quic/quic.js‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ const kEmptyObject = { __proto__: null };
182182

183183
const {
184184
kAttachFileHandle,
185+
kAvailable,
185186
kBlocked,
186187
kConnect,
187188
kDatagram,
@@ -946,6 +947,11 @@ setCallbacks({
946947
},
947948

948949
// QuicStream callbacks
950+
onStreamAvailable() {
951+
debug('stream available callback', this[kOwner]);
952+
this[kOwner][kAvailable]();
953+
},
954+
949955
onStreamBlocked() {
950956
debug('stream blocked callback', this[kOwner]);
951957
// Called when the stream C++ handle has been blocked by flow control.
@@ -1588,6 +1594,7 @@ class QuicStream {
15881594
fileHandle: undefined,
15891595
headers: undefined,
15901596
pendingTrailers: undefined,
1597+
pendingStream: PromiseWithResolvers(),
15911598
onerror: undefined,
15921599
onblocked: undefined,
15931600
onreset: undefined,
@@ -1641,6 +1648,10 @@ class QuicStream {
16411648
inner.state = new QuicStreamState(
16421649
kPrivateConstructor, handle.state, handle.stateByteOffset);
16431650

1651+
if (!inner.state.pending) {
1652+
inner.pendingStream.resolve();
1653+
}
1654+
16441655
if (hasObserver('quic')) {
16451656
startPerf(this, kPerfEntry, { type: 'quic', name: 'QuicStream' });
16461657
}
@@ -1705,6 +1716,15 @@ class QuicStream {
17051716
return this.#inner.state.pending;
17061717
}
17071718

1719+
/**
1720+
* Promise that resolves once the stream is available and not pending.
1721+
* @type {Promise<void>}
1722+
*/
1723+
get ready() {
1724+
assertIsQuicStream(this);
1725+
return this.#inner.pendingStream.promise;
1726+
}
1727+
17081728
/**
17091729
* True if any data on this stream was received as 0-RTT (early data)
17101730
* before the TLS handshake completed. Early data is less secure and
@@ -2566,6 +2586,13 @@ class QuicStream {
25662586
} else {
25672587
inner.pendingClose.resolve();
25682588
}
2589+
if (inner.pending) {
2590+
if (error !== undefined) {
2591+
inner.pendingStream.reject(error);
2592+
} else {
2593+
inner.pendingStream.resolve(error);
2594+
}
2595+
}
25692596
debug('stream closed');
25702597
if (onStreamClosedChannel.hasSubscribers) {
25712598
onStreamClosedChannel.publish({
@@ -2611,6 +2638,12 @@ class QuicStream {
26112638
}
26122639
}
26132640

2641+
[kAvailable]() {
2642+
// The formerly pending stream is now available
2643+
const inner = this.#inner;
2644+
inner.pendingStream.resolve();
2645+
}
2646+
26142647
[kBlocked]() {
26152648
const inner = this.#inner;
26162649
// The blocked event should only be called if the stream was created with

‎lib/internal/quic/symbols.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const {
2828
// public API.
2929

3030
const kAttachFileHandle = Symbol('kAttachFileHandle');
31+
const kAvailable = Symbol('kAvailable');
3132
const kBlocked = Symbol('kBlocked');
3233
const kConnect = Symbol('kConnect');
3334
const kDrain = Symbol('kDrain');
@@ -63,6 +64,7 @@ const kVersionNegotiation = Symbol('kVersionNegotiation');
6364

6465
module.exports = {
6566
kAttachFileHandle,
67+
kAvailable,
6668
kBlocked,
6769
kConnect,
6870
kDatagram,

‎src/quic/bindingdata.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class SessionManager;
5555
V(session_path_validation, SessionPathValidation) \
5656
V(session_ticket, SessionTicket) \
5757
V(session_version_negotiation, SessionVersionNegotiation) \
58+
V(stream_available, StreamAvailable) \
5859
V(stream_blocked, StreamBlocked) \
5960
V(stream_close, StreamClose) \
6061
V(stream_created, StreamCreated) \

‎src/quic/streams.cc‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1277,6 +1277,8 @@ void Stream::NotifyStreamOpened(stream_id id) {
12771277
// since the stream likely hasn't had any opporunity to get blocked
12781278
// yet, but just for completeness, let's make sure.
12791279
if (outbound_) session().ResumeStream(id);
1280+
// We inform, the js side that the pending stream is now available
1281+
EmitStreamAvailable();
12801282
}
12811283

12821284
void Stream::NotifyReadableEnded(error_code code) {
@@ -1886,6 +1888,14 @@ void Stream::SendStopSending(error_code code) {
18861888

18871889
// ============================================================================
18881890

1891+
void Stream::EmitStreamAvailable() {
1892+
if (!env()->can_call_into_js()) {
1893+
return;
1894+
}
1895+
CallbackScope<Stream> cb_scope(this);
1896+
MakeCallback(BindingData::Get(env()).stream_available_callback(), 0, nullptr);
1897+
}
1898+
18891899
void Stream::EmitBlocked() {
18901900
// state()->wants_block will be set from the javascript side if the
18911901
// stream object has a handler for the blocked event.

‎src/quic/streams.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,10 @@ class Stream final : public AsyncWrap,
428428

429429
// JavaScript callouts
430430

431+
// Notifies the JavaScript side that a previously pending stream
432+
// is now available.
433+
void EmitStreamAvailable();
434+
431435
// Notifies the JavaScript side that the stream has been destroyed.
432436
void EmitClose(const QuicError& error);
433437

‎test/parallel/test-quic-internal-setcallbacks.mjs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const callbacks = {
2626
onSessionOrigin() {},
2727
onSessionGoaway() {},
2828
onSessionVersionNegotiation() {},
29+
onStreamAvailable() {},
2930
onStreamCreated() {},
3031
onStreamBlocked() {},
3132
onStreamClose() {},

‎test/parallel/test-quic-stream-limits-pending.mjs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,20 +38,31 @@ const serverEndpoint = await listen(mustCall((serverSession) => {
3838
const clientSession = await connect(serverEndpoint.address);
3939
await clientSession.opened;
4040

41+
let ready = 0;
42+
4143
// First stream opens immediately (within the limit).
4244
const s1 = await clientSession.createBidirectionalStream({
4345
body: encoder.encode('stream 1'),
4446
});
4547

48+
s1.ready.then(() => {
49+
ready++;
50+
});
51+
4652
// Second stream is created but queued as pending because the
4753
// server only allows 1 concurrent bidi stream.
4854
const s2 = await clientSession.createBidirectionalStream({
4955
body: encoder.encode('stream 2'),
5056
});
5157

58+
s2.ready.then(() => {
59+
ready++;
60+
});
61+
5262
// s2 should be pending until s1 closes and the server grants
5363
// more stream credits.
5464
assert.strictEqual(s2.pending, true);
65+
assert.strictEqual(ready, 1);
5566

5667
// Drain and close the first stream.
5768
for await (const _ of s1) { /* drain */ } // eslint-disable-line no-unused-vars
@@ -60,6 +71,7 @@ await s1.closed;
6071
// After s1 closes, the server sends MAX_STREAMS which opens s2.
6172
// Wait for the server to receive both streams.
6273
await allDone.promise;
74+
assert.strictEqual(ready, 2);
6375

6476
// s2 should no longer be pending.
6577
for await (const _ of s2) { /* drain */ } // eslint-disable-line no-unused-vars

‎test/parallel/test-quic-stream-limits-uni.mjs‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,20 +34,33 @@ const serverEndpoint = await listen(mustCall((serverSession) => {
3434
const clientSession = await connect(serverEndpoint.address);
3535
await clientSession.opened;
3636

37+
let ready = 0;
38+
3739
// First uni stream opens immediately.
3840
const s1 = await clientSession.createUnidirectionalStream({
3941
body: encoder.encode('uni 1'),
4042
});
4143

44+
s1.ready.then(() => {
45+
ready++;
46+
});
47+
4248
// Second uni stream is pending (limit = 1).
4349
const s2 = await clientSession.createUnidirectionalStream({
4450
body: encoder.encode('uni 2'),
4551
});
52+
53+
s2.ready.then(() => {
54+
ready++;
55+
});
56+
assert.strictEqual(ready, 1);
57+
4658
assert.strictEqual(s2.pending, true);
4759

4860
// Wait for both to complete.
4961
await s1.closed;
5062
await allDone.promise;
63+
assert.strictEqual(ready, 2);
5164
await s2.closed;
5265

5366
await clientSession.close();

‎typings/internalBinding/quic.d.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ interface QuicCallbacks {
1919
versions: number[],
2020
supports: number[]) => void;
2121
onStreamCreated: (stream: Stream) => void;
22+
onStreamAvailable: () => void;
2223
onStreamBlocked: () => void;
2324
onStreamClose: (error: [number,bigint,string]) => void;
2425
onStreamReset: (error: [number,bigint,string]) => void;

0 commit comments

Comments
 (0)