Skip to content

Commit ba1e784

Browse files
committed
Reject writer.ready promise on errors, instead of fulfilling
In the process of testing this, ported several tests to web-platform-tests format, and introduced the recording stream helpers that I am using extensively in #512.
1 parent a531e46 commit ba1e784

File tree

11 files changed

+307
-189
lines changed

11 files changed

+307
-189
lines changed

index.bs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2737,8 +2737,9 @@ visible through the {{WritableStream}}'s public API.
27372737
1. If _writer_ is not undefined,
27382738
1. Reject _writer_.[[closedPromise]] with _e_.
27392739
1. If _state_ is `"writable"` and !
2740-
WritableStreamDefaultControllerGetBackpressure(_stream_.[[writableStreamController]]) is *true*, resolve
2741-
_writer_.[[readyPromise]] with *undefined*.
2740+
WritableStreamDefaultControllerGetBackpressure(_stream_.[[writableStreamController]]) is *true*, reject
2741+
_writer_.[[readyPromise]] with _e_.
2742+
1. Otherwise, set _writer_.[[readyPromise]] to a new promise rejected with _e_.
27422743
1. Set _stream_.[[state]] to `"errored"`.
27432744
1. Set _stream_.[[storedError]] to _e_.
27442745
</emu-alg>

reference-implementation/lib/writable-stream.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,9 @@ function WritableStreamError(stream, e) {
139139

140140
if (state === 'writable' &&
141141
WritableStreamDefaultControllerGetBackpressure(stream._writableStreamController) === true) {
142-
defaultWriterReadyPromiseResolve(writer);
142+
defaultWriterReadyPromiseReject(writer, e);
143+
} else {
144+
defaultWriterReadyPromiseResetToRejected(writer, e);
143145
}
144146
}
145147

reference-implementation/test/writable-stream-abort.js

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -194,25 +194,6 @@ test('Aborting a WritableStream puts it in an errored state, with stored error e
194194
);
195195
});
196196

197-
test('Aborting a WritableStream causes any outstanding ready promises to be fulfilled immediately', t => {
198-
const ws = new WritableStream({
199-
write() {
200-
return new Promise(() => { }); // forever-pending, so normally .ready would not fulfill.
201-
}
202-
});
203-
204-
const writer = ws.getWriter();
205-
206-
writer.write('a');
207-
208-
writer.ready.then(() => {
209-
t.end();
210-
});
211-
212-
const passedReason = new Error('Sorry, it just wasn\'t meant to be.');
213-
writer.abort(passedReason);
214-
});
215-
216197
test('Aborting a WritableStream causes any outstanding write() promises to be rejected with the abort reason', t => {
217198
t.plan(1);
218199

reference-implementation/test/writable-stream.js

Lines changed: 0 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -104,154 +104,6 @@ test('If close is called on a WritableStream in waiting state, ready will be ful
104104
}, 0);
105105
});
106106

107-
test('If sink rejects on a WritableStream in writable state, ready will return a fulfilled promise', t => {
108-
let rejectSinkWritePromise;
109-
const ws = new WritableStream({
110-
write() {
111-
return new Promise((r, reject) => {
112-
rejectSinkWritePromise = reject;
113-
});
114-
}
115-
});
116-
117-
setTimeout(() => {
118-
const writer = ws.getWriter();
119-
120-
const writePromise = writer.write('a');
121-
122-
const passedError = new Error('pass me');
123-
rejectSinkWritePromise(passedError);
124-
125-
writePromise.then(
126-
() => {
127-
t.fail('write promise was unexpectedly fulfilled');
128-
t.end();
129-
},
130-
r => {
131-
t.equal(r, passedError, 'write() should be rejected with the passed error');
132-
133-
writer.ready.then(v => {
134-
t.equal(v, undefined, 'ready promise was fulfilled with undefined');
135-
t.end();
136-
});
137-
}
138-
);
139-
}, 0);
140-
});
141-
142-
test('WritableStream if sink\'s close throws', t => {
143-
const passedError = new Error('pass me');
144-
const ws = new WritableStream({
145-
write() {
146-
t.fail('Unexpected write call');
147-
t.end();
148-
},
149-
close() {
150-
throw passedError;
151-
},
152-
abort() {
153-
t.fail('Unexpected abort call');
154-
t.end();
155-
}
156-
});
157-
158-
// Wait for ws to start.
159-
setTimeout(() => {
160-
const writer = ws.getWriter();
161-
162-
const closedPromise = writer.close();
163-
164-
closedPromise.then(
165-
() => {
166-
t.fail('closedPromise is fulfilled unexpectedly');
167-
t.end();
168-
},
169-
r => {
170-
t.equal(r, passedError, 'close() should be rejected with the passed error');
171-
172-
writer.ready.then(v => {
173-
t.equal(v, undefined, 'ready promise was fulfilled with undefined');
174-
t.end();
175-
});
176-
}
177-
);
178-
}, 0);
179-
});
180-
181-
test('WritableStream if the promise returned by sink\'s close rejects', t => {
182-
const passedError = new Error('pass me');
183-
const ws = new WritableStream({
184-
write() {
185-
t.fail('write of sink called');
186-
t.end();
187-
},
188-
close() {
189-
return Promise.reject(passedError);
190-
},
191-
abort() {
192-
t.fail('abort of sink called');
193-
t.end();
194-
}
195-
});
196-
197-
// Wait for ws to start.
198-
setTimeout(() => {
199-
const writer = ws.getWriter();
200-
201-
const closedPromise = writer.close();
202-
203-
closedPromise.then(
204-
() => {
205-
t.fail('closedPromise is fulfilled');
206-
t.end();
207-
},
208-
r => {
209-
t.equal(r, passedError, 'close() should be rejected with the passed error');
210-
211-
writer.ready.then(v => {
212-
t.equal(v, undefined, 'ready promise was fulfilled with undefined');
213-
t.end();
214-
});
215-
}
216-
);
217-
}, 0);
218-
});
219-
220-
test('If sink\'s write rejects on a WritableStream in waiting state, ready will return a rejected promise', t => {
221-
const passedError = new Error('pass me');
222-
const ws = new WritableStream({
223-
write(chunk) {
224-
if (chunk === 'first chunk succeeds') {
225-
return new Promise(resolve => setTimeout(resolve, 10));
226-
}
227-
return Promise.reject(passedError);
228-
}
229-
});
230-
231-
setTimeout(() => {
232-
const writer = ws.getWriter();
233-
234-
writer.write('first chunk succeeds');
235-
236-
const secondWritePromise = writer.write('all other chunks fail');
237-
238-
secondWritePromise.then(
239-
() => {
240-
t.fail('write promise was unexpectedly fulfilled');
241-
t.end();
242-
},
243-
r => {
244-
t.equal(r, passedError, 'write() should be rejected with the passed error');
245-
246-
writer.ready.then(v => {
247-
t.equal(v, undefined, 'ready promise was fulfilled with undefined');
248-
t.end();
249-
});
250-
}
251-
);
252-
}, 0);
253-
});
254-
255107
test('WritableStream should call underlying sink methods as methods', t => {
256108
t.plan(5);
257109

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"use strict";
2+
3+
self.recordingReadableStream = (extras = {}, strategy) => {
4+
let controllerToCopyOver;
5+
const stream = new ReadableStream({
6+
start(controller) {
7+
controllerToCopyOver = controller;
8+
9+
if (extras.start) {
10+
return extras.start(controller);
11+
}
12+
},
13+
pull(controller) {
14+
stream.events.push('pull');
15+
16+
if (extras.pull) {
17+
return extras.pull(controller);
18+
}
19+
},
20+
cancel(reason) {
21+
stream.events.push('cancel', reason);
22+
stream.eventsWithoutPulls.push('cancel', reason);
23+
24+
if (extras.cancel) {
25+
return extras.cancel(reason);
26+
}
27+
}
28+
}, strategy);
29+
30+
stream.controller = controllerToCopyOver;
31+
stream.events = [];
32+
stream.eventsWithoutPulls = [];
33+
34+
return stream;
35+
};
36+
37+
self.recordingWritableStream = (extras = {}, strategy) => {
38+
let controllerToCopyOver;
39+
const stream = new WritableStream({
40+
start(controller) {
41+
controllerToCopyOver = controller;
42+
43+
if (extras.start) {
44+
return extras.start(controller);
45+
}
46+
},
47+
write(chunk) {
48+
stream.events.push('write', chunk);
49+
50+
if (extras.write) {
51+
return extras.write(chunk);
52+
}
53+
},
54+
close() {
55+
stream.events.push('close');
56+
57+
if (extras.close) {
58+
return extras.close();
59+
}
60+
},
61+
abort(e) {
62+
stream.events.push('abort', e);
63+
64+
if (extras.abort) {
65+
return extras.abort(e);
66+
}
67+
}
68+
}, strategy);
69+
70+
stream.controller = controllerToCopyOver;
71+
stream.events = [];
72+
73+
return stream;
74+
};
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
'use strict';
2+
// This file already exists upstream. We are duplicating it here due to tooling limitations.
3+
4+
self.getterRejects = (t, obj, getterName, target) => {
5+
const getter = Object.getOwnPropertyDescriptor(obj, getterName).get;
6+
7+
return promise_rejects(t, new TypeError(), getter.call(target));
8+
};
9+
10+
self.methodRejects = (t, obj, methodName, target) => {
11+
const method = obj[methodName];
12+
13+
return promise_rejects(t, new TypeError(), method.call(target));
14+
};
15+
16+
self.getterThrows = (obj, getterName, target) => {
17+
const getter = Object.getOwnPropertyDescriptor(obj, getterName).get;
18+
19+
assert_throws(new TypeError(), () => getter.call(target), getterName + ' should throw a TypeError');
20+
};
21+
22+
self.methodThrows = (obj, methodName, target, args) => {
23+
const method = obj[methodName];
24+
25+
assert_throws(new TypeError(), () => method.apply(target, args), methodName + ' should throw a TypeError');
26+
};
27+
28+
self.garbageCollect = () => {
29+
if (self.gc) {
30+
// Use --expose_gc for V8 (and Node.js)
31+
// Exposed in SpiderMonkey shell as well
32+
self.gc();
33+
} else if (self.GCController) {
34+
// Present in some WebKit development environments
35+
GCController.collect();
36+
} else {
37+
/* eslint-disable no-console */
38+
console.warn('Tests are running without the ability to do manual garbage collection. They will still work, but ' +
39+
'coverage will be suboptimal.');
40+
/* eslint-enable no-console */
41+
}
42+
};
43+
44+
self.delay = ms => new Promise(resolve => step_timeout(resolve, ms));
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<meta charset="utf-8">
3+
<script src="/resources/testharness.js"></script>
4+
<script src="/resources/testharnessreport.js"></script>
5+
<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
6+
<script src="../resources/test-initializer.js"></script>
7+
8+
<script src="aborting.js"></script>
9+
<script>
10+
'use strict';
11+
worker_test('aborting.js');
12+
</script>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
'use strict';
2+
3+
if (self.importScripts) {
4+
self.importScripts('/resources/testharness.js');
5+
}
6+
7+
const error1 = new Error('error1');
8+
error1.name = 'error1';
9+
10+
promise_test(t => {
11+
12+
const ws = new WritableStream({
13+
write() {
14+
return new Promise(() => { }); // forever-pending, so normally .ready would not fulfill.
15+
}
16+
});
17+
18+
const writer = ws.getWriter();
19+
writer.write('a');
20+
21+
const readyPromise = writer.ready;
22+
23+
writer.abort(error1);
24+
25+
assert_equals(writer.ready, readyPromise, 'the ready promise property should not change');
26+
27+
return promise_rejects(t, new TypeError(), readyPromise, 'the ready promise should reject with a TypeError');
28+
29+
}, 'Aborting a WritableStream should cause the writer\'s unsettled ready promise to reject');
30+
31+
promise_test(t => {
32+
33+
const ws = new WritableStream();
34+
35+
const writer = ws.getWriter();
36+
writer.write('a');
37+
38+
const readyPromise = writer.ready;
39+
40+
return readyPromise.then(() => {
41+
writer.abort(error1);
42+
43+
assert_not_equals(writer.ready, readyPromise, 'the ready promise property should change');
44+
return promise_rejects(t, new TypeError(), writer.ready, 'the ready promise should reject with a TypeError');
45+
});
46+
47+
}, 'Aborting a WritableStream should cause the writer\'s fulfilled ready promise to reset to a rejected one');
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<!DOCTYPE html>
2+
<meta charset="utf-8">
3+
<script src="/resources/testharness.js"></script>
4+
<script src="/resources/testharnessreport.js"></script>
5+
<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
6+
<script src="../resources/test-utils.js"></script>
7+
<script src="../resources/recording-streams.js"></script>
8+
<script src="../resources/test-initializer.js"></script>
9+
10+
<script src="bad-underlying-sinks.js"></script>
11+
<script>
12+
'use strict';
13+
worker_test('bad-underlying-sinks.js');
14+
</script>

0 commit comments

Comments
 (0)