Skip to content

Commit 2003bae

Browse files
author
SparshGarg999
committed
quic: align stream priority with W3C WebTransport sendOrder
Adds the numeric sendOrder option to createBidirectionalStream, createUnidirectionalStream, and setPriority, aligning with the W3C WebTransport sendOrder concept where a lower value indicates a higher priority. This maps directly to the raw QUIC stream urgency value (an integer in the range [0, 7]). Additionally, the priority getter is updated to return sendOrder`nrepresenting the current raw QUIC stream urgency value. Fixes: #63654 Signed-off-by: SparshGarg999 <sparshgarg999@gmail.com>
1 parent a4821c8 commit 2003bae

2 files changed

Lines changed: 126 additions & 15 deletions

File tree

lib/internal/quic/quic.js

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ const {
1212
DataViewPrototypeGetByteLength,
1313
ErrorCaptureStackTrace,
1414
FunctionPrototypeBind,
15+
MathMax,
16+
MathMin,
1517
Number,
1618
ObjectDefineProperties,
1719
ObjectKeys,
@@ -296,6 +298,13 @@ const endpointRegistry = new SafeSet();
296298
* send. Only used when the negotiated application supports headers
297299
* (e.g. HTTP/3).
298300
* @property {'high'|'default'|'low'} [priority] The priority level of the stream.
301+
* @property {number} [sendOrder] A signed integer representing the send order,
302+
* aligned with the W3C WebTransport `sendOrder` concept. Any integer is
303+
* accepted; values are clamped to the QUIC urgency range [0, 7]. Note that
304+
* QUIC urgency semantics are inverted relative to W3C: a lower urgency value
305+
* means *higher* priority. When specified, overrides `priority`.
306+
* Full W3C `sendOrder` semantics (including `sendGroups`) require upstream
307+
* ngtcp2 support and are not yet available.
299308
* @property {boolean} [incremental] Whether to interleave data with same-priority streams.
300309
* @property {number} [highWaterMark] The high water mark for write
301310
* backpressure, in bytes. **Default:** `65536`.
@@ -513,6 +522,9 @@ const endpointRegistry = new SafeSet();
513522
/**
514523
* @typedef {object} StreamPriority
515524
* @property {'default' | 'low' | 'high'} level The priority level of the stream.
525+
* @property {number} sendOrder The QUIC urgency value in [0, 7] used by
526+
* this stream. Lower values mean higher priority. This corresponds to the
527+
* clamped input from W3C WebTransport `sendOrder`.
516528
* @property {boolean} incremental Whether to interleave data with same-priority streams.
517529
*/
518530

@@ -2443,7 +2455,9 @@ class QuicStream {
24432455
const urgency = packed >> 1;
24442456
const incremental = !!(packed & 1);
24452457
const level = urgency < 3 ? 'high' : urgency > 3 ? 'low' : 'default';
2446-
return { level, incremental };
2458+
// `sendOrder` exposes the raw QUIC urgency value (0-7), aligned with
2459+
// the W3C WebTransport sendOrder concept (lower value = higher priority).
2460+
return { level, sendOrder: urgency, incremental };
24472461
}
24482462

24492463
/**
@@ -2461,10 +2475,22 @@ class QuicStream {
24612475
const {
24622476
level = 'default',
24632477
incremental = false,
2478+
sendOrder,
24642479
} = options;
2465-
validateOneOf(level, 'options.level', ['default', 'low', 'high']);
24662480
validateBoolean(incremental, 'options.incremental');
2467-
const urgency = level === 'high' ? 0 : level === 'low' ? 7 : 3;
2481+
let urgency;
2482+
if (sendOrder !== undefined) {
2483+
// sendOrder is aligned with the W3C WebTransport sendOrder concept.
2484+
// W3C accepts any long long; we validate it is a finite integer and
2485+
// clamp to the QUIC urgency range [0, 7]. Note: QUIC urgency has
2486+
// inverted priority direction vs W3C sendOrder. Full W3C semantics
2487+
// (sendGroups, full range) require upstream ngtcp2 support.
2488+
validateInteger(sendOrder, 'options.sendOrder');
2489+
urgency = MathMax(0, MathMin(7, sendOrder));
2490+
} else {
2491+
validateOneOf(level, 'options.level', ['default', 'low', 'high']);
2492+
urgency = level === 'high' ? 0 : level === 'low' ? 7 : 3;
2493+
}
24682494
this.#handle.setPriority((urgency << 1) | (incremental ? 1 : 0));
24692495
}
24702496

@@ -3250,6 +3276,7 @@ class QuicSession {
32503276
const {
32513277
body,
32523278
priority = 'default',
3279+
sendOrder,
32533280
incremental = false,
32543281
highWaterMark = kDefaultHighWaterMark,
32553282
headers,
@@ -3270,7 +3297,18 @@ class QuicSession {
32703297
}
32713298

32723299
if (inner.state.isPrioritySupported) {
3273-
const urgency = priority === 'high' ? 0 : priority === 'low' ? 7 : 3;
3300+
let urgency;
3301+
if (sendOrder !== undefined) {
3302+
// sendOrder is aligned with the W3C WebTransport sendOrder concept.
3303+
// W3C accepts any long long; we validate it is a finite integer and
3304+
// clamp to the QUIC urgency range [0, 7]. Note: QUIC urgency has
3305+
// inverted priority direction vs W3C sendOrder. Full W3C semantics
3306+
// (sendGroups, full range) require upstream ngtcp2 support.
3307+
validateInteger(sendOrder, 'options.sendOrder');
3308+
urgency = MathMax(0, MathMin(7, sendOrder));
3309+
} else {
3310+
urgency = priority === 'high' ? 0 : priority === 'low' ? 7 : 3;
3311+
}
32743312
handle.setPriority((urgency << 1) | (incremental ? 1 : 0));
32753313
}
32763314

test/parallel/test-quic-h3-priority.mjs

Lines changed: 84 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import assert from 'node:assert';
1414
import * as fixtures from '../common/fixtures.mjs';
1515
const { readKey } = fixtures;
1616

17-
const { deepStrictEqual, strictEqual } = assert;
17+
const { deepStrictEqual, strictEqual, rejects } = assert;
1818

1919
if (!hasQuic) {
2020
skip('QUIC is not enabled');
@@ -39,6 +39,7 @@ const decoder = new TextDecoder();
3939
const pri = stream.priority;
4040
strictEqual(typeof pri, 'object');
4141
strictEqual(typeof pri.level, 'string');
42+
strictEqual(typeof pri.sendOrder, 'number');
4243
strictEqual(typeof pri.incremental, 'boolean');
4344
}, 4);
4445
}), {
@@ -75,7 +76,7 @@ const decoder = new TextDecoder();
7576
});
7677

7778
// Priority reflects what was set at creation.
78-
deepStrictEqual(stream1.priority, { level: 'high', incremental: false });
79+
deepStrictEqual(stream1.priority, { level: 'high', sendOrder: 0, incremental: false });
7980

8081
// Priority 'low' + incremental at creation.
8182
const stream2 = await clientSession.createBidirectionalStream({
@@ -91,7 +92,7 @@ const decoder = new TextDecoder();
9192
strictEqual(headers[':status'], '200');
9293
}),
9394
});
94-
deepStrictEqual(stream2.priority, { level: 'low', incremental: true });
95+
deepStrictEqual(stream2.priority, { level: 'low', sendOrder: 7, incremental: true });
9596

9697
// Default priority at creation.
9798
const stream3 = await clientSession.createBidirectionalStream({
@@ -105,7 +106,7 @@ const decoder = new TextDecoder();
105106
strictEqual(headers[':status'], '200');
106107
}),
107108
});
108-
deepStrictEqual(stream3.priority, { level: 'default', incremental: false });
109+
deepStrictEqual(stream3.priority, { level: 'default', sendOrder: 3, incremental: false });
109110

110111
// setPriority after creation.
111112
const stream4 = await clientSession.createBidirectionalStream({
@@ -120,19 +121,19 @@ const decoder = new TextDecoder();
120121
}),
121122
});
122123
// Default priority initially.
123-
deepStrictEqual(stream4.priority, { level: 'default', incremental: false });
124+
deepStrictEqual(stream4.priority, { level: 'default', sendOrder: 3, incremental: false });
124125

125126
// Change to high.
126127
stream4.setPriority({ level: 'high' });
127-
deepStrictEqual(stream4.priority, { level: 'high', incremental: false });
128+
deepStrictEqual(stream4.priority, { level: 'high', sendOrder: 0, incremental: false });
128129

129130
// Change to incremental.
130131
stream4.setPriority({ level: 'low', incremental: true });
131-
deepStrictEqual(stream4.priority, { level: 'low', incremental: true });
132+
deepStrictEqual(stream4.priority, { level: 'low', sendOrder: 7, incremental: true });
132133

133134
// Back to default.
134135
stream4.setPriority({ level: 'default', incremental: false });
135-
deepStrictEqual(stream4.priority, { level: 'default', incremental: false });
136+
deepStrictEqual(stream4.priority, { level: 'default', sendOrder: 3, incremental: false });
136137

137138
// Read all bodies.
138139
const allBodies = await Promise.all([
@@ -177,7 +178,7 @@ const decoder = new TextDecoder();
177178

178179
// The server's priority getter should reflect the
179180
// client's PRIORITY_UPDATE (high, incremental).
180-
deepStrictEqual(stream.priority, { level: 'high', incremental: true });
181+
deepStrictEqual(stream.priority, { level: 'high', sendOrder: 0, incremental: true });
181182
serverSawHighPriority.resolve();
182183

183184
await stream.closed;
@@ -221,14 +222,14 @@ const decoder = new TextDecoder();
221222
strictEqual(headers[':status'], '200');
222223
}),
223224
});
224-
deepStrictEqual(stream.priority, { level: 'default', incremental: false });
225+
deepStrictEqual(stream.priority, { level: 'default', sendOrder: 3, incremental: false });
225226

226227
// Change priority — this sends a PRIORITY_UPDATE frame on the
227228
// control stream. The body data was already provided at creation
228229
// but the PRIORITY_UPDATE travels on the control stream which
229230
// nghttp3 prioritizes over bidi streams.
230231
stream.setPriority({ level: 'high', incremental: true });
231-
deepStrictEqual(stream.priority, { level: 'high', incremental: true });
232+
deepStrictEqual(stream.priority, { level: 'high', sendOrder: 0, incremental: true });
232233

233234
// Read the response.
234235
const body = await bytes(stream);
@@ -241,3 +242,75 @@ const decoder = new TextDecoder();
241242
await clientSession.close();
242243
await serverEndpoint.close();
243244
}
245+
246+
// Test: sendOrder option and validation.
247+
{
248+
const serverEndpoint = await listen(mustCall(async (ss) => {
249+
ss.onstream = mustCall((stream) => {
250+
deepStrictEqual(stream.priority, { level: 'high', sendOrder: 1, incremental: false });
251+
});
252+
}), {
253+
sni: { '*': { keys: [key], certs: [cert] } },
254+
onheaders: mustCall(function(headers) {
255+
this.sendHeaders({ ':status': '200' });
256+
this.writer.writeSync(encoder.encode('ok'));
257+
this.writer.endSync();
258+
}),
259+
});
260+
261+
const clientSession = await connect(serverEndpoint.address, {
262+
servername: 'localhost',
263+
verifyPeer: 'manual',
264+
});
265+
await clientSession.opened;
266+
267+
// Set sendOrder at creation time.
268+
const stream1 = await clientSession.createBidirectionalStream({
269+
headers: {
270+
':method': 'GET',
271+
':path': '/',
272+
':scheme': 'https',
273+
':authority': 'localhost',
274+
},
275+
sendOrder: 1,
276+
incremental: false,
277+
});
278+
deepStrictEqual(stream1.priority, { level: 'high', sendOrder: 1, incremental: false });
279+
280+
// Update sendOrder via setPriority.
281+
stream1.setPriority({ sendOrder: 6, incremental: true });
282+
deepStrictEqual(stream1.priority, { level: 'low', sendOrder: 6, incremental: true });
283+
284+
// sendOrder accepts any integer per W3C long long semantics;
285+
// values outside [0, 7] are clamped to the QUIC urgency range.
286+
const streamNeg = await clientSession.createBidirectionalStream({ sendOrder: -100 });
287+
// Clamped to 0 (highest urgency)
288+
deepStrictEqual(streamNeg.priority, { level: 'high', sendOrder: 0, incremental: false });
289+
290+
const streamLarge = await clientSession.createBidirectionalStream({ sendOrder: 999 });
291+
// Clamped to 7 (lowest urgency)
292+
deepStrictEqual(streamLarge.priority, { level: 'low', sendOrder: 7, incremental: false });
293+
294+
// Non-integer values are still rejected.
295+
await rejects(
296+
clientSession.createBidirectionalStream({ sendOrder: 1.5 }),
297+
{ code: 'ERR_INVALID_ARG_TYPE' }
298+
);
299+
await rejects(
300+
clientSession.createBidirectionalStream({ sendOrder: '3' }),
301+
{ code: 'ERR_INVALID_ARG_TYPE' }
302+
);
303+
304+
// setPriority also accepts and clamps out-of-range integers.
305+
stream1.setPriority({ sendOrder: -5 });
306+
deepStrictEqual(stream1.priority, { level: 'high', sendOrder: 0, incremental: false });
307+
stream1.setPriority({ sendOrder: 100 });
308+
deepStrictEqual(stream1.priority, { level: 'low', sendOrder: 7, incremental: false });
309+
310+
const body = await bytes(stream1);
311+
strictEqual(decoder.decode(body), 'ok');
312+
313+
await Promise.all([stream1.closed]);
314+
await clientSession.close();
315+
await serverEndpoint.close();
316+
}

0 commit comments

Comments
 (0)