Skip to content

Commit 464d5a5

Browse files
committed
stream: use webidl validation semantics for args
Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode
1 parent f4ffb5f commit 464d5a5

19 files changed

Lines changed: 651 additions & 278 deletions

doc/api/stream_iter.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,12 @@ const { writer, readable } = push({
401401
A writer is any object conforming to the Writer interface. Only `write()` is
402402
required; all other methods are optional.
403403

404+
Writer arguments use Web IDL conversion semantics. A non-`Uint8Array` chunk is
405+
converted to a `USVString` and then UTF-8 encoded. `writev()` and
406+
`writevSync()` accept any iterable object whose values can be converted to
407+
chunks. Writer option dictionaries treat `null` as an empty dictionary and
408+
ignore unknown members.
409+
404410
Each async method has a synchronous `*Sync` counterpart designed for a
405411
try-fallback pattern: attempt the fast synchronous path first, and fall back
406412
to the async version only when the synchronous call indicates it could not
@@ -492,7 +498,7 @@ Synchronous write. Does not block; returns `false` if backpressure is active.
492498

493499
#### `writer.writev(chunks[, options])`
494500

495-
* `chunks` {Uint8Array\[]|string\[]}
501+
* `chunks` {Iterable} of {Uint8Array|string} values
496502
* `options` {Object}
497503
* `signal` {AbortSignal} Cancel just this write operation. The signal cancels
498504
only the pending `writev()` call; it does not fail the writer itself.
@@ -502,7 +508,7 @@ Write multiple chunks as a single batch.
502508

503509
#### `writer.writevSync(chunks)`
504510

505-
* `chunks` {Uint8Array\[]|string\[]}
511+
* `chunks` {Iterable} of {Uint8Array|string} values
506512
* Returns: {boolean} `true` if the write was accepted, `false` if the
507513
buffer is full.
508514

@@ -521,6 +527,12 @@ import { from, pull, bytes, Stream } from 'node:stream/iter';
521527
Stream.from('hello');
522528
```
523529

530+
Options dictionaries defined by the Iterable Streams API use Web IDL
531+
conversion semantics. `null` is treated as an empty dictionary, unknown
532+
members are ignored, and known members are converted to their declared types
533+
before the operation runs. Conversion failures use Node.js error codes such as
534+
`ERR_INVALID_ARG_TYPE`, `ERR_INVALID_ARG_VALUE`, and `ERR_OUT_OF_RANGE`.
535+
524536
```cjs
525537
// Named exports
526538
const { from, pull, bytes, Stream } = require('node:stream/iter');
@@ -860,7 +872,7 @@ added:
860872

861873
* `options` {Object}
862874
* `budget` {number} Buffer size in bytes for both directions.
863-
**Default:** `16384`.
875+
Must be >= 16384. **Default:** `16384`.
864876
* `backpressure` {string} Policy for both directions.
865877
**Default:** `'strict'`.
866878
* `signal` {AbortSignal} Cancellation signal for both channels.
@@ -1379,6 +1391,7 @@ added:
13791391
**Default:** `65536`.
13801392
* `backpressure` {string} `'strict'`, `'unbounded'`, `'drop-oldest'`, or
13811393
`'drop-newest'`. **Default:** `'strict'`.
1394+
* `signal` {AbortSignal}
13821395
* Returns: {Share}
13831396

13841397
Create a pull-model multi-consumer shared stream. Unlike `broadcast()`, the

lib/internal/fs/promises.js

Lines changed: 17 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -164,17 +164,19 @@ const lazyReadableStream = getLazy(() =>
164164
let newStreamsPull;
165165
let newStreamsPullSync;
166166
let newStreamsParsePullArgs;
167-
let newStreamsToUint8Array;
167+
let newStreamsToWriterUint8Array;
168168
let newStreamsConvertChunks;
169+
let newStreamsGetWriterSignal;
169170
function lazyNewStreams() {
170171
if (newStreamsPull === undefined) {
171172
const pullModule = require('internal/streams/iter/pull');
172173
newStreamsPull = pullModule.pull;
173174
newStreamsPullSync = pullModule.pullSync;
174175
const utils = require('internal/streams/iter/utils');
175176
newStreamsParsePullArgs = utils.parsePullArgs;
176-
newStreamsToUint8Array = utils.toUint8Array;
177+
newStreamsToWriterUint8Array = utils.toWriterUint8Array;
177178
newStreamsConvertChunks = utils.convertChunks;
179+
newStreamsGetWriterSignal = utils.getWriterSignal;
178180
}
179181
}
180182

@@ -885,24 +887,18 @@ if (getOptionValue('--experimental-stream-iter')) {
885887
return {
886888
__proto__: null,
887889
write(chunk, options = kNullPrototo) {
890+
chunk = newStreamsToWriterUint8Array(chunk);
891+
const signal = newStreamsGetWriterSignal(options);
888892
if (error) {
889893
return PromiseReject(error);
890894
}
891895
if (closed) {
892896
return PromiseReject(
893897
new ERR_INVALID_STATE.TypeError('The writer is closed'));
894898
}
895-
validateObject(options, 'options');
896-
const {
897-
signal,
898-
} = options;
899-
if (signal !== undefined) {
900-
validateAbortSignal(signal, 'options.signal');
901-
if (signal.aborted) {
902-
return PromiseReject(signal.reason);
903-
}
899+
if (signal?.aborted) {
900+
return PromiseReject(signal.reason);
904901
}
905-
chunk = newStreamsToUint8Array(chunk);
906902
if (bytesRemaining >= 0 && chunk.byteLength > bytesRemaining) {
907903
return PromiseReject(
908904
new ERR_OUT_OF_RANGE('write', `<= ${bytesRemaining} bytes`,
@@ -915,24 +911,18 @@ if (getOptionValue('--experimental-stream-iter')) {
915911
},
916912

917913
writev(chunks, options = kNullPrototo) {
914+
chunks = newStreamsConvertChunks(chunks);
915+
const signal = newStreamsGetWriterSignal(options);
918916
if (error) {
919917
return PromiseReject(error);
920918
}
921919
if (closed) {
922920
return PromiseReject(
923921
new ERR_INVALID_STATE.TypeError('The writer is closed'));
924922
}
925-
validateObject(options, 'options');
926-
const {
927-
signal,
928-
} = options;
929-
if (signal !== undefined) {
930-
validateAbortSignal(signal, 'options.signal');
931-
if (signal?.aborted) {
932-
return PromiseReject(signal.reason);
933-
}
923+
if (signal?.aborted) {
924+
return PromiseReject(signal.reason);
934925
}
935-
chunks = newStreamsConvertChunks(chunks);
936926
let totalSize = 0;
937927
for (let i = 0; i < chunks.length; i++) {
938928
totalSize += chunks[i].byteLength;
@@ -949,8 +939,8 @@ if (getOptionValue('--experimental-stream-iter')) {
949939
},
950940

951941
writeSync(chunk) {
942+
chunk = newStreamsToWriterUint8Array(chunk);
952943
if (error || closed || asyncPending) return false;
953-
chunk = newStreamsToUint8Array(chunk);
954944
const length = chunk.byteLength;
955945
if (length > syncWriteThreshold) return false;
956946
if (length === 0) return true;
@@ -980,8 +970,8 @@ if (getOptionValue('--experimental-stream-iter')) {
980970
},
981971

982972
writevSync(chunks) {
983-
if (error || closed || asyncPending) return false;
984973
chunks = newStreamsConvertChunks(chunks);
974+
if (error || closed || asyncPending) return false;
985975
let totalSize = 0;
986976
for (let i = 0; i < chunks.length; i++) {
987977
totalSize += chunks[i].byteLength;
@@ -1016,6 +1006,7 @@ if (getOptionValue('--experimental-stream-iter')) {
10161006
},
10171007

10181008
end(options = kNullPrototo) {
1009+
const signal = newStreamsGetWriterSignal(options);
10191010
if (error) {
10201011
return PromiseReject(error);
10211012
}
@@ -1025,15 +1016,8 @@ if (getOptionValue('--experimental-stream-iter')) {
10251016
if (closing) {
10261017
return pendingEndPromise;
10271018
}
1028-
validateObject(options, 'options');
1029-
const {
1030-
signal,
1031-
} = options;
1032-
if (signal !== undefined) {
1033-
validateAbortSignal(signal, 'options.signal');
1034-
if (signal.aborted) {
1035-
return PromiseReject(signal.reason);
1036-
}
1019+
if (signal?.aborted) {
1020+
return PromiseReject(signal.reason);
10371021
}
10381022
closing = true;
10391023
pendingEndPromise = PromisePrototypeThen(

lib/internal/quic/quic.js

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,9 @@ const {
138138
} = require('internal/streams/iter/types');
139139

140140
const {
141-
toUint8Array,
142141
convertChunks,
142+
getWriterSignal,
143+
toWriterUint8Array,
143144
} = require('internal/streams/iter/utils');
144145

145146
const {
@@ -164,7 +165,6 @@ const {
164165
} = require('internal/fs/promises');
165166

166167
const {
167-
validateAbortSignal,
168168
validateBoolean,
169169
validateFunction,
170170
validateInteger,
@@ -2197,15 +2197,14 @@ class QuicStream {
21972197
// signals backpressure additional writes are rejected until the buffer has
21982198
// capacity again.
21992199

2200-
function writeSync(chunk) {
2200+
function writeConvertedSync(chunk) {
22012201
// If the stream is closed, errored, or write-ended, we cannot accept
22022202
// more data. Refuse the sync write.
22032203
// If a drain is already pending, another operation is waiting
22042204
// for capacity. Refuse the sync write.
22052205
if (closed || errored || stream.#inner.state.writeEnded || drainWakeup != null) {
22062206
return false;
22072207
}
2208-
chunk = toUint8Array(chunk);
22092208
const len = TypedArrayPrototypeGetByteLength(chunk);
22102209
if (len === 0) return true;
22112210
// Refuse the write only when there is no available capacity at
@@ -2223,13 +2222,18 @@ class QuicStream {
22232222
return true;
22242223
}
22252224

2226-
async function write(chunk, options = kEmptyObject) {
2227-
validateObject(options, 'options');
2228-
const { signal } = options;
2229-
if (signal !== undefined) {
2230-
validateAbortSignal(signal, 'options.signal');
2231-
signal.throwIfAborted();
2232-
}
2225+
function writeSync(chunk) {
2226+
return writeConvertedSync(toWriterUint8Array(chunk));
2227+
}
2228+
2229+
function write(chunk, options = kEmptyObject) {
2230+
chunk = toWriterUint8Array(chunk);
2231+
const signal = getWriterSignal(options);
2232+
return writeAsync(chunk, signal);
2233+
}
2234+
2235+
async function writeAsync(chunk, signal) {
2236+
signal?.throwIfAborted();
22332237
if (errored) throw error;
22342238
if (closed || stream.#inner.state.writeEnded) {
22352239
throw new ERR_INVALID_STATE('Writer is closed');
@@ -2242,16 +2246,15 @@ class QuicStream {
22422246
throw new ERR_INVALID_STATE('Stream write buffer is full');
22432247
}
22442248

2245-
if (!writeSync(chunk)) {
2249+
if (!writeConvertedSync(chunk)) {
22462250
throw new ERR_INVALID_STATE('Stream write buffer is full');
22472251
}
22482252
}
22492253

2250-
function writevSync(chunks) {
2254+
function writevConvertedSync(chunks) {
22512255
if (closed || errored || stream.#inner.state.writeEnded || drainWakeup != null) {
22522256
return false;
22532257
}
2254-
chunks = convertChunks(chunks);
22552258
let len = 0;
22562259
for (const c of chunks) len += TypedArrayPrototypeGetByteLength(c);
22572260
if (len === 0) return true;
@@ -2262,13 +2265,18 @@ class QuicStream {
22622265
return true;
22632266
}
22642267

2265-
async function writev(chunks, options = kEmptyObject) {
2266-
validateObject(options, 'options');
2267-
const { signal } = options;
2268-
if (signal !== undefined) {
2269-
validateAbortSignal(signal, 'options.signal');
2270-
signal.throwIfAborted();
2271-
}
2268+
function writevSync(chunks) {
2269+
return writevConvertedSync(convertChunks(chunks));
2270+
}
2271+
2272+
function writev(chunks, options = kEmptyObject) {
2273+
chunks = convertChunks(chunks);
2274+
const signal = getWriterSignal(options);
2275+
return writevAsync(chunks, signal);
2276+
}
2277+
2278+
async function writevAsync(chunks, signal) {
2279+
signal?.throwIfAborted();
22722280

22732281
if (errored) throw error;
22742282
if (closed || stream.#inner.state.writeEnded) {
@@ -2283,7 +2291,7 @@ class QuicStream {
22832291
throw new ERR_INVALID_STATE('Stream write buffer is full');
22842292
}
22852293

2286-
if (!writevSync(chunks)) {
2294+
if (!writevConvertedSync(chunks)) {
22872295
throw new ERR_INVALID_STATE('Stream write buffer is full');
22882296
}
22892297
}
@@ -2308,11 +2316,13 @@ class QuicStream {
23082316
return totalBytesWritten;
23092317
}
23102318

2311-
async function end(options = kEmptyObject) {
2312-
validateObject(options, 'options');
2313-
const { signal } = options;
2319+
function end(options = kEmptyObject) {
2320+
const signal = getWriterSignal(options);
2321+
return endAsync(signal);
2322+
}
2323+
2324+
async function endAsync(signal) {
23142325
if (signal !== undefined) {
2315-
validateAbortSignal(signal, 'options.signal');
23162326
signal.throwIfAborted();
23172327
// TODO(@jasnell): The stream/iter spec allows individual sync end
23182328
// calls to be canceled via an AbortSignal. We currently do not support

0 commit comments

Comments
 (0)