Skip to content

Commit e09a50d

Browse files
committed
Fixup some wpt streams tests
1 parent b3ee4a9 commit e09a50d

File tree

3 files changed

+64
-14
lines changed

3 files changed

+64
-14
lines changed

src/workerd/api/streams/standard.c++

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,7 @@ class ReadableStreamJsController final: public ReadableStreamController {
721721
state = StreamStates::Closed();
722722

723723
kj::Maybe<uint64_t> expectedLength = kj::none;
724+
bool canceling = false;
724725

725726
// The lock state is separate because a closed or errored stream can still be locked.
726727
ReadableLockImpl lock;
@@ -1607,6 +1608,8 @@ struct ValueReadable final: private api::ValueQueue::ConsumerImpl::StateListener
16071608

16081609
using State = ReadableState<DefaultController, ValueQueue>;
16091610
kj::Maybe<State> state;
1611+
bool reading = false;
1612+
bool pendingCancel = false;
16101613

16111614
JSG_MEMORY_INFO(ValueReadable) {
16121615
KJ_IF_SOME(s, state) {
@@ -1647,10 +1650,17 @@ struct ValueReadable final: private api::ValueQueue::ConsumerImpl::StateListener
16471650
jsg::Promise<ReadResult> read(jsg::Lock& js) {
16481651
KJ_IF_SOME(s, state) {
16491652
auto prp = js.newPromiseAndResolver<ReadResult>();
1653+
reading = true;
16501654
s.consumer->read(js,
16511655
ValueQueue::ReadRequest{
16521656
.resolver = kj::mv(prp.resolver),
16531657
});
1658+
reading = false;
1659+
if (pendingCancel) {
1660+
// If we were canceled while reading, we need to drop our state now.
1661+
state = kj::none;
1662+
pendingCancel = false;
1663+
}
16541664
return kj::mv(prp.promise);
16551665
}
16561666

@@ -1666,10 +1676,17 @@ struct ValueReadable final: private api::ValueQueue::ConsumerImpl::StateListener
16661676
// the underlying controller only when the last reader is canceled.
16671677
// Here, we rely on the controller implementing the correct behavior since it owns
16681678
// the queue that knows about all of the attached consumers.
1679+
if (pendingCancel) return js.resolvedPromise();
16691680
KJ_IF_SOME(s, state) {
16701681
s.consumer->cancel(js, maybeReason);
16711682
auto promise = s.controller->cancel(js, kj::mv(maybeReason));
1672-
state = kj::none;
1683+
// If we're currently in a read, we need to wait for that to finish
1684+
// before dropping our state.
1685+
if (reading) {
1686+
pendingCancel = true;
1687+
} else {
1688+
state = kj::none;
1689+
}
16731690
return kj::mv(promise);
16741691
}
16751692

@@ -2251,9 +2268,13 @@ jsg::Promise<void> ReadableStreamJsController::cancel(
22512268
return js.rejectedPromise<void>(errored.addRef(js));
22522269
}
22532270
KJ_CASE_ONEOF(consumer, kj::Own<ValueReadable>) {
2271+
if (canceling) return js.resolvedPromise();
2272+
canceling = true;
22542273
return doCancel(consumer);
22552274
}
22562275
KJ_CASE_ONEOF(consumer, kj::Own<ByteReadable>) {
2276+
if (canceling) return js.resolvedPromise();
2277+
canceling = true;
22572278
return doCancel(consumer);
22582279
}
22592280
}
@@ -4106,6 +4127,22 @@ jsg::Ref<ReadableStream> ReadableStream::from(
41064127
(controller),
41074128
(jsg::Lock& js, kj::Maybe<jsg::Value> value) {
41084129
KJ_IF_SOME(v, value) {
4130+
auto handle = v.getHandle(js);
4131+
// Per the ReadableStream.from spec, if the value is a promise,
4132+
// the stream should wait for it to resolve and enqueue the
4133+
// resolved value...
4134+
// ... yes, this means that ReadableStream.from where the inputs
4135+
// are promises will be slow, but that's the spec.
4136+
if (handle->IsPromise()) {
4137+
return js.toPromise(handle.As<v8::Promise>()).then(js,
4138+
JSG_VISITABLE_LAMBDA(
4139+
(controller=controller.addRef()),
4140+
(controller),
4141+
(jsg::Lock& js, jsg::Value val) mutable {
4142+
controller->enqueue(js, val.getHandle(js));
4143+
return js.resolvedPromise();
4144+
}));
4145+
}
41094146
controller->enqueue(js, v.getHandle(js));
41104147
} else {
41114148
controller->close(js);
@@ -4119,7 +4156,7 @@ jsg::Ref<ReadableStream> ReadableStream::from(
41194156
}));
41204157
},
41214158
.cancel = [generator = rcGenerator.addRef()](jsg::Lock& js, auto reason) mutable {
4122-
return generator->generator.return_(js, kj::none)
4159+
return generator->generator.return_(js, js.v8Ref(reason))
41234160
.then(js, [generator = kj::mv(generator)](auto& lock, auto) {
41244161
// The generator might produce a value on return and might even want to continue,
41254162
// but the stream has been canceled at this point, so we stop here.

src/workerd/jsg/iterator.h

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,13 @@ class GeneratorWrapper {
389389
v8::Local<v8::Value> handle,
390390
Generator<T>*,
391391
kj::Maybe<v8::Local<v8::Object>> parentObject) {
392+
if (handle->IsString()) {
393+
// In order to be able to treat a string as a generator, we need to first
394+
// convert it to a String object. Yes, this means that each call to next
395+
// will yield a single character from the string, which is terrible but
396+
// that's the spec.
397+
handle = check(handle->ToObject(context));
398+
}
392399
if (handle->IsObject()) {
393400
auto isolate = js.v8Isolate;
394401
auto object = handle.As<v8::Object>();
@@ -410,12 +417,21 @@ class GeneratorWrapper {
410417
v8::Local<v8::Value> handle,
411418
AsyncGenerator<T>*,
412419
kj::Maybe<v8::Local<v8::Object>> parentObject) {
420+
if (handle->IsString()) {
421+
// In order to be able to treat a string as a generator, we need to first
422+
// convert it to a String object. Yes, this means that each call to next
423+
// will yield a single character from the string, which is terrible but
424+
// that's the spec.
425+
handle = check(handle->ToObject(context));
426+
}
413427
if (handle->IsObject()) {
414428
auto isolate = js.v8Isolate;
415429
auto object = handle.As<v8::Object>();
416430
auto iter = check(object->Get(context, v8::Symbol::GetAsyncIterator(isolate)));
417-
// If there is no async iterator, let's try a sync iterator
418-
if (iter->IsUndefined()) iter = check(object->Get(context, v8::Symbol::GetIterator(isolate)));
431+
// If there is no async iterator, let's try a sync iterator.
432+
if (iter->IsNullOrUndefined()) {
433+
iter = check(object->Get(context, v8::Symbol::GetIterator(isolate)));
434+
}
419435
if (iter->IsFunction()) {
420436
auto func = iter.As<v8::Function>();
421437
auto iterObj = check(func->Call(context, object, 0, nullptr));
@@ -488,6 +504,11 @@ class SequenceWrapper {
488504
kj::Maybe<v8::Local<v8::Object>> parentObject) {
489505
auto isolate = js.v8Isolate;
490506
auto& typeWrapper = TypeWrapper::from(isolate);
507+
// In this case, if handle is a string, we likely do not want to treat it as
508+
// a sequence of characters, which the Generator case would do. If someone
509+
// really wants to treat a string as a sequence of characters, then they
510+
// should use the Generator interface directly.
511+
if (handle->IsString()) return kj::none;
491512
KJ_IF_SOME(gen,
492513
typeWrapper.tryUnwrap(js, context, handle, (Generator<U>*)nullptr, parentObject)) {
493514
// The generator gives us no indication of how many items there might be, so we

src/wpt/streams-test.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -455,21 +455,13 @@ export default {
455455
comment: 'See comments on tests',
456456
disabledTests: [
457457
// A hanging promise was cancelled
458-
'ReadableStream.from: cancel() resolves when return() method is missing',
459458
'ReadableStream.from: cancel() rejects when return() rejects',
460459
'ReadableStream.from: cancel() rejects when return() fulfills with a non-object',
461-
// Heap use-after-free
462-
'ReadableStream.from: reader.cancel() inside return()',
463-
'ReadableStream.from: reader.cancel() inside next()',
464460
],
465461
expectedFailures: [
466-
// To be investigated
462+
// TODO(soon): This one is a bit pedantic. We ignore the case where return() is not
463+
// a method whereas the spec expects us to return a rejected promise in this case.
467464
'ReadableStream.from: cancel() rejects when return() is not a method',
468-
'ReadableStream.from accepts a string',
469-
'ReadableStream.from accepts an array of promises',
470-
'ReadableStream.from accepts a sync iterable of promises',
471-
'ReadableStream.from ignores a null @@asyncIterator',
472-
'ReadableStream.from: cancelling the returned stream calls and awaits return()',
473465
],
474466
},
475467
'readable-streams/garbage-collection.any.js': {

0 commit comments

Comments
 (0)