Skip to content

Commit 2a31bda

Browse files
committed
Expand writable suite: construction, abort matrix, queue math, reentrancy
1 parent 7a04f3a commit 2a31bda

8 files changed

Lines changed: 650 additions & 6 deletions

File tree

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
// Copyright (c) 2026 Cloudflare, Inc.
2+
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3+
// https://opensource.org/licenses/Apache-2.0
4+
5+
// Abort matrix: reason identity across writer promises, sink-hook
6+
// suppression, in-flight interactions with controller.error(), and the
7+
// controller.signal surface. WPT writable-streams/aborting.any.js lists
8+
// eleven C++ expectedFailures ("nitpickiness about the type of error");
9+
// the scenarios here probe the same territory and pin what each
10+
// implementation actually does — most of it is parity, with the
11+
// signal-reason default the notable divergence.
12+
13+
import { strictEqual, ok, rejects } from 'node:assert';
14+
import { usingTsImpl, pedanticWpt } from 'which-impl';
15+
16+
// Aborting before start() settles rejects ready and closed with the
17+
// abort reason (by identity); the abort promise fulfills.
18+
export const abortBeforeStartReasonIdentity = {
19+
async test() {
20+
const ws = new WritableStream({
21+
async start() {
22+
await scheduler.wait(5);
23+
},
24+
});
25+
const writer = ws.getWriter();
26+
const reason = new Error('r1');
27+
const abort = writer.abort(reason);
28+
29+
await rejects(writer.ready, (e) => e === reason);
30+
await rejects(writer.closed, (e) => e === reason);
31+
strictEqual(await abort, undefined);
32+
},
33+
};
34+
35+
// After an abort settles, writes reject with the very same reason object.
36+
export const erroredStateReasonIdentity = {
37+
async test() {
38+
const ws = new WritableStream();
39+
const writer = ws.getWriter();
40+
const reason = { custom: true };
41+
await writer.abort(reason);
42+
await rejects(writer.write('x'), (e) => e === reason);
43+
},
44+
};
45+
46+
// A stream errored by a throwing size() does not run the sink abort hook
47+
// when abort() arrives later; the abort still fulfills (parity; cf. the
48+
// WPT "sink abort() should not be called if stream was erroring due to
49+
// bad strategy" case).
50+
export const sinkAbortSkippedAfterBadStrategyError = {
51+
async test() {
52+
let abortCalled = false;
53+
const ws = new WritableStream(
54+
{
55+
abort() {
56+
abortCalled = true;
57+
},
58+
},
59+
{
60+
size() {
61+
throw new Error('bad size');
62+
},
63+
highWaterMark: 1,
64+
}
65+
);
66+
const writer = ws.getWriter();
67+
await rejects(writer.write('x'), { message: 'bad size' });
68+
strictEqual(await writer.abort('reason'), undefined);
69+
strictEqual(abortCalled, false);
70+
},
71+
};
72+
73+
// writer.abort() while a write is in flight, with the sink write then
74+
// REJECTING: the write surfaces its own failure, the abort fulfills, and
75+
// closed carries the abort reason (parity).
76+
export const inFlightWriteRejectionDuringAbort = {
77+
async test() {
78+
let rejectWrite;
79+
const ws = new WritableStream({
80+
write() {
81+
return new Promise((res, rej) => (rejectWrite = rej));
82+
},
83+
});
84+
const writer = ws.getWriter();
85+
await writer.ready;
86+
const write = writer.write('x');
87+
await scheduler.wait(1); // the sink write is now in flight on both sides
88+
const abortReason = new Error('abort-reason');
89+
const abort = writer.abort(abortReason);
90+
await scheduler.wait(1);
91+
rejectWrite(new Error('write-fail'));
92+
93+
await rejects(write, { message: 'write-fail' });
94+
strictEqual(await abort, undefined);
95+
await rejects(writer.closed, (e) => e === abortReason);
96+
},
97+
};
98+
99+
// writer.abort() then controller.error() while a write is in flight,
100+
// with the write finishing cleanly: the abort request wins, both write
101+
// and abort fulfill (parity).
102+
export const abortThenControllerErrorInFlight = {
103+
async test() {
104+
let resolveWrite;
105+
let controller;
106+
const ws = new WritableStream({
107+
start(c) {
108+
controller = c;
109+
},
110+
write() {
111+
return new Promise((res) => (resolveWrite = res));
112+
},
113+
});
114+
const writer = ws.getWriter();
115+
await writer.ready;
116+
const write = writer.write('x');
117+
await scheduler.wait(1);
118+
const abort = writer.abort(new Error('abort-reason'));
119+
controller.error(new Error('ctrl-error'));
120+
await scheduler.wait(1);
121+
resolveWrite();
122+
123+
strictEqual(await write, undefined);
124+
strictEqual(await abort, undefined);
125+
},
126+
};
127+
128+
// controller.error() then writer.abort() while a write is in flight: the
129+
// stream is already erroring, so the abort rejects with the
130+
// controller's error while the in-flight write still finishes (parity).
131+
export const controllerErrorThenAbortInFlight = {
132+
async test() {
133+
let resolveWrite;
134+
let controller;
135+
const ws = new WritableStream({
136+
start(c) {
137+
controller = c;
138+
},
139+
write() {
140+
return new Promise((res) => (resolveWrite = res));
141+
},
142+
});
143+
const writer = ws.getWriter();
144+
await writer.ready;
145+
const write = writer.write('x');
146+
await scheduler.wait(1);
147+
controller.error(new Error('ctrl-error'));
148+
const abort = writer.abort(new Error('abort-reason'));
149+
await scheduler.wait(1);
150+
resolveWrite();
151+
152+
strictEqual(await write, undefined);
153+
await rejects(abort, { message: 'ctrl-error' });
154+
},
155+
};
156+
157+
// DIVERGENCE: the signal reason for a reasonless abort(). The spec (and
158+
// TypeScript, and C++ under pedantic_wpt — standard.c++ WritableImpl::
159+
// abort) synthesizes an AbortError DOMException; C++ otherwise leaves
160+
// the reason undefined. An explicit reason is passed through verbatim
161+
// everywhere.
162+
export const abortSignalReason = {
163+
async test() {
164+
{
165+
let controller;
166+
const ws = new WritableStream({
167+
start(c) {
168+
controller = c;
169+
},
170+
});
171+
ws.abort();
172+
await scheduler.wait(1);
173+
strictEqual(controller.signal.aborted, true);
174+
if (usingTsImpl || pedanticWpt) {
175+
ok(controller.signal.reason instanceof DOMException);
176+
strictEqual(controller.signal.reason.name, 'AbortError');
177+
} else {
178+
strictEqual(controller.signal.reason, undefined);
179+
}
180+
}
181+
{
182+
let controller;
183+
const ws = new WritableStream({
184+
start(c) {
185+
controller = c;
186+
},
187+
});
188+
ws.abort('why');
189+
await scheduler.wait(1);
190+
strictEqual(controller.signal.reason, 'why');
191+
}
192+
},
193+
};

‎src/tests/streams/writable/backpressure.js‎

Lines changed: 159 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,22 @@
33
// https://opensource.org/licenses/Apache-2.0
44

55
// desiredSize accounting and ready-promise behavior under backpressure.
6-
// Migrated from streams-js-test.js and streams-backpressure-test.js.
7-
8-
import { strictEqual, ok } from 'node:assert';
6+
// Migrated from streams-js-test.js and streams-backpressure-test.js, plus
7+
// the floating-point queue-total pins mirroring WPT
8+
// writable-streams/floating-point-total-queue-size.any.js.
9+
//
10+
// DIVERGENCE (queue arithmetic): the spec tracks [[queueTotalSize]] as a
11+
// double. The C++ implementation converts each size() result to uint64
12+
// through the jsg boundary (fractions truncate toward zero; negatives,
13+
// NaN and infinities throw TypeError — common.h StreamQueuingStrategy)
14+
// and then narrows the ssize_t desiredSize through `int`, so totals past
15+
// 2^31 wrap (WritableStreamJsController::getDesiredSize). These are the
16+
// three WPT floating-point expectedFailures. TypeScript follows the spec
17+
// exactly, including RangeError("Invalid chunk size") for invalid size
18+
// returns.
19+
20+
import { strictEqual, ok, rejects } from 'node:assert';
21+
import { usingTsImpl, pedanticWpt } from 'which-impl';
922

1023
// desiredSize decrements per queued write and recovers; ready is replaced
1124
// once the queue drains.
@@ -143,3 +156,146 @@ export const backpressureWritableSlowSink = {
143156
await writer.close();
144157
},
145158
};
159+
160+
// Builds the WPT floating-point scenario: identity size(), highWaterMark
161+
// 0, and a gated sink so queued chunks stay queued while desiredSize is
162+
// read.
163+
function gatedFpStream() {
164+
let release;
165+
const gate = new Promise((res) => (release = res));
166+
const ws = new WritableStream(
167+
{
168+
async write() {
169+
await gate;
170+
},
171+
},
172+
{
173+
size(x) {
174+
return x;
175+
},
176+
highWaterMark: 0,
177+
}
178+
);
179+
return { writer: ws.getWriter(), release };
180+
}
181+
182+
// The four WPT floating-point queue-total scenarios, with each side's
183+
// exact arithmetic pinned.
184+
export const floatingPointQueueTotals = {
185+
async test() {
186+
// Chunk sizes, expected desiredSize while queued (ts: double math;
187+
// cpp: uint64-truncated sizes, so 1e-16 and 2e-16 count as 0), and
188+
// expected desiredSize after the queue drains.
189+
const cases = [
190+
{
191+
sizes: [2, Number.MAX_SAFE_INTEGER],
192+
// ts: -(2 + MAX_SAFE_INTEGER) rounded in double arithmetic; cpp:
193+
// the true total exceeds 2^31 and the int narrowing wraps to -1.
194+
during: usingTsImpl ? 0 - 2 - Number.MAX_SAFE_INTEGER : -1,
195+
after: 0,
196+
},
197+
{
198+
sizes: [1e-16, 1],
199+
during: usingTsImpl ? 0 - 1e-16 - 1 : -1,
200+
after: 0,
201+
},
202+
{
203+
sizes: [1e-16, 1, 2e-16],
204+
during: usingTsImpl ? 0 - 1e-16 - 1 - 2e-16 : -1,
205+
// ts: the spec's incremental subtraction leaves a residue that is
206+
// not clamped (the WPT "positive, and not clamped" case).
207+
after: usingTsImpl ? 0 - 1e-16 - 1 - 2e-16 + 1e-16 + 1 + 2e-16 : 0,
208+
},
209+
{
210+
sizes: [2e-16, 1],
211+
during: usingTsImpl ? 0 - 2e-16 - 1 : -1,
212+
after: 0,
213+
},
214+
];
215+
216+
for (const { sizes, during, after } of cases) {
217+
const { writer, release } = gatedFpStream();
218+
const writes = sizes.map((v) => writer.write(v));
219+
await scheduler.wait(1); // let the TypeScript side start the sink
220+
strictEqual(writer.desiredSize, during);
221+
release();
222+
await Promise.all(writes);
223+
strictEqual(writer.desiredSize, after);
224+
}
225+
},
226+
};
227+
228+
// A lone fractional chunk size: counted faithfully by TypeScript,
229+
// truncated to zero by the C++ uint64 conversion.
230+
export const fractionalSizeTruncation = {
231+
async test() {
232+
const { writer, release } = gatedFpStream();
233+
const write = writer.write(0.5);
234+
await scheduler.wait(1);
235+
strictEqual(writer.desiredSize, usingTsImpl ? -0.5 : 0);
236+
release();
237+
await write;
238+
strictEqual(writer.desiredSize, 0);
239+
},
240+
};
241+
242+
// Invalid size() return values reject the write and error the stream;
243+
// the error type diverges (C++ TypeError from the uint64 conversion with
244+
// value-specific messages, TypeScript the spec's RangeError).
245+
export const invalidSizeReturnRejects = {
246+
async test() {
247+
const cases = [
248+
{ ret: NaN, cppMessage: /not an integer/ },
249+
{ ret: -1, cppMessage: /negative/ },
250+
{ ret: Infinity, cppMessage: /not an integer/ },
251+
];
252+
for (const { ret, cppMessage } of cases) {
253+
const ws = new WritableStream(
254+
{},
255+
{
256+
size() {
257+
return ret;
258+
},
259+
highWaterMark: 5,
260+
}
261+
);
262+
const writer = ws.getWriter();
263+
const expected = usingTsImpl
264+
? { name: 'RangeError', message: 'Invalid chunk size' }
265+
: { name: 'TypeError', message: cppMessage };
266+
await rejects(writer.write('x'), expected);
267+
// The bad size does not just doom the write; the stream errors.
268+
await rejects(writer.closed, expected);
269+
}
270+
},
271+
};
272+
273+
// DIVERGENCE: while the stream is ERRORING (in-flight write outstanding,
274+
// controller.error() already called) the spec reports desiredSize null;
275+
// TypeScript and C++ with pedantic_wpt do that, while C++ otherwise still
276+
// reports the queue accounting value. Once fully errored, everyone
277+
// reports null.
278+
export const desiredSizeWhileErroring = {
279+
async test() {
280+
let resolveWrite;
281+
let controller;
282+
const ws = new WritableStream({
283+
start(c) {
284+
controller = c;
285+
},
286+
write() {
287+
return new Promise((res) => (resolveWrite = res));
288+
},
289+
});
290+
const writer = ws.getWriter();
291+
await writer.ready;
292+
const write = writer.write('x');
293+
await scheduler.wait(1); // the write is in flight; erroring can begin
294+
controller.error(new Error('e'));
295+
strictEqual(writer.desiredSize, usingTsImpl || pedanticWpt ? null : 0);
296+
resolveWrite();
297+
await write;
298+
strictEqual(writer.desiredSize, null);
299+
await writer.closed.catch(() => {});
300+
},
301+
};

0 commit comments

Comments
 (0)