diff --git a/src/workerd/api/container.c++ b/src/workerd/api/container.c++ index 4e1fb2eebb2..92eb86260e6 100644 --- a/src/workerd/api/container.c++ +++ b/src/workerd/api/container.c++ @@ -154,8 +154,8 @@ jsg::Promise> ExecProcess::output(jsg::Lock& js) { stdoutPromise = stream->getController() .readAllBytes(js, IoContext::current().getLimitEnforcer().getBufferingLimit()) - .then(js, [](jsg::Lock&, jsg::BufferSource bytes) { - return kj::heapArray(bytes.asArrayPtr()); + .then(js, [](jsg::Lock& js, jsg::JsRef bytes) { + return bytes.getHandle(js).copy(); }); } @@ -165,8 +165,8 @@ jsg::Promise> ExecProcess::output(jsg::Lock& js) { "Cannot call output() after stderr has started being consumed."); stderrPromise = stream->getController() .readAllBytes(js, kj::maxValue) - .then(js, [](jsg::Lock&, jsg::BufferSource bytes) { - return kj::heapArray(bytes.asArrayPtr()); + .then(js, [](jsg::Lock& js, jsg::JsRef bytes) { + return bytes.getHandle(js).copy(); }); } @@ -525,7 +525,7 @@ jsg::Promise> Container::exec( KJ_CASE_ONEOF(readable, jsg::Ref) { auto sink = newSystemStream(kj::mv(stdinWriter), StreamEncoding::IDENTITY, ioContext); auto pipePromise = - (ioContext.waitForDeferredProxy(readable->pumpTo(js, kj::mv(sink), true))); + (ioContext.waitForDeferredProxy(readable->pumpTo(js, kj::mv(sink), End::YES))); ioContext.addTask(pipePromise.attach(readable.addRef())); } // user sets "pipe"... they want to consume the API with the stdin WritableStream diff --git a/src/workerd/api/crypto/crypto.c++ b/src/workerd/api/crypto/crypto.c++ index 6cf3456554f..acd15bd4953 100644 --- a/src/workerd/api/crypto/crypto.c++ +++ b/src/workerd/api/crypto/crypto.c++ @@ -800,7 +800,7 @@ void DigestStream::dispose(jsg::Lock& js) { KJ_IF_SOME(ready, state.tryGet()) { auto reason = js.typeError("The DigestStream was disposed."); ready.resolver.reject(js, reason); - state.init(js.v8Ref(reason)); + state.init(reason.addRef(js)); } } JSG_CATCH(exception) { @@ -859,7 +859,7 @@ void DigestStream::abort(jsg::Lock& js, jsg::JsValue reason) { // If the state is already closed or errored, then this is a non-op KJ_IF_SOME(ready, state.tryGet()) { ready.resolver.reject(js, reason); - state.init(js.v8Ref(reason)); + state.init(reason.addRef(js)); } } @@ -870,7 +870,7 @@ jsg::Ref DigestStream::constructor(jsg::Lock& js, Algorithm algori interpretAlgorithmParam(kj::mv(algorithm)), kj::mv(paf.resolver), kj::mv(paf.promise)); // clang-format off - stream->getController().setup(js, UnderlyingSink{ + auto sink = kj::heap(js, UnderlyingSink{ .write = [&stream = *stream](jsg::Lock& js, v8::Local chunk, auto c) mutable { return js.tryCatch([&] { // Make sure what we got can be interpreted as bytes... @@ -916,9 +916,11 @@ jsg::Ref DigestStream::constructor(jsg::Lock& js, Algorithm algori return js.resolvedPromise(); }, [&](jsg::Value exception) { return js.rejectedPromise(kj::mv(exception)); }); } - }, kj::none); + }, StreamQueuingStrategy {}); // clang-format on + stream->getController().setup(js, kj::mv(sink)); + return kj::mv(stream); } diff --git a/src/workerd/api/eventsource.c++ b/src/workerd/api/eventsource.c++ index 8ac114dabcb..cb80dbbde01 100644 --- a/src/workerd/api/eventsource.c++ +++ b/src/workerd/api/eventsource.c++ @@ -474,8 +474,8 @@ void EventSource::run(jsg::Lock& js, // pumping the body into an EventSourceSink until the body is closed, canceled, // or errored. context - .awaitIo( - js, processBody(context, readable->pumpTo(js, kj::heap(*this), true))) + .awaitIo(js, + processBody(context, readable->pumpTo(js, kj::heap(*this), End::YES))) .then(js, kj::mv(onSuccess), kj::mv(onFailed)); } diff --git a/src/workerd/api/filesystem.c++ b/src/workerd/api/filesystem.c++ index 42006199e2f..8add03eaed9 100644 --- a/src/workerd/api/filesystem.c++ +++ b/src/workerd/api/filesystem.c++ @@ -490,7 +490,7 @@ void FileSystemModule::close(jsg::Lock& js, int fd) { } uint32_t FileSystemModule::write( - jsg::Lock& js, int fd, kj::Array data, WriteOptions options) { + jsg::Lock& js, int fd, kj::Array> data, WriteOptions options) { auto& vfs = workerd::VirtualFileSystem::current(js); KJ_IF_SOME(opened, vfs.tryGetFd(js, fd)) { @@ -513,7 +513,7 @@ uint32_t FileSystemModule::write( auto pos = getPosition(js, opened.addRef(), file.addRef(), options); uint32_t total = 0; for (auto& buffer: data) { - KJ_SWITCH_ONEOF(file->write(js, pos, buffer)) { + KJ_SWITCH_ONEOF(file->write(js, pos, buffer.getHandle(js).asArrayPtr())) { KJ_CASE_ONEOF(written, uint32_t) { pos += written; total += written; @@ -546,7 +546,7 @@ uint32_t FileSystemModule::write( } uint32_t FileSystemModule::read( - jsg::Lock& js, int fd, kj::Array data, WriteOptions options) { + jsg::Lock& js, int fd, kj::Array> data, WriteOptions options) { auto& vfs = workerd::VirtualFileSystem::current(js); KJ_IF_SOME(opened, vfs.tryGetFd(js, fd)) { if (!opened->read) { @@ -561,11 +561,12 @@ uint32_t FileSystemModule::read( } uint32_t total = 0; for (auto& buffer: data) { - auto read = file->read(js, pos, buffer); + auto handle = buffer.getHandle(js); + auto read = file->read(js, pos, handle.asArrayPtr()); // if read is less than the size of the buffer, we are at EOF. pos += read; total += read; - if (read < buffer.size()) break; + if (read < handle.size()) break; } // We only update the position if the options.position is not set. if (options.position == kj::none) { @@ -588,7 +589,7 @@ uint32_t FileSystemModule::read( } } -jsg::BufferSource FileSystemModule::readAll(jsg::Lock& js, kj::OneOf pathOrFd) { +jsg::JsUint8Array FileSystemModule::readAll(jsg::Lock& js, kj::OneOf pathOrFd) { auto& vfs = workerd::VirtualFileSystem::current(js); KJ_SWITCH_ONEOF(pathOrFd) { KJ_CASE_ONEOF(path, FilePath) { @@ -597,8 +598,8 @@ jsg::BufferSource FileSystemModule::readAll(jsg::Lock& js, kj::OneOf) { KJ_SWITCH_ONEOF(file->readAllBytes(js)) { - KJ_CASE_ONEOF(data, jsg::BufferSource) { - return kj::mv(data); + KJ_CASE_ONEOF(data, jsg::JsUint8Array) { + return data; } KJ_CASE_ONEOF(err, workerd::FsError) { throwFsError(js, err, "readAll"_kj); @@ -635,8 +636,8 @@ jsg::BufferSource FileSystemModule::readAll(jsg::Lock& js, kj::OneOfreadAllBytes(js)) { - KJ_CASE_ONEOF(data, jsg::BufferSource) { - return kj::mv(data); + KJ_CASE_ONEOF(data, jsg::JsUint8Array) { + return data; } KJ_CASE_ONEOF(err, workerd::FsError) { throwFsError(js, err, "freadAll"_kj); @@ -656,7 +657,7 @@ jsg::BufferSource FileSystemModule::readAll(jsg::Lock& js, kj::OneOf pathOrFd, - jsg::BufferSource data, + jsg::JsBufferSource data, WriteAllOptions options) { auto& vfs = workerd::VirtualFileSystem::current(js); @@ -684,7 +685,7 @@ uint32_t FileSystemModule::writeAll(jsg::Lock& js, // If the append option is set, we will write to the end of the file // instead of overwriting it. if (options.append) { - KJ_SWITCH_ONEOF(file->write(js, stat.size, data)) { + KJ_SWITCH_ONEOF(file->write(js, stat.size, data.asArrayPtr())) { KJ_CASE_ONEOF(written, uint32_t) { return written; } @@ -696,7 +697,7 @@ uint32_t FileSystemModule::writeAll(jsg::Lock& js, } // Otherwise, we overwrite the entire file. - KJ_SWITCH_ONEOF(file->writeAll(js, data)) { + KJ_SWITCH_ONEOF(file->writeAll(js, data.asArrayPtr())) { KJ_CASE_ONEOF(written, uint32_t) { return written; } @@ -737,7 +738,7 @@ uint32_t FileSystemModule::writeAll(jsg::Lock& js, node::THROW_ERR_UV_EPERM(js, "writeAll"_kj); } auto file = workerd::File::newWritable(js, static_cast(data.size())); - KJ_SWITCH_ONEOF(file->writeAll(js, data)) { + KJ_SWITCH_ONEOF(file->writeAll(js, data.asArrayPtr())) { KJ_CASE_ONEOF(written, uint32_t) { KJ_IF_SOME(err, dir->add(js, relative.name, kj::mv(file))) { throwFsError(js, err, "writeAll"_kj); @@ -788,14 +789,14 @@ uint32_t FileSystemModule::writeAll(jsg::Lock& js, // If the file descriptor was opened in append mode, or if the append option // is set, then we'll use write instead to append to the end of the file. if (opened->append || options.append) { - return write(js, fd, kj::arr(kj::mv(data)), + return write(js, fd, kj::arr(data.addRef(js)), { .position = stat.size, }); } // Otherwise, we overwrite the entire file. - KJ_SWITCH_ONEOF(file->writeAll(js, data)) { + KJ_SWITCH_ONEOF(file->writeAll(js, data.asArrayPtr())) { KJ_CASE_ONEOF(written, uint32_t) { return written; } @@ -1890,9 +1891,8 @@ jsg::Ref FileSystemModule::openAsBlob( } KJ_CASE_ONEOF(file, kj::Rc) { KJ_SWITCH_ONEOF(file->readAllBytes(js)) { - KJ_CASE_ONEOF(bytes, jsg::BufferSource) { - return js.alloc( - js, bytes.getJsHandle(js), kj::mv(options.type).orDefault(kj::String())); + KJ_CASE_ONEOF(bytes, jsg::JsUint8Array) { + return js.alloc(js, bytes, kj::mv(options.type).orDefault(kj::String())); } KJ_CASE_ONEOF(err, workerd::FsError) { throwFsError(js, err, "open"_kj); @@ -2557,10 +2557,10 @@ jsg::Promise> FileSystemFileHandle::getFile( KJ_CASE_ONEOF(file, kj::Rc) { auto stat = file->stat(js); KJ_SWITCH_ONEOF(file->readAllBytes(js)) { - KJ_CASE_ONEOF(bytes, jsg::BufferSource) { + KJ_CASE_ONEOF(bytes, jsg::JsUint8Array) { return js.resolvedPromise( - js.alloc(js, bytes.getJsHandle(js), jsg::USVString(kj::str(getName(js))), - kj::String(), (stat.lastModified - kj::UNIX_EPOCH) / kj::MILLISECONDS)); + js.alloc(js, bytes, jsg::USVString(kj::str(getName(js))), kj::String(), + (stat.lastModified - kj::UNIX_EPOCH) / kj::MILLISECONDS)); } KJ_CASE_ONEOF(err, workerd::FsError) { return js.rejectedPromise>( @@ -2713,7 +2713,9 @@ jsg::Promise> FileSystemFileHandle::creat return js.resolvedPromise(); }, [&](jsg::Value exception) { return js.rejectedPromise(kj::mv(exception)); }); }; - stream->getController().setup(js, kj::mv(sink), kj::none); + + stream->getController().setup( + js, kj::heap(js, kj::mv(sink), StreamQueuingStrategy{})); return js.resolvedPromise(kj::mv(stream)); } @@ -2724,7 +2726,7 @@ FileSystemWritableFileStream::FileSystemWritableFileStream( sharedState(kj::mv(sharedState)) {} jsg::Promise FileSystemWritableFileStream::write(jsg::Lock& js, - kj::OneOf, jsg::BufferSource, kj::String, WriteParams> data, + kj::OneOf, jsg::JsBufferSource, kj::String, WriteParams> data, const jsg::TypeHandler>& deHandler) { JSG_REQUIRE(!getController().isLockedToWriter(), TypeError, "Cannot write to a stream that is locked to a reader"); @@ -2750,8 +2752,8 @@ jsg::Promise FileSystemWritableFileStream::writeImpl(jsg::Lock& js, } } } - KJ_CASE_ONEOF(buffer, jsg::BufferSource) { - KJ_SWITCH_ONEOF(inner->write(js, state.position, buffer)) { + KJ_CASE_ONEOF(buffer, jsg::JsBufferSource) { + KJ_SWITCH_ONEOF(inner->write(js, state.position, buffer.asArrayPtr())) { KJ_CASE_ONEOF(written, uint32_t) { state.position += written; } @@ -2799,8 +2801,8 @@ jsg::Promise FileSystemWritableFileStream::writeImpl(jsg::Lock& js, } KJ_UNREACHABLE; } - KJ_CASE_ONEOF(buffer, jsg::BufferSource) { - KJ_SWITCH_ONEOF(inner->write(js, offset, buffer)) { + KJ_CASE_ONEOF(buffer, jsg::JsRef) { + KJ_SWITCH_ONEOF(inner->write(js, offset, buffer.getHandle(js).asArrayPtr())) { KJ_CASE_ONEOF(written, uint32_t) { state.position = offset + written; return js.resolvedPromise(); diff --git a/src/workerd/api/filesystem.h b/src/workerd/api/filesystem.h index b774fb45b79..113eceef813 100644 --- a/src/workerd/api/filesystem.h +++ b/src/workerd/api/filesystem.h @@ -103,10 +103,10 @@ class FileSystemModule final: public jsg::Object { JSG_STRUCT(position); }; - uint32_t write(jsg::Lock& js, int fd, kj::Array data, WriteOptions options); - uint32_t read(jsg::Lock& js, int fd, kj::Array data, WriteOptions options); + uint32_t write(jsg::Lock& js, int fd, kj::Array> data, WriteOptions options); + uint32_t read(jsg::Lock& js, int fd, kj::Array> data, WriteOptions options); - jsg::BufferSource readAll(jsg::Lock& js, kj::OneOf pathOrFd); + jsg::JsUint8Array readAll(jsg::Lock& js, kj::OneOf pathOrFd); struct WriteAllOptions { bool exclusive; @@ -116,7 +116,7 @@ class FileSystemModule final: public jsg::Object { uint32_t writeAll(jsg::Lock& js, kj::OneOf pathOrFd, - jsg::BufferSource data, + jsg::JsBufferSource data, WriteAllOptions options); struct RenameOrCopyOptions { @@ -298,12 +298,12 @@ struct FileSystemFileWriteParams { jsg::Optional position; // Yes, wrapping the kj::Maybe with a jsg::Optional is intentional here. We need to // be able to accept null or undefined values and handle them per the spec. - jsg::Optional, jsg::BufferSource, kj::String>>> data; + jsg::Optional, jsg::JsRef, kj::String>>> data; JSG_STRUCT(type, size, position, data); }; using FileSystemWritableData = - kj::OneOf, jsg::BufferSource, kj::String, FileSystemFileWriteParams>; + kj::OneOf, jsg::JsBufferSource, kj::String, FileSystemFileWriteParams>; class FileSystemFileHandle final: public FileSystemHandle { public: diff --git a/src/workerd/api/html-rewriter.c++ b/src/workerd/api/html-rewriter.c++ index 3a8c2f8fabf..5c040755b79 100644 --- a/src/workerd/api/html-rewriter.c++ +++ b/src/workerd/api/html-rewriter.c++ @@ -727,7 +727,7 @@ kj::Promise Rewriter::replacerThunkPromise( auto streamSink = kj::heap(sink, registration.isHtml); return ioContext.waitForDeferredProxy( - registration.stream->pumpTo(lock, kj::mv(streamSink), true)); + registration.stream->pumpTo(lock, kj::mv(streamSink), End::YES)); }); } @@ -1294,10 +1294,10 @@ jsg::Ref HTMLRewriter::transform(jsg::Lock& js, jsg::Ref res // after we know that nothing else (like invalid encoding) could cause an exception. // Drive and flush the parser asynchronously. - ioContext.addTask( - ioContext - .waitForDeferredProxy(KJ_ASSERT_NONNULL(maybeInput)->pumpTo(js, kj::mv(rewriter), true)) - .catch_([](kj::Exception&& e) { + ioContext.addTask(ioContext + .waitForDeferredProxy( + KJ_ASSERT_NONNULL(maybeInput)->pumpTo(js, kj::mv(rewriter), End::YES)) + .catch_([](kj::Exception&& e) { // Errors in pumpTo() are already propagated to the destination stream. We don't want to // throw them from here since it'll cause an uncaught exception to be reported via taskFailed(), // which would poison the IoContext even though the application may have handled the error. diff --git a/src/workerd/api/http.c++ b/src/workerd/api/http.c++ index 1b86fe02681..419fb2beb3c 100644 --- a/src/workerd/api/http.c++ +++ b/src/workerd/api/http.c++ @@ -109,7 +109,7 @@ Body::ExtractedBody Body::extractBody(jsg::Lock& js, Initializer init) { KJ_CASE_ONEOF(stream, jsg::Ref) { return kj::mv(stream); } - KJ_CASE_ONEOF(gen, jsg::AsyncGeneratorIgnoringStrings) { + KJ_CASE_ONEOF(gen, jsg::AsyncGeneratorIgnoringStrings>) { return ReadableStream::from(js, gen.release()); } KJ_CASE_ONEOF(text, kj::String) { @@ -242,7 +242,7 @@ bool Body::getBodyUsed() { } return false; } -jsg::Promise Body::arrayBuffer(jsg::Lock& js) { +jsg::Promise> Body::arrayBuffer(jsg::Lock& js) { KJ_IF_SOME(i, impl) { return js.evalNow([&] { JSG_REQUIRE(!i.stream->isDisturbed(), TypeError, @@ -255,13 +255,15 @@ jsg::Promise Body::arrayBuffer(jsg::Lock& js) { // If there's no body, we just return an empty array. // See https://fetch.spec.whatwg.org/#concept-body-consume-body - auto backing = jsg::BackingStore::alloc(js, 0); - return js.resolvedPromise(jsg::BufferSource(js, kj::mv(backing))); + auto empty = jsg::JsArrayBuffer::create(js, 0); + return js.resolvedPromise(empty.addRef(js)); } -jsg::Promise Body::bytes(jsg::Lock& js) { - return arrayBuffer(js).then(js, - [](jsg::Lock& js, jsg::BufferSource data) { return data.getTypedView(js); }); +jsg::Promise> Body::bytes(jsg::Lock& js) { + return arrayBuffer(js).then(js, [](jsg::Lock& js, jsg::JsRef data) { + jsg::JsUint8Array u8 = data.getHandle(js); + return u8.addRef(js); + }); } jsg::Promise Body::text(jsg::Lock& js) { @@ -331,7 +333,7 @@ jsg::Promise Body::json(jsg::Lock& js) { } jsg::Promise> Body::blob(jsg::Lock& js) { - return arrayBuffer(js).then(js, [this](jsg::Lock& js, jsg::BufferSource buffer) { + return arrayBuffer(js).then(js, [this](jsg::Lock& js, jsg::JsRef buffer) { kj::String contentType = headersRef.getCommon(js, capnp::CommonHeaderName::CONTENT_TYPE) .map([](auto&& b) -> kj::String { return kj::mv(b); @@ -344,7 +346,7 @@ jsg::Promise> Body::blob(jsg::Lock& js) { }).orDefault(nullptr); } - return js.alloc(js, buffer.getJsHandle(js), kj::mv(contentType)); + return js.alloc(js, buffer.getHandle(js), kj::mv(contentType)); }); } @@ -1353,7 +1355,7 @@ kj::Promise> Response::send(jsg::Lock& js, // We need to enter the AsyncContextFrame that was captured when the // Response was created before starting the loop. jsg::AsyncContextFrame::Scope scope(js, asyncContext); - return jsBody->pumpTo(js, kj::mv(stream), true); + return jsBody->pumpTo(js, kj::mv(stream), End::YES); } else { outer.send(statusCode, getStatusText(), outHeaders, static_cast(0)); return addNoopDeferredProxy(kj::READY_NOW); @@ -1700,7 +1702,7 @@ jsg::Promise> fetchImplNoOutputLock(jsg::Lock& js, // TODO(someday): Allow deferred proxying for bidirectional streaming. ioContext.addWaitUntil(handleCancelablePump( AbortSignal::maybeCancelWrap( - js, signal, ioContext.waitForDeferredProxy(jsBody->pumpTo(js, kj::mv(stream), true))), + js, signal, ioContext.waitForDeferredProxy(jsBody->pumpTo(js, kj::mv(stream), End::YES))), jsBody.addRef())); } else { nativeRequest = client->request(jsRequest->getMethodEnum(), url, headers, static_cast(0)); diff --git a/src/workerd/api/http.h b/src/workerd/api/http.h index df9f1a4c4f9..f6a6b3d9857 100644 --- a/src/workerd/api/http.h +++ b/src/workerd/api/http.h @@ -50,7 +50,7 @@ class Body: public jsg::Object { jsg::Ref, jsg::Ref, jsg::Ref, - jsg::AsyncGeneratorIgnoringStrings>; + jsg::AsyncGeneratorIgnoringStrings>>; struct RefcountedBytes final: public kj::Refcounted { kj::Array bytes; @@ -164,8 +164,8 @@ class Body: public jsg::Object { kj::Maybe> getBody(); bool getBodyUsed(); - jsg::Promise arrayBuffer(jsg::Lock& js); - jsg::Promise bytes(jsg::Lock& js); + jsg::Promise> arrayBuffer(jsg::Lock& js); + jsg::Promise> bytes(jsg::Lock& js); jsg::Promise text(jsg::Lock& js); jsg::Promise> formData(jsg::Lock& js); jsg::Promise json(jsg::Lock& js); @@ -362,7 +362,8 @@ class Fetcher: public JsRpcClientProvider { kj::OneOf, kj::String> requestOrUrl, jsg::Optional>> requestInit); - using GetResult = kj::OneOf, jsg::BufferSource, kj::String, jsg::Value>; + using GetResult = + kj::OneOf, jsg::JsRef, kj::String, jsg::Value>; jsg::Promise get(jsg::Lock& js, kj::String url, jsg::Optional type); diff --git a/src/workerd/api/kv.c++ b/src/workerd/api/kv.c++ index 8dc8ab7bd58..0d73d43de48 100644 --- a/src/workerd/api/kv.c++ +++ b/src/workerd/api/kv.c++ @@ -633,7 +633,7 @@ jsg::Promise KvNamespace::put(jsg::Lock& js, [dest = newSystemStream(kj::mv(req.body), StreamEncoding::IDENTITY, context), stream = kj::mv(stream)](jsg::Lock& js) mutable { return IoContext::current().waitForDeferredProxy( - stream->pumpTo(js, kj::mv(dest), true)); + stream->pumpTo(js, kj::mv(dest), End::YES)); }); } } diff --git a/src/workerd/api/queue.c++ b/src/workerd/api/queue.c++ index 6f9012e5850..b3c48fbd2c7 100644 --- a/src/workerd/api/queue.c++ +++ b/src/workerd/api/queue.c++ @@ -176,7 +176,7 @@ jsg::JsValue deserialize( if (type == IncomingQueueMessage::ContentType::TEXT) { return js.str(body); } else if (type == IncomingQueueMessage::ContentType::BYTES) { - return jsg::JsValue(js.bytes(kj::mv(body)).getHandle(js)); + return jsg::JsUint8Array::create(js, body); } else if (type == IncomingQueueMessage::ContentType::JSON) { return jsg::JsValue::fromJson(js, body.asChars()); } else if (type == IncomingQueueMessage::ContentType::V8) { @@ -196,8 +196,7 @@ jsg::JsValue deserialize(jsg::Lock& js, rpc::QueueMessage::Reader message) { if (type == IncomingQueueMessage::ContentType::TEXT) { return js.str(message.getData().asChars()); } else if (type == IncomingQueueMessage::ContentType::BYTES) { - kj::Array bytes = kj::heapArray(message.getData().asBytes()); - return jsg::JsValue(js.bytes(kj::mv(bytes)).getHandle(js)); + return jsg::JsUint8Array::create(js, message.getData().asBytes()); } else if (type == IncomingQueueMessage::ContentType::JSON) { return jsg::JsValue::fromJson(js, message.getData().asChars()); } else if (type == IncomingQueueMessage::ContentType::V8) { diff --git a/src/workerd/api/r2-bucket.c++ b/src/workerd/api/r2-bucket.c++ index 40decac2ec1..bce363d5575 100644 --- a/src/workerd/api/r2-bucket.c++ +++ b/src/workerd/api/r2-bucket.c++ @@ -572,7 +572,7 @@ jsg::Promise>> R2Bucket::put(jsg::Lock& KJ_SWITCH_ONEOF(v) { KJ_CASE_ONEOF(v, jsg::Ref) { (*v).cancel(js, - js.v8Error( + js.error( "Stream cancelled because the associated put operation encountered an error.")); } KJ_CASE_ONEOF_DEFAULT {} @@ -1367,7 +1367,7 @@ void R2Bucket::HeadResult::writeHttpMetadata(jsg::Lock& js, Headers& headers) { } } -jsg::Promise R2Bucket::GetResult::arrayBuffer(jsg::Lock& js) { +jsg::Promise> R2Bucket::GetResult::arrayBuffer(jsg::Lock& js) { return js.evalNow([&] { JSG_REQUIRE(!body->isDisturbed(), TypeError, "Body has already been used. " @@ -1378,7 +1378,7 @@ jsg::Promise R2Bucket::GetResult::arrayBuffer(jsg::Lock& js) }); } -jsg::Promise R2Bucket::GetResult::bytes(jsg::Lock& js) { +jsg::Promise> R2Bucket::GetResult::bytes(jsg::Lock& js) { return js.evalNow([&] { JSG_REQUIRE(!body->isDisturbed(), TypeError, "Body has already been used. " @@ -1387,8 +1387,9 @@ jsg::Promise R2Bucket::GetResult::bytes(jsg::Lock& js) { auto& context = IoContext::current(); return body->getController() .readAllBytes(js, context.getLimitEnforcer().getBufferingLimit()) - .then(js, [](jsg::Lock& js, jsg::BufferSource data) { - return data.getTypedView(js); + .then(js, [](jsg::Lock& js, jsg::JsRef data) { + jsg::JsUint8Array u8 = data.getHandle(js); + return u8.addRef(js); }); }); } @@ -1422,11 +1423,11 @@ jsg::Promise R2Bucket::GetResult::json(jsg::Lock& js) { jsg::Promise> R2Bucket::GetResult::blob(jsg::Lock& js) { // Copy-pasted from http.c++ - return arrayBuffer(js).then(js, [this](jsg::Lock& js, jsg::BufferSource buffer) { + return arrayBuffer(js).then(js, [this](jsg::Lock& js, jsg::JsRef buffer) { // httpMetadata can't be null because GetResult always populates it. kj::String contentType = mapCopyString(KJ_REQUIRE_NONNULL(httpMetadata).contentType).orDefault(nullptr); - return js.alloc(js, buffer.getJsHandle(js), kj::mv(contentType)); + return js.alloc(js, buffer.getHandle(js), kj::mv(contentType)); }); } diff --git a/src/workerd/api/r2-bucket.h b/src/workerd/api/r2-bucket.h index 3712ba7eb86..7bde2783811 100644 --- a/src/workerd/api/r2-bucket.h +++ b/src/workerd/api/r2-bucket.h @@ -387,8 +387,8 @@ class R2Bucket: public jsg::Object { return body->isDisturbed(); } - jsg::Promise arrayBuffer(jsg::Lock& js); - jsg::Promise bytes(jsg::Lock& js); + jsg::Promise> arrayBuffer(jsg::Lock& js); + jsg::Promise> bytes(jsg::Lock& js); jsg::Promise text(jsg::Lock& js); jsg::Promise json(jsg::Lock& js); jsg::Promise> blob(jsg::Lock& js); diff --git a/src/workerd/api/r2-rpc.c++ b/src/workerd/api/r2-rpc.c++ index f98b0f75124..de3ad009414 100644 --- a/src/workerd/api/r2-rpc.c++ +++ b/src/workerd/api/r2-rpc.c++ @@ -233,7 +233,8 @@ kj::Promise doR2HTTPPutRequest(kj::Own client, co_await context.run( [dest = newSystemStream(kj::mv(request.body), StreamEncoding::IDENTITY, context), stream = kj::mv(stream)](jsg::Lock& js) mutable { - return IoContext::current().waitForDeferredProxy(stream->pumpTo(js, kj::mv(dest), true)); + return IoContext::current().waitForDeferredProxy( + stream->pumpTo(js, kj::mv(dest), End::YES)); }); } } diff --git a/src/workerd/api/sockets-test.c++ b/src/workerd/api/sockets-test.c++ index 1649f250202..284c83641cf 100644 --- a/src/workerd/api/sockets-test.c++ +++ b/src/workerd/api/sockets-test.c++ @@ -123,8 +123,8 @@ KJ_TEST("socket writes are blocked by output gate") { auto blocker = actor.getOutputGate().lockWhile(kj::mv(paf.promise), nullptr); auto writable = socket->getWritable(); auto data = kj::heapArray({'h', 'i'}); - auto jsBuffer = env.js.bytes(kj::mv(data)).getHandle(env.js); - writable->getController().write(env.js, jsBuffer).markAsHandled(env.js); + auto u8 = jsg::JsUint8Array::create(env.js, data); + writable->getController().write(env.js, u8).markAsHandled(env.js); // With autogate (@all-autogates), connect is deferred. Wait for it. // After co_await, Worker lock is released — no V8 calls allowed. diff --git a/src/workerd/api/sockets.c++ b/src/workerd/api/sockets.c++ index 12f676d389f..2ce9d0faf91 100644 --- a/src/workerd/api/sockets.c++ +++ b/src/workerd/api/sockets.c++ @@ -370,7 +370,7 @@ jsg::Ref Socket::startTls(jsg::Lock& js, jsg::Optional tlsOp auto& context = IoContext::current(); self->writable->detach(js); - self->readable->detach(js, true); + self->readable->detach(js, IgnoreDisturbed::YES); // We should set this before closedResolver.resolve() in order to give the user // the option to check if the closed promise is resolved due to upgrade or not. @@ -566,7 +566,7 @@ kj::Own Socket::takeConnectionStream(jsg::Lock& js) { // We do not care if the socket was disturbed, we require the user to ensure the socket is not // being used. writable->detach(js); - readable->detach(js, true); + readable->detach(js, IgnoreDisturbed::YES); // Move the stream out of the socket, to ensure the stream is properly destroyed when the // caller is done with it. diff --git a/src/workerd/api/streams-test.c++ b/src/workerd/api/streams-test.c++ index 8f87442dd7f..a4bf8eb2110 100644 --- a/src/workerd/api/streams-test.c++ +++ b/src/workerd/api/streams-test.c++ @@ -58,12 +58,13 @@ KJ_TEST("Reading from default reader") { KJ_ASSERT(!readResult.done); auto& value = KJ_REQUIRE_NONNULL(readResult.value); auto handle = value.getHandle(js); - KJ_ASSERT(handle->IsUint8Array()); + KJ_ASSERT(handle.isUint8Array()); + jsg::JsBufferSource source(handle); if (util::Autogate::isEnabled(util::AutogateKey::UPDATED_AUTO_ALLOCATE_CHUNK_SIZE)) { // With 16KB buffer, the entire 10KB stream fits in one read. - KJ_ASSERT(streamLength == handle.As()->ByteLength()); + KJ_ASSERT(streamLength == source.size()); } else { - KJ_ASSERT(4 * 1024 == handle.As()->ByteLength()); + KJ_ASSERT(4 * 1024 == source.size()); } }))); }); @@ -95,9 +96,7 @@ KJ_TEST("Reading from byob reader") { KJ_REQUIRE(reader.is>()); auto& byobReader = reader.get>(); - auto buffer = v8::Uint8Array::New( - v8::ArrayBuffer::New(js.v8Isolate, test.bufferSize), 0, test.bufferSize); - + auto buffer = jsg::JsUint8Array::create(js, test.bufferSize); return env.context.awaitJs(js, byobReader->read(js, buffer, {}).then(js, JSG_VISITABLE_LAMBDA( (test, reader = byobReader.addRef(), stream = stream.addRef()), @@ -106,10 +105,9 @@ KJ_TEST("Reading from byob reader") { auto& value = KJ_REQUIRE_NONNULL(readResult.value); auto handle = value.getHandle(js); - KJ_ASSERT(handle->IsUint8Array()); - auto view = handle.As(); - KJ_ASSERT(kj::min(test.streamLength, test.bufferSize) == view->ByteLength()); - KJ_ASSERT(test.bufferSize == view->Buffer()->ByteLength()); + auto view = KJ_REQUIRE_NONNULL(handle.tryCast()); + KJ_ASSERT(kj::min(test.streamLength, test.bufferSize) == view.size()); + KJ_ASSERT(test.bufferSize == view.getBuffer().size()); }))); return kj::READY_NOW; }); @@ -179,7 +177,8 @@ KJ_TEST("PumpToReader regression") { [](jsg::Lock& js, auto controller) { auto& c = KJ_REQUIRE_NONNULL( controller.template tryGet>()); - c->enqueue(js, v8::ArrayBuffer::New(js.v8Isolate, 10)); + auto ab = jsg::JsArrayBuffer::create(js, 10); + c->enqueue(js, ab); c->close(js); return js.resolvedPromise(); }}, @@ -187,7 +186,7 @@ KJ_TEST("PumpToReader regression") { auto sink = kj::heap(events); auto writePromise = kj::mv(sink->paf.promise); - auto promise = stream->pumpTo(js, kj::mv(sink), true); + auto promise = stream->pumpTo(js, kj::mv(sink), End::YES); return writePromise.attach(kj::mv(promise)); }); diff --git a/src/workerd/api/streams/common.c++ b/src/workerd/api/streams/common.c++ index 09339cd4bf5..43b12bd1a5d 100644 --- a/src/workerd/api/streams/common.c++ +++ b/src/workerd/api/streams/common.c++ @@ -4,17 +4,22 @@ #include "common.h" +#include "identity-transform-stream.h" + +#include +#include + namespace workerd::api { WritableStreamController::PendingAbort::PendingAbort( - jsg::Lock& js, jsg::PromiseResolverPair prp, v8::Local reason, bool reject) + jsg::Lock& js, jsg::PromiseResolverPair prp, jsg::JsValue reason, Reject reject) : resolver(kj::mv(prp.resolver)), promise(kj::mv(prp.promise)), - reason(js.v8Ref(reason)), + reason(reason.addRef(js)), reject(reject) {} WritableStreamController::PendingAbort::PendingAbort( - jsg::Lock& js, v8::Local reason, bool reject) + jsg::Lock& js, jsg::JsValue reason, Reject reject) : WritableStreamController::PendingAbort(js, js.newPromiseAndResolver(), reason, reject) { } @@ -26,7 +31,7 @@ void WritableStreamController::PendingAbort::complete(jsg::Lock& js) { } } -void WritableStreamController::PendingAbort::fail(jsg::Lock& js, v8::Local reason) { +void WritableStreamController::PendingAbort::fail(jsg::Lock& js, jsg::JsValue reason) { maybeRejectPromise(js, resolver, reason); } @@ -35,4 +40,185 @@ kj::Maybe> WritableStreamControl return kj::mv(maybePendingAbort); } +// ==================================================================================== + +UnderlyingSinkImpl::UnderlyingSinkImpl( + jsg::Lock& js, UnderlyingSink sink, StreamQueuingStrategy strategy) + : start_(kj::mv(sink.start)), + write_(kj::mv(sink.write)), + writev_(kj::mv(sink.writev)), + abort_(kj::mv(sink.abort)), + close_(kj::mv(sink.close)), + size_(kj::mv(strategy.size)), + highWaterMark_(strategy.highWaterMark.orDefault(DEFAULT_HIGH_WATER_MARK)) { + // Per the streams spec, the size function should be called with `undefined` as `this`, + // not as a method on the strategy object. + KJ_IF_SOME(size, size_) { + size.setReceiver(js.v8Ref(js.v8Undefined())); + } + if (FeatureFlags::get(js).getPedanticWpt()) { + // Per the spec, the type property for WritableStream's underlying sink must be undefined. + // If it's anything else, throw a RangeError. + JSG_REQUIRE(sink.type == kj::none, RangeError, + "Invalid underlying sink type. Only undefined is valid."); + } +} + +void UnderlyingSinkImpl::clearStart() { + start_ = kj::none; +} + +void UnderlyingSinkImpl::clear() { + start_ = kj::none; + write_ = kj::none; + writev_ = kj::none; + abort_ = kj::none; + close_ = kj::none; + size_ = kj::none; +} + +UnderlyingSourceImpl::UnderlyingSourceImpl( + jsg::Lock& js, UnderlyingSource source, StreamQueuingStrategy strategy) + : start_(kj::mv(source.start)), + pull_(kj::mv(source.pull)), + cancel_(kj::mv(source.cancel)), + size_(kj::mv(strategy.size)), + isBytes_(source.type.map([](kj::StringPtr s) { return s == "bytes"; }).orDefault(false)), + highWaterMark_(strategy.highWaterMark.orDefault( + isBytes_ ? DEFAULT_HIGH_WATER_MARK_BYTES : DEFAULT_HIGH_WATER_MARK_VALUE)), + expectedLength_(source.expectedLength), + autoAllocateChunkSize_(source.autoAllocateChunkSize) { + // Per the streams spec, the size function should be called with `undefined` as `this`, + // not as a method on the strategy object. + KJ_IF_SOME(size, size_) { + size.setReceiver(js.v8Ref(js.v8Undefined())); + } + // Per the spec, the type property for ReadableStream's underlying source must be + // undefined, the empty string, or "bytes". + KJ_IF_SOME(type, source.type) { + JSG_REQUIRE(type == "" || type == "bytes", RangeError, + "Invalid underlying source type. Only undefined, '' and 'bytes' are valid."); + } +} + +void UnderlyingSourceImpl::clearStart() { + start_ = kj::none; +} + +void UnderlyingSourceImpl::clear() { + start_ = kj::none; + pull_ = kj::none; + cancel_ = kj::none; + size_ = kj::none; +} + +TransformerImpl::TransformerImpl(jsg::Lock& js, Transformer transformer) + : start_(kj::mv(transformer.start)), + transform_(kj::mv(transformer.transform)), + flush_(kj::mv(transformer.flush)), + cancel_(kj::mv(transformer.cancel)), + transformv_(kj::mv(transformer.transformv)) { + // Per the spec, both readableType and writableType must be undefined. + JSG_REQUIRE(transformer.readableType == kj::none, RangeError, + "Invalid transformer readableType. Only undefined is valid."); + JSG_REQUIRE(transformer.writableType == kj::none, RangeError, + "Invalid transformer writableType. Only undefined is valid."); +} + +void TransformerImpl::clearStart() { + start_ = kj::none; +} + +void TransformerImpl::clear() { + start_ = kj::none; + transform_ = kj::none; + flush_ = kj::none; + cancel_ = kj::none; + transformv_ = kj::none; +} + +// Adapt ReadableStreamSource to kj::AsyncInputStream's interface for use with `kj::newTee()`. +TeeAdapter::TeeAdapter(kj::Own inner): inner(kj::mv(inner)) {} + +kj::Promise TeeAdapter::tryRead(void* buffer, size_t minBytes, size_t maxBytes) { + return inner->tryRead(buffer, minBytes, maxBytes); +} + +kj::Maybe TeeAdapter::tryGetLength() { + return inner->tryGetLength(StreamEncoding::IDENTITY); +} + +TeeBranch::TeeBranch(kj::Own inner): inner(kj::mv(inner)) {} + +kj::Promise TeeBranch::tryRead(void* buffer, size_t minBytes, size_t maxBytes) { + return inner->tryRead(buffer, minBytes, maxBytes); +} + +kj::Promise> TeeBranch::pumpTo(WritableStreamSink& output, End end) { +#ifdef KJ_NO_RTTI + // Yes, I'm paranoid. + static_assert(!KJ_NO_RTTI, "Need RTTI for correctness"); +#endif + + // HACK: If `output` is another TransformStream, we don't allow pumping to it, in order to + // guarantee that we can't create cycles. Note that currently TeeBranch only ever wraps + // TransformStreams, never system streams. + JSG_REQUIRE(!isIdentityTransformStream(output), TypeError, + "Inter-TransformStream ReadableStream.pipeTo() is not implemented."); + + // It is important we actually call `inner->pumpTo()` so that `kj::newTee()` is aware of this + // pump operation's backpressure. So we can't use the default `ReadableStreamSource::pumpTo()` + // implementation, and have to implement our own. + + PumpAdapter outputAdapter(output); + co_await inner->pumpTo(outputAdapter); + + if (end) { + co_await output.end(); + } + + // We only use `TeeBranch` when a locally-sourced stream was tee'd (because system streams + // implement `tryTee()` in a different way that doesn't use `TeeBranch`). So, we know that + // none of the pump can be performed without the IoContext active, and thus we do not + // `KJ_CO_MAGIC BEGIN_DEFERRED_PROXYING`. + co_return; +} + +kj::Maybe TeeBranch::tryGetLength(StreamEncoding encoding) { + if (encoding == StreamEncoding::IDENTITY) { + return inner->tryGetLength(); + } else { + return kj::none; + } +} + +kj::Maybe TeeBranch::tryTee(uint64_t limit) { + KJ_IF_SOME(t, inner->tryTee(limit)) { + auto branch = kj::heap(newTeeErrorAdapter(kj::mv(t))); + auto consumed = kj::heap(kj::mv(inner)); + return Tee{kj::mv(branch), kj::mv(consumed)}; + } + + return kj::none; +} + +void TeeBranch::cancel(kj::Exception reason) { + // TODO(someday): What to do? +} + +TeeBranch::PumpAdapter::PumpAdapter(WritableStreamSink& inner): inner(inner) {} + +kj::Promise TeeBranch::PumpAdapter::write(kj::ArrayPtr buffer) { + return inner.write(buffer); +} + +kj::Promise TeeBranch::PumpAdapter::write( + kj::ArrayPtr> pieces) { + return inner.write(pieces); +} + +kj::Promise TeeBranch::PumpAdapter::whenWriteDisconnected() { + KJ_UNIMPLEMENTED("whenWriteDisconnected() not expected on PumpAdapter"); +} + } // namespace workerd::api diff --git a/src/workerd/api/streams/common.h b/src/workerd/api/streams/common.h index a129b575685..3c746aa954e 100644 --- a/src/workerd/api/streams/common.h +++ b/src/workerd/api/streams/common.h @@ -31,6 +31,12 @@ class TransformStreamDefaultController; using rpc::StreamEncoding; +WD_STRONG_BOOL(MarkAsHandled); +WD_STRONG_BOOL(End); +WD_STRONG_BOOL(IgnoreDisturbed); +WD_STRONG_BOOL(Reject); +WD_STRONG_BOOL(UpdateBackpressure); + enum class ReadAllTextOption : uint8_t { NONE = 0, NULL_TERMINATE = 1 << 0, @@ -57,7 +63,7 @@ inline bool hasUtf8Bom(kj::ArrayPtr data) { } struct ReadResult { - jsg::Optional value; + jsg::Optional> value; bool done; JSG_STRUCT(value, done); @@ -80,7 +86,7 @@ struct DrainingReadResult { }; struct StreamQueuingStrategy { - using SizeAlgorithm = uint64_t(v8::Local); + using SizeAlgorithm = uint64_t(jsg::JsValue); jsg::Optional highWaterMark; jsg::Optional> size; @@ -96,7 +102,7 @@ struct UnderlyingSource { kj::OneOf, jsg::Ref>; using StartAlgorithm = jsg::Promise(Controller); using PullAlgorithm = jsg::Promise(Controller); - using CancelAlgorithm = jsg::Promise(v8::Local reason); + using CancelAlgorithm = jsg::Promise(jsg::JsValue reason); // The autoAllocateChunkSize mechanism allows byte streams to operate as if a BYOB // reader is being used even if it is just a default reader. Support is optional @@ -149,11 +155,116 @@ struct UnderlyingSource { }); }; +class UnderlyingSourceImpl { + public: + // Default high water mark: 1 for value streams, 0 for byte streams (type="bytes"). + static constexpr uint64_t DEFAULT_HIGH_WATER_MARK_VALUE = 1; + static constexpr uint64_t DEFAULT_HIGH_WATER_MARK_BYTES = 0; + + using StartAlgorithm = UnderlyingSource::StartAlgorithm; + using PullAlgorithm = UnderlyingSource::PullAlgorithm; + using CancelAlgorithm = UnderlyingSource::CancelAlgorithm; + using SizeAlgorithm = StreamQueuingStrategy::SizeAlgorithm; + + UnderlyingSourceImpl(jsg::Lock& js, UnderlyingSource source, StreamQueuingStrategy strategy); + virtual ~UnderlyingSourceImpl() noexcept(false) = default; + KJ_DISALLOW_COPY_AND_MOVE(UnderlyingSourceImpl); + + inline uint64_t getHighWaterMark() const { + return highWaterMark_; + } + + inline bool isBytes() const { + return isBytes_; + } + + inline kj::Maybe getExpectedLength() const { + return expectedLength_; + } + + inline kj::Maybe getAutoAllocateChunkSize() const { + return autoAllocateChunkSize_; + } + + inline kj::Maybe>& start() KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT { + return start_; + } + + inline kj::Maybe>& pull() KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT { + return pull_; + } + + inline kj::Maybe>& cancel() + KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT { + return cancel_; + } + + inline kj::Maybe>& size() KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT { + return size_; + } + + void clearStart(); + + void clear(); + + virtual kj::Maybe> tryReleaseSource() KJ_WARN_UNUSED_RESULT { + return kj::none; + } + + struct Tee { + kj::Own branch1; + kj::Own branch2; + }; + virtual kj::Maybe tryTee(uint64_t limit) KJ_WARN_UNUSED_RESULT { + return kj::none; + } + + virtual bool isInternal() const { + return false; + } + + // Called after the ReadableStream is fully constructed. Allows internal source + // implementations to store a back-reference to the owning stream (e.g., for + // signalEof support). Default is a no-op. + virtual void setOwner(ReadableStream& stream) {} + + virtual StreamEncoding getPreferredEncoding() { + return StreamEncoding::IDENTITY; + } + + virtual kj::Maybe tryGetLength(StreamEncoding encoding) { + return kj::none; + } + + JSG_MEMORY_INFO(UnderlyingSourceImpl) { + tracker.trackField("start", start_); + tracker.trackField("pull", pull_); + tracker.trackField("cancel", cancel_); + tracker.trackField("size", size_); + } + + void visitForGc(jsg::GcVisitor& visitor) { + visitor.visit(start_, pull_, cancel_, size_); + } + + protected: + UnderlyingSourceImpl() = default; + kj::Maybe> start_; + kj::Maybe> pull_; + kj::Maybe> cancel_; + kj::Maybe> size_; + bool isBytes_ = false; + uint64_t highWaterMark_; + kj::Maybe expectedLength_; + kj::Maybe autoAllocateChunkSize_; +}; + struct UnderlyingSink { using Controller = jsg::Ref; using StartAlgorithm = jsg::Promise(Controller); - using WriteAlgorithm = jsg::Promise(v8::Local, Controller); - using AbortAlgorithm = jsg::Promise(v8::Local reason); + using WriteAlgorithm = jsg::Promise(jsg::JsValue, Controller); + using WritevAlgorithm = jsg::Promise(kj::Array>, Controller); + using AbortAlgorithm = jsg::Promise(jsg::JsValue); using CloseAlgorithm = jsg::Promise(); // Per the spec, the type property for the UnderlyingSink should always be either @@ -174,14 +285,109 @@ struct UnderlyingSink { abort?: (reason: any) => void | Promise; close?: () => void | Promise; }); + + // Non-standard extension: vectorized write algorithm. When set, allows the stream + // to batch multiple queued writes into a single call. Only set by internal C++ sinks + // that support vectorized I/O, never by user-provided JS sinks. + jsg::Optional> writev; +}; + +class UnderlyingSinkImpl { + public: + constexpr static uint64_t DEFAULT_HIGH_WATER_MARK = 1; + using StartAlgorithm = UnderlyingSink::StartAlgorithm; + using WriteAlgorithm = UnderlyingSink::WriteAlgorithm; + using AbortAlgorithm = UnderlyingSink::AbortAlgorithm; + using CloseAlgorithm = UnderlyingSink::CloseAlgorithm; + using SizeAlgorithm = StreamQueuingStrategy::SizeAlgorithm; + + // Non-standard extension: vectorized write algorithm. When set, allows the stream + // to batch multiple queued writes into a single call. Only set by internal C++ sinks + // that support vectorized I/O, never by user-provided JS sinks. + using WritevAlgorithm = UnderlyingSink::WritevAlgorithm; + + UnderlyingSinkImpl(jsg::Lock& js, UnderlyingSink sink, StreamQueuingStrategy strategy); + virtual ~UnderlyingSinkImpl() noexcept(false) = default; + KJ_DISALLOW_COPY_AND_MOVE(UnderlyingSinkImpl); + + inline uint64_t getHighWaterMark() const { + return highWaterMark_; + } + + inline kj::Maybe>& start() KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT { + return start_; + } + inline kj::Maybe>& write() KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT { + return write_; + } + inline kj::Maybe>& abort() KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT { + return abort_; + } + inline kj::Maybe>& close() KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT { + return close_; + } + inline kj::Maybe>& size() KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT { + return size_; + } + inline kj::Maybe>& writev() + KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT { + return writev_; + } + + inline void setWritev(jsg::Function writev) { + writev_ = kj::mv(writev); + } + + void clearStart(); + + void clear(); + + virtual kj::Maybe> tryReleaseSink() KJ_WARN_UNUSED_RESULT { + return kj::none; + } + + // Returns a non-owning reference to the underlying WritableStreamSink, if this + // is an internal source. The sink remains owned by this impl. Returns kj::none + // for JS-backed sinks or if the sink is not in an active state. + virtual kj::Maybe tryGetSink() KJ_WARN_UNUSED_RESULT { + return kj::none; + } + + virtual bool isInternal() const { + return false; + } + + JSG_MEMORY_INFO(UnderlyingSinkImpl) { + tracker.trackField("start", start_); + tracker.trackField("write", write_); + tracker.trackField("writev", writev_); + tracker.trackField("abort", abort_); + tracker.trackField("close", close_); + tracker.trackField("size", size_); + } + + void visitForGc(jsg::GcVisitor& visitor) { + visitor.visit(start_, write_, writev_, abort_, close_, size_); + } + + protected: + UnderlyingSinkImpl() = default; + kj::Maybe> start_; + kj::Maybe> write_; + kj::Maybe> writev_; + kj::Maybe> abort_; + kj::Maybe> close_; + kj::Maybe> size_; + uint64_t highWaterMark_ = DEFAULT_HIGH_WATER_MARK; }; struct Transformer { using Controller = jsg::Ref; using StartAlgorithm = jsg::Promise(Controller); - using TransformAlgorithm = jsg::Promise(v8::Local, Controller); + using TransformAlgorithm = jsg::Promise(jsg::JsValue, Controller); + using TransformvAlgorithm = jsg::Promise(kj::Array>, Controller); using FlushAlgorithm = jsg::Promise(Controller); - using CancelAlgorithm = jsg::Promise(jsg::JsValue reason); + using CancelAlgorithm = jsg::Promise(jsg::JsValue); jsg::Optional readableType; jsg::Optional writableType; @@ -204,6 +410,67 @@ struct Transformer { cancel?: (reason: any) => void | Promise; expectedLength?: number; }); + + // Non-standard extension: vectorized transform algorithm. When set, allows the stream + // to transform multiple queued chunks into a single call. + jsg::Optional> transformv; +}; + +class TransformerImpl { + public: + using StartAlgorithm = Transformer::StartAlgorithm; + using TransformAlgorithm = Transformer::TransformAlgorithm; + using TransformvAlgorithm = Transformer::TransformvAlgorithm; + using FlushAlgorithm = Transformer::FlushAlgorithm; + using CancelAlgorithm = Transformer::CancelAlgorithm; + + TransformerImpl(jsg::Lock& js, Transformer transformer); + virtual ~TransformerImpl() noexcept(false) = default; + KJ_DISALLOW_COPY_AND_MOVE(TransformerImpl); + + inline kj::Maybe>& start() KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT { + return start_; + } + inline kj::Maybe>& transform() + KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT { + return transform_; + } + inline kj::Maybe>& flush() KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT { + return flush_; + } + inline kj::Maybe>& cancel() + KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT { + return cancel_; + } + + inline kj::Maybe>& transformv() + KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT { + return transformv_; + } + + void clearStart(); + + void clear(); + + JSG_MEMORY_INFO(TransformerImpl) { + tracker.trackField("start", start_); + tracker.trackField("transform", transform_); + tracker.trackField("flush", flush_); + tracker.trackField("cancel", cancel_); + tracker.trackField("transformv", transformv_); + } + + void visitForGc(jsg::GcVisitor& visitor) { + visitor.visit(start_, transform_, flush_, cancel_, transformv_); + } + + protected: + TransformerImpl() = default; + kj::Maybe> start_; + kj::Maybe> transform_; + kj::Maybe> flush_; + kj::Maybe> cancel_; + kj::Maybe> transformv_; }; // ReadableStreamSource and WritableStreamSink @@ -236,11 +503,12 @@ class WritableStreamSink { // Must call to flush and finish the stream. virtual kj::Maybe>> tryPumpFrom( - ReadableStreamSource& input, bool end); + ReadableStreamSource& input, End end) KJ_WARN_UNUSED_RESULT; virtual void abort(kj::Exception reason) = 0; - // TODO(conform): abort() should return a promise after which closed fulfillers should be - // rejected. This may necessitate an "erroring" state. + // Note: The JS-facing WritableStream abort API returns a promise and uses an "erroring" + // state — both are handled by the controller layer (WritableStreamJsController). This + // internal WritableStreamSink interface intentionally uses void + kj::Exception. // Tells the sink that it is no longer to be responsible for encoding in the correct format. // Instead, the caller takes responsibility. The expected encoding is returned; the caller @@ -254,7 +522,8 @@ class WritableStreamSink { class ReadableStreamSource { public: - virtual kj::Promise tryRead(void* buffer, size_t minBytes, size_t maxBytes) = 0; + virtual kj::Promise tryRead( + void* buffer, size_t minBytes, size_t maxBytes) KJ_WARN_UNUSED_RESULT = 0; // The ReadableStreamSource version of pumpTo() has no `amount` parameter, since the Streams spec // only defines pumping everything. @@ -262,7 +531,8 @@ class ReadableStreamSource { // If `end` is true, then `output.end()` will be called after pumping. Note that it's especially // important to take advantage of this when using deferred proxying since calling `end()` // directly might attempt to use the `IoContext` to call `registerPendingEvent()`. - virtual kj::Promise> pumpTo(WritableStreamSink& output, bool end); + virtual kj::Promise> pumpTo( + WritableStreamSink& output, End end) KJ_WARN_UNUSED_RESULT; // If pumpTo() pumps to a system stream, what is the best encoding for that system stream to // use? This is just a hint. @@ -272,23 +542,16 @@ class ReadableStreamSource { virtual kj::Maybe tryGetLength(StreamEncoding encoding); - kj::Promise> readAllBytes(uint64_t limit); - kj::Promise readAllText( - uint64_t limit, ReadAllTextOption option = ReadAllTextOption::NULL_TERMINATE); + kj::Promise> readAllBytes(uint64_t limit) KJ_WARN_UNUSED_RESULT; + kj::Promise readAllText(uint64_t limit, + ReadAllTextOption option = ReadAllTextOption::NULL_TERMINATE) KJ_WARN_UNUSED_RESULT; - // Hook to inform this ReadableStreamSource that the ReadableStream has been canceled. This only - // really means anything to TransformStreams, which are supposed to propagate the error to the - // writable side, and custom ReadableStreams, which we don't implement yet. - // - // NOTE: By "propagate the error back to the writable stream", I mean: if the WritableStream is in - // the Writable state, set it to the Errored state and reject its closed fulfiller with - // `reason`. I'm not sure how I'm going to do this yet. + // Hook to inform this ReadableStreamSource that the ReadableStream has been canceled. + // This is primarily meaningful for TransformStreams, which propagate cancellation to + // the writable side. The JS-facing ReadableStream.cancel() API (which accepts any JS + // value and returns a promise) is handled by the controller layer; this internal + // interface intentionally uses kj::Exception and returns void. virtual void cancel(kj::Exception reason); - // TODO(conform): Should return promise. - // - // TODO(conform): `reason` should be allowed to be any JS value, and not just an exception. - // That is, something silly like `stream.cancel(42)` should be allowed and trigger a - // rejection with the integer `42`. struct Tee { kj::Own branches[2]; @@ -296,7 +559,7 @@ class ReadableStreamSource { // Implement this if your ReadableStreamSource has a better way to tee a stream than the naive // method, which relies upon `tryRead()`. The default implementation returns nullptr. - virtual kj::Maybe tryTee(uint64_t limit); + virtual kj::Maybe tryTee(uint64_t limit) KJ_WARN_UNUSED_RESULT; }; struct PipeToOptions { @@ -319,12 +582,12 @@ namespace StreamStates { struct Closed { static constexpr kj::StringPtr NAME KJ_UNUSED = "closed"_kj; }; -using Errored = jsg::Value; +using Errored = jsg::JsRef; struct Erroring { static constexpr kj::StringPtr NAME KJ_UNUSED = "erroring"_kj; - jsg::Value reason; + jsg::JsRef reason; - Erroring(jsg::Value reason): reason(kj::mv(reason)) {} + Erroring(jsg::Lock& js, jsg::JsValue reason): reason(reason.addRef(js)) {} void visitForGc(jsg::GcVisitor& visitor) { visitor.visit(reason); @@ -333,19 +596,15 @@ struct Erroring { } // namespace StreamStates // A ReadableStreamController provides the underlying implementation for a ReadableStream. -// We will generally have three implementations: -// * ReadableStreamDefaultController -// * ReadableByteStreamController -// * ReadableStreamInternalController -// -// The ReadableStreamDefaultController and ReadableByteStreamController are defined by the -// streams standard and source all of the stream data from JavaScript functions provided by -// user code. +// There are two implementations: +// * ReadableStreamJsController — JS-backed streams (standard). Internally delegates to +// a ReadableStreamDefaultController (value streams) or ReadableByteStreamController +// (byte streams), which are the JS-visible controller objects defined by the spec. +// * ReadableStreamInternalController — Workers runtime specific, bridges to the +// kj-backed ReadableStreamSource API. // -// The ReadableStreamInternalController is Workers runtime specific and provides a bridge -// to the existing ReadableStreamSource API. At the API contract layer, the -// ReadableByteStreamController and ReadableStreamInternalController will appear to be -// identical. Internally, however, they will be very different from one another. +// At the API contract layer, byte-oriented JS streams and internal streams appear +// identical. Internally, however, they are very different from one another. // // The ReadableStreamController instance is meant to be a private member of the ReadableStream, // e.g. @@ -393,9 +652,7 @@ class ReadableStreamController { struct ByobOptions { static constexpr size_t DEFAULT_AT_LEAST = 1; - jsg::V8Ref bufferView; - size_t byteOffset = 0; - size_t byteLength; + jsg::JsRef bufferView; // The minimum number of elements that should be read. When not specified, the default // is DEFAULT_AT_LEAST. This is a non-standard, Workers-specific extension to @@ -428,7 +685,7 @@ class ReadableStreamController { virtual ~Branch() noexcept(false) {} virtual void doClose(jsg::Lock& js) = 0; - virtual void doError(jsg::Lock& js, v8::Local reason) = 0; + virtual void doError(jsg::Lock& js, jsg::JsValue reason) = 0; virtual void handleData(jsg::Lock& js, ReadResult result) = 0; }; @@ -445,7 +702,7 @@ class ReadableStreamController { inner->doClose(js); } - inline void doError(jsg::Lock& js, v8::Local reason) { + inline void doError(jsg::Lock& js, jsg::JsValue reason) { inner->doError(js, reason); } @@ -470,7 +727,7 @@ class ReadableStreamController { virtual void close(jsg::Lock& js) = 0; - virtual void error(jsg::Lock& js, v8::Local reason) = 0; + virtual void error(jsg::Lock& js, jsg::JsValue reason) = 0; virtual void ensurePulling(jsg::Lock& js) = 0; @@ -486,31 +743,36 @@ class ReadableStreamController { public: virtual ~PipeController() noexcept(false) {} virtual bool isClosed() = 0; - virtual kj::Maybe> tryGetErrored(jsg::Lock& js) = 0; - virtual void cancel(jsg::Lock& js, v8::Local reason) = 0; + virtual kj::Maybe tryGetErrored(jsg::Lock& js) KJ_WARN_UNUSED_RESULT = 0; + virtual void cancel(jsg::Lock& js, jsg::JsValue reason) = 0; virtual void close(jsg::Lock& js) = 0; - virtual void error(jsg::Lock& js, v8::Local reason) = 0; - virtual void release(jsg::Lock& js, kj::Maybe> maybeError = kj::none) = 0; - virtual kj::Maybe> tryPumpTo(WritableStreamSink& sink, bool end) = 0; - virtual jsg::Promise read(jsg::Lock& js) = 0; + virtual void error(jsg::Lock& js, jsg::JsValue reason) = 0; + virtual void release(jsg::Lock& js, kj::Maybe maybeError = kj::none) = 0; + virtual kj::Maybe> tryPumpTo( + WritableStreamSink& sink, End end) KJ_WARN_UNUSED_RESULT = 0; + virtual jsg::Promise read(jsg::Lock& js) KJ_WARN_UNUSED_RESULT = 0; }; virtual ~ReadableStreamController() noexcept(false) {} virtual void setOwnerRef(ReadableStream& stream) = 0; - virtual jsg::Ref addRef() = 0; + virtual jsg::Ref addRef() KJ_WARN_UNUSED_RESULT = 0; // Returns true if the underlying source for this controller is byte-oriented and // therefore supports the pull into API. When false, the stream can be used to pass // any arbitrary JavaScript value through. virtual bool isByteOriented() const = 0; + virtual bool isInternal() const = 0; + virtual kj::Maybe> tryReleaseSource() KJ_WARN_UNUSED_RESULT { + return kj::none; + } // Reads data from the stream. If the stream is byte-oriented, then the ByobOptions can be // specified to provide a v8::ArrayBuffer to be filled by the read operation. If the ByobOptions // are provided and the stream is not byte-oriented, the operation will return a rejected promise. virtual kj::Maybe> read( - jsg::Lock& js, kj::Maybe byobOptions) = 0; + jsg::Lock& js, kj::Maybe byobOptions) KJ_WARN_UNUSED_RESULT = 0; // Performs a draining read operation that: // 1. Drains all currently buffered data from the queue @@ -527,22 +789,24 @@ class ReadableStreamController { // reaches maxRead (after finishing the current item). This prevents unbounded memory // accumulation when a fast producer outpaces a slow consumer. virtual kj::Maybe> drainingRead( - jsg::Lock& js, size_t maxRead = kj::maxValue) = 0; + jsg::Lock& js, size_t maxRead = kj::maxValue) KJ_WARN_UNUSED_RESULT = 0; // The pipeTo implementation fully consumes the stream by directing all of its data at the // destination. Controllers should try to be as efficient as possible here. For instance, if // a ReadableStreamInternalController is piping to a WritableStreamInternalController, then // a more efficient kj pipe should be possible. - virtual jsg::Promise pipeTo( - jsg::Lock& js, WritableStreamController& destination, PipeToOptions options) = 0; + virtual jsg::Promise pipeTo(jsg::Lock& js, + WritableStreamController& destination, + PipeToOptions options) KJ_WARN_UNUSED_RESULT = 0; // Indicates that the consumer no longer has any interest in the streams data. - virtual jsg::Promise cancel(jsg::Lock& js, jsg::Optional> reason) = 0; + virtual jsg::Promise cancel( + jsg::Lock& js, jsg::Optional reason) KJ_WARN_UNUSED_RESULT = 0; // Branches the ReadableStreamController into two ReadableStream instances that will receive // this streams data. The specific details of how the branching occurs is entirely up to the // controller implementation. - virtual Tee tee(jsg::Lock& js) = 0; + virtual Tee tee(jsg::Lock& js) KJ_WARN_UNUSED_RESULT = 0; virtual bool isClosedOrErrored() const = 0; @@ -562,7 +826,7 @@ class ReadableStreamController { // If maybeJs is set, the reader's closed promise will be resolved. virtual void releaseReader(Reader& reader, kj::Maybe maybeJs) = 0; - virtual kj::Maybe tryPipeLock() = 0; + virtual kj::Maybe tryPipeLock() KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT = 0; virtual void visitForGc(jsg::GcVisitor& visitor) {}; @@ -573,7 +837,8 @@ class ReadableStreamController { // // limit specifies an upper maximum bound on the number of bytes permitted to be read. // The promise will reject if the read will produce more bytes than the limit. - virtual jsg::Promise readAllBytes(jsg::Lock& js, uint64_t limit) = 0; + virtual jsg::Promise> readAllBytes( + jsg::Lock& js, uint64_t limit) KJ_WARN_UNUSED_RESULT = 0; // Fully consumes the ReadableStream. If the stream is already locked to a reader or // errored, the returned JS promise will reject. If the stream is already closed, the @@ -582,16 +847,15 @@ class ReadableStreamController { // // limit specifies an upper maximum bound on the number of bytes permitted to be read. // The promise will reject if the read will produce more bytes than the limit. - virtual jsg::Promise readAllText(jsg::Lock& js, uint64_t limit) = 0; + virtual jsg::Promise readAllText( + jsg::Lock& js, uint64_t limit) KJ_WARN_UNUSED_RESULT = 0; virtual kj::Maybe tryGetLength(StreamEncoding encoding) = 0; - virtual void setup(jsg::Lock& js, - jsg::Optional maybeUnderlyingSource, - jsg::Optional maybeQueuingStrategy) {} + virtual void setup(jsg::Lock& js, kj::Own source) {} virtual kj::Promise> pumpTo( - jsg::Lock& js, kj::Own sink, bool end) = 0; + jsg::Lock& js, kj::Own sink, End end) KJ_WARN_UNUSED_RESULT = 0; // If pumpTo() pumps to a system stream, what is the best encoding for that system stream to // use? This is just a hint. @@ -599,7 +863,8 @@ class ReadableStreamController { return StreamEncoding::IDENTITY; } - virtual kj::Own detach(jsg::Lock& js, bool ignoreDisturbed) = 0; + virtual kj::Own detach( + jsg::Lock& js, IgnoreDisturbed ignoreDisturbed) KJ_WARN_UNUSED_RESULT = 0; // Used by sockets to signal that the ReadableStream shouldn't allow reads due to pending // closure. @@ -673,29 +938,27 @@ class WritableStreamController { struct PendingAbort { kj::Maybe::Resolver> resolver; jsg::Promise promise; - jsg::Value reason; + jsg::JsRef reason; bool reject = false; - PendingAbort(jsg::Lock& js, - jsg::PromiseResolverPair prp, - v8::Local reason, - bool reject); + PendingAbort( + jsg::Lock& js, jsg::PromiseResolverPair prp, jsg::JsValue reason, Reject reject); - PendingAbort(jsg::Lock& js, v8::Local reason, bool reject); + PendingAbort(jsg::Lock& js, jsg::JsValue reason, Reject reject); void complete(jsg::Lock& js); - void fail(jsg::Lock& js, v8::Local reason); + void fail(jsg::Lock& js, jsg::JsValue reason); - inline jsg::Promise whenResolved(jsg::Lock& js) { + inline jsg::Promise whenResolved(jsg::Lock& js) KJ_WARN_UNUSED_RESULT { return promise.whenResolved(js); } - inline jsg::Promise whenResolved(auto&& func) { + inline jsg::Promise whenResolved(auto&& func) KJ_WARN_UNUSED_RESULT { return promise.whenResolved(kj::fwd(func)); } - inline jsg::Promise whenResolved(auto&& func, auto&& errFunc) { + inline jsg::Promise whenResolved(auto&& func, auto&& errFunc) KJ_WARN_UNUSED_RESULT { return promise.whenResolved(kj::fwd(func), kj::fwd(errFunc)); } @@ -704,7 +967,7 @@ class WritableStreamController { } static kj::Maybe> dequeue( - kj::Maybe>& maybePendingAbort); + kj::Maybe>& maybePendingAbort) KJ_WARN_UNUSED_RESULT; JSG_MEMORY_INFO(PendingAbort) { tracker.trackField("resolver", resolver); @@ -717,35 +980,41 @@ class WritableStreamController { virtual void setOwnerRef(WritableStream& stream) = 0; - virtual jsg::Ref addRef() = 0; + virtual jsg::Ref addRef() KJ_WARN_UNUSED_RESULT = 0; // The controller implementation will determine what kind of JavaScript data // it is capable of writing, returning a rejected promise if the written // data type is not supported. - virtual jsg::Promise write(jsg::Lock& js, jsg::Optional> value) = 0; + virtual jsg::Promise write( + jsg::Lock& js, jsg::Optional value) KJ_WARN_UNUSED_RESULT = 0; // Indicates that no additional data will be written to the controller. All // existing pending writes should be allowed to complete. - virtual jsg::Promise close(jsg::Lock& js, bool markAsHandled = false) = 0; + virtual jsg::Promise close( + jsg::Lock& js, MarkAsHandled markAsHandled = MarkAsHandled::NO) KJ_WARN_UNUSED_RESULT = 0; // Waits for pending data to be written. The returned promise is resolved when all pending writes // have completed. - virtual jsg::Promise flush(jsg::Lock& js, bool markAsHandled = false) = 0; + virtual jsg::Promise flush( + jsg::Lock& js, MarkAsHandled markAsHandled = MarkAsHandled::NO) KJ_WARN_UNUSED_RESULT = 0; // Immediately interrupts existing pending writes and errors the stream. - virtual jsg::Promise abort(jsg::Lock& js, jsg::Optional> reason) = 0; + virtual jsg::Promise abort( + jsg::Lock& js, jsg::Optional reason) KJ_WARN_UNUSED_RESULT = 0; // The tryPipeFrom attempts to establish a data pipe where source's data // is delivered to this WritableStreamController as efficiently as possible. - virtual kj::Maybe> tryPipeFrom( - jsg::Lock& js, jsg::Ref source, PipeToOptions options) = 0; + virtual kj::Maybe> tryPipeFrom(jsg::Lock& js, + jsg::Ref source, + PipeToOptions options) KJ_WARN_UNUSED_RESULT = 0; // Only byte-oriented WritableStreamController implementations will have a WritableStreamSink // that can be detached using removeSink. A nullptr should be returned by any controller that // does not support removing the sink. After the WritableStreamSink has been released, all other // methods on the controller should fail with an exception as the WritableStreamSink should be // the only way to interact with the underlying sink. - virtual kj::Maybe> removeSink(jsg::Lock& js) = 0; + virtual kj::Maybe> removeSink( + jsg::Lock& js) KJ_WARN_UNUSED_RESULT = 0; // Detaches the WritableStreamController from its underlying implementation, leaving the // writable stream locked and in a state where no further writes can be made. @@ -765,13 +1034,11 @@ class WritableStreamController { // If maybeJs is set, the writer's closed and ready promises will be resolved. virtual void releaseWriter(Writer& writer, kj::Maybe maybeJs) = 0; - virtual kj::Maybe> isErroring(jsg::Lock& js) = 0; + virtual kj::Maybe isErroring(jsg::Lock& js) = 0; virtual void visitForGc(jsg::GcVisitor& visitor) {}; - virtual void setup(jsg::Lock& js, - jsg::Optional underlyingSink, - jsg::Optional queuingStrategy) {} + virtual void setup(jsg::Lock& js, kj::Own underlyingSink) {} virtual bool isClosedOrClosing() = 0; virtual bool isErrored() = 0; @@ -783,6 +1050,9 @@ class WritableStreamController { // closure. virtual void setPendingClosure() = 0; + // Used by sockets to ensure the connection is established before close. + virtual void setClosureWaitable(jsg::Promise waitable) {} + // For memory tracking virtual kj::StringPtr jsgGetMemoryName() const = 0; virtual size_t jsgGetMemorySelfSize() const = 0; @@ -827,15 +1097,15 @@ class ReaderLocked { visitor.visit(closedFulfiller); } - ReadableStreamController::Reader& getReader() { + ReadableStreamController::Reader& getReader() KJ_LIFETIMEBOUND { return KJ_ASSERT_NONNULL(reader); } - kj::Maybe::Resolver>& getClosedFulfiller() { + kj::Maybe::Resolver>& getClosedFulfiller() KJ_WARN_UNUSED_RESULT { return closedFulfiller; } - kj::Maybe>& getCanceler() { + kj::Maybe>& getCanceler() KJ_LIFETIMEBOUND { return canceler; } @@ -879,15 +1149,15 @@ class WriterLocked { visitor.visit(closedFulfiller, readyFulfiller); } - WritableStreamController::Writer& getWriter() { + WritableStreamController::Writer& getWriter() KJ_LIFETIMEBOUND { return KJ_ASSERT_NONNULL(writer); } - kj::Maybe::Resolver>& getClosedFulfiller() { + kj::Maybe::Resolver>& getClosedFulfiller() KJ_WARN_UNUSED_RESULT { return closedFulfiller; } - kj::Maybe::Resolver>& getReadyFulfiller() { + kj::Maybe::Resolver>& getReadyFulfiller() KJ_WARN_UNUSED_RESULT { return readyFulfiller; } @@ -935,7 +1205,7 @@ inline void maybeResolvePromise( template void maybeRejectPromise(jsg::Lock& js, kj::Maybe::Resolver>& maybeResolver, - v8::Local reason) { + jsg::JsValue reason) { KJ_IF_SOME(resolver, maybeResolver) { resolver.reject(js, reason); maybeResolver = kj::none; @@ -944,7 +1214,7 @@ void maybeRejectPromise(jsg::Lock& js, template jsg::Promise rejectedMaybeHandledPromise( - jsg::Lock& js, v8::Local reason, bool handled) { + jsg::Lock& js, jsg::JsValue reason, MarkAsHandled handled) { auto prp = js.newPromiseAndResolver(); if (handled) { prp.promise.markAsHandled(js); @@ -958,4 +1228,57 @@ inline kj::Maybe tryGetIoContext() { return IoContext::tryCurrent(); } +inline bool isByteSource(const jsg::JsValue& value) { + return value.isArrayBuffer() || value.isSharedArrayBuffer() || value.isArrayBufferView() || + value.isString(); +} + +// ====================================================================================== + +// Adapt ReadableStreamSource to kj::AsyncInputStream's interface for use with `kj::newTee()`. +class TeeAdapter final: public kj::AsyncInputStream { + public: + explicit TeeAdapter(kj::Own inner); + kj::Promise tryRead( + void* buffer, size_t minBytes, size_t maxBytes) override KJ_WARN_UNUSED_RESULT; + kj::Maybe tryGetLength() override; + + private: + kj::Own inner; +}; + +class TeeBranch final: public ReadableStreamSource { + public: + explicit TeeBranch(kj::Own inner); + + kj::Promise tryRead( + void* buffer, size_t minBytes, size_t maxBytes) override KJ_WARN_UNUSED_RESULT; + + kj::Promise> pumpTo( + WritableStreamSink& output, End end) override KJ_WARN_UNUSED_RESULT; + + kj::Maybe tryGetLength(StreamEncoding encoding) override; + + kj::Maybe tryTee(uint64_t limit) override KJ_WARN_UNUSED_RESULT; + + void cancel(kj::Exception reason) override; + + private: + // Adapt WritableStreamSink to kj::AsyncOutputStream's interface for use in + // `TeeBranch::pumpTo()`. If you squint, the write logic looks very similar to TeeAdapter's + // read logic. + class PumpAdapter final: public kj::AsyncOutputStream { + public: + explicit PumpAdapter(WritableStreamSink& inner); + kj::Promise write(kj::ArrayPtr buffer) override KJ_WARN_UNUSED_RESULT; + kj::Promise write( + kj::ArrayPtr> pieces) override KJ_WARN_UNUSED_RESULT; + kj::Promise whenWriteDisconnected() override KJ_WARN_UNUSED_RESULT; + + WritableStreamSink& inner; + }; + + kj::Own inner; +}; + } // namespace workerd::api diff --git a/src/workerd/api/streams/encoding.c++ b/src/workerd/api/streams/encoding.c++ index 105ff2593e2..79f3e83f101 100644 --- a/src/workerd/api/streams/encoding.c++ +++ b/src/workerd/api/streams/encoding.c++ @@ -42,9 +42,9 @@ struct Holder: public kj::Refcounted { jsg::Ref TextEncoderStream::constructor(jsg::Lock& js) { auto state = kj::rc(); - auto transform = [holder = state.addRef()](jsg::Lock& js, v8::Local chunk, + auto transform = [holder = state.addRef()](jsg::Lock& js, jsg::JsValue chunk, jsg::Ref controller) mutable { - auto str = jsg::check(chunk->ToString(js.v8Context())); + v8::Local str = chunk.toJsString(js); size_t length = str->Length(); if (length == 0) return js.resolvedPromise(); @@ -75,15 +75,13 @@ jsg::Ref TextEncoderStream::constructor(jsg::Lock& js) { auto utf8Length = result.count; KJ_DASSERT(utf8Length > 0 && utf8Length >= end); - auto backingStore = js.allocBackingStore(utf8Length, jsg::Lock::AllocOption::UNINITIALIZED); - auto dest = kj::ArrayPtr(static_cast(backingStore->Data()), utf8Length); - [[maybe_unused]] auto written = - simdutf::convert_utf16_to_utf8(slice.begin(), slice.size(), dest.begin()); + auto dest = jsg::JsArrayBuffer::create(js, utf8Length); + [[maybe_unused]] auto written = simdutf::convert_utf16_to_utf8( + slice.begin(), slice.size(), dest.asArrayPtr().asChars().begin()); KJ_DASSERT(written == utf8Length, "simdutf should write exactly utf8Length bytes"); - auto array = v8::Uint8Array::New( - v8::ArrayBuffer::New(js.v8Isolate, kj::mv(backingStore)), 0, utf8Length); - controller->enqueue(js, jsg::JsUint8Array(array)); + auto u8 = jsg::JsUint8Array::create(js, dest); + controller->enqueue(js, u8); return js.resolvedPromise(); }; @@ -91,13 +89,90 @@ jsg::Ref TextEncoderStream::constructor(jsg::Lock& js) { jsg::Lock& js, jsg::Ref controller) mutable { // If stream ends with orphaned high surrogate, emit replacement character if (holder->pending != kj::none) { - auto backingStore = js.allocBackingStore(3, jsg::Lock::AllocOption::UNINITIALIZED); - memcpy(backingStore->Data(), REPLACEMENT_UTF8, 3); - controller->enqueue(js, jsg::JsUint8Array::create(js, kj::mv(backingStore), 0, 3)); + auto u8 = jsg::JsUint8Array::create(js, 3); + u8.asArrayPtr().copyFrom(REPLACEMENT_UTF8); + controller->enqueue(js, u8); } return js.resolvedPromise(); }; + // Batch transform: encode multiple queued string chunks into UTF-8 output. + // Processes chunks in sub-batches to bound memory usage — without this, + // a large queue (e.g., 500K tiny writes) would allocate massive transient + // buffers causing GC pressure and progressive slowdown. + auto transformv = [holder = state.addRef()](jsg::Lock& js, + kj::Array> chunks, + jsg::Ref controller) mutable { + static constexpr size_t kMaxBatchUtf8Bytes = 64 * 1024; // 64KB of UTF-8 output + + size_t i = 0; + while (i < chunks.size()) { + // Collect a sub-batch up to the byte limit. + v8::LocalVector strings(js.v8Isolate); + size_t totalLength = 0; + while (i < chunks.size()) { + auto str = chunks[i].getHandle(js).toJsString(js); + size_t len = str.utf8Length(js); + if (totalLength + len > kMaxBatchUtf8Bytes && totalLength > 0) { + // This chunk would exceed the limit and we already have data. + break; + } + strings.push_back(str); + totalLength += len; + i++; + } + + if (totalLength == 0 && holder->pending == kj::none) continue; + + // Allocate a single contiguous buffer for this sub-batch. + size_t prefix = (holder->pending == kj::none) ? 0 : 1; + size_t end = prefix + totalLength; + auto buf = kj::heapArray(end); + + // Copy pending surrogate into slot 0 if present. + KJ_IF_SOME(lead, holder->pending) { + buf.begin()[0] = lead; + holder->pending = kj::none; + } + + // Copy all string contents into the buffer. + size_t offset = prefix; + for (auto& str: strings) { + size_t len = str->Length(); + if (len > 0) { + str->WriteV2(js.v8Isolate, 0, len, reinterpret_cast(buf.begin() + offset)); + offset += len; + } + } + KJ_DASSERT(offset == end); + + // If buffer ends with high surrogate, save it for next sub-batch. + if (end > 0 && U_IS_LEAD(buf[end - 1])) { + holder->pending = buf[--end]; + } + if (end == 0) continue; + + // Encode to UTF-8 and enqueue. + auto slice = buf.first(end); + auto result = simdutf::utf8_length_from_utf16_with_replacement(slice.begin(), slice.size()); + if (result.error == simdutf::error_code::SURROGATE) { + simdutf::to_well_formed_utf16(slice.begin(), slice.size(), slice.begin()); + } + auto utf8Length = result.count; + KJ_DASSERT(utf8Length > 0 && utf8Length >= end); + + auto dest = jsg::JsArrayBuffer::create(js, utf8Length); + [[maybe_unused]] auto written = simdutf::convert_utf16_to_utf8( + slice.begin(), slice.size(), dest.asArrayPtr().asChars().begin()); + KJ_DASSERT(written == utf8Length); + + auto u8 = jsg::JsUint8Array::create(js, dest); + controller->enqueue(js, u8); + } + + return js.resolvedPromise(); + }; + // Per the WHATWG Encoding spec, the readable side HWM should be 0, so writes // block until a reader pulls. Previously StreamQueuingStrategy{} was passed, // which bypassed the orDefault() in TransformStream::constructor and caused @@ -108,9 +183,13 @@ jsg::Ref TextEncoderStream::constructor(jsg::Lock& js) { if (!FeatureFlags::get(js).getEncoderStreamSpecCompliantBackpressure()) { readableStrategy = StreamQueuingStrategy{}; } - auto transformer = TransformStream::constructor(js, - Transformer{.transform = jsg::Function(kj::mv(transform)), - .flush = jsg::Function(kj::mv(flush))}, + + auto transformer = TransformStream::constructorNoCheck(js, + Transformer{ + .transform = jsg::Function(kj::mv(transform)), + .flush = jsg::Function(kj::mv(flush)), + .transformv = jsg::Function(kj::mv(transformv)), + }, StreamQueuingStrategy{}, kj::mv(readableStrategy)); return js.alloc(transformer->getReadable(), transformer->getWritable()); @@ -135,6 +214,88 @@ jsg::Ref TextDecoderStream::constructor( }; })); + auto transform = JSG_VISITABLE_LAMBDA( + (decoder = decoder.addRef()), (decoder), (jsg::Lock& js, auto chunk, auto controller) { + JSG_REQUIRE( + chunk.isArrayBuffer() || chunk.isSharedArrayBuffer() || chunk.isArrayBufferView(), + TypeError, + "This TransformStream is being used as a byte stream, " + "but received a value that is not a BufferSource."); + jsg::JsBufferSource source(chunk); + auto decoded = JSG_REQUIRE_NONNULL(decoder->decodePtr(js, source.asArrayPtr(), false), + TypeError, "Failed to decode input."); + // Only enqueue if there's actual output - don't emit empty chunks + // for incomplete multi-byte sequences + if (decoded.length(js) > 0) { + controller->enqueue(js, decoded); + } + return js.resolvedPromise(); + }); + + auto flush = JSG_VISITABLE_LAMBDA( + (decoder = decoder.addRef()), (decoder), (jsg::Lock& js, auto controller) { + auto decoded = JSG_REQUIRE_NONNULL(decoder->decodePtr(js, kj::ArrayPtr(), true), + TypeError, "Failed to decode input."); + // Only enqueue if there's actual output + if (decoded.length(js) > 0) { + controller->enqueue(js, decoded); + } + return js.resolvedPromise(); + }); + + // Batch transform: concatenate multiple byte chunks and decode in sub-batches. + // Bounds memory usage for large queues while reducing per-chunk decodePtr and + // enqueue calls. The TextDecoder's internal state handles partial multi-byte + // sequences at sub-batch boundaries. + auto transformv = JSG_VISITABLE_LAMBDA((decoder = decoder.addRef()), (decoder), + (jsg::Lock& js, kj::Array> chunks, auto controller) { + static constexpr size_t kMaxBatchBytes = 64 * 1024; // 64KB per sub-batch + + size_t i = 0; + while (i < chunks.size()) { + // Collect a sub-batch up to the byte limit. + size_t totalSize = 0; + size_t batchStart = i; + while (i < chunks.size()) { + auto handle = chunks[i].getHandle(js); + JSG_REQUIRE( + handle.isArrayBuffer() || handle.isSharedArrayBuffer() || handle.isArrayBufferView(), + TypeError, + "This TransformStream is being used as a byte stream, " + "but received a value that is not a BufferSource."); + jsg::JsBufferSource source(handle); + size_t len = source.size(); + if (totalSize + len > kMaxBatchBytes && totalSize > 0) { + break; + } + totalSize += len; + i++; + } + + if (totalSize == 0) continue; + + // Concatenate this sub-batch into a single buffer. + auto combined = kj::heapArray(totalSize); + size_t offset = 0; + for (size_t j = batchStart; j < i; j++) { + jsg::JsBufferSource source(chunks[j].getHandle(js)); + auto ptr = source.asArrayPtr(); + combined.slice(offset, offset + ptr.size()).copyFrom(ptr); + offset += ptr.size(); + } + KJ_DASSERT(offset == totalSize); + + // Decode and enqueue. + auto decoded = JSG_REQUIRE_NONNULL( + decoder->decodePtr(js, combined.asPtr(), false), TypeError, "Failed to decode input."); + if (decoded.length(js) > 0) { + controller->enqueue(js, decoded); + } + } + + return js.resolvedPromise(); + }); + // The controller will store c++ references to both the readable and writable // streams underlying controllers. // See comment in TextEncoderStream::constructor for why we conditionally pass @@ -143,36 +304,12 @@ jsg::Ref TextDecoderStream::constructor( if (!FeatureFlags::get(js).getEncoderStreamSpecCompliantBackpressure()) { readableStrategy = StreamQueuingStrategy{}; } - auto transformer = TransformStream::constructor(js, - Transformer{.transform = jsg::Function( JSG_VISITABLE_LAMBDA( - (decoder = decoder.addRef()), (decoder), - (jsg::Lock& js, auto chunk, auto controller) { - JSG_REQUIRE(chunk->IsArrayBuffer() || chunk->IsArrayBufferView(), TypeError, - "This TransformStream is being used as a byte stream, " - "but received a value that is not a BufferSource."); - jsg::BufferSource source(js, chunk); - auto decoded = - JSG_REQUIRE_NONNULL(decoder->decodePtr(js, source.asArrayPtr(), false), - TypeError, "Failed to decode input."); - // Only enqueue if there's actual output - don't emit empty chunks - // for incomplete multi-byte sequences - if (decoded.length(js) > 0) { - controller->enqueue(js, decoded); - } - return js.resolvedPromise(); - })), - .flush = jsg::Function( - JSG_VISITABLE_LAMBDA((decoder = decoder.addRef()), (decoder), - (jsg::Lock& js, auto controller) { - auto decoded = - JSG_REQUIRE_NONNULL(decoder->decodePtr(js, kj::ArrayPtr(), true), - TypeError, "Failed to decode input."); - // Only enqueue if there's actual output - if (decoded.length(js) > 0) { - controller->enqueue(js, decoded); - } - return js.resolvedPromise(); - }))}, + auto transformer = TransformStream::constructorNoCheck(js, + Transformer{ + .transform = jsg::Function(kj::mv(transform)), + .flush = jsg::Function(kj::mv(flush)), + .transformv = jsg::Function(kj::mv(transformv)), + }, StreamQueuingStrategy{}, kj::mv(readableStrategy)); return js.alloc( diff --git a/src/workerd/api/streams/identity-transform-stream.c++ b/src/workerd/api/streams/identity-transform-stream.c++ index 115c2621eaf..15f94f19b78 100644 --- a/src/workerd/api/streams/identity-transform-stream.c++ +++ b/src/workerd/api/streams/identity-transform-stream.c++ @@ -123,7 +123,7 @@ class IdentityTransformStreamImpl final: public kj::Refcounted, return promise; } - kj::Promise> pumpTo(WritableStreamSink& output, bool end) override { + kj::Promise> pumpTo(WritableStreamSink& output, End end) override { #ifdef KJ_NO_RTTI // Yes, I'm paranoid. static_assert(!KJ_NO_RTTI, "Need RTTI for correctness"); diff --git a/src/workerd/api/streams/internal-test.c++ b/src/workerd/api/streams/internal-test.c++ index d6baca04935..239af31655a 100644 --- a/src/workerd/api/streams/internal-test.c++ +++ b/src/workerd/api/streams/internal-test.c++ @@ -62,8 +62,9 @@ TestFixture makeAbortClearsQueueTestFixture() { // Creates a BYOB-capable ReadableStream jsg::Ref makeByteStream(jsg::Lock& js) { auto rs = js.alloc(newReadableStreamJsController()); - rs->getController().setup( - js, UnderlyingSource{.type = kj::str("bytes")}, StreamQueuingStrategy{}); + rs->getController().setup(js, + kj::heap( + js, UnderlyingSource{.type = kj::str("bytes")}, StreamQueuingStrategy{})); return rs; } @@ -280,12 +281,12 @@ KJ_TEST("WritableStreamInternalController queue size assertion") { "is currently locked to a writer."); } - auto buffersource = env.js.bytes(kj::heapArray(10)); + auto u8 = jsg::JsUint8Array::create(env.js, 10); bool writeFailed = false; auto write = sink->getController() - .write(env.js, buffersource.getHandle(env.js)) + .write(env.js, u8) .catch_(env.js, [&](jsg::Lock& js, jsg::Value value) { writeFailed = true; auto ex = js.exceptionToKj(kj::mv(value)); @@ -293,7 +294,7 @@ KJ_TEST("WritableStreamInternalController queue size assertion") { ex.getDescription() == "jsg.TypeError: This WritableStream is currently being piped to."); }); - source->getController().cancel(env.js, kj::none); + auto _ KJ_UNUSED = source->getController().cancel(env.js, kj::none); env.js.runMicrotasks(); @@ -343,7 +344,7 @@ KJ_TEST("WritableStreamInternalController operations reject when piped to") { KJ_FAIL_ASSERT("Expected tryPipeFrom to return a promise"); } - source->getController().cancel(env.js, kj::none); + auto _ KJ_UNUSED = source->getController().cancel(env.js, kj::none); env.js.runMicrotasks(); KJ_ASSERT(closeFailed); @@ -376,9 +377,9 @@ KJ_TEST("WritableStreamInternalController observability") { stream = env.js.alloc(env.context, kj::heap(), kj::mv(myObserver)); auto write = [&](size_t size) { - auto buffersource = env.js.bytes(kj::heapArray(size)); - return env.context.awaitJs(env.js, - KJ_ASSERT_NONNULL(stream)->getController().write(env.js, buffersource.getHandle(env.js))); + auto u8 = jsg::JsUint8Array::create(env.js, size); + return env.context.awaitJs( + env.js, KJ_ASSERT_NONNULL(stream)->getController().write(env.js, u8)); }; KJ_ASSERT(observer.queueSize == 0); @@ -427,8 +428,7 @@ KJ_TEST("WritableStreamInternalController pipeLoop abort during pending read") { auto& c = KJ_ASSERT_NONNULL(controller.tryGet>()); if (pullCount == 1) { // First pull: enqueue some data so the pipe loop can make progress - auto data = js.bytes(kj::heapArray({1, 2, 3, 4})); - c->enqueue(js, data.getHandle(js)); + c->enqueue(js, jsg::JsUint8Array::create(js, 4)); } // Second pull onwards: don't enqueue anything, leaving the read pending. // This simulates an async data source that hasn't received data yet. @@ -445,7 +445,7 @@ KJ_TEST("WritableStreamInternalController pipeLoop abort during pending read") { env.js.runMicrotasks(); // Abort while pipeLoop is waiting for a pending read - auto abortPromise = sink->getController().abort(env.js, env.js.v8TypeError("Test abort"_kj)); + auto abortPromise = sink->getController().abort(env.js, env.js.typeError("Test abort"_kj)); abortPromise.markAsHandled(env.js); env.js.runMicrotasks(); @@ -646,7 +646,7 @@ KJ_TEST("DrainingReader on closed stream (internal stream)") { fixture.runInIoContext([&](const TestFixture::Environment& env) { auto rs = env.js.alloc(env.context, kj::heap()); - rs->getController().cancel(env.js, kj::none); + auto _ KJ_UNUSED = rs->getController().cancel(env.js, kj::none); KJ_IF_SOME(reader, DrainingReader::create(env.js, *rs)) { bool done = false; @@ -753,8 +753,7 @@ KJ_TEST("ReadableStreamBYOBReader rejects read with zero-sized buffer") { auto rs = makeByteStream(env.js); auto reader = ReadableStreamBYOBReader::constructor(env.js, rs.addRef()); - auto buffer = v8::ArrayBuffer::New(env.js.v8Isolate, 0); - auto view = v8::Uint8Array::New(buffer, 0, 0); + auto view = jsg::JsUint8Array::create(env.js, 0); bool rejected = false; reader->read(env.js, view, kj::none) @@ -777,8 +776,7 @@ KJ_TEST("ReadableStreamBYOBReader rejects read with atLeast=0") { auto rs = makeByteStream(env.js); auto reader = ReadableStreamBYOBReader::constructor(env.js, rs.addRef()); - auto buffer = v8::ArrayBuffer::New(env.js.v8Isolate, 10); - auto view = v8::Uint8Array::New(buffer, 0, 10); + auto view = jsg::JsUint8Array::create(env.js, 10); bool rejected = false; reader->readAtLeast(env.js, 0, view) @@ -801,8 +799,7 @@ KJ_TEST("ReadableStreamBYOBReader rejects read when atLeast exceeds buffer size" auto rs = makeByteStream(env.js); auto reader = ReadableStreamBYOBReader::constructor(env.js, rs.addRef()); - auto buffer = v8::ArrayBuffer::New(env.js.v8Isolate, 10); - auto view = v8::Uint8Array::New(buffer, 0, 10); + auto view = jsg::JsUint8Array::create(env.js, 10); bool rejected = false; reader->readAtLeast(env.js, 20, view) @@ -832,7 +829,7 @@ KJ_TEST("ReadableStreamBYOBReader readAtLeast with element count within capacity auto view = v8::Uint32Array::New(buffer, 0, 10); bool rejected = false; - reader->readAtLeast(env.js, 10, view) + reader->readAtLeast(env.js, 10, jsg::JsArrayBufferView(view)) .catch_(env.js, [&](jsg::Lock& js, jsg::Value reason) -> ReadResult { rejected = true; auto ex = js.exceptionToKj(kj::mv(reason)); @@ -859,7 +856,7 @@ KJ_TEST("ReadableStreamBYOBReader readAtLeast rejects when element count exceeds auto view = v8::Uint32Array::New(buffer, 0, 10); bool rejected = false; - reader->readAtLeast(env.js, 11, view) + reader->readAtLeast(env.js, 11, jsg::JsArrayBufferView(view)) .catch_(env.js, [&](jsg::Lock& js, jsg::Value reason) -> ReadResult { rejected = true; auto ex = js.exceptionToKj(kj::mv(reason)); @@ -883,7 +880,7 @@ KJ_TEST("ReadableStreamBYOBReader readAtLeast rejects byteLength as element coun auto view = v8::Uint32Array::New(buffer, 0, 1024); bool rejected = false; - reader->readAtLeast(env.js, 4096, view) + reader->readAtLeast(env.js, 4096, jsg::JsArrayBufferView(view)) .catch_(env.js, [&](jsg::Lock& js, jsg::Value reason) -> ReadResult { rejected = true; auto ex = js.exceptionToKj(kj::mv(reason)); @@ -911,7 +908,7 @@ KJ_TEST("ReadableStreamBYOBReader read() with min exceeding element capacity rej ReadableStreamBYOBReader::ReadableStreamBYOBReaderReadOptions opts; opts.min = 11; bool rejected = false; - reader->read(env.js, view, kj::mv(opts)) + reader->read(env.js, jsg::JsArrayBufferView(view), kj::mv(opts)) .catch_(env.js, [&](jsg::Lock& js, jsg::Value reason) -> ReadResult { rejected = true; auto ex = js.exceptionToKj(kj::mv(reason)); @@ -930,8 +927,7 @@ KJ_TEST("ReadableStreamBYOBReader rejects read after releaseLock") { auto reader = ReadableStreamBYOBReader::constructor(env.js, rs.addRef()); reader->releaseLock(env.js); - auto buffer = v8::ArrayBuffer::New(env.js.v8Isolate, 10); - auto view = v8::Uint8Array::New(buffer, 0, 10); + auto view = jsg::JsUint8Array::create(env.js, 10); bool rejected = false; reader->read(env.js, view, kj::none) diff --git a/src/workerd/api/streams/internal.c++ b/src/workerd/api/streams/internal.c++ index cd65aa69645..2424c18e845 100644 --- a/src/workerd/api/streams/internal.c++ +++ b/src/workerd/api/streams/internal.c++ @@ -33,7 +33,7 @@ namespace { kj::str(JSG_EXCEPTION(TypeError) ": ", message))); } -kj::Promise pumpTo(ReadableStreamSource& input, WritableStreamSink& output, bool end) { +kj::Promise pumpTo(ReadableStreamSource& input, WritableStreamSink& output, End end) { kj::byte buffer[65536]{}; while (true) { @@ -253,10 +253,10 @@ class AllReader final { }; kj::Exception reasonToException(jsg::Lock& js, - jsg::Optional> maybeReason, + jsg::Optional maybeReason, kj::String defaultDescription = kj::str(JSG_EXCEPTION(Error) ": Stream was cancelled.")) { KJ_IF_SOME(reason, maybeReason) { - return js.exceptionToKj(js.v8Ref(reason)); + return js.exceptionToKj(reason); } else { // We get here if the caller is something like `r.cancel()` (or `r.cancel(undefined)`). return kj::Exception( @@ -264,116 +264,11 @@ kj::Exception reasonToException(jsg::Lock& js, } } -// ======================================================================================= - -// Adapt ReadableStreamSource to kj::AsyncInputStream's interface for use with `kj::newTee()`. -class TeeAdapter final: public kj::AsyncInputStream { - public: - explicit TeeAdapter(kj::Own inner): inner(kj::mv(inner)) {} - - kj::Promise tryRead(void* buffer, size_t minBytes, size_t maxBytes) override { - return inner->tryRead(buffer, minBytes, maxBytes); - } - - kj::Maybe tryGetLength() override { - return inner->tryGetLength(StreamEncoding::IDENTITY); - } - - private: - kj::Own inner; -}; - -class TeeBranch final: public ReadableStreamSource { - public: - explicit TeeBranch(kj::Own inner): inner(kj::mv(inner)) {} - - kj::Promise tryRead(void* buffer, size_t minBytes, size_t maxBytes) override { - return inner->tryRead(buffer, minBytes, maxBytes); - } - - kj::Promise> pumpTo(WritableStreamSink& output, bool end) override { -#ifdef KJ_NO_RTTI - // Yes, I'm paranoid. - static_assert(!KJ_NO_RTTI, "Need RTTI for correctness"); -#endif - - // HACK: If `output` is another TransformStream, we don't allow pumping to it, in order to - // guarantee that we can't create cycles. Note that currently TeeBranch only ever wraps - // TransformStreams, never system streams. - JSG_REQUIRE(!isIdentityTransformStream(output), TypeError, - "Inter-TransformStream ReadableStream.pipeTo() is not implemented."); - - // It is important we actually call `inner->pumpTo()` so that `kj::newTee()` is aware of this - // pump operation's backpressure. So we can't use the default `ReadableStreamSource::pumpTo()` - // implementation, and have to implement our own. - - PumpAdapter outputAdapter(output); - co_await inner->pumpTo(outputAdapter); - - if (end) { - co_await output.end(); - } - - // We only use `TeeBranch` when a locally-sourced stream was tee'd (because system streams - // implement `tryTee()` in a different way that doesn't use `TeeBranch`). So, we know that - // none of the pump can be performed without the IoContext active, and thus we do not - // `KJ_CO_MAGIC BEGIN_DEFERRED_PROXYING`. - co_return; - } - - kj::Maybe tryGetLength(StreamEncoding encoding) override { - if (encoding == StreamEncoding::IDENTITY) { - return inner->tryGetLength(); - } else { - return kj::none; - } - } - - kj::Maybe tryTee(uint64_t limit) override { - KJ_IF_SOME(t, inner->tryTee(limit)) { - auto branch = kj::heap(newTeeErrorAdapter(kj::mv(t))); - auto consumed = kj::heap(kj::mv(inner)); - return Tee{kj::mv(branch), kj::mv(consumed)}; - } - - return kj::none; - } - - void cancel(kj::Exception reason) override { - // TODO(someday): What to do? - } - - private: - // Adapt WritableStreamSink to kj::AsyncOutputStream's interface for use in - // `TeeBranch::pumpTo()`. If you squint, the write logic looks very similar to TeeAdapter's - // read logic. - class PumpAdapter final: public kj::AsyncOutputStream { - public: - explicit PumpAdapter(WritableStreamSink& inner): inner(inner) {} - - kj::Promise write(kj::ArrayPtr buffer) override { - return inner.write(buffer); - } - - kj::Promise write(kj::ArrayPtr> pieces) override { - return inner.write(pieces); - } - - kj::Promise whenWriteDisconnected() override { - KJ_UNIMPLEMENTED("whenWriteDisconnected() not expected on PumpAdapter"); - } - - WritableStreamSink& inner; - }; - - kj::Own inner; -}; } // namespace // ======================================================================================= -kj::Promise> ReadableStreamSource::pumpTo( - WritableStreamSink& output, bool end) { +kj::Promise> ReadableStreamSource::pumpTo(WritableStreamSink& output, End end) { KJ_IF_SOME(p, output.tryPumpFrom(*this, end)) { return kj::mv(p); } @@ -392,7 +287,6 @@ kj::Promise> ReadableStreamSource::readAllBytes(uint64_t limit) AllReader allReader(*this, limit); co_return co_await allReader.readAllBytes(); } catch (...) { - // TODO(soon): Temporary logging. auto ex = kj::getCaughtExceptionAsKj(); if (ex.getDescription().endsWith("exceeded before EOF.")) { LOG_WARNING_PERIODICALLY("NOSENTRY Internal Stream readAllBytes - Exceeded limit"); @@ -407,7 +301,6 @@ kj::Promise ReadableStreamSource::readAllText( AllReader allReader(*this, limit); co_return co_await allReader.readAllText(option); } catch (...) { - // TODO(soon): Temporary logging. auto ex = kj::getCaughtExceptionAsKj(); if (ex.getDescription().endsWith("exceeded before EOF.")) { LOG_WARNING_PERIODICALLY("NOSENTRY Internal Stream readAllText - Exceeded limit"); @@ -423,7 +316,7 @@ kj::Maybe ReadableStreamSource::tryTee(uint64_t limit } kj::Maybe>> WritableStreamSink::tryPumpFrom( - ReadableStreamSource& input, bool end) { + ReadableStreamSource& input, End end) { return kj::none; } @@ -444,45 +337,40 @@ kj::Maybe> ReadableStreamInternalController::read( if (isPendingClosure) { return js.rejectedPromise( - js.v8TypeError("This ReadableStream belongs to an object that is closing."_kj)); + js.typeError("This ReadableStream belongs to an object that is closing."_kj)); } - v8::Local store; - size_t byteLength = 0; - size_t byteOffset = 0; + kj::Maybe view; size_t atLeast = 1; KJ_IF_SOME(byobOptions, maybeByobOptions) { - store = byobOptions.bufferView.getHandle(js)->Buffer(); - byteOffset = byobOptions.byteOffset; - byteLength = byobOptions.byteLength; + auto handle = byobOptions.bufferView.getHandle(js); atLeast = byobOptions.atLeast.orDefault(atLeast); if (byobOptions.detachBuffer) { - if (!store->IsDetachable()) { + if (!handle.isDetachable()) { return js.rejectedPromise( - js.v8TypeError("Unable to use non-detachable ArrayBuffer"_kj)); + js.typeError("Unable to use non-detachable ArrayBuffer"_kj)); } - auto backing = store->GetBackingStore(); - jsg::check(store->Detach(v8::Local())); - store = v8::ArrayBuffer::New(js.v8Isolate, kj::mv(backing)); + view = handle.detachAndTake(js); + } else { + view = handle; } } - auto getOrInitStore = [&](bool errorCase = false) { - if (store.IsEmpty()) { - if (errorCase) { - byteLength = 0; - } else if (util::Autogate::isEnabled(util::AutogateKey::UPDATED_AUTO_ALLOCATE_CHUNK_SIZE)) { - byteLength = UnderlyingSource::DEFAULT_AUTO_ALLOCATE_CHUNK_SIZE_2; - } else { - byteLength = UnderlyingSource::DEFAULT_AUTO_ALLOCATE_CHUNK_SIZE; - } + auto getOrInitView = [&](bool errorCase = false) -> kj::Maybe { + KJ_IF_SOME(v, view) { + return v; + } - if (!v8::ArrayBuffer::MaybeNew(js.v8Isolate, byteLength).ToLocal(&store)) { - return v8::Local(); - } + if (errorCase) { + jsg::JsArrayBufferView v = jsg::JsUint8Array::create(js, 0); + return v; + } else if (util::Autogate::isEnabled(util::AutogateKey::UPDATED_AUTO_ALLOCATE_CHUNK_SIZE)) { + return jsg::JsUint8Array::tryCreate(js, UnderlyingSource::DEFAULT_AUTO_ALLOCATE_CHUNK_SIZE_2) + .map([](auto u8) -> jsg::JsArrayBufferView { return u8; }); } - return store; + return jsg::JsUint8Array::tryCreate(js, UnderlyingSource::DEFAULT_AUTO_ALLOCATE_CHUNK_SIZE) + .map([](auto u8) -> jsg::JsArrayBufferView { return u8; }); }; disturbed = true; @@ -492,15 +380,15 @@ kj::Maybe> ReadableStreamInternalController::read( if (maybeByobOptions != kj::none && FeatureFlags::get(js).getInternalStreamByobReturn()) { // When using the BYOB reader, we must return a sized-0 Uint8Array that is backed // by the ArrayBuffer passed in the options. - auto theStore = getOrInitStore(true); - if (theStore.IsEmpty()) { + KJ_IF_SOME(view, getOrInitView(true)) { + return js.resolvedPromise(ReadResult{ + .value = jsg::JsValue(view.slice(js, 0, 0)).addRef(js), + .done = true, + }); + } else { return js.rejectedPromise( - js.v8TypeError("Unable to allocate memory for read"_kj)); + js.typeError("Unable to allocate memory for read"_kj)); } - return js.resolvedPromise(ReadResult{ - .value = js.v8Ref(v8::Uint8Array::New(theStore, 0, 0).As()), - .done = true, - }); } return js.resolvedPromise(ReadResult{.done = true}); } @@ -515,172 +403,134 @@ kj::Maybe> ReadableStreamInternalController::read( // TransformStream implementation is primarily (only?) used for constructing manually // streamed Responses, and no teed ReadableStream has ever supported them. if (readPending) { - return js.rejectedPromise(js.v8TypeError( + return js.rejectedPromise(js.typeError( "This ReadableStream only supports a single pending read request at a time."_kj)); } readPending = true; - auto theStore = getOrInitStore(); - if (theStore.IsEmpty()) { - return js.rejectedPromise( - js.v8TypeError("Unable to allocate memory for read"_kj)); - } + KJ_IF_SOME(view, getOrInitView()) { + // For resizable ArrayBuffers, the buffer may be resized while the read is + // pending, decommitting memory pages and making the pointer invalid (SIGSEGV). + // We read into a temporary buffer and copy the data back in the .then() + // callback, where we can validate the buffer is still large enough. - // In the case the ArrayBuffer is detached/transfered while the read is pending, we - // need to make sure that the ptr remains stable, so we grab a shared ptr to the - // backing store and use that to get the pointer to the data. If the buffer is detached - // while the read is pending, this does mean that the read data will end up being lost, - // but there's not really a better option. The best we can do here is warn the user - // that this is happening so they can avoid doing it in the future. - // Also, the user really shouldn't do this because the read will end up completing into - // the detached backing store still which could cause issues with whatever code now actually - // owns the transfered buffer. Below we'll warn the user about this if it happens so they - // can avoid doing it in the future. - auto backing = theStore->GetBackingStore(); - - // For resizable ArrayBuffers, the buffer may be resized while the read is - // pending, decommitting memory pages and making the pointer invalid (SIGSEGV). - // We read into a temporary buffer and copy the data back in the .then() - // callback, where we can validate the buffer is still large enough. - bool isResizable = theStore->IsResizableByUserJavaScript(); - - kj::Array tempBuffer; - kj::byte* readPtr; - if (isResizable) { - auto currentByteLength = theStore->ByteLength(); - if (byteOffset >= currentByteLength) { - readPending = false; + auto bytes = view.asArrayPtr(); + if (bytes.size() == 0) { + // There's no point in trying to read into a zero-length buffer. return js.resolvedPromise(ReadResult{ - .value = js.v8Ref(v8::Uint8Array::New(theStore, 0, 0).As()), + .value = jsg::JsValue(view.slice(js, 0, 0)).addRef(js), .done = false, }); } - if (byteOffset + byteLength > currentByteLength) { - byteLength = currentByteLength - byteOffset; - if (atLeast > byteLength) { - atLeast = byteLength > 0 ? byteLength : 1; - } - } - tempBuffer = kj::heapArray(byteLength); - readPtr = tempBuffer.begin(); - } else { - auto ptr = static_cast(backing->Data()); - readPtr = ptr + byteOffset; - } - auto bytes = kj::arrayPtr(readPtr, byteLength); - KJ_ASSERT(atLeast <= bytes.size(), "minBytes must not exceed maxBytes in tryRead"); + KJ_ASSERT(atLeast <= bytes.size(), "minBytes must not exceed maxBytes in tryRead"); - auto promise = kj::evalNow([&] { - return readable->tryRead(bytes.begin(), atLeast, bytes.size()).attach(kj::mv(backing)); - }); - KJ_IF_SOME(readerLock, readState.tryGetUnsafe()) { - promise = KJ_ASSERT_NONNULL(readerLock.getCanceler())->wrap(kj::mv(promise)); - } - - // TODO(soon): We use awaitIoLegacy() here because if the stream terminates in JavaScript in - // this same isolate, then the promise may actually be waiting on JavaScript to do something, - // and so should not be considered waiting on external I/O. We will need to use - // registerPendingEvent() manually when reading from an external stream. Ideally, we would - // refactor the implementation so that when waiting on a JavaScript stream, we strictly use - // jsg::Promises and not kj::Promises, so that it doesn't look like I/O at all, and there's - // no need to drop the isolate lock and take it again every time some data is read/written. - // That's a larger refactor, though. - auto& ioContext = IoContext::current(); - return ioContext.awaitIoLegacy(js, kj::mv(promise)) - .then(js, ioContext.addFunctor(JSG_VISITABLE_LAMBDA( - (this, ref = addRef(), store = js.v8Ref(store), - byteOffset, byteLength, isByob = maybeByobOptions != kj::none, - isResizable, readPtr, tempBuffer = kj::mv(tempBuffer)), - (ref), - (jsg::Lock& js, size_t amount) mutable -> jsg::Promise { - readPending = false; - KJ_ASSERT(amount <= byteLength); - if (amount == 0) { - if (!state.is()) { - doClose(js); - } - KJ_IF_SOME(o, owner) { - o.signalEof(js); - } else {} - if (isByob && FeatureFlags::get(js).getInternalStreamByobReturn()) { - // When using the BYOB reader, we must return a sized-0 Uint8Array that is backed - // by the ArrayBuffer passed in the options. - auto u8 = v8::Uint8Array::New(store.getHandle(js), 0, 0); - return js.resolvedPromise(ReadResult{ - .value = js.v8Ref(u8.As()), - .done = true, - }); - } - return js.resolvedPromise(ReadResult{.done = true}); + auto dest = kj::heapArray(bytes.size()); + auto promise = + kj::evalNow([&] { return readable->tryRead(dest.begin(), atLeast, dest.size()); }); + KJ_IF_SOME(readerLock, readState.tryGetUnsafe()) { + promise = KJ_ASSERT_NONNULL(readerLock.getCanceler())->wrap(kj::mv(promise)); } - // Return a slice so the script can see how many bytes were read. - - // We have to check to see if the store was detached or resized while we were waiting - // for the read to complete. - auto handle = store.getHandle(js); - if (handle->WasDetached()) { - // If the buffer was detached, we resolve with a new zero-length ArrayBuffer. - // The bytes that were read are lost, but this is a valid result. - - // Silly user, trix are for kids. - IoContext::current().logWarningOnce( - "A buffer that was being used for a read operation on a ReadableStream was detached " - "while the read was pending. The read completed with a zero-length buffer and the data " - "that was read is lost. Avoid detaching buffers that are being used for active read " - "operations on streams, or use the streams_byob_reader_detaches_buffer compatibility " - "flag, to prevent this from happening."_kj); - - auto buffer = v8::ArrayBuffer::New(js.v8Isolate, 0); - return js.resolvedPromise(ReadResult{ - .value = js.v8Ref(v8::Uint8Array::New(buffer, 0, 0).As()), - .done = false, - }); - } - - if (byteOffset + amount > handle->ByteLength()) { - // If the buffer was resized smaller, we return a truncated result. - IoContext::current().logWarningOnce( - "A buffer that was being used for a read operation on a ReadableStream was resized " - "smaller while the read was pending. The read completed with a truncated buffer " - "containing only the bytes that fit within the new size. Avoid resizing buffers that " - "are being used for active read operations on streams, or use the " - "streams_byob_reader_detaches_buffer compatibility flag, to prevent this from " - "happening."_kj); + // TODO(soon): We use awaitIoLegacy() here because if the stream terminates in JavaScript + // in this same isolate, then the promise may actually be waiting on JavaScript to do + // something, and so should not be considered waiting on external I/O. We will need to use + // registerPendingEvent() manually when reading from an external stream. Ideally, we would + // refactor the implementation so that when waiting on a JavaScript stream, we strictly use + // jsg::Promises and not kj::Promises, so that it doesn't look like I/O at all, and there's + // no need to drop the isolate lock and take it again every time some data is read/written. + // That's a larger refactor, though. + auto& ioContext = IoContext::current(); + return ioContext.awaitIoLegacy(js, kj::mv(promise)) + .then(js, ioContext.addFunctor(JSG_VISITABLE_LAMBDA( + (this, ref = addRef(), + view = view.addRef(js), + dest = kj::mv(dest), + isByob = maybeByobOptions != kj::none), + (ref, view), + (jsg::Lock& js, size_t amount) mutable -> jsg::Promise { + readPending = false; + KJ_ASSERT(amount <= dest.size()); + auto handle = view.getHandle(js); + if (amount == 0) { + if (!state.is()) { + doClose(js); + } + KJ_IF_SOME(o, owner) { + o.signalEof(js); + } else {} + if (isByob && FeatureFlags::get(js).getInternalStreamByobReturn()) { + return js.resolvedPromise(ReadResult{ + .value = jsg::JsValue(handle.slice(js, 0, 0)).addRef(js), + .done = true, + }); + } + return js.resolvedPromise(ReadResult{.done = true}); + } + // Return a slice so the script can see how many bytes were read. + + // We have to check to see if the store was detached while we were waiting + // for the read to complete. + if (handle.isDetached()) { + // If the buffer was detached, we resolve with a new zero-length ArrayBuffer. + // The bytes that were read are lost, but this is a valid result. + + // Silly user, trix are for kids. + IoContext::current().logWarningOnce( + "A buffer that was being used for a read operation on a ReadableStream was " + "detached while the read was pending. The read completed with a zero-length buffer " + "and the data that was read is lost. Avoid detaching buffers that are being used " + "for active read operations on streams, or use the " + "streams_byob_reader_detaches_buffer compatibility flag, to prevent this from " + "happening."_kj); - if (byteOffset >= handle->ByteLength()) { return js.resolvedPromise(ReadResult{ - .value = js.v8Ref(v8::Uint8Array::New(store.getHandle(js), 0, 0).As()), + .value = jsg::JsValue(handle.slice(js, 0, 0)).addRef(js), .done = false, }); } - amount = handle->ByteLength() - byteOffset; - } - if (isResizable && byteOffset + amount <= handle->ByteLength()) { - // For resizable buffers, the data was read into a temporary buffer. - // Copy it back into the user's (still valid) buffer region. - auto destPtr = static_cast(handle->GetBackingStore()->Data()); - memcpy(destPtr + byteOffset, readPtr, amount); - } + // If the buffer was resized smaller, we return a truncated result. + if (amount > handle.size()) { + IoContext::current().logWarningOnce( + "A buffer that was being used for a read operation on a ReadableStream was resized " + "smaller while the read was pending. The read completed with a truncated buffer " + "containing only the bytes that fit within the new size. Avoid resizing buffers " + "that are being used for active read operations on streams, or use the " + "streams_byob_reader_detaches_buffer compatibility flag, to prevent this from " + "happening."_kj); + + if (handle.size() == 0) { + return js.resolvedPromise(ReadResult{ + .value = jsg::JsValue(handle.slice(js, 0, 0)).addRef(js), + .done = false, + }); + } + amount = handle.size(); + } - return js.resolvedPromise(ReadResult{ - .value = js.v8Ref( - v8::Uint8Array::New(store.getHandle(js), byteOffset, amount).As()), - .done = false, - }); - })), - ioContext.addFunctor(JSG_VISITABLE_LAMBDA( + handle.asArrayPtr().first(amount).copyFrom(dest.asPtr().first(amount)); + return js.resolvedPromise(ReadResult{ + .value = jsg::JsValue(handle.slice(js, 0, amount)).addRef(js), + .done = false, + }); + })), + ioContext.addFunctor(JSG_VISITABLE_LAMBDA( (this, ref = addRef()), (ref), (jsg::Lock& js, jsg::Value reason) -> jsg::Promise { readPending = false; + auto handle = jsg::JsValue(reason.getHandle(js)); if (!state.is()) { - doError(js, reason.getHandle(js)); + doError(js, handle); } - return js.rejectedPromise(kj::mv(reason)); + return js.rejectedPromise(handle); }))); + + } else { + return js.rejectedPromise( + js.typeError("Unable to allocate memory for read"_kj)); + } } } KJ_UNREACHABLE; @@ -699,7 +549,7 @@ kj::Maybe> ReadableStreamInternalController::dr if (isPendingClosure) { return js.rejectedPromise( - js.v8TypeError("This ReadableStream belongs to an object that is closing."_kj)); + js.typeError("This ReadableStream belongs to an object that is closing."_kj)); } static constexpr size_t kAtLeast = 1; @@ -715,7 +565,7 @@ kj::Maybe> ReadableStreamInternalController::dr } KJ_CASE_ONEOF(readable, Readable) { if (readPending) { - return js.rejectedPromise(js.v8TypeError( + return js.rejectedPromise(js.typeError( "This ReadableStream only supports a single pending read request at a time."_kj)); } readPending = true; @@ -773,10 +623,11 @@ kj::Maybe> ReadableStreamInternalController::dr (ref), (jsg::Lock& js, jsg::Value reason) -> jsg::Promise { readPending = false; + auto handle = jsg::JsValue(reason.getHandle(js)); if (!state.is()) { - doError(js, reason.getHandle(js)); + doError(js, handle); } - return js.rejectedPromise(kj::mv(reason)); + return js.rejectedPromise(handle); }))); } } @@ -791,7 +642,7 @@ jsg::Promise ReadableStreamInternalController::pipeTo( if (isPendingClosure) { return js.rejectedPromise( - js.v8TypeError("This ReadableStream belongs to an object that is closing."_kj)); + js.typeError("This ReadableStream belongs to an object that is closing."_kj)); } disturbed = true; @@ -801,11 +652,11 @@ jsg::Promise ReadableStreamInternalController::pipeTo( } return js.rejectedPromise( - js.v8TypeError("This ReadableStream cannot be piped to this WritableStream."_kj)); + js.typeError("This ReadableStream cannot be piped to this WritableStream."_kj)); } jsg::Promise ReadableStreamInternalController::cancel( - jsg::Lock& js, jsg::Optional> maybeReason) { + jsg::Lock& js, jsg::Optional maybeReason) { disturbed = true; KJ_IF_SOME(errored, state.tryGetUnsafe()) { @@ -818,7 +669,7 @@ jsg::Promise ReadableStreamInternalController::cancel( } void ReadableStreamInternalController::doCancel( - jsg::Lock& js, jsg::Optional> maybeReason) { + jsg::Lock& js, jsg::Optional maybeReason) { auto exception = reasonToException(js, maybeReason); KJ_IF_SOME(locked, readState.tryGetUnsafe()) { KJ_IF_SOME(canceler, locked.getCanceler()) { @@ -843,11 +694,11 @@ void ReadableStreamInternalController::doClose(jsg::Lock& js) { } } -void ReadableStreamInternalController::doError(jsg::Lock& js, v8::Local reason) { +void ReadableStreamInternalController::doError(jsg::Lock& js, jsg::JsValue reason) { // If already in a terminal state, nothing to do. if (state.isTerminal()) return; - state.transitionTo(js.v8Ref(reason)); + state.transitionTo(reason.addRef(js)); KJ_IF_SOME(locked, readState.tryGetUnsafe()) { maybeRejectPromise(js, locked.getClosedFulfiller(), reason); } else { @@ -908,7 +759,7 @@ ReadableStreamController::Tee ReadableStreamInternalController::tee(jsg::Lock& j } kj::Maybe> ReadableStreamInternalController::removeSource( - jsg::Lock& js, bool ignoreDisturbed) { + jsg::Lock& js, IgnoreDisturbed ignoreDisturbed) { JSG_REQUIRE( !isLockedToReader(), TypeError, "This ReadableStream is currently locked to a reader."); JSG_REQUIRE(!disturbed || ignoreDisturbed, TypeError, "This ReadableStream is disturbed."); @@ -982,7 +833,7 @@ void ReadableStreamInternalController::releaseReader( "Cannot call releaseLock() on a reader with outstanding read promises."); } maybeRejectPromise(js, locked.getClosedFulfiller(), - js.v8TypeError("This ReadableStream reader has been released."_kj)); + js.typeError("This ReadableStream reader has been released."_kj)); } locked.clear(); @@ -1013,18 +864,41 @@ jsg::Ref WritableStreamInternalController::addRef() { } jsg::Promise WritableStreamInternalController::write( - jsg::Lock& js, jsg::Optional> value) { + jsg::Lock& js, jsg::Optional value) { if (isPendingClosure) { return js.rejectedPromise( - js.v8TypeError("This WritableStream belongs to an object that is closing."_kj)); + js.typeError("This WritableStream belongs to an object that is closing."_kj)); } if (isClosedOrClosing()) { - return js.rejectedPromise(js.v8TypeError("This WritableStream has been closed."_kj)); + return js.rejectedPromise(js.typeError("This WritableStream has been closed."_kj)); } if (isPiping()) { return js.rejectedPromise( - js.v8TypeError("This WritableStream is currently being piped to."_kj)); - } + js.typeError("This WritableStream is currently being piped to."_kj)); + } + + auto processChunk = [this](jsg::Lock& js, kj::ArrayPtr chunk) { + auto prp = js.newPromiseAndResolver(); + adjustWriteBufferSize(js, chunk.size()); + KJ_IF_SOME(o, observer) { + o->onChunkEnqueued(chunk.size()); + } + + auto data = kj::heapArray(chunk.size()); + data.asPtr().copyFrom(chunk); + auto ptr = data.asPtr(); + queue.push_back( + WriteEvent{.outputLock = IoContext::current().waitForOutputLocksIfNecessaryIoOwn(), + .event = kj::heap({ + .promise = kj::mv(prp.resolver), + .totalBytes = data.size(), + .ownBytes = kj::mv(data), + .bytes = ptr, + })}); + + ensureWriting(js); + return kj::mv(prp.promise); + }; KJ_SWITCH_ONEOF(state) { KJ_CASE_ONEOF(closed, StreamStates::Closed) { @@ -1040,58 +914,28 @@ jsg::Promise WritableStreamInternalController::write( } auto chunk = KJ_ASSERT_NONNULL(value); - std::shared_ptr store; - size_t byteLength = 0; - size_t byteOffset = 0; - if (chunk->IsArrayBuffer()) { - auto buffer = chunk.As(); - store = buffer->GetBackingStore(); - byteLength = buffer->ByteLength(); - } else if (chunk->IsArrayBufferView()) { - auto view = chunk.As(); - store = view->Buffer()->GetBackingStore(); - byteLength = view->ByteLength(); - byteOffset = view->ByteOffset(); - } else if (chunk->IsString()) { - // TODO(later): This really ought to return a rejected promise and not a sync throw. - // This case caused me a moment of confusion during testing, so I think it's worth - // a specific error message. - throwTypeErrorAndConsoleWarn( - "This TransformStream is being used as a byte stream, but received a string on its " - "writable side. If you wish to write a string, you'll probably want to explicitly " - "UTF-8-encode it with TextEncoder."); - } else { - // TODO(later): This really ought to return a rejected promise and not a sync throw. - throwTypeErrorAndConsoleWarn( - "This TransformStream is being used as a byte stream, but received an object of " - "non-ArrayBuffer/ArrayBufferView type on its writable side."); + KJ_IF_SOME(ab, chunk.tryCast()) { + if (ab.size() == 0) return js.resolvedPromise(); + return processChunk(js, ab.asArrayPtr()); } - - if (byteLength == 0) { - return js.resolvedPromise(); + KJ_IF_SOME(sab, chunk.tryCast()) { + if (sab.size() == 0) return js.resolvedPromise(); + return processChunk(js, sab.asArrayPtr()); } - - auto prp = js.newPromiseAndResolver(); - adjustWriteBufferSize(js, byteLength); - KJ_IF_SOME(o, observer) { - o->onChunkEnqueued(byteLength); + KJ_IF_SOME(view, chunk.tryCast()) { + if (view.size() == 0) return js.resolvedPromise(); + return processChunk(js, view.asArrayPtr()); } - - auto src = kj::arrayPtr(static_cast(store->Data()) + byteOffset, byteLength); - auto data = kj::heapArray(src.size()); - data.asPtr().copyFrom(src); - auto ptr = data.asPtr(); - queue.push_back( - WriteEvent{.outputLock = IoContext::current().waitForOutputLocksIfNecessaryIoOwn(), - .event = kj::heap({ - .promise = kj::mv(prp.resolver), - .totalBytes = store->ByteLength(), - .ownBytes = kj::mv(data), - .bytes = ptr, - })}); - - ensureWriting(js); - return kj::mv(prp.promise); + KJ_IF_SOME(str, chunk.tryCast()) { + auto kjstr = str.toDOMString(js); + if (kjstr.size() == 0) return js.resolvedPromise(); + // Trim the null terminator + return processChunk(js, kjstr.asBytes().slice(0, kjstr.size())); + } + // TODO(later): This really ought to return a rejected promise and not a sync throw. + throwTypeErrorAndConsoleWarn( + "This TransformStream is being used as a byte stream, but received an object of " + "non-ArrayBuffer/ArrayBufferView/string type on its writable side."); } } @@ -1103,11 +947,12 @@ void WritableStreamInternalController::adjustWriteBufferSize(jsg::Lock& js, int6 currentWriteBufferSize += amount; KJ_IF_SOME(highWaterMark, maybeHighWaterMark) { int64_t desiredSize = highWaterMark - currentWriteBufferSize; - updateBackpressure(js, desiredSize <= 0); + updateBackpressure(js, desiredSize <= 0 ? UpdateBackpressure::YES : UpdateBackpressure::NO); } } -void WritableStreamInternalController::updateBackpressure(jsg::Lock& js, bool backpressure) { +void WritableStreamInternalController::updateBackpressure( + jsg::Lock& js, UpdateBackpressure backpressure) { KJ_IF_SOME(writerLock, writeState.tryGetUnsafe()) { if (backpressure) { // Per the spec, when backpressure is updated and is true, we replace the existing @@ -1128,12 +973,13 @@ void WritableStreamInternalController::setHighWaterMark(uint64_t highWaterMark) maybeHighWaterMark = highWaterMark; } -jsg::Promise WritableStreamInternalController::closeImpl(jsg::Lock& js, bool markAsHandled) { +jsg::Promise WritableStreamInternalController::closeImpl( + jsg::Lock& js, MarkAsHandled markAsHandled) { if (isClosedOrClosing()) { return js.resolvedPromise(); } if (isPiping()) { - auto reason = js.v8TypeError("This WritableStream is currently being piped to."_kj); + auto reason = js.typeError("This WritableStream is currently being piped to."_kj); return rejectedMaybeHandledPromise(js, reason, markAsHandled); } @@ -1162,7 +1008,8 @@ jsg::Promise WritableStreamInternalController::closeImpl(jsg::Lock& js, bo KJ_UNREACHABLE; } -jsg::Promise WritableStreamInternalController::close(jsg::Lock& js, bool markAsHandled) { +jsg::Promise WritableStreamInternalController::close( + jsg::Lock& js, MarkAsHandled markAsHandled) { KJ_IF_SOME(closureWaitable, maybeClosureWaitable) { // If we're already waiting on the closure waitable, then we do not want to try scheduling // it again, let's just wait for the existing one to be resolved. @@ -1184,13 +1031,14 @@ jsg::Promise WritableStreamInternalController::close(jsg::Lock& js, bool m } } -jsg::Promise WritableStreamInternalController::flush(jsg::Lock& js, bool markAsHandled) { +jsg::Promise WritableStreamInternalController::flush( + jsg::Lock& js, MarkAsHandled markAsHandled) { if (isClosedOrClosing()) { - auto reason = js.v8TypeError("This WritableStream has been closed."_kj); + auto reason = js.typeError("This WritableStream has been closed."_kj); return rejectedMaybeHandledPromise(js, reason, markAsHandled); } if (isPiping()) { - auto reason = js.v8TypeError("This WritableStream is currently being piped to."_kj); + auto reason = js.typeError("This WritableStream is currently being piped to."_kj); return rejectedMaybeHandledPromise(js, reason, markAsHandled); } @@ -1220,15 +1068,15 @@ jsg::Promise WritableStreamInternalController::flush(jsg::Lock& js, bool m } jsg::Promise WritableStreamInternalController::abort( - jsg::Lock& js, jsg::Optional> maybeReason) { + jsg::Lock& js, jsg::Optional maybeReason) { // While it may be confusing to users to throw `undefined` rather than a more helpful Error here, // doing so is required by the relevant spec: // https://streams.spec.whatwg.org/#writable-stream-abort - return doAbort(js, maybeReason.orDefault(js.v8Undefined())); + return doAbort(js, maybeReason.orDefault(js.undefined())); } jsg::Promise WritableStreamInternalController::doAbort( - jsg::Lock& js, v8::Local reason, AbortOptions options) { + jsg::Lock& js, jsg::JsValue reason, AbortOptions options) { // If maybePendingAbort is set, then the returned abort promise will be rejected // with the specified error once the abort is completed, otherwise the promise will // be resolved with undefined. @@ -1245,7 +1093,7 @@ jsg::Promise WritableStreamInternalController::doAbort( } KJ_IF_SOME(writable, state.tryGetUnsafe>()) { - auto exception = js.exceptionToKj(js.v8Ref(reason)); + auto exception = js.exceptionToKj(reason.addRef(js)); if (FeatureFlags::get(js).getInternalWritableStreamAbortClearsQueue()) { // If this flag is set, we will clear the queue proactively and immediately @@ -1255,18 +1103,21 @@ jsg::Promise WritableStreamInternalController::doAbort( // immediately and an immediately resolved or rejected promise will be returned. writable->abort(exception.clone()); drain(js, reason); - return options.reject ? rejectedMaybeHandledPromise(js, reason, options.handled) + return options.reject ? rejectedMaybeHandledPromise(js, reason, + options.handled ? MarkAsHandled::YES : MarkAsHandled::NO) : js.resolvedPromise(); } if (queue.empty()) { writable->abort(exception.clone()); doError(js, reason); - return options.reject ? rejectedMaybeHandledPromise(js, reason, options.handled) + return options.reject ? rejectedMaybeHandledPromise(js, reason, + options.handled ? MarkAsHandled::YES : MarkAsHandled::NO) : js.resolvedPromise(); } - maybePendingAbort = kj::heap(js, reason, options.reject); + maybePendingAbort = + kj::heap(js, reason, options.reject ? Reject::YES : Reject::NO); auto promise = KJ_ASSERT_NONNULL(maybePendingAbort)->whenResolved(js); if (options.handled) { promise.markAsHandled(js); @@ -1274,7 +1125,8 @@ jsg::Promise WritableStreamInternalController::doAbort( return kj::mv(promise); } - return options.reject ? rejectedMaybeHandledPromise(js, reason, options.handled) + return options.reject ? rejectedMaybeHandledPromise( + js, reason, options.handled ? MarkAsHandled::YES : MarkAsHandled::NO) : js.resolvedPromise(); } @@ -1294,15 +1146,17 @@ kj::Maybe> WritableStreamInternalController::tryPipeFrom( auto pipeThrough = options.pipeThrough; if (isPiping()) { - auto reason = js.v8TypeError("This WritableStream is currently being piped to."_kj); - return rejectedMaybeHandledPromise(js, reason, pipeThrough); + auto reason = js.typeError("This WritableStream is currently being piped to."_kj); + return rejectedMaybeHandledPromise( + js, reason, pipeThrough ? MarkAsHandled::YES : MarkAsHandled::NO); } // If a signal is provided, we need to check that it is not already triggered. If it // is, we return a rejected promise using the signal's reason. KJ_IF_SOME(signal, options.signal) { if (signal->getAborted(js)) { - return rejectedMaybeHandledPromise(js, signal->getReason(js), pipeThrough); + return rejectedMaybeHandledPromise( + js, signal->getReason(js), pipeThrough ? MarkAsHandled::YES : MarkAsHandled::NO); } } @@ -1326,7 +1180,8 @@ kj::Maybe> WritableStreamInternalController::tryPipeFrom( // If preventAbort was true, we're going to unlock the destination now. writeState.transitionTo(); - return rejectedMaybeHandledPromise(js, errored, pipeThrough); + return rejectedMaybeHandledPromise( + js, errored, pipeThrough ? MarkAsHandled::YES : MarkAsHandled::NO); } // If the destination has errored, the spec requires us to reject the pipe promise and, if @@ -1339,7 +1194,8 @@ kj::Maybe> WritableStreamInternalController::tryPipeFrom( } else { sourceLock.release(js); } - return rejectedMaybeHandledPromise(js, errored.getHandle(js), pipeThrough); + return rejectedMaybeHandledPromise( + js, errored.getHandle(js), pipeThrough ? MarkAsHandled::YES : MarkAsHandled::NO); } // If the source has closed, the spec requires us to close the destination if preventClose @@ -1365,7 +1221,7 @@ kj::Maybe> WritableStreamInternalController::tryPipeFrom( // If the destination has closed, the spec requires us to close the source if // preventCancel is false (Propagate closing backward). if (isClosedOrClosing()) { - auto destClosed = js.v8TypeError("This destination writable stream is closed."_kj); + auto destClosed = js.typeError("This destination writable stream is closed."_kj); writeState.transitionTo(); if (!preventCancel) { @@ -1374,7 +1230,8 @@ kj::Maybe> WritableStreamInternalController::tryPipeFrom( sourceLock.release(js); } - return rejectedMaybeHandledPromise(js, destClosed, pipeThrough); + return rejectedMaybeHandledPromise( + js, destClosed, pipeThrough ? MarkAsHandled::YES : MarkAsHandled::NO); } // The pipe will continue until either the source closes or errors, or until the destination @@ -1502,7 +1359,7 @@ void WritableStreamInternalController::releaseWriter( KJ_ASSERT(&locked.getWriter() == &writer); KJ_IF_SOME(js, maybeJs) { maybeRejectPromise(js, locked.getClosedFulfiller(), - js.v8TypeError("This WritableStream writer has been released."_kj)); + js.typeError("This WritableStream writer has been released."_kj)); } locked.clear(); @@ -1544,14 +1401,14 @@ void WritableStreamInternalController::doClose(jsg::Lock& js) { } else { (void)writeState.transitionFromTo(); } - PendingAbort::dequeue(maybePendingAbort); + auto _ KJ_UNUSED = PendingAbort::dequeue(maybePendingAbort); } -void WritableStreamInternalController::doError(jsg::Lock& js, v8::Local reason) { +void WritableStreamInternalController::doError(jsg::Lock& js, jsg::JsValue reason) { // If already in a terminal state, nothing to do. if (state.isTerminal()) return; - state.transitionTo(js.v8Ref(reason)); + state.transitionTo(reason.addRef(js)); KJ_IF_SOME(locked, writeState.tryGetUnsafe()) { maybeRejectPromise(js, locked.getClosedFulfiller(), reason); maybeResolvePromise(js, locked.getReadyFulfiller()); @@ -1559,7 +1416,7 @@ void WritableStreamInternalController::doError(jsg::Lock& js, v8::Local(); } - PendingAbort::dequeue(maybePendingAbort); + auto _ KJ_UNUSED = PendingAbort::dequeue(maybePendingAbort); } void WritableStreamInternalController::ensureWriting(jsg::Lock& js) { @@ -1589,7 +1446,7 @@ void WritableStreamInternalController::finishClose(jsg::Lock& js) { doClose(js); } -void WritableStreamInternalController::finishError(jsg::Lock& js, v8::Local reason) { +void WritableStreamInternalController::finishError(jsg::Lock& js, jsg::JsValue reason) { KJ_IF_SOME(pendingAbort, PendingAbort::dequeue(maybePendingAbort)) { // In this case, and only this case, we ignore any pending rejection // that may be stored in the pendingAbort. The current exception takes @@ -1725,7 +1582,7 @@ jsg::Promise WritableStreamInternalController::writeLoopAfterFrontOutputLo jsg::Lock& js, jsg::Value reason) -> jsg::Promise { // Under some conditions, the clean up has already happened. if (queue.empty()) return js.resolvedPromise(); - auto handle = reason.getHandle(js); + auto handle = jsg::JsValue(reason.getHandle(js)); auto& request = check.template operator()(); auto& writable = state.getUnsafe>(); adjustWriteBufferSize(js, -amountToWrite); @@ -1772,7 +1629,7 @@ jsg::Promise WritableStreamInternalController::writeLoopAfterFrontOutputLo // If the source is errored, the spec requires us to error the destination unless the // preventAbort option is true. if (!request->preventAbort()) { - auto ex = js.exceptionToKj(js.v8Ref(errored)); + auto ex = js.exceptionToKj(errored.addRef(js)); writable->abort(kj::mv(ex)); drain(js, errored); } else { @@ -1823,7 +1680,7 @@ jsg::Promise WritableStreamInternalController::writeLoopAfterFrontOutputLo if (!preventClose) { // Note: unlike a real Close request, it's not possible for us to have been aborted. - return close(js, true); + return close(js, MarkAsHandled::YES); } else { writeState.transitionTo(); } @@ -1831,7 +1688,7 @@ jsg::Promise WritableStreamInternalController::writeLoopAfterFrontOutputLo }), ioContext.addFunctor( [this, check, preventAbort](jsg::Lock& js, jsg::Value reason) mutable { - auto handle = reason.getHandle(js); + auto handle = jsg::JsValue(reason.getHandle(js)); auto& request = check.template operator()(); maybeRejectPromise(js, request.promise(), handle); // TODO(conform): Remember all those checks we performed in ReadableStream::pipeTo()? @@ -1840,7 +1697,7 @@ jsg::Promise WritableStreamInternalController::writeLoopAfterFrontOutputLo // deeper integration with the implementation of pumpTo(). Oh well. One consequence // of this is that if there is an error on the writable side, we error the readable // side, rather than close (cancel) it, which is what the spec would have us do. - // TODO(now): Warn on the console about this. + // TODO(conform): Consider warning on the console about this. request.source().error(js, handle); queue.pop_front(); if (!preventAbort) { @@ -1851,7 +1708,9 @@ jsg::Promise WritableStreamInternalController::writeLoopAfterFrontOutputLo })); }; - KJ_IF_SOME(promise, request->source().tryPumpTo(*writable->sink, !request->preventClose())) { + KJ_IF_SOME(promise, + request->source().tryPumpTo( + *writable->sink, request->preventClose() ? End::NO : End::YES)) { return handlePromise(js, ioContext.awaitIo(js, writable->canceler.wrap( @@ -1882,7 +1741,7 @@ jsg::Promise WritableStreamInternalController::writeLoopAfterFrontOutputLo ioContext.addFunctor([this, check](jsg::Lock& js, jsg::Value reason) { // Under some conditions, the clean up has already happened. if (queue.empty()) return; - auto handle = reason.getHandle(js); + auto handle = jsg::JsValue(reason.getHandle(js)); auto& request = check.template operator()(); maybeRejectPromise(js, request.promise, handle); queue.pop_front(); @@ -1890,8 +1749,8 @@ jsg::Promise WritableStreamInternalController::writeLoopAfterFrontOutputLo })); } KJ_CASE_ONEOF(request, kj::Own) { - // This is not a standards-defined state for a WritableStream and is only used internally - // for Socket's startTls call. + // Flush is a non-standard extension. Originally added for Socket's startTls, it is + // now available as a general-purpose flush mechanism via WritableStream::flush(). // // Flushing is similar to closing the stream, the main difference is that `finishClose` // and `writable->end()` are never called. @@ -1936,7 +1795,7 @@ bool WritableStreamInternalController::Pipe::State::checkSignal(jsg::Lock& js) { parent.writeState.transitionTo(); } if (!preventCancelCopy) { - sourceRef.release(js, v8::Local(reason)); + sourceRef.release(js, reason); } else { sourceRef.release(js); } @@ -1948,40 +1807,36 @@ bool WritableStreamInternalController::Pipe::State::checkSignal(jsg::Lock& js) { } jsg::Promise WritableStreamInternalController::Pipe::State::write( - v8::Local handle) { - auto& writable = parent.state.getUnsafe>(); - // TODO(soon): Once jsg::BufferSource lands and we're able to use it, this can be simplified. - KJ_ASSERT(handle->IsArrayBuffer() || handle->IsArrayBufferView()); - std::shared_ptr store; - size_t byteLength = 0; - size_t byteOffset = 0; - if (handle->IsArrayBuffer()) { - auto buffer = handle.template As(); - store = buffer->GetBackingStore(); - byteLength = buffer->ByteLength(); - } else { - auto view = handle.template As(); - store = view->Buffer()->GetBackingStore(); - byteLength = view->ByteLength(); - byteOffset = view->ByteOffset(); - } - kj::byte* data = reinterpret_cast(store->Data()) + byteOffset; - // TODO(cleanup): Have this method accept a jsg::Lock& from the caller instead of using - // v8::Isolate::GetCurrent(); - auto& js = jsg::Lock::current(); - - // For resizable ArrayBuffers or shared backing stores, we must eagerly copy - // the data. A resizable ArrayBuffer's logical byte length can be changed by user - // JS after write() returns but before the sink consumes the data, making the - // cached byteLength stale. - // But also just beacuse of V8 Sandbox requirements, we really should be copying - // the data from the ArrayBuffer anyway... We incur an allocation and copy cost - // here but that's to be expected. - auto backing = kj::heapArray(byteLength); - backing.asPtr().copyFrom(kj::arrayPtr(data, byteLength)); - return IoContext::current().awaitIo(js, - writable->canceler.wrap(writable->sink->write(backing)).attach(kj::mv(backing)), - [](jsg::Lock&) {}); + jsg::Lock& js, jsg::JsValue handle) { + KJ_DASSERT(isByteSource(handle)); + + auto processChunk = [this](jsg::Lock& js, kj::ArrayPtr data) { + auto& writable = parent.state.getUnsafe>(); + auto backing = kj::heapArray(data.size()); + backing.asPtr().copyFrom(data); + return IoContext::current().awaitIo(js, + writable->canceler.wrap(writable->sink->write(backing)).attach(kj::mv(backing)), + [](jsg::Lock&) {}); + }; + + KJ_IF_SOME(ab, handle.tryCast()) { + if (ab.size() == 0) return js.resolvedPromise(); + return processChunk(js, ab.asArrayPtr()); + } + KJ_IF_SOME(sab, handle.tryCast()) { + if (sab.size() == 0) return js.resolvedPromise(); + return processChunk(js, sab.asArrayPtr()); + } + KJ_IF_SOME(view, handle.tryCast()) { + if (view.size() == 0) return js.resolvedPromise(); + return processChunk(js, view.asArrayPtr()); + } + KJ_IF_SOME(str, handle.tryCast()) { + auto kjstr = str.toDOMString(js); + if (kjstr.size() == 0) return js.resolvedPromise(); + return processChunk(js, kjstr.asBytes().slice(0, kjstr.size())); + } + KJ_UNREACHABLE; } jsg::Promise WritableStreamInternalController::Pipe::State::pipeLoop(jsg::Lock& js) { @@ -2015,7 +1870,7 @@ jsg::Promise WritableStreamInternalController::Pipe::State::pipeLoop(jsg:: source.release(js); if (!preventAbort) { KJ_IF_SOME(writable, parent.state.tryGetUnsafe>()) { - auto ex = js.exceptionToKj(js.v8Ref(errored)); + auto ex = js.exceptionToKj(errored.addRef(js)); writable->abort(kj::mv(ex)); return js.rejectedPromise(errored); } @@ -2054,7 +1909,7 @@ jsg::Promise WritableStreamInternalController::Pipe::State::pipeLoop(jsg:: }), ioContext.addFunctor([state = kj::addRef(*this)](jsg::Lock& js, jsg::Value reason) { if (state->aborted) return; - state->parent.finishError(js, reason.getHandle(js)); + state->parent.finishError(js, jsg::JsValue(reason.getHandle(js))); })); } parent.writeState.transitionTo(); @@ -2063,7 +1918,7 @@ jsg::Promise WritableStreamInternalController::Pipe::State::pipeLoop(jsg:: } if (parent.isClosedOrClosing()) { - auto destClosed = js.v8TypeError("This destination writable stream is closed."_kj); + auto destClosed = js.typeError("This destination writable stream is closed."_kj); parent.writeState.transitionTo(); if (!preventCancel) { @@ -2087,36 +1942,37 @@ jsg::Promise WritableStreamInternalController::Pipe::State::pipeLoop(jsg:: // we sent those bytes on to the WritableStreamSink. KJ_IF_SOME(value, result.value) { auto handle = value.getHandle(js); - if (handle->IsArrayBuffer() || handle->IsArrayBufferView()) { - return state->write(handle).then(js, - [state = kj::addRef(*state)](jsg::Lock& js) mutable -> jsg::Promise { + if (isByteSource(handle)) { + return state->write(js, handle) + .then(js, + [state = kj::addRef(*state)](jsg::Lock& js) mutable -> jsg::Promise { if (state->aborted) { return js.resolvedPromise(); } // The signal will be checked again at the start of the next loop iteration. return state->pipeLoop(js); }, - [state = kj::addRef(*state)]( - jsg::Lock& js, jsg::Value reason) mutable -> jsg::Promise { + [state = kj::addRef(*state)]( + jsg::Lock& js, jsg::Value reason) mutable -> jsg::Promise { if (state->aborted) { return js.resolvedPromise(); } - state->parent.doError(js, reason.getHandle(js)); + state->parent.doError(js, jsg::JsValue(reason.getHandle(js))); return state->pipeLoop(js); }); } } // Undefined and null are perfectly valid values to pass through a ReadableStream, // but we can't interpret them as bytes so if we get them here, we error the pipe. - auto error = js.v8TypeError("This WritableStream only supports writing byte types."_kj); + auto error = js.typeError("This WritableStream only supports writing byte types."_kj); auto& writable = state->parent.state.getUnsafe>(); - auto ex = js.exceptionToKj(js.v8Ref(error)); + auto ex = js.exceptionToKj(error); writable->abort(kj::mv(ex)); // The error condition will be handled at the start of the next iteration. return state->pipeLoop(js); }), - ioContext.addFunctor([state = kj::addRef(*this)]( - jsg::Lock& js, jsg::Value reason) mutable -> jsg::Promise { + ioContext.addFunctor( + [state = kj::addRef(*this)](jsg::Lock& js, jsg::Value) mutable -> jsg::Promise { if (state->aborted) { return js.resolvedPromise(); } @@ -2125,7 +1981,7 @@ jsg::Promise WritableStreamInternalController::Pipe::State::pipeLoop(jsg:: })); } -void WritableStreamInternalController::drain(jsg::Lock& js, v8::Local reason) { +void WritableStreamInternalController::drain(jsg::Lock& js, jsg::JsValue reason) { doError(js, reason); while (!queue.empty()) { KJ_SWITCH_ONEOF(queue.front().event) { @@ -2192,16 +2048,14 @@ bool ReadableStreamInternalController::PipeLocked::isClosed() { return inner.state.is(); } -kj::Maybe> ReadableStreamInternalController::PipeLocked::tryGetErrored( - jsg::Lock& js) { +kj::Maybe ReadableStreamInternalController::PipeLocked::tryGetErrored(jsg::Lock& js) { KJ_IF_SOME(errored, inner.state.tryGetUnsafe()) { return errored.getHandle(js); } return kj::none; } -void ReadableStreamInternalController::PipeLocked::cancel( - jsg::Lock& js, v8::Local reason) { +void ReadableStreamInternalController::PipeLocked::cancel(jsg::Lock& js, jsg::JsValue reason) { if (inner.state.is()) { inner.doCancel(js, reason); } @@ -2211,13 +2065,12 @@ void ReadableStreamInternalController::PipeLocked::close(jsg::Lock& js) { inner.doClose(js); } -void ReadableStreamInternalController::PipeLocked::error( - jsg::Lock& js, v8::Local reason) { +void ReadableStreamInternalController::PipeLocked::error(jsg::Lock& js, jsg::JsValue reason) { inner.doError(js, reason); } void ReadableStreamInternalController::PipeLocked::release( - jsg::Lock& js, kj::Maybe> maybeError) { + jsg::Lock& js, kj::Maybe maybeError) { KJ_IF_SOME(error, maybeError) { cancel(js, error); } @@ -2225,7 +2078,7 @@ void ReadableStreamInternalController::PipeLocked::release( } kj::Maybe> ReadableStreamInternalController::PipeLocked::tryPumpTo( - WritableStreamSink& sink, bool end) { + WritableStreamSink& sink, End end) { // This is safe because the caller should have already checked isClosed and tryGetErrored // and handled those before calling tryPumpTo. auto& readable = KJ_ASSERT_NONNULL(inner.state.tryGetUnsafe()); @@ -2236,23 +2089,23 @@ jsg::Promise ReadableStreamInternalController::PipeLocked::read(jsg: return KJ_ASSERT_NONNULL(inner.read(js, kj::none)); } -jsg::Promise ReadableStreamInternalController::readAllBytes( +jsg::Promise> ReadableStreamInternalController::readAllBytes( jsg::Lock& js, uint64_t limit) { if (isLockedToReader()) { - return js.rejectedPromise(KJ_EXCEPTION( + return js.rejectedPromise>(KJ_EXCEPTION( FAILED, "jsg.TypeError: This ReadableStream is currently locked to a reader.")); } if (isPendingClosure) { - return js.rejectedPromise( - js.v8TypeError("This ReadableStream belongs to an object that is closing."_kj)); + return js.rejectedPromise>( + js.typeError("This ReadableStream belongs to an object that is closing."_kj)); } KJ_SWITCH_ONEOF(state) { KJ_CASE_ONEOF(closed, StreamStates::Closed) { - auto backing = jsg::BackingStore::alloc(js, 0); - return js.resolvedPromise(jsg::BufferSource(js, kj::mv(backing))); + auto ab = jsg::JsArrayBuffer::create(js, 0); + return js.resolvedPromise(ab.addRef(js)); } KJ_CASE_ONEOF(errored, StreamStates::Errored) { - return js.rejectedPromise(errored.addRef(js)); + return js.rejectedPromise>(errored.addRef(js)); } KJ_CASE_ONEOF(readable, Readable) { auto source = KJ_ASSERT_NONNULL(removeSource(js)); @@ -2261,10 +2114,9 @@ jsg::Promise ReadableStreamInternalController::readAllBytes( // the sandbox. This will require a change to the API of ReadableStreamSource::readAllBytes. // For now, we'll read and allocate into a proper backing store. return context.awaitIoLegacy(js, source->readAllBytes(limit).attach(kj::mv(source))) - .then(js, [](jsg::Lock& js, kj::Array bytes) -> jsg::BufferSource { - auto backing = jsg::BackingStore::alloc(js, bytes.size()); - backing.asArrayPtr().copyFrom(bytes); - return jsg::BufferSource(js, kj::mv(backing)); + .then(js, [](jsg::Lock& js, kj::Array bytes) -> jsg::JsRef { + auto ab = jsg::JsArrayBuffer::create(js, bytes); + return ab.addRef(js); }); } } @@ -2279,7 +2131,7 @@ jsg::Promise ReadableStreamInternalController::readAllText( } if (isPendingClosure) { return js.rejectedPromise( - js.v8TypeError("This ReadableStream belongs to an object that is closing."_kj)); + js.typeError("This ReadableStream belongs to an object that is closing."_kj)); } KJ_SWITCH_ONEOF(state) { KJ_CASE_ONEOF(closed, StreamStates::Closed) { @@ -2319,13 +2171,13 @@ kj::Maybe ReadableStreamInternalController::tryGetLength(StreamEncodin } kj::Own ReadableStreamInternalController::detach( - jsg::Lock& js, bool ignoreDetached) { + jsg::Lock& js, IgnoreDisturbed ignoreDetached) { return newReadableStreamInternalController( IoContext::current(), KJ_ASSERT_NONNULL(removeSource(js, ignoreDetached))); } kj::Promise> ReadableStreamInternalController::pumpTo( - jsg::Lock& js, kj::Own sink, bool end) { + jsg::Lock& js, kj::Own sink, End end) { auto source = KJ_ASSERT_NONNULL(removeSource(js)); struct Holder: public kj::Refcounted { diff --git a/src/workerd/api/streams/internal.h b/src/workerd/api/streams/internal.h index 5580db65292..edaa1d6569f 100644 --- a/src/workerd/api/streams/internal.h +++ b/src/workerd/api/streams/internal.h @@ -28,7 +28,7 @@ namespace workerd::api { // The ReadableStreamInternalController is always in one of three states: Readable, Closed, // or Errored. When the state is Readable, the controller has an associated ReadableStreamSource. // When the state is Errored, the ReadableStreamSource has been released and the controller -// stores a jsg::Value with whatever value was used to error. When Closed, the +// stores a JS value with whatever value was used to error. When Closed, the // ReadableStreamSource has been released. // Likewise, the WritableStreamInternalController is always either Writable, Closed, or Errored. @@ -52,6 +52,22 @@ class ReadableStreamInternalController: public ReadableStreamController { ~ReadableStreamInternalController() noexcept(false) override; + bool isInternal() const override { + return true; + } + kj::Maybe> tryReleaseSource() override { + if (isLockedToReader()) return kj::none; + if (disturbed) return kj::none; + KJ_IF_SOME(readable, state.tryGetUnsafe()) { + readState.transitionTo(); + disturbed = true; + auto result = kj::mv(readable); + state.transitionTo(); + return kj::mv(result); + } + return kj::none; + } + void setOwnerRef(ReadableStream& stream) override { owner = stream; } @@ -71,12 +87,12 @@ class ReadableStreamInternalController: public ReadableStreamController { jsg::Promise pipeTo( jsg::Lock& js, WritableStreamController& destination, PipeToOptions options) override; - jsg::Promise cancel(jsg::Lock& js, jsg::Optional> reason) override; + jsg::Promise cancel(jsg::Lock& js, jsg::Optional reason) override; Tee tee(jsg::Lock& js) override; kj::Maybe> removeSource( - jsg::Lock& js, bool ignoreDisturbed = false); + jsg::Lock& js, IgnoreDisturbed ignoreDisturbed = IgnoreDisturbed::NO); bool isClosedOrErrored() const override { return state.is() || state.is(); @@ -103,17 +119,17 @@ class ReadableStreamInternalController: public ReadableStreamController { void visitForGc(jsg::GcVisitor& visitor) override; - jsg::Promise readAllBytes(jsg::Lock& js, uint64_t limit) override; + jsg::Promise> readAllBytes(jsg::Lock& js, uint64_t limit) override; jsg::Promise readAllText(jsg::Lock& js, uint64_t limit) override; kj::Maybe tryGetLength(StreamEncoding encoding) override; kj::Promise> pumpTo( - jsg::Lock& js, kj::Own sink, bool end) override; + jsg::Lock& js, kj::Own sink, End end) override; StreamEncoding getPreferredEncoding() override; - kj::Own detach(jsg::Lock& js, bool ignoreDisturbed) override; + kj::Own detach(jsg::Lock& js, IgnoreDisturbed ignoreDisturbed) override; void setPendingClosure() override { isPendingClosure = true; @@ -124,9 +140,9 @@ class ReadableStreamInternalController: public ReadableStreamController { void jsgGetMemoryInfo(jsg::MemoryTracker& info) const override; private: - void doCancel(jsg::Lock& js, jsg::Optional> reason); + void doCancel(jsg::Lock& js, jsg::Optional reason); void doClose(jsg::Lock& js); - void doError(jsg::Lock& js, v8::Local reason); + void doError(jsg::Lock& js, jsg::JsValue reason); class PipeLocked: public PipeController { public: @@ -135,17 +151,17 @@ class ReadableStreamInternalController: public ReadableStreamController { bool isClosed() override; - kj::Maybe> tryGetErrored(jsg::Lock& js) override; + kj::Maybe tryGetErrored(jsg::Lock& js) override; - void cancel(jsg::Lock& js, v8::Local reason) override; + void cancel(jsg::Lock& js, jsg::JsValue reason) override; void close(jsg::Lock& js) override; - void error(jsg::Lock& js, v8::Local reason) override; + void error(jsg::Lock& js, jsg::JsValue reason) override; - void release(jsg::Lock& js, kj::Maybe> maybeError = kj::none) override; + void release(jsg::Lock& js, kj::Maybe maybeError = kj::none) override; - kj::Maybe> tryPumpTo(WritableStreamSink& sink, bool end) override; + kj::Maybe> tryPumpTo(WritableStreamSink& sink, End end) override; jsg::Promise read(jsg::Lock& js) override; @@ -222,13 +238,13 @@ class WritableStreamInternalController: public WritableStreamController { jsg::Ref addRef() override; - jsg::Promise write(jsg::Lock& js, jsg::Optional> value) override; + jsg::Promise write(jsg::Lock& js, jsg::Optional value) override; - jsg::Promise close(jsg::Lock& js, bool markAsHandled = false) override; + jsg::Promise close(jsg::Lock& js, MarkAsHandled markAsHandled = MarkAsHandled::NO) override; - jsg::Promise flush(jsg::Lock& js, bool markAsHandled = false) override; + jsg::Promise flush(jsg::Lock& js, MarkAsHandled markAsHandled = MarkAsHandled::NO) override; - jsg::Promise abort(jsg::Lock& js, jsg::Optional> reason) override; + jsg::Promise abort(jsg::Lock& js, jsg::Optional reason) override; kj::Maybe> tryPipeFrom( jsg::Lock& js, jsg::Ref source, PipeToOptions options) override; @@ -247,7 +263,7 @@ class WritableStreamInternalController: public WritableStreamController { void releaseWriter(Writer& writer, kj::Maybe maybeJs) override; // See the comment for releaseWriter in common.h for details on the use of maybeJs - kj::Maybe> isErroring(jsg::Lock& js) override { + kj::Maybe isErroring(jsg::Lock& js) override { // TODO(later): The internal controller has no concept of an "erroring" // state, so for now we just return kj::none here. return kj::none; @@ -280,18 +296,18 @@ class WritableStreamInternalController: public WritableStreamController { }; jsg::Promise doAbort(jsg::Lock& js, - v8::Local reason, + jsg::JsValue reason, AbortOptions options = {.reject = false, .handled = false}); void doClose(jsg::Lock& js); - void doError(jsg::Lock& js, v8::Local reason); + void doError(jsg::Lock& js, jsg::JsValue reason); void ensureWriting(jsg::Lock& js); jsg::Promise writeLoop(jsg::Lock& js, IoContext& ioContext); jsg::Promise writeLoopAfterFrontOutputLock(jsg::Lock& js); - void drain(jsg::Lock& js, v8::Local reason); + void drain(jsg::Lock& js, jsg::JsValue reason); void finishClose(jsg::Lock& js); - void finishError(jsg::Lock& js, v8::Local reason); - jsg::Promise closeImpl(jsg::Lock& js, bool markAsHandled); + void finishError(jsg::Lock& js, jsg::JsValue reason); + jsg::Promise closeImpl(jsg::Lock& js, MarkAsHandled markAsHandled); struct PipeLocked { static constexpr kj::StringPtr NAME KJ_UNUSED = "pipe-locked"_kj; @@ -344,7 +360,7 @@ class WritableStreamInternalController: public WritableStreamController { bool isPendingClosure = false; void adjustWriteBufferSize(jsg::Lock& js, int64_t amount); - void updateBackpressure(jsg::Lock& js, bool backpressure); + void updateBackpressure(jsg::Lock& js, UpdateBackpressure backpressure); struct Write { kj::Maybe::Resolver> promise; @@ -405,7 +421,7 @@ class WritableStreamInternalController: public WritableStreamController { bool checkSignal(jsg::Lock& js); jsg::Promise pipeLoop(jsg::Lock& js); - jsg::Promise write(v8::Local value); + jsg::Promise write(jsg::Lock& js, jsg::JsValue value); JSG_MEMORY_INFO(State) { tracker.trackField("resolver", promise); @@ -462,8 +478,8 @@ class WritableStreamInternalController: public WritableStreamController { jsg::Promise pipeLoop(jsg::Lock& js) { return state->pipeLoop(js); } - jsg::Promise write(v8::Local value) { - return state->write(value); + jsg::Promise write(jsg::Lock& js, jsg::JsValue value) { + return state->write(js, value); } JSG_MEMORY_INFO(Pipe) { diff --git a/src/workerd/api/streams/queue-test.c++ b/src/workerd/api/streams/queue-test.c++ index 0babee6f993..95b921badd3 100644 --- a/src/workerd/api/streams/queue-test.c++ +++ b/src/workerd/api/streams/queue-test.c++ @@ -81,17 +81,18 @@ auto read(jsg::Lock& js, auto& consumer) { auto byobRead(jsg::Lock& js, auto& consumer, int size) { auto prp = js.newPromiseAndResolver(); + auto view = jsg::JsUint8Array::create(js, size); consumer.read(js, ByteQueue::ReadRequest(kj::mv(prp.resolver), { - .store = jsg::BufferSource(js, jsg::BackingStore::alloc(js, size)), + .store = jsg::JsArrayBufferView(view).addRef(js), .type = ByteQueue::ReadRequest::Type::BYOB, })); return kj::mv(prp.promise); }; auto getEntry(jsg::Lock& js, auto size) { - return kj::rc(js.v8Ref(v8::True(js.v8Isolate).As()), size); + return kj::rc(js, js.boolean(true), size); } #pragma region ValueQueue Tests @@ -129,7 +130,7 @@ KJ_TEST("ValueQueue erroring works") { preamble([](jsg::Lock& js) { ValueQueue queue(2); - queue.error(js, js.v8Ref(js.v8Error("boom"_kj))); + queue.error(js, js.error("boom"_kj)); KJ_ASSERT(queue.desiredSize() == 0); @@ -162,10 +163,10 @@ KJ_TEST("ValueQueue with single consumer") { auto prp = js.newPromiseAndResolver(); consumer.read(js, ValueQueue::ReadRequest{.resolver = kj::mv(prp.resolver)}); - MustCall readContinuation([&](jsg::Lock& js, auto&& result) -> auto { + MustCall readContinuation([&](jsg::Lock& js, auto result) -> auto { KJ_ASSERT(!result.done); auto& value = KJ_ASSERT_NONNULL(result.value); - KJ_ASSERT(value.getHandle(js)->IsTrue()); + KJ_ASSERT(value.getHandle(js).isTrue()); KJ_ASSERT(consumer.size() == 0); KJ_ASSERT(queue.size() == 0); @@ -199,10 +200,10 @@ KJ_TEST("ValueQueue with multiple consumers") { KJ_ASSERT(queue.size() == 2); KJ_ASSERT(queue.desiredSize() == 0); - MustCall read1Continuation([&](jsg::Lock& js, auto&& result) -> auto { + MustCall read1Continuation([&](jsg::Lock& js, auto result) -> auto { KJ_ASSERT(!result.done); auto& value = KJ_ASSERT_NONNULL(result.value); - KJ_ASSERT(value.getHandle(js)->IsTrue()); + KJ_ASSERT(value.getHandle(js).isTrue()); KJ_ASSERT(consumer1.size() == 0); KJ_ASSERT(consumer2.size() == 2); @@ -214,10 +215,10 @@ KJ_TEST("ValueQueue with multiple consumers") { return read(js, consumer2); }); - MustCall read2Continuation([&](jsg::Lock& js, auto&& result) -> auto { + MustCall read2Continuation([&](jsg::Lock& js, auto result) -> auto { KJ_ASSERT(!result.done); auto& value = KJ_ASSERT_NONNULL(result.value); - KJ_ASSERT(value.getHandle(js)->IsTrue()); + KJ_ASSERT(value.getHandle(js).isTrue()); KJ_ASSERT(consumer2.size() == 0); @@ -262,10 +263,10 @@ KJ_TEST("ValueQueue consumer with multiple-reads") { ValueQueue::Consumer consumer(queue); // The first read will produce a value. - MustCall read1Continuation([&](jsg::Lock& js, auto&& result) -> auto { + MustCall read1Continuation([&](jsg::Lock& js, auto result) -> auto { KJ_ASSERT(!result.done); auto& value = KJ_ASSERT_NONNULL(result.value); - KJ_ASSERT(value.getHandle(js)->IsTrue()); + KJ_ASSERT(value.getHandle(js).isTrue()); return js.resolvedPromise(kj::mv(result)); }); read(js, consumer).then(js, read1Continuation); @@ -307,7 +308,7 @@ KJ_TEST("ValueQueue errors consumer with multiple-reads") { read(js, consumer).then(js, readContinuation, errorContinuation); read(js, consumer).then(js, readContinuation, errorContinuation); - queue.error(js, js.v8Ref(js.v8Error("boom"_kj))); + queue.error(js, js.error("boom"_kj)); js.runMicrotasks(); }); @@ -325,7 +326,7 @@ KJ_TEST("ValueQueue with multiple consumers with pending reads") { MustCall readContinuation([&](jsg::Lock& js, auto&& result) -> auto { KJ_ASSERT(!result.done); auto& value = KJ_ASSERT_NONNULL(result.value); - KJ_ASSERT(value.getHandle(js)->IsTrue()); + KJ_ASSERT(value.getHandle(js).isTrue()); // Both reads were fulfilled immediately without buffering. KJ_ASSERT(consumer1.size() == 0); @@ -360,7 +361,8 @@ KJ_TEST("ByteQueue basics work") { KJ_ASSERT(queue.desiredSize() == 2); KJ_ASSERT(queue.size() == 0); - auto entry = kj::rc(jsg::BufferSource(js, jsg::BackingStore::alloc(js, 4))); + auto ab = jsg::JsUint8Array::create(js, 4); + auto entry = kj::rc(js, jsg::JsBufferSource(ab)); queue.push(js, kj::mv(entry)); @@ -372,7 +374,8 @@ KJ_TEST("ByteQueue basics work") { queue.close(js); try { - auto entry = kj::rc(jsg::BufferSource(js, jsg::BackingStore::alloc(js, 4))); + auto ab = jsg::JsUint8Array::create(js, 4); + auto entry = kj::rc(js, jsg::JsBufferSource(ab)); queue.push(js, kj::mv(entry)); KJ_FAIL_ASSERT("The queue push after close should have failed."); } catch (kj::Exception& ex) { @@ -388,12 +391,13 @@ KJ_TEST("ByteQueue erroring works") { preamble([](jsg::Lock& js) { ByteQueue queue(2); - queue.error(js, js.v8Ref(js.v8Error("boom"_kj))); + queue.error(js, js.error("boom"_kj)); KJ_ASSERT(queue.desiredSize() == 0); try { - auto entry = kj::rc(jsg::BufferSource(js, jsg::BackingStore::alloc(js, 4))); + auto ab = jsg::JsUint8Array::create(js, 4); + auto entry = kj::rc(js, jsg::JsBufferSource(ab)); queue.push(js, kj::mv(entry)); KJ_FAIL_ASSERT("The queue push after close should have failed."); } catch (kj::Exception& ex) { @@ -410,10 +414,10 @@ KJ_TEST("ByteQueue with single consumer") { KJ_ASSERT(queue.desiredSize() == 2); - auto store = jsg::BackingStore::alloc(js, 4); - store.asArrayPtr().fill('a'); + auto u8 = jsg::JsUint8Array::create(js, 4); + u8.asArrayPtr().fill('a'); - auto entry = kj::rc(jsg::BufferSource(js, kj::mv(store))); + auto entry = kj::rc(js, jsg::JsBufferSource(u8)); queue.push(js, kj::mv(entry)); // The item was pushed into the consumer. @@ -424,17 +428,18 @@ KJ_TEST("ByteQueue with single consumer") { KJ_ASSERT(queue.desiredSize() == -2); auto prp = js.newPromiseAndResolver(); + auto u8_2 = jsg::JsUint8Array::create(js, 4); consumer.read(js, ByteQueue::ReadRequest(kj::mv(prp.resolver), { - .store = jsg::BufferSource(js, jsg::BackingStore::alloc(js, 4)), + .store = jsg::JsArrayBufferView(u8_2).addRef(js), })); - MustCall readContinuation([&](jsg::Lock& js, auto&& result) -> auto { + MustCall readContinuation([&](jsg::Lock& js, auto result) -> auto { KJ_ASSERT(!result.done); auto& value = KJ_ASSERT_NONNULL(result.value); - KJ_ASSERT(value.getHandle(js)->IsArrayBufferView()); - jsg::BufferSource source(js, value.getHandle(js)); + KJ_ASSERT(value.getHandle(js).isArrayBufferView()); + jsg::JsBufferSource source(value.getHandle(js)); KJ_ASSERT(source.size() == 4); KJ_ASSERT(source.asArrayPtr()[0] == 'a'); KJ_ASSERT(source.asArrayPtr()[1] == 'a'); @@ -461,18 +466,19 @@ KJ_TEST("ByteQueue with single byob consumer") { ByteQueue::Consumer consumer(queue); auto prp = js.newPromiseAndResolver(); + auto u8 = jsg::JsUint8Array::create(js, 4); consumer.read(js, ByteQueue::ReadRequest(kj::mv(prp.resolver), { - .store = jsg::BufferSource(js, jsg::BackingStore::alloc(js, 4)), + .store = jsg::JsArrayBufferView(u8).addRef(js), .type = ByteQueue::ReadRequest::Type::BYOB, })); - MustCall readContinuation([&](jsg::Lock& js, auto&& result) -> auto { + MustCall readContinuation([&](jsg::Lock& js, auto result) -> auto { KJ_ASSERT(!result.done); auto& value = KJ_ASSERT_NONNULL(result.value); - KJ_ASSERT(value.getHandle(js)->IsArrayBufferView()); - jsg::BufferSource source(js, value.getHandle(js)); + KJ_ASSERT(value.getHandle(js).isArrayBufferView()); + jsg::JsBufferSource source(value.getHandle(js)); auto ptr = source.asArrayPtr(); KJ_ASSERT(source.size() == 3); KJ_ASSERT(ptr[0] == 'b'); @@ -493,7 +499,7 @@ KJ_TEST("ByteQueue with single byob consumer") { KJ_ASSERT(!pendingByob->isInvalidated()); auto& req = pendingByob->getRequest(); - auto ptr = req.pullInto.store.asArrayPtr(); + auto ptr = req.pullInto.store.getHandle(js).asArrayPtr(); ptr.first(3).fill('b'); pendingByob->respond(js, 3); KJ_ASSERT(pendingByob->isInvalidated()); @@ -515,18 +521,19 @@ KJ_TEST("ByteQueue with byob consumer and default consumer") { ByteQueue::Consumer consumer2(queue); auto prp = js.newPromiseAndResolver(); + auto u8 = jsg::JsUint8Array::create(js, 4); consumer1.read(js, ByteQueue::ReadRequest(kj::mv(prp.resolver), { - .store = jsg::BufferSource(js, jsg::BackingStore::alloc(js, 4)), + .store = jsg::JsArrayBufferView(u8).addRef(js), .type = ByteQueue::ReadRequest::Type::BYOB, })); - MustCall readContinuation([&](jsg::Lock& js, auto&& result) -> auto { + MustCall readContinuation([&](jsg::Lock& js, auto result) -> auto { KJ_ASSERT(!result.done); auto& value = KJ_ASSERT_NONNULL(result.value); - KJ_ASSERT(value.getHandle(js)->IsArrayBufferView()); - jsg::BufferSource source(js, value.getHandle(js)); + KJ_ASSERT(value.getHandle(js).isArrayBufferView()); + jsg::JsBufferSource source(value.getHandle(js)); auto ptr = source.asArrayPtr(); KJ_ASSERT(source.size() == 3); KJ_ASSERT(ptr[0] == 'b'); @@ -548,7 +555,7 @@ KJ_TEST("ByteQueue with byob consumer and default consumer") { KJ_ASSERT(!pendingByob->isInvalidated()); auto& req = pendingByob->getRequest(); - auto ptr = req.pullInto.store.asArrayPtr(); + auto ptr = req.pullInto.store.getHandle(js).asArrayPtr(); ptr.first(3).fill('b'); pendingByob->respond(js, 3); KJ_ASSERT(pendingByob->isInvalidated()); @@ -561,11 +568,11 @@ KJ_TEST("ByteQueue with byob consumer and default consumer") { js.runMicrotasks(); - MustCall read2Continuation([&](jsg::Lock& js, auto&& result) -> auto { + MustCall read2Continuation([&](jsg::Lock& js, auto result) -> auto { KJ_ASSERT(!result.done); auto& value = KJ_ASSERT_NONNULL(result.value); - KJ_ASSERT(value.getHandle(js)->IsArrayBufferView()); - jsg::BufferSource source(js, value.getHandle(js)); + KJ_ASSERT(value.getHandle(js).isArrayBufferView()); + jsg::JsBufferSource source(value.getHandle(js)); auto ptr = source.asArrayPtr(); // The second consumer receives exactly the same data. KJ_ASSERT(source.size() == 3); @@ -581,10 +588,11 @@ KJ_TEST("ByteQueue with byob consumer and default consumer") { }); auto prp2 = js.newPromiseAndResolver(); + auto u8_2 = jsg::JsUint8Array::create(js, 4); consumer2.read(js, ByteQueue::ReadRequest(kj::mv(prp2.resolver), { - .store = jsg::BufferSource(js, jsg::BackingStore::alloc(js, 4)), + .store = jsg::JsArrayBufferView(u8_2).addRef(js), .type = ByteQueue::ReadRequest::Type::DEFAULT, })); prp2.promise.then(js, read2Continuation); @@ -600,11 +608,11 @@ KJ_TEST("ByteQueue with multiple byob consumers") { ByteQueue::Consumer consumer1(queue); ByteQueue::Consumer consumer2(queue); - MustCall readContinuation([&](jsg::Lock& js, auto&& result) -> auto { + MustCall readContinuation([&](jsg::Lock& js, auto result) -> auto { KJ_ASSERT(!result.done); auto& value = KJ_ASSERT_NONNULL(result.value); - KJ_ASSERT(value.getHandle(js)->IsArrayBufferView()); - jsg::BufferSource source(js, value.getHandle(js)); + KJ_ASSERT(value.getHandle(js).isArrayBufferView()); + jsg::JsBufferSource source(value.getHandle(js)); auto ptr = source.asArrayPtr(); KJ_ASSERT(source.size() == 3); KJ_ASSERT(ptr[0] == 'b'); @@ -630,7 +638,7 @@ KJ_TEST("ByteQueue with multiple byob consumers") { KJ_ASSERT(!pendingByob->isInvalidated()); auto& req = pendingByob->getRequest(); - auto ptr = req.pullInto.store.asArrayPtr(); + auto ptr = req.pullInto.store.getHandle(js).asArrayPtr(); ptr.first(3).fill('b'); pendingByob->respond(js, 3); KJ_ASSERT(pendingByob->isInvalidated()); @@ -656,11 +664,11 @@ KJ_TEST("ByteQueue with multiple byob consumers") { ByteQueue::Consumer consumer1(queue); ByteQueue::Consumer consumer2(queue); - MustCall readContinuation([&](jsg::Lock& js, auto&& result) -> auto { + MustCall readContinuation([&](jsg::Lock& js, auto result) -> auto { KJ_ASSERT(!result.done); auto& value = KJ_ASSERT_NONNULL(result.value); - KJ_ASSERT(value.getHandle(js)->IsArrayBufferView()); - jsg::BufferSource source(js, value.getHandle(js)); + KJ_ASSERT(value.getHandle(js).isArrayBufferView()); + jsg::JsBufferSource source(value.getHandle(js)); auto ptr = source.asArrayPtr(); KJ_ASSERT(source.size() == 3); KJ_ASSERT(ptr[0] == 'b'); @@ -686,7 +694,7 @@ KJ_TEST("ByteQueue with multiple byob consumers") { KJ_ASSERT(!pendingByob->isInvalidated()); auto& req = pendingByob->getRequest(); - auto ptr = req.pullInto.store.asArrayPtr(); + auto ptr = req.pullInto.store.getHandle(js).asArrayPtr(); ptr.first(3).fill('b'); pendingByob->respond(js, 3); KJ_ASSERT(pendingByob->isInvalidated()); @@ -712,11 +720,11 @@ KJ_TEST("ByteQueue with multiple byob consumers (multi-reads)") { ByteQueue::Consumer consumer1(queue); ByteQueue::Consumer consumer2(queue); - MustCall readConsumer1([&](jsg::Lock& js, auto&& result) -> auto { + MustCall readConsumer1([&](jsg::Lock& js, auto result) -> auto { KJ_ASSERT(!result.done); auto& value = KJ_ASSERT_NONNULL(result.value); - KJ_ASSERT(value.getHandle(js)->IsArrayBufferView()); - jsg::BufferSource source(js, value.getHandle(js)); + KJ_ASSERT(value.getHandle(js).isArrayBufferView()); + jsg::JsBufferSource source(value.getHandle(js)); auto ptr = source.asArrayPtr(); KJ_ASSERT(source.size() == 3); KJ_ASSERT(ptr[0] == 'a'); @@ -726,11 +734,11 @@ KJ_TEST("ByteQueue with multiple byob consumers (multi-reads)") { return js.resolvedPromise(kj::mv(result)); }); - MustCall readConsumer2([&](jsg::Lock& js, auto&& result) -> auto { + MustCall readConsumer2([&](jsg::Lock& js, auto result) -> auto { KJ_ASSERT(!result.done); auto& value = KJ_ASSERT_NONNULL(result.value); - KJ_ASSERT(value.getHandle(js)->IsArrayBufferView()); - jsg::BufferSource source(js, value.getHandle(js)); + KJ_ASSERT(value.getHandle(js).isArrayBufferView()); + jsg::JsBufferSource source(value.getHandle(js)); auto ptr = source.asArrayPtr(); KJ_ASSERT(source.size() == 3); KJ_ASSERT(ptr[0] == 'a'); @@ -740,11 +748,11 @@ KJ_TEST("ByteQueue with multiple byob consumers (multi-reads)") { return byobRead(js, consumer2, 4); }); - MustCall secondReadBothConsumers([&](jsg::Lock& js, auto&& result) -> auto { + MustCall secondReadBothConsumers([&](jsg::Lock& js, auto result) -> auto { KJ_ASSERT(!result.done); auto& value = KJ_ASSERT_NONNULL(result.value); - KJ_ASSERT(value.getHandle(js)->IsArrayBufferView()); - jsg::BufferSource source(js, value.getHandle(js)); + KJ_ASSERT(value.getHandle(js).isArrayBufferView()); + jsg::JsBufferSource source(value.getHandle(js)); auto ptr = source.asArrayPtr(); KJ_ASSERT(source.size() == 2); KJ_ASSERT(ptr[0] == 'b'); @@ -766,7 +774,7 @@ KJ_TEST("ByteQueue with multiple byob consumers (multi-reads)") { MustCall respond([&](jsg::Lock&, auto& pending) { static uint counter = 0; auto& req = pending.getRequest(); - auto ptr = req.pullInto.store.asArrayPtr(); + auto ptr = req.pullInto.store.getHandle(js).asArrayPtr(); auto num = 3 - counter; ptr.first(num).fill('a' + counter++); pending.respond(js, num); @@ -793,11 +801,11 @@ KJ_TEST("ByteQueue with multiple byob consumers (multi-reads, 2)") { ByteQueue::Consumer consumer1(queue); ByteQueue::Consumer consumer2(queue); - MustCall readConsumer1([&](jsg::Lock& js, auto&& result) -> auto { + MustCall readConsumer1([&](jsg::Lock& js, auto result) -> auto { KJ_ASSERT(!result.done); auto& value = KJ_ASSERT_NONNULL(result.value); - KJ_ASSERT(value.getHandle(js)->IsArrayBufferView()); - jsg::BufferSource source(js, value.getHandle(js)); + KJ_ASSERT(value.getHandle(js).isArrayBufferView()); + jsg::JsBufferSource source(value.getHandle(js)); auto ptr = source.asArrayPtr(); KJ_ASSERT(source.size() == 3); KJ_ASSERT(ptr[0] == 'a'); @@ -806,11 +814,11 @@ KJ_TEST("ByteQueue with multiple byob consumers (multi-reads, 2)") { return js.resolvedPromise(kj::mv(result)); }); - MustCall readConsumer2([&](jsg::Lock& js, auto&& result) -> auto { + MustCall readConsumer2([&](jsg::Lock& js, auto result) -> auto { KJ_ASSERT(!result.done); auto& value = KJ_ASSERT_NONNULL(result.value); - KJ_ASSERT(value.getHandle(js)->IsArrayBufferView()); - jsg::BufferSource source(js, value.getHandle(js)); + KJ_ASSERT(value.getHandle(js).isArrayBufferView()); + jsg::JsBufferSource source(value.getHandle(js)); auto ptr = source.asArrayPtr(); KJ_ASSERT(source.size() == 3); KJ_ASSERT(ptr[0] == 'a'); @@ -820,11 +828,11 @@ KJ_TEST("ByteQueue with multiple byob consumers (multi-reads, 2)") { return byobRead(js, consumer2, 4); }); - MustCall secondReadBothConsumers([&](jsg::Lock& js, auto&& result) -> auto { + MustCall secondReadBothConsumers([&](jsg::Lock& js, auto result) -> auto { KJ_ASSERT(!result.done); auto& value = KJ_ASSERT_NONNULL(result.value); - KJ_ASSERT(value.getHandle(js)->IsArrayBufferView()); - jsg::BufferSource source(js, value.getHandle(js)); + KJ_ASSERT(value.getHandle(js).isArrayBufferView()); + jsg::JsBufferSource source(value.getHandle(js)); auto ptr = source.asArrayPtr(); KJ_ASSERT(source.size() == 2); KJ_ASSERT(ptr[0] == 'b'); @@ -846,7 +854,7 @@ KJ_TEST("ByteQueue with multiple byob consumers (multi-reads, 2)") { MustCall respond([&](jsg::Lock&, auto& pending) { static uint counter = 0; auto& req = pending.getRequest(); - auto ptr = req.pullInto.store.asArrayPtr(); + auto ptr = req.pullInto.store.getHandle(js).asArrayPtr(); auto num = 3 - counter; ptr.first(num).fill('a' + counter++); pending.respond(js, num); @@ -874,10 +882,11 @@ KJ_TEST("ByteQueue with default consumer with atLeast") { const auto read = [&](jsg::Lock& js, uint atLeast) { auto prp = js.newPromiseAndResolver(); + auto u8 = jsg::JsUint8Array::create(js, 5); consumer.read(js, ByteQueue::ReadRequest(kj::mv(prp.resolver), { - .store = jsg::BufferSource(js, jsg::BackingStore::alloc(js, 5)), + .store = jsg::JsArrayBufferView(u8).addRef(js), .atLeast = atLeast, })); return kj::mv(prp.promise); @@ -885,18 +894,18 @@ KJ_TEST("ByteQueue with default consumer with atLeast") { const auto push = [&](auto store) { try { - queue.push(js, kj::rc(jsg::BufferSource(js, kj::mv(store)))); + queue.push(js, kj::rc(js, jsg::JsBufferSource(store))); } catch (kj::Exception& ex) { KJ_DBG(ex.getDescription()); } }; - MustCall readContinuation([&](jsg::Lock& js, auto&& result) { + MustCall readContinuation([&](jsg::Lock& js, auto result) { KJ_ASSERT(!result.done); auto& value = KJ_ASSERT_NONNULL(result.value); auto view = value.getHandle(js); - KJ_ASSERT(view->IsArrayBufferView()); - jsg::BufferSource source(js, view); + KJ_ASSERT(view.isArrayBufferView()); + jsg::JsBufferSource source(view); auto ptr = source.asArrayPtr(); KJ_ASSERT(ptr[0] == 1); KJ_ASSERT(ptr[1] == 2); @@ -908,12 +917,12 @@ KJ_TEST("ByteQueue with default consumer with atLeast") { return read(js, 1); }); - MustCall read2Continuation([&](jsg::Lock& js, auto&& result) { + MustCall read2Continuation([&](jsg::Lock& js, auto result) { KJ_ASSERT(!result.done); auto& value = KJ_ASSERT_NONNULL(result.value); auto view = value.getHandle(js); - KJ_ASSERT(view->IsArrayBufferView()); - jsg::BufferSource source(js, view); + KJ_ASSERT(view.isArrayBufferView()); + jsg::JsBufferSource source(view); KJ_ASSERT(source.asArrayPtr()[0], 6); KJ_ASSERT(source.size() == 1); return js.resolvedPromise(kj::mv(result)); @@ -921,25 +930,25 @@ KJ_TEST("ByteQueue with default consumer with atLeast") { read(js, 5).then(js, readContinuation).then(js, read2Continuation); - auto store1 = jsg::BackingStore::alloc(js, 2); + auto store1 = jsg::JsUint8Array::create(js, 2); store1.asArrayPtr()[0] = 1; store1.asArrayPtr()[1] = 2; - push(kj::mv(store1)); + push(store1); KJ_ASSERT(queue.desiredSize() == 0); - auto store2 = jsg::BackingStore::alloc(js, 2); + auto store2 = jsg::JsUint8Array::create(js, 2); store2.asArrayPtr()[0] = 3; store2.asArrayPtr()[1] = 4; - push(kj::mv(store2)); + push(store2); // Backpressure should be accumulating because the read has not yet fullilled. KJ_ASSERT(queue.desiredSize() == -2); - auto store3 = jsg::BackingStore::alloc(js, 2); + auto store3 = jsg::JsUint8Array::create(js, 2); store3.asArrayPtr()[0] = 5; store3.asArrayPtr()[1] = 6; - push(kj::mv(store3)); + push(store3); // Some backpressure should be released because pushing the final minimum // amount into the queue should have caused the read to be fulfilled. @@ -962,10 +971,11 @@ KJ_TEST("ByteQueue with multiple default consumers with atLeast (same rate)") { const auto read = [&](jsg::Lock& js, auto& consumer, uint atLeast = 1) { auto prp = js.newPromiseAndResolver(); + auto u8 = jsg::JsUint8Array::create(js, 5); consumer.read(js, ByteQueue::ReadRequest(kj::mv(prp.resolver), { - .store = jsg::BufferSource(js, jsg::BackingStore::alloc(js, 5)), + .store = jsg::JsArrayBufferView(u8).addRef(js), .atLeast = atLeast, })); return kj::mv(prp.promise); @@ -973,18 +983,18 @@ KJ_TEST("ByteQueue with multiple default consumers with atLeast (same rate)") { const auto push = [&](auto store) { try { - queue.push(js, kj::rc(jsg::BufferSource(js, kj::mv(store)))); + queue.push(js, kj::rc(js, jsg::JsBufferSource(store))); } catch (kj::Exception& ex) { KJ_DBG(ex.getDescription()); } }; - MustCall read1Continuation([&](jsg::Lock& js, auto&& result) { + MustCall read1Continuation([&](jsg::Lock& js, auto result) { KJ_ASSERT(!result.done); auto& value = KJ_ASSERT_NONNULL(result.value); auto view = value.getHandle(js); - KJ_ASSERT(view->IsArrayBufferView()); - jsg::BufferSource source(js, view); + KJ_ASSERT(view.isArrayBufferView()); + jsg::JsBufferSource source(view); auto ptr = source.asArrayPtr(); KJ_ASSERT(ptr[0] == 1); KJ_ASSERT(ptr[1] == 2); @@ -996,12 +1006,12 @@ KJ_TEST("ByteQueue with multiple default consumers with atLeast (same rate)") { return read(js, consumer1); }); - MustCall read2Continuation([&](jsg::Lock& js, auto&& result) { + MustCall read2Continuation([&](jsg::Lock& js, auto result) { KJ_ASSERT(!result.done); auto& value = KJ_ASSERT_NONNULL(result.value); auto view = value.getHandle(js); - KJ_ASSERT(view->IsArrayBufferView()); - jsg::BufferSource source(js, view); + KJ_ASSERT(view.isArrayBufferView()); + jsg::JsBufferSource source(view); auto ptr = source.asArrayPtr(); KJ_ASSERT(ptr[0] == 1); KJ_ASSERT(ptr[1] == 2); @@ -1013,12 +1023,12 @@ KJ_TEST("ByteQueue with multiple default consumers with atLeast (same rate)") { return read(js, consumer2); }); - MustCall readFinalContinuation([&](jsg::Lock& js, auto&& result) { + MustCall readFinalContinuation([&](jsg::Lock& js, auto result) { KJ_ASSERT(!result.done); auto& value = KJ_ASSERT_NONNULL(result.value); auto view = value.getHandle(js); - KJ_ASSERT(view->IsArrayBufferView()); - jsg::BufferSource source(js, view); + KJ_ASSERT(view.isArrayBufferView()); + jsg::JsBufferSource source(view); KJ_ASSERT(source.asArrayPtr()[0], 6); KJ_ASSERT(source.size() == 1); return js.resolvedPromise(kj::mv(result)); @@ -1027,25 +1037,25 @@ KJ_TEST("ByteQueue with multiple default consumers with atLeast (same rate)") { read(js, consumer1, 5).then(js, read1Continuation).then(js, readFinalContinuation); read(js, consumer2, 5).then(js, read2Continuation).then(js, readFinalContinuation); - auto store1 = jsg::BackingStore::alloc(js, 2); + auto store1 = jsg::JsUint8Array::create(js, 2); store1.asArrayPtr()[0] = 1; store1.asArrayPtr()[1] = 2; - push(kj::mv(store1)); + push(store1); KJ_ASSERT(queue.desiredSize() == 0); - auto store2 = jsg::BackingStore::alloc(js, 2); + auto store2 = jsg::JsUint8Array::create(js, 2); store2.asArrayPtr()[0] = 3; store2.asArrayPtr()[1] = 4; - push(kj::mv(store2)); + push(store2); // Backpressure should be accumulating because the read has not yet fullilled. KJ_ASSERT(queue.desiredSize() == -2); - auto store3 = jsg::BackingStore::alloc(js, 2); + auto store3 = jsg::JsUint8Array::create(js, 2); store3.asArrayPtr()[0] = 5; store3.asArrayPtr()[1] = 6; - push(kj::mv(store3)); + push(store3); // Some backpressure should be released because pushing the final minimum // amount into the queue should have caused the read to be fulfilled. @@ -1068,10 +1078,11 @@ KJ_TEST("ByteQueue with multiple default consumers with atLeast (different rate) const auto read = [&](jsg::Lock& js, auto& consumer, uint atLeast = 1) { auto prp = js.newPromiseAndResolver(); + auto u8 = jsg::JsUint8Array::create(js, 5); consumer.read(js, ByteQueue::ReadRequest(kj::mv(prp.resolver), { - .store = jsg::BufferSource(js, jsg::BackingStore::alloc(js, 5)), + .store = jsg::JsArrayBufferView(u8).addRef(js), .atLeast = atLeast, })); return kj::mv(prp.promise); @@ -1079,18 +1090,18 @@ KJ_TEST("ByteQueue with multiple default consumers with atLeast (different rate) const auto push = [&](auto store) { try { - queue.push(js, kj::rc(jsg::BufferSource(js, kj::mv(store)))); + queue.push(js, kj::rc(js, jsg::JsBufferSource(store))); } catch (kj::Exception& ex) { KJ_DBG(ex.getDescription()); } }; - MustCall read1Continuation([&](jsg::Lock& js, auto&& result) { + MustCall read1Continuation([&](jsg::Lock& js, auto result) { KJ_ASSERT(!result.done); auto& value = KJ_ASSERT_NONNULL(result.value); auto view = value.getHandle(js); - KJ_ASSERT(view->IsArrayBufferView()); - jsg::BufferSource source(js, view); + KJ_ASSERT(view.isArrayBufferView()); + jsg::JsBufferSource source(view); KJ_ASSERT(source.size() == 4); auto ptr = source.asArrayPtr(); // Our read was for at least 3 bytes, with a maximum of 5. @@ -1103,12 +1114,12 @@ KJ_TEST("ByteQueue with multiple default consumers with atLeast (different rate) return js.resolvedPromise(kj::mv(result)); }); - MustCall read1FinalContinuation([&](jsg::Lock& js, auto&& result) { + MustCall read1FinalContinuation([&](jsg::Lock& js, auto result) { KJ_ASSERT(!result.done); auto& value = KJ_ASSERT_NONNULL(result.value); auto view = value.getHandle(js); - KJ_ASSERT(view->IsArrayBufferView()); - jsg::BufferSource source(js, view); + KJ_ASSERT(view.isArrayBufferView()); + jsg::JsBufferSource source(view); KJ_ASSERT(source.size() == 2); auto ptr = source.asArrayPtr(); KJ_ASSERT(ptr[0] == 5); @@ -1116,12 +1127,12 @@ KJ_TEST("ByteQueue with multiple default consumers with atLeast (different rate) return js.resolvedPromise(kj::mv(result)); }); - MustCall read2Continuation([&](jsg::Lock& js, auto&& result) { + MustCall read2Continuation([&](jsg::Lock& js, auto result) { KJ_ASSERT(!result.done); auto& value = KJ_ASSERT_NONNULL(result.value); auto view = value.getHandle(js); - KJ_ASSERT(view->IsArrayBufferView()); - jsg::BufferSource source(js, view); + KJ_ASSERT(view.isArrayBufferView()); + jsg::JsBufferSource source(view); auto ptr = source.asArrayPtr(); KJ_ASSERT(source.size() == 5); KJ_ASSERT(ptr[0] == 1); @@ -1133,12 +1144,12 @@ KJ_TEST("ByteQueue with multiple default consumers with atLeast (different rate) return read(js, consumer2); }); - MustCall read2FinalContinuation([&](jsg::Lock& js, auto&& result) { + MustCall read2FinalContinuation([&](jsg::Lock& js, auto result) { KJ_ASSERT(!result.done); auto& value = KJ_ASSERT_NONNULL(result.value); auto view = value.getHandle(js); - KJ_ASSERT(view->IsArrayBufferView()); - jsg::BufferSource source(js, view); + KJ_ASSERT(view.isArrayBufferView()); + jsg::JsBufferSource source(view); KJ_ASSERT(source.asArrayPtr()[0] == 6); KJ_ASSERT(source.size() == 1); return js.resolvedPromise(kj::mv(result)); @@ -1151,17 +1162,17 @@ KJ_TEST("ByteQueue with multiple default consumers with atLeast (different rate) // Consumer 2 will read serially with a larger minimum chunk... read(js, consumer2, 5).then(js, read2Continuation).then(js, read2FinalContinuation); - auto store1 = jsg::BackingStore::alloc(js, 2); + auto store1 = jsg::JsUint8Array::create(js, 2); store1.asArrayPtr()[0] = 1; store1.asArrayPtr()[1] = 2; - push(kj::mv(store1)); + push(store1); KJ_ASSERT(queue.desiredSize() == 0); - auto store2 = jsg::BackingStore::alloc(js, 2); + auto store2 = jsg::JsUint8Array::create(js, 2); store2.asArrayPtr()[0] = 3; store2.asArrayPtr()[1] = 4; - push(kj::mv(store2)); + push(store2); // Consumer1 should not have any data buffered since its first read was for // between 3 and 5 bytes and it has received four so far. @@ -1174,10 +1185,10 @@ KJ_TEST("ByteQueue with multiple default consumers with atLeast (different rate) // Queue backpressure should reflect that consumer2 has data buffered. KJ_ASSERT(queue.desiredSize() == -2); - auto store3 = jsg::BackingStore::alloc(js, 2); + auto store3 = jsg::JsUint8Array::create(js, 2); store3.asArrayPtr()[0] = 5; store3.asArrayPtr()[1] = 6; - push(kj::mv(store3)); + push(store3); // Most of the backpressure should have been resolved since we delivered 5 bytes // to consumer2, but there's still one byte remaining. @@ -1243,7 +1254,7 @@ KJ_TEST("ValueQueue push to errored consumer is safe") { ValueQueue::Consumer consumer2(queue); // Error consumer2 - consumer2.error(js, js.v8Ref(js.v8Error("error reason"_kj))); + consumer2.error(js, js.error("error reason"_kj)); // Now push to the queue queue.push(js, getEntry(js, 4)); @@ -1266,9 +1277,9 @@ KJ_TEST("ByteQueue push to closed consumer is safe") { consumer2.close(js); // Now push to the queue - auto store = jsg::BackingStore::alloc(js, 4); + auto store = jsg::JsUint8Array::create(js, 4); memset(store.asArrayPtr().begin(), 'A', 4); - auto entry = kj::rc(jsg::BufferSource(js, kj::mv(store))); + auto entry = kj::rc(js, jsg::JsBufferSource(store)); queue.push(js, kj::mv(entry)); // consumer1 should have received the data @@ -1291,17 +1302,16 @@ KJ_TEST("ValueQueue draining read with buffered data") { ValueQueue::Consumer consumer(queue); // Push an ArrayBuffer - auto store = jsg::BackingStore::alloc(js, 4); + auto store = jsg::JsUint8Array::create(js, 4); store.asArrayPtr()[0] = 'a'; store.asArrayPtr()[1] = 'b'; store.asArrayPtr()[2] = 'c'; store.asArrayPtr()[3] = 'd'; - auto ab = jsg::BufferSource(js, kj::mv(store)).getHandle(js); - queue.push(js, kj::rc(js.v8Ref(ab.As()), 4)); + queue.push(js, kj::rc(js, store, 4)); // Push a string - auto str = jsg::v8Str(js.v8Isolate, "hello"); - queue.push(js, kj::rc(js.v8Ref(str.As()), 5)); + auto str = js.str("hello"_kj); + queue.push(js, kj::rc(js, str, 5)); KJ_ASSERT(consumer.size() == 9); @@ -1404,7 +1414,7 @@ KJ_TEST("ValueQueue draining read on errored stream") { ValueQueue queue(10); ValueQueue::Consumer consumer(queue); - queue.error(js, js.v8Ref(js.v8Error("boom"_kj))); + queue.error(js, js.error("boom"_kj)); MustNotCall readContinuation; MustCall errorContinuation([&](jsg::Lock& js, auto&& value) { @@ -1423,19 +1433,19 @@ KJ_TEST("ByteQueue draining read with buffered data") { ByteQueue::Consumer consumer(queue); // Push first chunk - auto store1 = jsg::BackingStore::alloc(js, 4); + auto store1 = jsg::JsUint8Array::create(js, 4); store1.asArrayPtr()[0] = 'a'; store1.asArrayPtr()[1] = 'b'; store1.asArrayPtr()[2] = 'c'; store1.asArrayPtr()[3] = 'd'; - queue.push(js, kj::rc(jsg::BufferSource(js, kj::mv(store1)))); + queue.push(js, kj::rc(js, jsg::JsBufferSource(store1))); // Push second chunk - auto store2 = jsg::BackingStore::alloc(js, 3); + auto store2 = jsg::JsUint8Array::create(js, 3); store2.asArrayPtr()[0] = 'e'; store2.asArrayPtr()[1] = 'f'; store2.asArrayPtr()[2] = 'g'; - queue.push(js, kj::rc(jsg::BufferSource(js, kj::mv(store2)))); + queue.push(js, kj::rc(js, jsg::JsBufferSource(store2))); KJ_ASSERT(consumer.size() == 7); @@ -1472,10 +1482,11 @@ KJ_TEST("ByteQueue draining read rejects with pending reads") { // Queue a regular read auto prp = js.newPromiseAndResolver(); + auto u8 = jsg::JsUint8Array::create(js, 4); consumer.read(js, ByteQueue::ReadRequest(kj::mv(prp.resolver), { - .store = jsg::BufferSource(js, jsg::BackingStore::alloc(js, 4)), + .store = jsg::JsArrayBufferView(u8).addRef(js), })); KJ_ASSERT(consumer.hasReadRequests()); @@ -1511,10 +1522,11 @@ KJ_TEST("ByteQueue read rejects with pending draining read") { return js.rejectedPromise(kj::mv(value)); }); + auto u8 = jsg::JsUint8Array::create(js, 4); consumer.read(js, ByteQueue::ReadRequest(kj::mv(prp.resolver), { - .store = jsg::BufferSource(js, jsg::BackingStore::alloc(js, 4)), + .store = jsg::JsArrayBufferView(u8).addRef(js), })); prp.promise.then(js, readContinuation, errorContinuation); js.runMicrotasks(); @@ -1544,7 +1556,7 @@ KJ_TEST("ByteQueue draining read on errored stream") { ByteQueue queue(10); ByteQueue::Consumer consumer(queue); - queue.error(js, js.v8Ref(js.v8Error("boom"_kj))); + queue.error(js, js.error("boom"_kj)); MustNotCall readContinuation; MustCall errorContinuation([&](jsg::Lock& js, auto&& value) { @@ -1563,18 +1575,17 @@ KJ_TEST("ValueQueue draining read with close signal") { ValueQueue::Consumer consumer(queue); // Push some data - auto store = jsg::BackingStore::alloc(js, 4); + auto store = jsg::JsUint8Array::create(js, 4); store.asArrayPtr()[0] = 'a'; store.asArrayPtr()[1] = 'b'; store.asArrayPtr()[2] = 'c'; store.asArrayPtr()[3] = 'd'; - auto ab = jsg::BufferSource(js, kj::mv(store)).getHandle(js); - queue.push(js, kj::rc(js.v8Ref(ab.As()), 4)); + queue.push(js, kj::rc(js, store, 4)); // Close the queue queue.close(js); - MustCall readContinuation([&](jsg::Lock& js, auto&& result) { + MustCall readContinuation([&](jsg::Lock& js, auto result) { // Should have the data and done should be true since stream is closed KJ_ASSERT(result.done); KJ_ASSERT(result.chunks.size() == 1); @@ -1593,17 +1604,17 @@ KJ_TEST("ByteQueue draining read with close signal") { ByteQueue::Consumer consumer(queue); // Push some data - auto store = jsg::BackingStore::alloc(js, 4); + auto store = jsg::JsUint8Array::create(js, 4); store.asArrayPtr()[0] = 'a'; store.asArrayPtr()[1] = 'b'; store.asArrayPtr()[2] = 'c'; store.asArrayPtr()[3] = 'd'; - queue.push(js, kj::rc(jsg::BufferSource(js, kj::mv(store)))); + queue.push(js, kj::rc(js, jsg::JsBufferSource(store))); // Close the queue queue.close(js); - MustCall readContinuation([&](jsg::Lock& js, auto&& result) { + MustCall readContinuation([&](jsg::Lock& js, auto result) { // Should have the data and done should be true since stream is closed KJ_ASSERT(result.done); KJ_ASSERT(result.chunks.size() == 1); @@ -1624,8 +1635,7 @@ KJ_TEST("ValueQueue draining read errors on non-byte value") { ValueQueue::Consumer consumer(queue); // Push a plain object - this cannot be converted to bytes - auto obj = v8::Object::New(js.v8Isolate); - queue.push(js, kj::rc(js.v8Ref(obj.As()), 1)); + queue.push(js, kj::rc(js, js.obj(), 1)); KJ_ASSERT(consumer.size() == 1); @@ -1659,8 +1669,7 @@ KJ_TEST("ValueQueue draining read errors on number value") { ValueQueue::Consumer consumer(queue); // Push a number - this cannot be converted to bytes - auto num = v8::Number::New(js.v8Isolate, 42); - queue.push(js, kj::rc(js.v8Ref(num.As()), 1)); + queue.push(js, kj::rc(js, js.num(42), 1)); MustNotCall readContinuation; MustCall errorContinuation([&](jsg::Lock& js, auto&& value) { @@ -1691,15 +1700,13 @@ KJ_TEST("ValueQueue draining read respects maxRead during buffer drain") { ValueQueue::Consumer consumer(queue); // Buffer 200 bytes of data (two 100-byte chunks) - auto store1 = jsg::BackingStore::alloc(js, 100); + auto store1 = jsg::JsUint8Array::create(js, 100); store1.asArrayPtr().fill(0xAA); - auto ab1 = jsg::BufferSource(js, kj::mv(store1)).getHandle(js); - queue.push(js, kj::rc(js.v8Ref(ab1.As()), 100)); + queue.push(js, kj::rc(js, store1, 100)); - auto store2 = jsg::BackingStore::alloc(js, 100); + auto store2 = jsg::JsUint8Array::create(js, 100); store2.asArrayPtr().fill(0xBB); - auto ab2 = jsg::BufferSource(js, kj::mv(store2)).getHandle(js); - queue.push(js, kj::rc(js.v8Ref(ab2.As()), 100)); + queue.push(js, kj::rc(js, store2, 100)); KJ_ASSERT(consumer.size() == 200); @@ -1727,19 +1734,19 @@ KJ_TEST("ByteQueue draining read respects maxRead during buffer drain") { ByteQueue::Consumer consumer(queue); // Buffer 200 bytes of data (two 100-byte chunks) - auto store1 = jsg::BackingStore::alloc(js, 100); + auto store1 = jsg::JsUint8Array::create(js, 100); store1.asArrayPtr().fill(0xAA); - queue.push(js, kj::rc(jsg::BufferSource(js, kj::mv(store1)))); + queue.push(js, kj::rc(js, jsg::JsBufferSource(store1))); - auto store2 = jsg::BackingStore::alloc(js, 100); + auto store2 = jsg::JsUint8Array::create(js, 100); store2.asArrayPtr().fill(0xBB); - queue.push(js, kj::rc(jsg::BufferSource(js, kj::mv(store2)))); + queue.push(js, kj::rc(js, jsg::JsBufferSource(store2))); KJ_ASSERT(consumer.size() == 200); // maxRead=50: first 100-byte chunk is drained, then stops. Second chunk stays buffered. MustCall readContinuation( - [&](jsg::Lock& js, DrainingReadResult&& result) { + [&](jsg::Lock& js, DrainingReadResult result) { KJ_ASSERT(!result.done); KJ_ASSERT(result.chunks.size() == 1); KJ_ASSERT(result.chunks[0].size() == 100); @@ -1758,15 +1765,13 @@ KJ_TEST("ValueQueue draining read with large maxRead drains entire buffer") { ValueQueue::Consumer consumer(queue); // Buffer 200 bytes (two 100-byte chunks) - auto store1 = jsg::BackingStore::alloc(js, 100); + auto store1 = jsg::JsUint8Array::create(js, 100); store1.asArrayPtr().fill(0xAA); - auto ab1 = jsg::BufferSource(js, kj::mv(store1)).getHandle(js); - queue.push(js, kj::rc(js.v8Ref(ab1.As()), 100)); + queue.push(js, kj::rc(js, store1, 100)); - auto store2 = jsg::BackingStore::alloc(js, 100); + auto store2 = jsg::JsUint8Array::create(js, 100); store2.asArrayPtr().fill(0xBB); - auto ab2 = jsg::BufferSource(js, kj::mv(store2)).getHandle(js); - queue.push(js, kj::rc(js.v8Ref(ab2.As()), 100)); + queue.push(js, kj::rc(js, store2, 100)); KJ_ASSERT(consumer.size() == 200); @@ -1792,14 +1797,12 @@ KJ_TEST("ValueQueue draining read with default maxRead (unlimited)") { ValueQueue::Consumer consumer(queue); // Buffer some data - auto store = jsg::BackingStore::alloc(js, 100); + auto store = jsg::JsUint8Array::create(js, 100); store.asArrayPtr().fill(0xAA); - auto ab = jsg::BufferSource(js, kj::mv(store)).getHandle(js); - queue.push(js, kj::rc(js.v8Ref(ab.As()), 100)); + queue.push(js, kj::rc(js, store, 100)); // Default maxRead (kj::maxValue) should drain buffer normally - MustCall readContinuation( - [&](jsg::Lock& js, DrainingReadResult&& result) { + MustCall readContinuation([&](jsg::Lock& js, auto result) { KJ_ASSERT(!result.done); KJ_ASSERT(result.chunks.size() == 1); KJ_ASSERT(result.chunks[0].size() == 100); @@ -1820,16 +1823,15 @@ KJ_TEST("ValueQueue draining read maxRead bounds multiple iterations") { // Buffer 400 bytes: four 100-byte chunks for (int i = 0; i < 4; i++) { - auto store = jsg::BackingStore::alloc(js, 100); + auto store = jsg::JsUint8Array::create(js, 100); store.asArrayPtr().fill(0x10 * (i + 1)); - auto ab = jsg::BufferSource(js, kj::mv(store)).getHandle(js); - queue.push(js, kj::rc(js.v8Ref(ab.As()), 100)); + queue.push(js, kj::rc(js, store, 100)); } KJ_ASSERT(consumer.size() == 400); // First read with maxRead=150: drains first chunk (100 bytes, now totalRead=100 < 150), // then drains second chunk (200 bytes total, now >= 150), stops. - MustCall read1([&](jsg::Lock& js, DrainingReadResult&& result) { + MustCall read1([&](jsg::Lock& js, auto result) { KJ_ASSERT(!result.done); KJ_ASSERT(result.chunks.size() == 2); KJ_ASSERT(consumer.size() == 200); @@ -1839,7 +1841,7 @@ KJ_TEST("ValueQueue draining read maxRead bounds multiple iterations") { js.runMicrotasks(); // Second read with maxRead=150: drains next two chunks similarly - MustCall read2([&](jsg::Lock& js, DrainingReadResult&& result) { + MustCall read2([&](jsg::Lock& js, auto result) { KJ_ASSERT(!result.done); KJ_ASSERT(result.chunks.size() == 2); KJ_ASSERT(consumer.size() == 0); @@ -1911,9 +1913,9 @@ KJ_TEST("ByteQueue destroyed before consumer doesn't crash") { auto queue = kj::heap(2); auto consumer = kj::heap(*queue); - auto store = jsg::BackingStore::alloc(js, 4); + auto store = jsg::JsUint8Array::create(js, 4); store.asArrayPtr().fill('a'); - queue->push(js, kj::rc(jsg::BufferSource(js, kj::mv(store)))); + queue->push(js, kj::rc(js, jsg::JsBufferSource(store))); KJ_ASSERT(consumer->size() == 4); // Destroy queue before consumer @@ -1965,7 +1967,7 @@ KJ_TEST("ValueQueue error then destroy before consumer doesn't crash") { auto consumer = kj::heap(*queue); // Error the queue first - queue->error(js, js.v8Ref(js.v8Error("boom"_kj))); + queue->error(js, js.error("boom"_kj)); // Then destroy it queue = nullptr; @@ -2003,9 +2005,9 @@ KJ_TEST("ByteQueue push skips consumer removed from queue during iteration") { // Push data - should not crash even though consumer2 was in the queue // when it was created but is now destroyed. - auto store = jsg::BackingStore::alloc(js, 4); + auto store = jsg::JsUint8Array::create(js, 4); store.asArrayPtr().fill('x'); - queue.push(js, kj::rc(jsg::BufferSource(js, kj::mv(store)))); + queue.push(js, kj::rc(js, jsg::JsBufferSource(store))); // consumer1 should have received the data KJ_ASSERT(consumer1->size() == 4); @@ -2037,10 +2039,11 @@ KJ_TEST("ByteQueue push handles consumer destroyed by microtask between pushes") // Set up a pending read on consumer1 auto prp = js.newPromiseAndResolver(); + auto u8 = jsg::JsUint8Array::create(js, 4); consumer1->read(js, ByteQueue::ReadRequest(kj::mv(prp.resolver), { - .store = jsg::BufferSource(js, jsg::BackingStore::alloc(js, 4)), + .store = jsg::JsArrayBufferView(u8).addRef(js), })); // The continuation destroys consumer2 @@ -2051,17 +2054,17 @@ KJ_TEST("ByteQueue push handles consumer destroyed by microtask between pushes") prp.promise.then(js, readContinuation); // First push - resolves consumer1's read, schedules microtask that will destroy consumer2 - auto store1 = jsg::BackingStore::alloc(js, 4); + auto store1 = jsg::JsUint8Array::create(js, 4); store1.asArrayPtr().fill('x'); - queue.push(js, kj::rc(jsg::BufferSource(js, kj::mv(store1)))); + queue.push(js, kj::rc(js, jsg::JsBufferSource(store1))); // Run microtasks - this destroys consumer2 js.runMicrotasks(); // Second push - consumer2 is now destroyed, should not crash - auto store2 = jsg::BackingStore::alloc(js, 4); + auto store2 = jsg::JsUint8Array::create(js, 4); store2.asArrayPtr().fill('y'); - queue.push(js, kj::rc(jsg::BufferSource(js, kj::mv(store2)))); + queue.push(js, kj::rc(js, jsg::JsBufferSource(store2))); // consumer1 should have the second push's data buffered KJ_ASSERT(consumer1->size() == 4); @@ -2076,9 +2079,9 @@ KJ_TEST("ByteQueue maybeUpdateBackpressure skips destroyed consumers") { auto consumer2 = kj::heap(queue); // Push some data so consumers have size - auto store = jsg::BackingStore::alloc(js, 4); + auto store = jsg::JsUint8Array::create(js, 4); store.asArrayPtr().fill('x'); - queue.push(js, kj::rc(jsg::BufferSource(js, kj::mv(store)))); + queue.push(js, kj::rc(js, jsg::JsBufferSource(store))); KJ_ASSERT(consumer1->size() == 4); KJ_ASSERT(consumer2->size() == 4); @@ -2088,9 +2091,9 @@ KJ_TEST("ByteQueue maybeUpdateBackpressure skips destroyed consumers") { consumer2 = nullptr; // Trigger backpressure recalculation by pushing more data - auto store2 = jsg::BackingStore::alloc(js, 4); + auto store2 = jsg::JsUint8Array::create(js, 4); store2.asArrayPtr().fill('y'); - queue.push(js, kj::rc(jsg::BufferSource(js, kj::mv(store2)))); + queue.push(js, kj::rc(js, jsg::JsBufferSource(store2))); // Should not crash, and size should reflect only consumer1 KJ_ASSERT(consumer1->size() == 8); diff --git a/src/workerd/api/streams/queue.c++ b/src/workerd/api/streams/queue.c++ index 35d03535f8c..9fd081e69e6 100644 --- a/src/workerd/api/streams/queue.c++ +++ b/src/workerd/api/streams/queue.c++ @@ -23,25 +23,31 @@ void ValueQueue::ReadRequest::resolveAsDone(jsg::Lock& js) { resolver.resolve(js, ReadResult{.done = true}); } -void ValueQueue::ReadRequest::resolve(jsg::Lock& js, jsg::Value value) { - resolver.resolve(js, ReadResult{.value = kj::mv(value), .done = false}); +void ValueQueue::ReadRequest::resolve(jsg::Lock& js, jsg::JsValue value) { + resolver.resolve(js, + ReadResult{ + .value = value.addRef(js), + .done = false, + }); } -void ValueQueue::ReadRequest::reject(jsg::Lock& js, jsg::Value& value) { - resolver.reject(js, value.getHandle(js)); +void ValueQueue::ReadRequest::reject(jsg::Lock& js, jsg::JsValue value) { + resolver.reject(js, value); } #pragma endregion ValueQueue::ReadRequest #pragma region ValueQueue::Entry -ValueQueue::Entry::Entry(jsg::Value value, size_t size): value(kj::mv(value)), size(size) {} +ValueQueue::Entry::Entry(jsg::Lock& js, jsg::JsValue value, size_t size) + : value(value.addRef(js)), + size(size) {} -jsg::Value ValueQueue::Entry::getValue(jsg::Lock& js) { - return value.addRef(js); +jsg::JsValue ValueQueue::Entry::getValue(jsg::Lock& js) { + return value.getHandle(js); } -size_t ValueQueue::Entry::getSize() const { +size_t ValueQueue::Entry::getSize(jsg::Lock&) const { return size; } @@ -76,7 +82,7 @@ ValueQueue::Consumer::Consumer( ValueQueue::Consumer::Consumer(kj::Maybe stateListener) : impl(stateListener) {} -void ValueQueue::Consumer::cancel(jsg::Lock& js, jsg::Optional> maybeReason) { +void ValueQueue::Consumer::cancel(jsg::Lock& js, jsg::Optional maybeReason) { impl.cancel(js, maybeReason); } @@ -84,12 +90,12 @@ void ValueQueue::Consumer::close(jsg::Lock& js) { impl.close(js); }; -bool ValueQueue::Consumer::empty() { +bool ValueQueue::Consumer::empty() const { return impl.empty(); } -void ValueQueue::Consumer::error(jsg::Lock& js, jsg::Value reason) { - impl.error(js, kj::mv(reason)); +void ValueQueue::Consumer::error(jsg::Lock& js, jsg::JsValue reason) { + impl.error(js, reason); }; void ValueQueue::Consumer::read(jsg::Lock& js, ReadRequest request) { @@ -104,7 +110,7 @@ void ValueQueue::Consumer::reset() { impl.reset(); }; -size_t ValueQueue::Consumer::size() { +size_t ValueQueue::Consumer::size() const { return impl.size(); } @@ -133,23 +139,21 @@ bool ValueQueue::Consumer::hasPendingDrainingRead() { namespace { // Helper to convert a JS value to bytes. Returns kj::none if the value cannot be converted. -kj::Maybe> valueToBytes(jsg::Lock& js, jsg::Value& value) { - auto jsval = jsg::JsValue(value.getHandle(js)); - +kj::Maybe> valueToBytes(jsg::Lock& js, const jsg::JsValue& value) { // Try ArrayBuffer first. - KJ_IF_SOME(ab, jsval.tryCast()) { + KJ_IF_SOME(ab, value.tryCast()) { auto src = ab.asArrayPtr(); return kj::heapArray(src); } // Try ArrayBufferView. - KJ_IF_SOME(abView, jsval.tryCast()) { + KJ_IF_SOME(abView, value.tryCast()) { auto src = abView.asArrayPtr(); return kj::heapArray(src); } // Try string - convert to UTF-8. - KJ_IF_SOME(str, jsval.tryCast()) { + KJ_IF_SOME(str, value.tryCast()) { auto data = str.toUSVString(js); return kj::heapArray(data.asBytes()); } @@ -206,12 +210,12 @@ jsg::Promise ValueQueue::Consumer::drainingRead(jsg::Lock& j KJ_IF_SOME(bytes, valueToBytes(js, value)) { totalRead += bytes.size(); chunks.add(kj::mv(bytes)); - ready.queueTotalSize -= entry.entry->getSize(); + ready.queueTotalSize -= entry.entry->getSize(js); ready.buffer.pop_front(); } else { auto error = js.typeError( "Draining read encountered a value that cannot be converted to bytes"_kj); - impl.error(js, jsg::Value(js.v8Isolate, error)); + impl.error(js, error); return js.rejectedPromise(error); } } @@ -314,9 +318,19 @@ jsg::Promise ValueQueue::Consumer::drainingRead(jsg::Lock& j listener.onConsumerWantsData(js); } - // Transform the ReadResult promise to DrainingReadResult. - return prp.promise.then( - js, [this](jsg::Lock& js, ReadResult result) mutable -> DrainingReadResult { + // Transform the ReadResult promise to DrainingReadResult using a persistent + // continuation to avoid per-call OpaqueWrappable and v8::Function allocations. + KJ_IF_SOME(dc, drainingReadContinuation) { + return prp.promise.thenRef(js, dc); + } + drainingReadContinuation.emplace( + DrainingReadContinuationType::create(js, DrainingReadCallbacks{impl.selfRef.addRef()})); + return prp.promise.thenRef(js, KJ_ASSERT_NONNULL(drainingReadContinuation)); +} + +DrainingReadResult ValueQueue::Consumer::DrainingReadCallbacks::thenFunc( + jsg::Lock& js, ReadResult result) { + KJ_IF_SOME(impl, weakImpl->tryGet()) { KJ_IF_SOME(ready, impl.state.tryGetActiveUnsafe()) { ready.hasPendingDrainingRead = false; } @@ -325,26 +339,35 @@ jsg::Promise ValueQueue::Consumer::drainingRead(jsg::Lock& j return DrainingReadResult{.chunks = nullptr, .done = true}; } - // Convert the value to bytes. kj::Vector> chunks; KJ_IF_SOME(val, result.value) { - KJ_IF_SOME(bytes, valueToBytes(js, val)) { + KJ_IF_SOME(bytes, valueToBytes(js, val.getHandle(js))) { chunks.add(kj::mv(bytes)); + } else { + // Value couldn't be converted to bytes (not an ArrayBuffer, ArrayBufferView, or string). + // Consistent with the synchronous drainBuffer path, treat as an error. + js.throwException(js.typeError("This ReadableStream did not return bytes."_kj)); + KJ_UNREACHABLE; } - // If valueToBytes returned kj::none, we just return empty chunks. - // The error case should have been caught earlier. } return DrainingReadResult{ .chunks = chunks.releaseAsArray(), .done = false, }; - }, [this](jsg::Lock& js, jsg::Value exception) mutable -> DrainingReadResult { + } else { + return DrainingReadResult{.chunks = nullptr, .done = true}; + } +} + +DrainingReadResult ValueQueue::Consumer::DrainingReadCallbacks::catchFunc( + jsg::Lock& js, jsg::Value exception) { + KJ_IF_SOME(impl, weakImpl->tryGet()) { KJ_IF_SOME(ready, impl.state.tryGetActiveUnsafe()) { ready.hasPendingDrainingRead = false; } - js.throwException(kj::mv(exception)); - }); + } + js.throwException(kj::mv(exception)); } void ValueQueue::Consumer::cancelPendingReads(jsg::Lock& js, jsg::JsValue reason) { @@ -353,6 +376,9 @@ void ValueQueue::Consumer::cancelPendingReads(jsg::Lock& js, jsg::JsValue reason void ValueQueue::Consumer::visitForGc(jsg::GcVisitor& visitor) { visitor.visit(impl); + KJ_IF_SOME(dc, drainingReadContinuation) { + dc.visitForGc(visitor); + } } #pragma endregion ValueQueue::Consumer @@ -367,8 +393,8 @@ ssize_t ValueQueue::desiredSize() const { return impl.desiredSize(); } -void ValueQueue::error(jsg::Lock& js, jsg::Value reason) { - impl.error(js, kj::mv(reason)); +void ValueQueue::error(jsg::Lock& js, jsg::JsValue reason) { + impl.error(js, reason); } void ValueQueue::maybeUpdateBackpressure() { @@ -388,7 +414,7 @@ void ValueQueue::handlePush( // If there are no pending reads, just add the entry to the buffer and return, adjusting // the size of the queue in the process. if (state.readRequests.empty()) { - state.queueTotalSize += entry->getSize(); + state.queueTotalSize += entry->getSize(js); state.buffer.push_back(QueueEntry{.entry = kj::mv(entry)}); return; } @@ -436,7 +462,7 @@ void ValueQueue::handleRead(jsg::Lock& js, auto freed = kj::mv(entry); state.buffer.pop_front(); request.resolve(js, freed.entry->getValue(js)); - state.queueTotalSize -= freed.entry->getSize(); + state.queueTotalSize -= freed.entry->getSize(js); return; } } @@ -473,7 +499,7 @@ bool ValueQueue::wantsRead() const { return impl.wantsRead(); } -bool ValueQueue::hasPartiallyFulfilledRead() { +bool ValueQueue::hasPartiallyFulfilledRead(jsg::Lock&) { // A ValueQueue can never have a partially fulfilled read. return false; } @@ -511,26 +537,39 @@ void ByteQueue::ReadRequest::resolveAsDone(jsg::Lock& js) { if (pullInto.filled > 0) { // There's been at least some data written, we need to respond but not // set done to true since that's what the streams spec requires. - pullInto.store.trim(js, pullInto.store.size() - pullInto.filled); - resolver.resolve( - js, ReadResult{.value = js.v8Ref(pullInto.store.getHandle(js)), .done = false}); + return resolve(js); + } + if (pullInto.autoAllocated) { + // Auto-allocated reads originate from a default reader. Per the spec, + // the default reader's close steps resolve with {value: undefined, done: true}. + resolver.resolve(js, ReadResult{.done = true}); } else { - // Otherwise, we set the length to zero - pullInto.store.trim(js, pullInto.store.size()); - KJ_ASSERT(pullInto.store.size() == 0); - resolver.resolve(js, ReadResult{.value = js.v8Ref(pullInto.store.getHandle(js)), .done = true}); + // Explicit BYOB reads. Per the spec, the BYOB reader's close steps resolve + // with {value: , done: true}. + auto handle = pullInto.store.getHandle(js).clone(js); + handle = handle.slice(js, 0, 0); + resolver.resolve(js, + ReadResult{ + .value = jsg::JsValue(handle).addRef(js), + .done = true, + }); } maybeInvalidateByobRequest(byobReadRequest); } void ByteQueue::ReadRequest::resolve(jsg::Lock& js) { - pullInto.store.trim(js, pullInto.store.size() - pullInto.filled); - resolver.resolve(js, ReadResult{.value = js.v8Ref(pullInto.store.getHandle(js)), .done = false}); + auto handle = pullInto.store.getHandle(js).clone(js); + // We need to create a new handle over the same underlying data + resolver.resolve(js, + ReadResult{ + .value = jsg::JsValue(handle.slice(js, 0, pullInto.filled)).addRef(js), + .done = false, + }); maybeInvalidateByobRequest(byobReadRequest); } -void ByteQueue::ReadRequest::reject(jsg::Lock& js, jsg::Value& value) { - resolver.reject(js, value.getHandle(js)); +void ByteQueue::ReadRequest::reject(jsg::Lock& js, jsg::JsValue value) { + resolver.reject(js, value); maybeInvalidateByobRequest(byobReadRequest); } @@ -545,14 +584,14 @@ kj::Own ByteQueue::ReadRequest::makeByobReadRequest( #pragma region ByteQueue::Entry -ByteQueue::Entry::Entry(jsg::BufferSource store): store(kj::mv(store)) {} +ByteQueue::Entry::Entry(jsg::Lock& js, jsg::JsBufferSource store): store(store.addRef(js)) {} -kj::ArrayPtr ByteQueue::Entry::toArrayPtr() { - return store.asArrayPtr(); +kj::ArrayPtr ByteQueue::Entry::toArrayPtr(jsg::Lock& js) { + return store.getHandle(js).asArrayPtr(); } -size_t ByteQueue::Entry::getSize() const { - return store.size(); +size_t ByteQueue::Entry::getSize(jsg::Lock& js) const { + return store.getHandle(js).size(); } kj::Rc ByteQueue::Entry::clone(jsg::Lock& js) { @@ -587,7 +626,7 @@ ByteQueue::Consumer::Consumer( ByteQueue::Consumer::Consumer(kj::Maybe stateListener) : impl(stateListener) {} -void ByteQueue::Consumer::cancel(jsg::Lock& js, jsg::Optional> maybeReason) { +void ByteQueue::Consumer::cancel(jsg::Lock& js, jsg::Optional maybeReason) { impl.cancel(js, maybeReason); } @@ -599,8 +638,8 @@ bool ByteQueue::Consumer::empty() const { return impl.empty(); } -void ByteQueue::Consumer::error(jsg::Lock& js, jsg::Value reason) { - impl.error(js, kj::mv(reason)); +void ByteQueue::Consumer::error(jsg::Lock& js, jsg::JsValue reason) { + impl.error(js, reason); } void ByteQueue::Consumer::read(jsg::Lock& js, ReadRequest request) { @@ -672,7 +711,7 @@ jsg::Promise ByteQueue::Consumer::drainingRead(jsg::Lock& js // Drains buffered byte data into chunks. Stops draining when totalRead reaches // or exceeds maxRead (after finishing the current item). - static const auto drainBuffer = [](ConsumerImpl::Ready& ready, + static const auto drainBuffer = [](jsg::Lock& js, ConsumerImpl::Ready& ready, kj::Vector>& chunks, size_t& totalRead, bool& isClosing, size_t maxRead) { while (!ready.buffer.empty() && !isClosing && totalRead < maxRead) { @@ -683,7 +722,7 @@ jsg::Promise ByteQueue::Consumer::drainingRead(jsg::Lock& js break; } KJ_CASE_ONEOF(entry, QueueEntry) { - auto ptr = entry.entry->toArrayPtr(); + auto ptr = entry.entry->toArrayPtr(js); auto offset = entry.offset; auto size = ptr.size() - offset; totalRead += size; @@ -696,7 +735,7 @@ jsg::Promise ByteQueue::Consumer::drainingRead(jsg::Lock& js }; // Drain the buffer up to maxRead bytes, then pump for more if under the limit. - drainBuffer(ready, chunks, totalRead, isClosing, maxRead); + drainBuffer(js, ready, chunks, totalRead, isClosing, maxRead); // Pump the controller for more synchronously available data. // maxRead is checked here: we only proceed with pumping if we haven't exceeded it. @@ -711,7 +750,7 @@ jsg::Promise ByteQueue::Consumer::drainingRead(jsg::Lock& js if (!impl.state.isActive()) break; // Drain buffered data that was added by the pull, respecting maxRead. - drainBuffer(ready, chunks, totalRead, isClosing, maxRead); + drainBuffer(js, ready, chunks, totalRead, isClosing, maxRead); // If pull is async or no new data was added, stop pumping. if (!pullCompletedSync || chunks.size() == prevChunkCount) { @@ -741,7 +780,7 @@ jsg::Promise ByteQueue::Consumer::drainingRead(jsg::Lock& js if (impl.queue == kj::none) { // Drain remaining buffer up to maxRead. If there's still more, the caller // will loop back and we'll drain the rest on subsequent calls. - drainBuffer(ready, chunks, totalRead, isClosing, maxRead); + drainBuffer(js, ready, chunks, totalRead, isClosing, maxRead); ready.hasPendingDrainingRead = false; bool done = ready.buffer.empty() || isClosing; // If isClosing, finalize the consumer so onConsumerClose fires promptly. @@ -773,11 +812,11 @@ jsg::Promise ByteQueue::Consumer::drainingRead(jsg::Lock& js // We allocate a buffer for the read - the data will be copied into it. // The flag remains set (was set at the start) and will be cleared by the promise callbacks. constexpr size_t kDefaultReadSize = 16384; // 16KB default buffer - KJ_IF_SOME(store, jsg::BufferSource::tryAllocUnsafe(js, kDefaultReadSize)) { + KJ_IF_SOME(store, jsg::JsUint8Array::tryCreate(js, kDefaultReadSize)) { auto prp = js.newPromiseAndResolver(); ReadRequest::PullInto pullInto{ - .store = kj::mv(store), + .store = jsg::JsArrayBufferView(store).addRef(js), .filled = 0, .atLeast = 1, .type = ReadRequest::Type::DEFAULT, @@ -789,49 +828,72 @@ jsg::Promise ByteQueue::Consumer::drainingRead(jsg::Lock& js listener.onConsumerWantsData(js); } - // Transform the ReadResult promise to DrainingReadResult. - return prp.promise.then( - js, [this](jsg::Lock& js, ReadResult result) mutable -> DrainingReadResult { - KJ_IF_SOME(ready, impl.state.tryGetActiveUnsafe()) { - ready.hasPendingDrainingRead = false; - } + // Transform the ReadResult promise to DrainingReadResult using a persistent + // continuation to avoid per-call OpaqueWrappable and v8::Function allocations. + KJ_IF_SOME(dc, drainingReadContinuation) { + return prp.promise.thenRef(js, dc); + } + drainingReadContinuation.emplace( + DrainingReadContinuationType::create(js, DrainingReadCallbacks{impl.selfRef.addRef()})); + return prp.promise.thenRef(js, KJ_ASSERT_NONNULL(drainingReadContinuation)); + } else { + return js.rejectedPromise( + js.error("Failed to allocate buffer for draining read"_kj)); + } +} - if (result.done) { - return DrainingReadResult{.chunks = nullptr, .done = true}; - } +DrainingReadResult ByteQueue::Consumer::DrainingReadCallbacks::thenFunc( + jsg::Lock& js, ReadResult result) { + KJ_IF_SOME(impl, weakImpl->tryGet()) { + KJ_IF_SOME(ready, impl.state.tryGetActiveUnsafe()) { + ready.hasPendingDrainingRead = false; + } - kj::Vector> chunks; - KJ_IF_SOME(val, result.value) { - auto jsval = jsg::JsValue(val.getHandle(js)); - KJ_IF_SOME(ab, jsval.tryCast()) { - chunks.add(kj::heapArray(ab.asArrayPtr())); - } else KJ_IF_SOME(abView, jsval.tryCast()) { - chunks.add(kj::heapArray(abView.asArrayPtr())); - } - } + if (result.done) { + return DrainingReadResult{.chunks = nullptr, .done = true}; + } - return DrainingReadResult{ - .chunks = chunks.releaseAsArray(), - .done = false, - }; - }, [this](jsg::Lock& js, jsg::Value exception) mutable -> DrainingReadResult { - KJ_IF_SOME(ready, impl.state.tryGetActiveUnsafe()) { - ready.hasPendingDrainingRead = false; + kj::Vector> chunks; + KJ_IF_SOME(val, result.value) { + auto jsval = val.getHandle(js); + KJ_IF_SOME(ab, jsval.tryCast()) { + chunks.add(kj::heapArray(ab.asArrayPtr())); + } else KJ_IF_SOME(abView, jsval.tryCast()) { + chunks.add(kj::heapArray(abView.asArrayPtr())); + } else { + js.throwException(js.typeError("This ReadableStream did not return bytes."_kj)); + KJ_UNREACHABLE; } - js.throwException(kj::mv(exception)); - }); + } + + return DrainingReadResult{ + .chunks = chunks.releaseAsArray(), + .done = false, + }; } else { - return js.rejectedPromise( - js.error("Failed to allocate buffer for draining read"_kj)); + return DrainingReadResult{.chunks = nullptr, .done = true}; } } +DrainingReadResult ByteQueue::Consumer::DrainingReadCallbacks::catchFunc( + jsg::Lock& js, jsg::Value exception) { + KJ_IF_SOME(impl, weakImpl->tryGet()) { + KJ_IF_SOME(ready, impl.state.tryGetActiveUnsafe()) { + ready.hasPendingDrainingRead = false; + } + } + js.throwException(kj::mv(exception)); +} + void ByteQueue::Consumer::cancelPendingReads(jsg::Lock& js, jsg::JsValue reason) { impl.cancelPendingReads(js, reason); } void ByteQueue::Consumer::visitForGc(jsg::GcVisitor& visitor) { visitor.visit(impl); + KJ_IF_SOME(dc, drainingReadContinuation) { + dc.visitForGc(visitor); + } } #pragma endregion ByteQueue::Consumer @@ -849,9 +911,10 @@ void ByteQueue::ByobRequest::invalidate() { } } -bool ByteQueue::ByobRequest::isPartiallyFulfilled() { - return !isInvalidated() && getRequest().pullInto.filled > 0 && - getRequest().pullInto.store.getElementSize() > 1; +bool ByteQueue::ByobRequest::isPartiallyFulfilled(jsg::Lock& js) { + if (isInvalidated()) return false; + auto handle = getRequest().pullInto.store.getHandle(js); + return getRequest().pullInto.filled > 0 && handle.getElementSize() > 1; } bool ByteQueue::ByobRequest::respond(jsg::Lock& js, size_t amount) { @@ -866,27 +929,29 @@ bool ByteQueue::ByobRequest::respond(jsg::Lock& js, size_t amount) { // rejected already. auto& req = KJ_REQUIRE_NONNULL(request, "the pending byob read request was already invalidated"); + auto handle = req.pullInto.store.getHandle(js); // The amount cannot be more than the total space in the request store. - JSG_REQUIRE(req.pullInto.filled + amount <= req.pullInto.store.size(), RangeError, + JSG_REQUIRE(req.pullInto.filled + amount <= handle.size(), RangeError, kj::str("Too many bytes [", amount, "] in response to a BYOB read request.")); - auto sourcePtr = req.pullInto.store.asArrayPtr(); + auto sourcePtr = handle.asArrayPtr(); if (queue.getConsumerCount() > 1) { // Allocate the entry into which we will be copying the provided data for the // other consumers of the queue. - KJ_IF_SOME(store, jsg::BufferSource::tryAllocUnsafe(js, amount)) { - auto entry = kj::rc(kj::mv(store)); + KJ_IF_SOME(store, jsg::JsUint8Array::tryCreate(js, amount)) { + auto entry = kj::rc(js, jsg::JsBufferSource(store)); auto start = sourcePtr.slice(req.pullInto.filled); // Safely copy the data over into the entry. - entry->toArrayPtr().first(amount).copyFrom(start.first(amount)); + entry->toArrayPtr(js).first(amount).copyFrom(start.first(amount)); // Push the entry into the other consumers. queue.push(js, kj::mv(entry), consumer); } else { js.throwException(js.error("Failed to allocate memory for the byob read response."_kj)); + KJ_UNREACHABLE; } } @@ -911,7 +976,7 @@ bool ByteQueue::ByobRequest::respond(jsg::Lock& js, size_t amount) { // There is no need to adjust the pullInto.atLeast here because we are resolving // the read immediately. - auto unaligned = req.pullInto.filled % req.pullInto.store.getElementSize(); + auto unaligned = req.pullInto.filled % handle.getElementSize(); // It is possible that the request was partially filled already. req.pullInto.filled -= unaligned; @@ -921,9 +986,9 @@ bool ByteQueue::ByobRequest::respond(jsg::Lock& js, size_t amount) { if (unaligned > 0) { auto start = sourcePtr.slice(amount - unaligned); - KJ_IF_SOME(store, jsg::BufferSource::tryAllocUnsafe(js, unaligned)) { - auto excess = kj::rc(kj::mv(store)); - excess->toArrayPtr().first(unaligned).copyFrom(start.first(unaligned)); + KJ_IF_SOME(store, jsg::JsUint8Array::tryCreate(js, unaligned)) { + auto excess = kj::rc(js, jsg::JsBufferSource(store)); + excess->toArrayPtr(js).first(unaligned).copyFrom(start.first(unaligned)); consumer.push(js, kj::mv(excess)); } else { js.throwException(js.error("Failed to allocate memory for the byob read response."_kj)); @@ -933,7 +998,7 @@ bool ByteQueue::ByobRequest::respond(jsg::Lock& js, size_t amount) { return true; } -bool ByteQueue::ByobRequest::respondWithNewView(jsg::Lock& js, jsg::BufferSource view) { +bool ByteQueue::ByobRequest::respondWithNewView(jsg::Lock& js, jsg::JsBufferSource view) { // The idea here is that rather than filling the view that the controller was given, // it chose to create its own view and fill that, likely over the same ArrayBuffer. // What we do here is perform some basic validations on what we were given, and if @@ -942,15 +1007,24 @@ bool ByteQueue::ByobRequest::respondWithNewView(jsg::Lock& js, jsg::BufferSource auto& req = KJ_REQUIRE_NONNULL(request, "the pending byob read request was already invalidated"); auto amount = view.size(); - JSG_REQUIRE(view.canDetach(js), TypeError, "Unable to use non-detachable ArrayBuffer."); - JSG_REQUIRE(req.pullInto.store.getOffset() + req.pullInto.filled == view.getOffset(), RangeError, + auto handle = req.pullInto.store.getHandle(js); + JSG_REQUIRE(view.isDetachable(), TypeError, "Unable to use non-detachable ArrayBuffer."); + JSG_REQUIRE(handle.getOffset() + req.pullInto.filled == view.getOffset(), RangeError, "The given view has an invalid byte offset."); - JSG_REQUIRE(req.pullInto.store.size() == view.underlyingArrayBufferSize(js), RangeError, + JSG_REQUIRE(handle.size() == view.underlyingArrayBufferSize(js), RangeError, "The underlying ArrayBuffer is not the correct length."); - JSG_REQUIRE(req.pullInto.filled + amount <= req.pullInto.store.size(), RangeError, + JSG_REQUIRE(req.pullInto.filled + amount <= handle.size(), RangeError, "The view is not the correct length."); - req.pullInto.store = jsg::BufferSource(js, view.detach(js)); + KJ_IF_SOME(actualView, jsg::JsValue(view).tryCast()) { + req.pullInto.store = actualView.addRef(js); + } else KJ_IF_SOME(sab, jsg::JsValue(view).tryCast()) { + req.pullInto.store = jsg::JsArrayBufferView(jsg::JsUint8Array::create(js, sab)).addRef(js); + } else { + auto ab = KJ_ASSERT_NONNULL(jsg::JsValue(view).tryCast()); + req.pullInto.store = jsg::JsArrayBufferView(jsg::JsUint8Array::create(js, ab)).addRef(js); + } + return respond(js, amount); } @@ -961,28 +1035,26 @@ size_t ByteQueue::ByobRequest::getAtLeast() const { return 0; } -v8::Local ByteQueue::ByobRequest::getView(jsg::Lock& js) { +kj::Maybe ByteQueue::ByobRequest::getView(jsg::Lock& js) { KJ_IF_SOME(req, request) { - return req.pullInto.store - .getTypedViewSlice(js, req.pullInto.filled, req.pullInto.store.size()) - .getHandle(js) - .As(); + jsg::JsUint8Array handle = req.pullInto.store.getHandle(js).clone(js); + return handle.slice(js, req.pullInto.filled, handle.size() - req.pullInto.filled); } - return v8::Local(); + return kj::none; } size_t ByteQueue::ByobRequest::getOriginalBufferByteLength(jsg::Lock& js) const { KJ_IF_SOME(req, request) { - KJ_IF_SOME(size, req.pullInto.store.underlyingArrayBufferSize(js)) { - return size; - } + auto handle = req.pullInto.store.getHandle(js); + return handle.getBuffer().size(); } return 0; } -size_t ByteQueue::ByobRequest::getOriginalByteOffsetPlusBytesFilled() const { +size_t ByteQueue::ByobRequest::getOriginalByteOffsetPlusBytesFilled(jsg::Lock& js) const { KJ_IF_SOME(req, request) { - return req.pullInto.store.getOffset() + req.pullInto.filled; + auto handle = req.pullInto.store.getHandle(js); + return handle.getOffset() + req.pullInto.filled; } return 0; } @@ -1012,8 +1084,8 @@ ssize_t ByteQueue::desiredSize() const { return impl.desiredSize(); } -void ByteQueue::error(jsg::Lock& js, jsg::Value reason) { - impl.error(js, kj::mv(reason)); +void ByteQueue::error(jsg::Lock& js, jsg::JsValue reason) { + impl.error(js, reason); } void ByteQueue::maybeUpdateBackpressure() { @@ -1047,7 +1119,7 @@ void ByteQueue::handlePush(jsg::Lock& js, kj::Maybe queue, kj::Rc newEntry) { const auto bufferData = [&](size_t offset) { - state.queueTotalSize += newEntry->getSize() - offset; + state.queueTotalSize += newEntry->getSize(js) - offset; state.buffer.emplace_back(QueueEntry{ .entry = kj::mv(newEntry), .offset = offset, @@ -1064,7 +1136,7 @@ void ByteQueue::handlePush(jsg::Lock& js, // are >= the pending reads atLeast, then we will fulfill the pending // read, and keep fulfilling pending reads as long as they are available. // Once we are out of pending reads, we will buffer the remaining data. - auto entrySize = newEntry->getSize(); + auto entrySize = newEntry->getSize(js); auto amountAvailable = state.queueTotalSize + entrySize; size_t entryOffset = 0; @@ -1095,11 +1167,12 @@ void ByteQueue::handlePush(jsg::Lock& js, KJ_FAIL_ASSERT("The consumer is closed."); } KJ_CASE_ONEOF(entry, QueueEntry) { - auto sourcePtr = entry.entry->toArrayPtr(); + auto sourcePtr = entry.entry->toArrayPtr(js); auto sourceSize = sourcePtr.size() - entry.offset; - auto destPtr = pending.pullInto.store.asArrayPtr().slice(pending.pullInto.filled); - auto destAmount = pending.pullInto.store.size() - pending.pullInto.filled; + auto handle = pending.pullInto.store.getHandle(js); + auto destPtr = handle.asArrayPtr().slice(pending.pullInto.filled); + auto destAmount = handle.size() - pending.pullInto.filled; // sourceSize is the amount of data remaining in the current entry to copy. // destAmount is the amount of space remaining to be filled in the pending read. @@ -1131,8 +1204,10 @@ void ByteQueue::handlePush(jsg::Lock& js, // At this point, there shouldn't be any data remaining in the buffer. KJ_REQUIRE(state.queueTotalSize == 0); + auto handle = pending.pullInto.store.getHandle(js); + // And there should be data remaining in the pending pullInto destination. - KJ_REQUIRE(pending.pullInto.filled < pending.pullInto.store.size()); + KJ_REQUIRE(pending.pullInto.filled < handle.size()); // And the amountAvailable should be equal to the current push size. KJ_REQUIRE(amountAvailable == entrySize - entryOffset); @@ -1141,8 +1216,7 @@ void ByteQueue::handlePush(jsg::Lock& js, // destination pullInto by taking the lesser of amountAvailable and // destination pullInto size - filled (which gives us the amount of space // remaining in the destination). - auto amountToCopy = - kj::min(amountAvailable, pending.pullInto.store.size() - pending.pullInto.filled); + auto amountToCopy = kj::min(amountAvailable, handle.size() - pending.pullInto.filled); // The amountToCopy should not be more than the entry size minus the entryOffset // (which is the amount of data remaining to be consumed in the current entry). @@ -1151,14 +1225,14 @@ void ByteQueue::handlePush(jsg::Lock& js, // The amountToCopy plus pending.pullInto.filled should be more than or equal to atLeast // and less than or equal pending.pullInto.store.size(). KJ_REQUIRE(amountToCopy + pending.pullInto.filled >= pending.pullInto.atLeast && - amountToCopy + pending.pullInto.filled <= pending.pullInto.store.size()); + amountToCopy + pending.pullInto.filled <= handle.size()); // Awesome, so now we safely copy amountToCopy bytes from the current entry into // the remaining space in pending.pullInto.store, being careful to account for // the entryOffset and pending.pullInto.filled offsets to determine the range // where we start copying. - auto entryPtr = newEntry->toArrayPtr(); - auto destPtr = pending.pullInto.store.asArrayPtr().slice(pending.pullInto.filled); + auto entryPtr = newEntry->toArrayPtr(js); + auto destPtr = handle.asArrayPtr().slice(pending.pullInto.filled); destPtr.first(amountToCopy).copyFrom(entryPtr.slice(entryOffset).first(amountToCopy)); // Yay! this pending read has been fulfilled. There might be more tho. Let's adjust @@ -1221,7 +1295,7 @@ void ByteQueue::handleRead(jsg::Lock& js, KJ_REQUIRE(!state.buffer.empty()); // There must be at least one item in the buffer. auto& item = state.buffer.front(); - + auto handle = request.pullInto.store.getHandle(js); KJ_SWITCH_ONEOF(item) { KJ_CASE_ONEOF(c, ConsumerImpl::Close) { // We reached the end of the buffer! All data has been consumed. @@ -1230,10 +1304,10 @@ void ByteQueue::handleRead(jsg::Lock& js, KJ_CASE_ONEOF(entry, QueueEntry) { // The amount to copy is the lesser of the current entry size minus // offset and the data remaining in the destination to fill. - auto entrySize = entry.entry->getSize(); - auto amountToCopy = kj::min( - entrySize - entry.offset, request.pullInto.store.size() - request.pullInto.filled); - auto elementSize = request.pullInto.store.getElementSize(); + auto entrySize = entry.entry->getSize(js); + auto amountToCopy = + kj::min(entrySize - entry.offset, handle.size() - request.pullInto.filled); + auto elementSize = handle.getElementSize(); if (amountToCopy > elementSize) { amountToCopy -= amountToCopy % elementSize; } @@ -1243,8 +1317,8 @@ void ByteQueue::handleRead(jsg::Lock& js, // Once we have the amount, we safely copy amountToCopy bytes from the // entry into the destination request, accounting properly for the offsets. - auto sourcePtr = entry.entry->toArrayPtr().slice(entry.offset); - auto destPtr = request.pullInto.store.asArrayPtr().slice(request.pullInto.filled); + auto sourcePtr = entry.entry->toArrayPtr(js).slice(entry.offset); + auto destPtr = handle.asArrayPtr().slice(request.pullInto.filled); destPtr.first(amountToCopy).copyFrom(sourcePtr.first(amountToCopy)); @@ -1302,7 +1376,8 @@ void ByteQueue::handleRead(jsg::Lock& js, // to minimally fill this read request! The amount to copy is the lesser // of the queue total size and the maximum amount of space in the request // pull into. - if (consume(kj::min(state.queueTotalSize, request.pullInto.store.size()))) { + auto handle = request.pullInto.store.getHandle(js); + if (consume(kj::min(state.queueTotalSize, handle.size()))) { // If consume returns true, the consumer hit the end and we need to // just resolve the request as done and return. @@ -1370,11 +1445,12 @@ bool ByteQueue::handleMaybeClose(jsg::Lock& js, return true; } KJ_CASE_ONEOF(entry, QueueEntry) { - auto sourcePtr = entry.entry->toArrayPtr(); + auto sourcePtr = entry.entry->toArrayPtr(js); auto sourceSize = sourcePtr.size() - entry.offset; - auto destPtr = pending.pullInto.store.asArrayPtr().slice(pending.pullInto.filled); - auto destAmount = pending.pullInto.store.size() - pending.pullInto.filled; + auto handle = pending.pullInto.store.getHandle(js); + auto destPtr = handle.asArrayPtr().slice(pending.pullInto.filled); + auto destAmount = handle.size() - pending.pullInto.filled; // There should be space available to copy into and data to copy from, or // something else went wrong. @@ -1499,11 +1575,11 @@ kj::Maybe> ByteQueue::nextPendingByobReadRequest return kj::none; } -bool ByteQueue::hasPartiallyFulfilledRead() { +bool ByteQueue::hasPartiallyFulfilledRead(jsg::Lock& js) { KJ_IF_SOME(state, impl.getState()) { if (!state.pendingByobReadRequests.empty()) { auto& pending = state.pendingByobReadRequests.front(); - if (pending->isPartiallyFulfilled()) { + if (pending->isPartiallyFulfilled(js)) { return true; } } diff --git a/src/workerd/api/streams/queue.h b/src/workerd/api/streams/queue.h index dbd0d4f10e7..13d6fb99338 100644 --- a/src/workerd/api/streams/queue.h +++ b/src/workerd/api/streams/queue.h @@ -42,7 +42,7 @@ namespace workerd::api { // entries are freed. The underlying data is freed once the last // reference is released. // -// - Every consumer has an remaining buffer size, which is the sum of the sizes +// - Every consumer has a remaining buffer size, which is the sum of the sizes // of all entries remaining to be consumed in its internal buffer. // // - A queue has a total queue size, which is the remaining buffer size of the @@ -194,14 +194,14 @@ class QueueImpl final { // which will, in turn, reset their internal buffers and reject // all pending consume promises. // If we are already closed or errored, do nothing here. - void error(jsg::Lock& js, jsg::Value reason) { + void error(jsg::Lock& js, jsg::JsValue reason) { if (state.isActive()) { #ifdef KJ_DEBUG isClosingOrErroring = true; KJ_DEFER(isClosingOrErroring = false); #endif - allConsumers.forEach([&](ConsumerImpl& consumer) { consumer.error(js, reason.addRef(js)); }); - state.template transitionTo(kj::mv(reason)); + allConsumers.forEach([&](ConsumerImpl& consumer) { consumer.error(js, reason); }); + state.template transitionTo(reason.addRef(js)); } } @@ -274,7 +274,7 @@ class QueueImpl final { }; struct Errored { static constexpr kj::StringPtr NAME KJ_UNUSED = "errored"_kj; - jsg::Value reason; + jsg::JsRef reason; }; struct Ready final: public State { @@ -337,7 +337,7 @@ class ConsumerImpl final { public: struct StateListener { virtual void onConsumerClose(jsg::Lock& js) = 0; - virtual void onConsumerError(jsg::Lock& js, jsg::Value reason) = 0; + virtual void onConsumerError(jsg::Lock& js, jsg::JsValue reason) = 0; // Called when the consumer has a pending read and needs data. // Returns true if the pull algorithm completed synchronously (meaning // more pumping might yield additional synchronous data), false if the @@ -400,7 +400,7 @@ class ConsumerImpl final { queue = kj::none; } - void cancel(jsg::Lock& js, jsg::Optional> maybeReason) { + void cancel(jsg::Lock& js, jsg::Optional) { // Already closed or errored - nothing to do. KJ_IF_SOME(ready, state.tryGetActiveUnsafe()) { for (auto& request: ready.readRequests) { @@ -428,11 +428,11 @@ class ConsumerImpl final { return size() == 0; } - void error(jsg::Lock& js, jsg::Value reason) { + void error(jsg::Lock& js, jsg::JsValue reason) { // If we are already closed or errored, then we do nothing here. // The new error doesn't matter. if (state.isActive()) { - maybeDrainAndSetState(js, kj::mv(reason)); + maybeDrainAndSetState(js, reason); } } @@ -444,7 +444,7 @@ class ConsumerImpl final { KJ_IF_SOME(ready, state.tryGetActiveUnsafe()) { // If the consumer is already closing or the entry is empty, do nothing. // Also skip if queue is none (consumer cloned from closed stream). - if (isClosing() || entry->getSize() == 0 || queue == kj::none) { + if (isClosing() || entry->getSize(js) == 0 || queue == kj::none) { return; } @@ -458,13 +458,12 @@ class ConsumerImpl final { return request.resolveAsDone(js); } KJ_IF_SOME(errored, state.tryGetErrorUnsafe()) { - return request.reject(js, errored.reason); + return request.reject(js, errored.reason.getHandle(js)); } auto& ready = state.requireActiveUnsafe(); // Mutual exclusion with draining reads. if (ready.hasPendingDrainingRead) { - auto error = jsg::Value( - js.v8Isolate, js.typeError("Cannot call read while there is a pending draining read"_kj)); + auto error = js.typeError("Cannot call read while there is a pending draining read"_kj); return request.reject(js, error); } Self::handleRead(js, ready, *this, queue, kj::mv(request)); @@ -573,7 +572,7 @@ class ConsumerImpl final { }; struct Errored { static constexpr kj::StringPtr NAME KJ_UNUSED = "errored"_kj; - jsg::Value reason; + jsg::JsRef reason; }; struct Ready { static constexpr kj::StringPtr NAME KJ_UNUSED = "ready"_kj; @@ -636,7 +635,7 @@ class ConsumerImpl final { return result; } - void maybeDrainAndSetState(jsg::Lock& js, kj::Maybe maybeReason = kj::none) { + void maybeDrainAndSetState(jsg::Lock& js, kj::Maybe maybeReason = kj::none) { // If the state is already errored or closed then there is nothing to drain. KJ_IF_SOME(ready, state.tryGetActiveUnsafe()) { UpdateBackpressureScope scope(*this); @@ -667,7 +666,7 @@ class ConsumerImpl final { weak->runIfAlive([&](ConsumerImpl& self) { self.state.template transitionTo(reason.addRef(js)); KJ_IF_SOME(listener, self.stateListener) { - listener.onConsumerError(js, kj::mv(reason)); + listener.onConsumerError(js, reason); // After this point, we should not assume that this consumer can // be safely used at all. It's most likely the stateListener has // released it. @@ -730,8 +729,8 @@ class ValueQueue final { jsg::Promise::Resolver resolver; void resolveAsDone(jsg::Lock& js); - void resolve(jsg::Lock& js, jsg::Value value); - void reject(jsg::Lock& js, jsg::Value& value); + void resolve(jsg::Lock& js, jsg::JsValue value); + void reject(jsg::Lock& js, jsg::JsValue value); JSG_MEMORY_INFO(ValueQueue::ReadRequest) { tracker.trackField("resolver", resolver); @@ -742,12 +741,12 @@ class ValueQueue final { // calculated by the size algorithm function provided in the stream constructor. class Entry: public kj::Refcounted { public: - explicit Entry(jsg::Value value, size_t size); + explicit Entry(jsg::Lock&, jsg::JsValue value, size_t size); KJ_DISALLOW_COPY_AND_MOVE(Entry); - jsg::Value getValue(jsg::Lock& js); + jsg::JsValue getValue(jsg::Lock& js); - size_t getSize() const; + size_t getSize(jsg::Lock& js) const; void visitForGc(jsg::GcVisitor& visitor); @@ -758,7 +757,7 @@ class ValueQueue final { } private: - jsg::Value value; + jsg::JsRef value; size_t size; }; @@ -767,7 +766,8 @@ class ValueQueue final { QueueEntry clone(jsg::Lock& js); JSG_MEMORY_INFO(ValueQueue::QueueEntry) { - tracker.trackFieldWithSize("entry", entry->getSize()); + // TODO(soon): Add support for kj::Rc types in memory tracker + //tracker.trackFieldWithSize("entry", entry->getSize()); } }; @@ -782,13 +782,13 @@ class ValueQueue final { Consumer& operator=(Consumer&&) = delete; Consumer& operator=(Consumer&) = delete; - void cancel(jsg::Lock& js, jsg::Optional> maybeReason); + void cancel(jsg::Lock& js, jsg::Optional maybeReason); void close(jsg::Lock& js); - bool empty(); + bool empty() const; - void error(jsg::Lock& js, jsg::Value reason); + void error(jsg::Lock& js, jsg::JsValue reason); void read(jsg::Lock& js, ReadRequest request); @@ -805,7 +805,7 @@ class ValueQueue final { void reset(); - size_t size(); + size_t size() const; kj::Own clone( jsg::Lock& js, kj::Maybe stateListener = kj::none); @@ -823,6 +823,19 @@ class ValueQueue final { private: ConsumerImpl impl; + // Persistent continuation for the drainingRead promise transform + // (ReadResult → DrainingReadResult). Lazily initialized on first async + // drainingRead and reused for subsequent calls. + struct DrainingReadCallbacks { + kj::Rc> weakImpl; + + DrainingReadResult thenFunc(jsg::Lock& js, ReadResult result); + DrainingReadResult catchFunc(jsg::Lock& js, jsg::Value exception); + }; + using DrainingReadContinuationType = + jsg::PersistentContinuation; + kj::Maybe drainingReadContinuation; + friend class ValueQueue; }; @@ -832,7 +845,7 @@ class ValueQueue final { ssize_t desiredSize() const; - void error(jsg::Lock& js, jsg::Value reason); + void error(jsg::Lock& js, jsg::JsValue reason); void maybeUpdateBackpressure(); @@ -844,7 +857,7 @@ class ValueQueue final { bool wantsRead() const; - bool hasPartiallyFulfilledRead(); + bool hasPartiallyFulfilledRead(jsg::Lock& js); void visitForGc(jsg::GcVisitor& visitor); @@ -889,10 +902,15 @@ class ByteQueue final { kj::Maybe byobReadRequest; struct PullInto { - jsg::BufferSource store; + jsg::JsRef store; size_t filled = 0; size_t atLeast = 1; Type type = Type::DEFAULT; + // True when this read was auto-allocated on behalf of a default reader. + // Processed as BYOB by the queue (type remains BYOB), but close semantics + // follow the default reader spec: {done: true, value: undefined} instead + // of {done: true, value: }. + bool autoAllocated = false; JSG_MEMORY_INFO(ByteQueue::ReadRequest::PullInto) { tracker.trackField("store", store); @@ -905,7 +923,7 @@ class ByteQueue final { ~ReadRequest() noexcept(false); void resolveAsDone(jsg::Lock& js); void resolve(jsg::Lock& js); - void reject(jsg::Lock& js, jsg::Value& value); + void reject(jsg::Lock& js, jsg::JsValue value); kj::Own makeByobReadRequest(ConsumerImpl& consumer, QueueImpl& queue); @@ -938,7 +956,7 @@ class ByteQueue final { bool respond(jsg::Lock& js, size_t amount); - bool respondWithNewView(jsg::Lock& js, jsg::BufferSource view); + bool respondWithNewView(jsg::Lock& js, jsg::JsBufferSource view); // Disconnects this ByobRequest instance from the associated ByteQueue::ReadRequest. // The term "invalidate" is adopted from the streams spec for handling BYOB requests. @@ -948,21 +966,27 @@ class ByteQueue final { return request == kj::none; } - bool isPartiallyFulfilled(); + bool isPartiallyFulfilled(jsg::Lock& js); size_t getAtLeast() const; - v8::Local getView(jsg::Lock& js); + kj::Maybe getView(jsg::Lock& js) KJ_WARN_UNUSED_RESULT; // Returns the byte length of the original underlying ArrayBuffer. size_t getOriginalBufferByteLength(jsg::Lock& js) const; // Returns the byte offset of the original view plus bytes filled. - size_t getOriginalByteOffsetPlusBytesFilled() const; + size_t getOriginalByteOffsetPlusBytesFilled(jsg::Lock& js) const; JSG_MEMORY_INFO(ByteQueue::ByobRequest) {} private: + // These raw references are safe because ByobRequest instances are only ever + // accessed through ReadableStreamBYOBRequest (in standard.h), which guards all + // operations behind its own validity check (maybeImpl). The ReadableStreamBYOBRequest + // is invalidated when the controller is closed/errored or when the BYOB request is + // consumed, ensuring these references are never accessed after the consumer or queue + // is destroyed. kj::Maybe request; ConsumerImpl& consumer; QueueImpl& queue; @@ -980,15 +1004,15 @@ class ByteQueue final { } }; - // A byte queue entry consists of a jsg::BufferSource containing a non-zero-length + // A byte queue entry consists of a JsBufferSource containing a non-zero-length // sequence of bytes. The size is determined by the number of bytes in the entry. class Entry: public kj::Refcounted { public: - explicit Entry(jsg::BufferSource store); + explicit Entry(jsg::Lock& js, jsg::JsBufferSource store); - kj::ArrayPtr toArrayPtr(); + kj::ArrayPtr toArrayPtr(jsg::Lock& js); - size_t getSize() const; + size_t getSize(jsg::Lock& js) const; void visitForGc(jsg::GcVisitor& visitor); @@ -999,7 +1023,7 @@ class ByteQueue final { } private: - jsg::BufferSource store; + jsg::JsRef store; }; struct QueueEntry { @@ -1009,7 +1033,8 @@ class ByteQueue final { QueueEntry clone(jsg::Lock& js); JSG_MEMORY_INFO(ByteQueue::QueueEntry) { - tracker.trackFieldWithSize("entry", entry->getSize()); + // TODO(soon): Add support for kj::Rc types to memory tracker + //tracker.trackFieldWithSize("entry", entry->getSize()); } }; @@ -1024,13 +1049,13 @@ class ByteQueue final { Consumer& operator=(Consumer&&) = delete; Consumer& operator=(Consumer&) = delete; - void cancel(jsg::Lock& js, jsg::Optional> maybeReason); + void cancel(jsg::Lock& js, jsg::Optional maybeReason); void close(jsg::Lock& js); bool empty() const; - void error(jsg::Lock& js, jsg::Value reason); + void error(jsg::Lock& js, jsg::JsValue reason); void read(jsg::Lock& js, ReadRequest request); @@ -1062,6 +1087,16 @@ class ByteQueue final { private: ConsumerImpl impl; + + struct DrainingReadCallbacks { + kj::Rc> weakImpl; + + DrainingReadResult thenFunc(jsg::Lock& js, ReadResult result); + DrainingReadResult catchFunc(jsg::Lock& js, jsg::Value exception); + }; + using DrainingReadContinuationType = + jsg::PersistentContinuation; + kj::Maybe drainingReadContinuation; }; explicit ByteQueue(size_t highWaterMark); @@ -1070,7 +1105,7 @@ class ByteQueue final { ssize_t desiredSize() const; - void error(jsg::Lock& js, jsg::Value reason); + void error(jsg::Lock& js, jsg::JsValue reason); void maybeUpdateBackpressure(); @@ -1082,7 +1117,7 @@ class ByteQueue final { bool wantsRead() const; - bool hasPartiallyFulfilledRead(); + bool hasPartiallyFulfilledRead(jsg::Lock& js); // nextPendingByobReadRequest will be used to support the ReadableStreamBYOBRequest interface // that is part of ReadableByteStreamController. When user code calls the `controller.byobRequest` diff --git a/src/workerd/api/streams/readable-source-adapter-test.c++ b/src/workerd/api/streams/readable-source-adapter-test.c++ index 0a57c29bf01..b8125b4439e 100644 --- a/src/workerd/api/streams/readable-source-adapter-test.c++ +++ b/src/workerd/api/streams/readable-source-adapter-test.c++ @@ -114,9 +114,10 @@ KJ_TEST("Adapter shutdown with no reads") { adapter->shutdown(env.js); // second call is no-op // Read after shutdown should be resolved immediate + auto u8 = jsg::JsUint8Array::create(env.js, 10); auto read = adapter->read(env.js, ReadableStreamSourceJsAdapter::ReadOptions{ - .buffer = jsg::BufferSource(env.js, jsg::BackingStore::alloc(env.js, 10)), + .buffer = jsg::JsArrayBufferView(u8).addRef(env.js), }); KJ_ASSERT(read.getState(env.js) == jsg::Promise::State::FULFILLED, @@ -144,9 +145,10 @@ KJ_TEST("Adapter cancel with no reads") { adapter->cancel(env.js, env.js.error("boom")); + auto u8 = jsg::JsUint8Array::create(env.js, 10); auto read = adapter->read(env.js, ReadableStreamSourceJsAdapter::ReadOptions{ - .buffer = jsg::BufferSource(env.js, jsg::BackingStore::alloc(env.js, 10)), + .buffer = jsg::JsArrayBufferView(u8).addRef(env.js), }); KJ_ASSERT(read.getState(env.js) == jsg::Promise::State::REJECTED, @@ -200,25 +202,21 @@ KJ_TEST("Adapter with single read (ArrayBuffer)") { KJ_ASSERT( adapter->isCanceled() == kj::none, "Adapter should not be canceled upon construction"); - const size_t bufferSize = 10; - auto backing = jsg::BackingStore::alloc(env.js, bufferSize); + auto u8 = jsg::JsUint8Array::create(env.js, 10); return env.context .awaitJs(env.js, adapter ->read(env.js, ReadableStreamSourceJsAdapter::ReadOptions{ - .buffer = jsg::BufferSource(env.js, kj::mv(backing)), + .buffer = jsg::JsArrayBufferView(u8).addRef(env.js), .minBytes = 5, }) .then(env.js, [](jsg::Lock& js, auto result) { - KJ_ASSERT(!result.done, "Stream should not be done yet"); - KJ_ASSERT(result.buffer.asArrayPtr().size() == 10, "Read buffer should be full size"); - KJ_ASSERT(result.buffer.asArrayPtr() == "aaaaaaaaaa"_kjb); - - // BufferSource should be an ArrayBuffer auto handle = result.buffer.getHandle(js); - KJ_ASSERT(handle->IsArrayBuffer()); + KJ_ASSERT(!result.done, "Stream should not be done yet"); + KJ_ASSERT(handle.asArrayPtr().size() == 10, "Read buffer should be full size"); + KJ_ASSERT(handle.asArrayPtr() == "aaaaaaaaaa"_kjb); })).attach(kj::mv(adapter)); }); } @@ -236,25 +234,22 @@ KJ_TEST("Adapter with single read (Uint8Array)") { KJ_ASSERT( adapter->isCanceled() == kj::none, "Adapter should not be canceled upon construction"); - const size_t bufferSize = 10; - auto backing = jsg::BackingStore::alloc(env.js, bufferSize); + auto u8 = jsg::JsUint8Array::create(env.js, 10); return env.context .awaitJs(env.js, adapter ->read(env.js, ReadableStreamSourceJsAdapter::ReadOptions{ - .buffer = jsg::BufferSource(env.js, kj::mv(backing)), + .buffer = jsg::JsArrayBufferView(u8).addRef(env.js), .minBytes = 5, }) .then(env.js, [](jsg::Lock& js, auto result) { - KJ_ASSERT(!result.done, "Stream should not be done yet"); - KJ_ASSERT(result.buffer.asArrayPtr().size() == 10, "Read buffer should be full size"); - KJ_ASSERT(result.buffer.asArrayPtr() == "aaaaaaaaaa"_kjb); - - // BufferSource should be an ArrayBuffer auto handle = result.buffer.getHandle(js); - KJ_ASSERT(handle->IsUint8Array()); + KJ_ASSERT(!result.done, "Stream should not be done yet"); + KJ_ASSERT(handle.asArrayPtr().size() == 10, "Read buffer should be full size"); + KJ_ASSERT(handle.asArrayPtr() == "aaaaaaaaaa"_kjb); + KJ_ASSERT(handle.isUint8Array()); })).attach(kj::mv(adapter)); }); } @@ -272,25 +267,24 @@ KJ_TEST("Adapter with single read (Int32Array)") { KJ_ASSERT( adapter->isCanceled() == kj::none, "Adapter should not be canceled upon construction"); - const size_t bufferSize = 16; - auto backing = jsg::BackingStore::alloc(env.js, bufferSize); + auto ab = jsg::JsArrayBuffer::create(env.js, 16); + auto i32 = v8::Int32Array::New(ab, 0, 4); + auto i32View = jsg::JsArrayBufferView(i32); return env.context .awaitJs(env.js, adapter ->read(env.js, ReadableStreamSourceJsAdapter::ReadOptions{ - .buffer = jsg::BufferSource(env.js, kj::mv(backing)), + .buffer = i32View.addRef(env.js), .minBytes = 5, }) .then(env.js, [](jsg::Lock& js, auto result) { - KJ_ASSERT(!result.done, "Stream should not be done yet"); - KJ_ASSERT(result.buffer.asArrayPtr().size() == 16, "Read buffer should be full size"); - KJ_ASSERT(result.buffer.asArrayPtr() == "aaaaaaaaaaaaaaaa"_kjb); - - // BufferSource should be an ArrayBuffer auto handle = result.buffer.getHandle(js); - KJ_ASSERT(handle->IsInt32Array()); + KJ_ASSERT(!result.done, "Stream should not be done yet"); + KJ_ASSERT(handle.asArrayPtr().size() == 16, "Read buffer should be full size"); + KJ_ASSERT(handle.asArrayPtr() == "aaaaaaaaaaaaaaaa"_kjb); + KJ_ASSERT(handle.isInt32Array()); })).attach(kj::mv(adapter)); }); } @@ -308,24 +302,21 @@ KJ_TEST("Adapter with single large read (ArrayBuffer)") { KJ_ASSERT( adapter->isCanceled() == kj::none, "Adapter should not be canceled upon construction"); - const size_t bufferSize = 16 * 1024; - auto backing = jsg::BackingStore::alloc(env.js, bufferSize); + auto u8 = jsg::JsUint8Array::create(env.js, 16 * 1024); return env.context .awaitJs(env.js, adapter ->read(env.js, ReadableStreamSourceJsAdapter::ReadOptions{ - .buffer = jsg::BufferSource(env.js, kj::mv(backing)), + .buffer = jsg::JsArrayBufferView(u8).addRef(env.js), .minBytes = 5, }) .then(env.js, [](jsg::Lock& js, auto result) { - KJ_ASSERT(!result.done, "Stream should not be done yet"); - KJ_ASSERT(result.buffer.asArrayPtr().size() == 16 * 1024, "Read buffer should be full size"); - - // BufferSource should be an ArrayBuffer auto handle = result.buffer.getHandle(js); - KJ_ASSERT(handle->IsArrayBuffer()); + KJ_ASSERT(!result.done, "Stream should not be done yet"); + KJ_ASSERT(handle.asArrayPtr().size() == 16 * 1024, "Read buffer should be full size"); + KJ_ASSERT(handle.isUint8Array()); })).attach(kj::mv(adapter)); }); } @@ -343,24 +334,21 @@ KJ_TEST("Adapter with single small read (ArrayBuffer)") { KJ_ASSERT( adapter->isCanceled() == kj::none, "Adapter should not be canceled upon construction"); - const size_t bufferSize = 1; - auto backing = jsg::BackingStore::alloc(env.js, bufferSize); + auto u8 = jsg::JsUint8Array::create(env.js, 1); return env.context .awaitJs(env.js, adapter ->read(env.js, ReadableStreamSourceJsAdapter::ReadOptions{ - .buffer = jsg::BufferSource(env.js, kj::mv(backing)), + .buffer = jsg::JsArrayBufferView(u8).addRef(env.js), .minBytes = 5, }) .then(env.js, [](jsg::Lock& js, auto result) { - KJ_ASSERT(!result.done, "Stream should not be done yet"); - KJ_ASSERT(result.buffer.asArrayPtr().size() == 1, "Read buffer should be full size"); - - // BufferSource should be an ArrayBuffer auto handle = result.buffer.getHandle(js); - KJ_ASSERT(handle->IsArrayBuffer()); + KJ_ASSERT(!result.done, "Stream should not be done yet"); + KJ_ASSERT(handle.asArrayPtr().size() == 1, "Read buffer should be full size"); + KJ_ASSERT(handle.isUint8Array()); })).attach(kj::mv(adapter)); }); } @@ -378,23 +366,20 @@ KJ_TEST("Adapter with minimal reads (Uint8Array)") { KJ_ASSERT( adapter->isCanceled() == kj::none, "Adapter should not be canceled upon construction"); - const size_t bufferSize = 10; - auto backing = jsg::BackingStore::alloc(env.js, bufferSize); + auto u8 = jsg::JsUint8Array::create(env.js, 10); auto promise = adapter ->read(env.js, ReadableStreamSourceJsAdapter::ReadOptions{ - .buffer = jsg::BufferSource(env.js, kj::mv(backing)), + .buffer = jsg::JsArrayBufferView(u8).addRef(env.js), .minBytes = 3, }) .then(env.js, [](jsg::Lock& js, auto result) { - KJ_ASSERT(!result.done, "Stream should not be done yet"); - KJ_ASSERT(result.buffer.asArrayPtr().size() == 3, "Read buffer should be three bytes"); - KJ_ASSERT(result.buffer.asArrayPtr() == "aaa"_kjb); - - // BufferSource should be an ArrayBuffer auto handle = result.buffer.getHandle(js); - KJ_ASSERT(handle->IsUint8Array()); + KJ_ASSERT(!result.done, "Stream should not be done yet"); + KJ_ASSERT(handle.asArrayPtr().size() == 3, "Read buffer should be three bytes"); + KJ_ASSERT(handle.asArrayPtr() == "aaa"_kjb); + KJ_ASSERT(handle.isUint8Array()); }); return env.context.awaitJs(env.js, kj::mv(promise)).attach(kj::mv(adapter)); @@ -414,23 +399,22 @@ KJ_TEST("Adapter with minimal reads (Uint32Array)") { KJ_ASSERT( adapter->isCanceled() == kj::none, "Adapter should not be canceled upon construction"); - const size_t bufferSize = 16; - auto backing = jsg::BackingStore::alloc(env.js, bufferSize); + auto ab = jsg::JsArrayBuffer::create(env.js, 16); + auto u32 = v8::Uint32Array::New(ab, 0, 4); + auto u32View = jsg::JsArrayBufferView(u32); auto promise = adapter ->read(env.js, ReadableStreamSourceJsAdapter::ReadOptions{ - .buffer = jsg::BufferSource(env.js, kj::mv(backing)), + .buffer = u32View.addRef(env.js), .minBytes = 3, // Impl with round up to 4 }) .then(env.js, [](jsg::Lock& js, auto result) { - KJ_ASSERT(!result.done, "Stream should not be done yet"); - KJ_ASSERT(result.buffer.asArrayPtr().size() == 4, "Read buffer should be four bytes"); - KJ_ASSERT(result.buffer.asArrayPtr() == "aaaa"_kjb); - - // BufferSource should be an ArrayBuffer auto handle = result.buffer.getHandle(js); - KJ_ASSERT(handle->IsUint32Array()); + KJ_ASSERT(!result.done, "Stream should not be done yet"); + KJ_ASSERT(handle.asArrayPtr().size() == 4, "Read buffer should be four bytes"); + KJ_ASSERT(handle.asArrayPtr() == "aaaa"_kjb); + KJ_ASSERT(handle.isUint32Array()); }); return env.context.awaitJs(env.js, kj::mv(promise)).attach(kj::mv(adapter)); @@ -450,23 +434,22 @@ KJ_TEST("Adapter with over large min reads (Uint32Array)") { KJ_ASSERT( adapter->isCanceled() == kj::none, "Adapter should not be canceled upon construction"); - const size_t bufferSize = 16; - auto backing = jsg::BackingStore::alloc(env.js, bufferSize); + auto ab = jsg::JsArrayBuffer::create(env.js, 16); + auto u32 = v8::Uint32Array::New(ab, 0, 4); + auto u32View = jsg::JsArrayBufferView(u32); auto promise = adapter ->read(env.js, ReadableStreamSourceJsAdapter::ReadOptions{ - .buffer = jsg::BufferSource(env.js, kj::mv(backing)), + .buffer = u32View.addRef(env.js), .minBytes = 24, // Impl with round up to 4 }) .then(env.js, [](jsg::Lock& js, auto result) { - KJ_ASSERT(!result.done, "Stream should not be done yet"); - KJ_ASSERT(result.buffer.asArrayPtr().size() == 16, "Read buffer should be four bytes"); - KJ_ASSERT(result.buffer.asArrayPtr() == "aaaaaaaaaaaaaaaa"_kjb); - - // BufferSource should be an ArrayBuffer auto handle = result.buffer.getHandle(js); - KJ_ASSERT(handle->IsUint32Array()); + KJ_ASSERT(!result.done, "Stream should not be done yet"); + KJ_ASSERT(handle.asArrayPtr().size() == 16, "Read buffer should be four bytes"); + KJ_ASSERT(handle.asArrayPtr() == "aaaaaaaaaaaaaaaa"_kjb); + KJ_ASSERT(handle.isUint32Array()); }); return env.context.awaitJs(env.js, kj::mv(promise)).attach(kj::mv(adapter)); @@ -484,19 +467,18 @@ KJ_TEST("Adapter with over large min reads (Uint32Array)") { KJ_ASSERT( adapter->isCanceled() == kj::none, "Adapter should not be canceled upon construction"); - const size_t bufferSize = 1; - auto backing = jsg::BackingStore::alloc(env.js, bufferSize); + auto u8 = jsg::JsUint8Array::create(env.js, 1); auto promise = adapter ->read(env.js, ReadableStreamSourceJsAdapter::ReadOptions{ - .buffer = jsg::BufferSource(env.js, kj::mv(backing)), + .buffer = jsg::JsArrayBufferView(u8).addRef(env.js), }) .then(env.js, [](jsg::Lock& js, auto result) { - KJ_ASSERT(result.done, "Stream should be done"); - KJ_ASSERT(result.buffer.asArrayPtr().size() == 0, "Read buffer should be 0 bytes"); auto handle = result.buffer.getHandle(js); - KJ_ASSERT(handle->IsArrayBuffer()); + KJ_ASSERT(result.done, "Stream should be done"); + KJ_ASSERT(handle.asArrayPtr().size() == 0, "Read buffer should be 0 bytes"); + KJ_ASSERT(handle.isUint8Array()); }); return env.context.awaitJs(env.js, kj::mv(promise)).attach(kj::mv(adapter)); @@ -518,20 +500,21 @@ KJ_TEST("Adapter with multiple reads (Uint8Array)") { const size_t bufferSize = 10; + auto u81 = jsg::JsUint8Array::create(env.js, bufferSize); + auto u82 = jsg::JsUint8Array::create(env.js, bufferSize); + auto u83 = jsg::JsUint8Array::create(env.js, bufferSize); + auto read1 = adapter->read(env.js, ReadableStreamSourceJsAdapter::ReadOptions{ - .buffer = jsg::BufferSource( - env.js, jsg::BackingStore::alloc(env.js, bufferSize)), + .buffer = jsg::JsArrayBufferView(u81).addRef(env.js), }); auto read2 = adapter->read(env.js, ReadableStreamSourceJsAdapter::ReadOptions{ - .buffer = jsg::BufferSource( - env.js, jsg::BackingStore::alloc(env.js, bufferSize)), + .buffer = jsg::JsArrayBufferView(u82).addRef(env.js), }); auto read3 = adapter->read(env.js, ReadableStreamSourceJsAdapter::ReadOptions{ - .buffer = jsg::BufferSource( - env.js, jsg::BackingStore::alloc(env.js, bufferSize)), + .buffer = jsg::JsArrayBufferView(u83).addRef(env.js), }); return env.context @@ -539,20 +522,23 @@ KJ_TEST("Adapter with multiple reads (Uint8Array)") { read1 .then(env.js, [read2 = kj::mv(read2)](jsg::Lock& js, auto result) mutable { + auto handle = result.buffer.getHandle(js); KJ_ASSERT(!result.done, "Stream should not be done yet"); - KJ_ASSERT(result.buffer.asArrayPtr().size() == 10, "Read buffer should be full size"); - KJ_ASSERT(result.buffer.asArrayPtr() == "aaaaaaaaaa"_kjb); + KJ_ASSERT(handle.asArrayPtr().size() == 10, "Read buffer should be full size"); + KJ_ASSERT(handle.asArrayPtr() == "aaaaaaaaaa"_kjb); return kj::mv(read2); }) .then(env.js, [read3 = kj::mv(read3)](jsg::Lock& js, auto result) mutable { + auto handle = result.buffer.getHandle(js); KJ_ASSERT(!result.done, "Stream should not be done yet"); - KJ_ASSERT(result.buffer.asArrayPtr().size() == 10, "Read buffer should be full size"); - KJ_ASSERT(result.buffer.asArrayPtr() == "aaaaaaaaaa"_kjb); + KJ_ASSERT(handle.asArrayPtr().size() == 10, "Read buffer should be full size"); + KJ_ASSERT(handle.asArrayPtr() == "aaaaaaaaaa"_kjb); return kj::mv(read3); }).then(env.js, [](jsg::Lock& js, auto result) mutable { + auto handle = result.buffer.getHandle(js); KJ_ASSERT(!result.done, "Stream should not be done yet"); - KJ_ASSERT(result.buffer.asArrayPtr().size() == 10, "Read buffer should be full size"); - KJ_ASSERT(result.buffer.asArrayPtr() == "aaaaaaaaaa"_kjb); + KJ_ASSERT(handle.asArrayPtr().size() == 10, "Read buffer should be full size"); + KJ_ASSERT(handle.asArrayPtr() == "aaaaaaaaaa"_kjb); return js.resolvedPromise(); })).attach(kj::mv(adapter)); }); @@ -573,20 +559,21 @@ KJ_TEST("Adapter with multiple reads shutdown") { const size_t bufferSize = 10; + auto u81 = jsg::JsUint8Array::create(env.js, bufferSize); + auto u82 = jsg::JsUint8Array::create(env.js, bufferSize); + auto u83 = jsg::JsUint8Array::create(env.js, bufferSize); + auto read1 = adapter->read(env.js, ReadableStreamSourceJsAdapter::ReadOptions{ - .buffer = jsg::BufferSource( - env.js, jsg::BackingStore::alloc(env.js, bufferSize)), + .buffer = jsg::JsArrayBufferView(u81).addRef(env.js), }); auto read2 = adapter->read(env.js, ReadableStreamSourceJsAdapter::ReadOptions{ - .buffer = jsg::BufferSource( - env.js, jsg::BackingStore::alloc(env.js, bufferSize)), + .buffer = jsg::JsArrayBufferView(u82).addRef(env.js), }); auto read3 = adapter->read(env.js, ReadableStreamSourceJsAdapter::ReadOptions{ - .buffer = jsg::BufferSource( - env.js, jsg::BackingStore::alloc(env.js, bufferSize)), + .buffer = jsg::JsArrayBufferView(u83).addRef(env.js), }); adapter->shutdown(env.js); @@ -634,20 +621,21 @@ KJ_TEST("Adapter with multiple reads cancel") { const size_t bufferSize = 10; + auto u81 = jsg::JsUint8Array::create(env.js, bufferSize); + auto u82 = jsg::JsUint8Array::create(env.js, bufferSize); + auto u83 = jsg::JsUint8Array::create(env.js, bufferSize); + auto read1 = adapter->read(env.js, ReadableStreamSourceJsAdapter::ReadOptions{ - .buffer = jsg::BufferSource( - env.js, jsg::BackingStore::alloc(env.js, bufferSize)), + .buffer = jsg::JsArrayBufferView(u81).addRef(env.js), }); auto read2 = adapter->read(env.js, ReadableStreamSourceJsAdapter::ReadOptions{ - .buffer = jsg::BufferSource( - env.js, jsg::BackingStore::alloc(env.js, bufferSize)), + .buffer = jsg::JsArrayBufferView(u82).addRef(env.js), }); auto read3 = adapter->read(env.js, ReadableStreamSourceJsAdapter::ReadOptions{ - .buffer = jsg::BufferSource( - env.js, jsg::BackingStore::alloc(env.js, bufferSize)), + .buffer = jsg::JsArrayBufferView(u83).addRef(env.js), }); adapter->cancel(env.js, env.js.error("boom")); @@ -699,9 +687,11 @@ KJ_TEST("Adapter close after read") { auto adapter = kj::heap( env.js, env.context, newReadableSource(kj::mv(fake))); + auto u8 = jsg::JsUint8Array::create(env.js, 10); + auto read = adapter->read(env.js, ReadableStreamSourceJsAdapter::ReadOptions{ - .buffer = jsg::BufferSource(env.js, jsg::BackingStore::alloc(env.js, 10)), + .buffer = jsg::JsArrayBufferView(u8).addRef(env.js), }); auto closePromise = adapter->close(env.js); @@ -731,9 +721,11 @@ KJ_TEST("Adapter close") { auto closePromise = adapter->close(env.js); // reads after close should be resoved immediately. + auto u8 = jsg::JsUint8Array::create(env.js, 10); + auto read = adapter->read(env.js, ReadableStreamSourceJsAdapter::ReadOptions{ - .buffer = jsg::BufferSource(env.js, jsg::BackingStore::alloc(env.js, 10)), + .buffer = jsg::JsArrayBufferView(u8).addRef(env.js), }); KJ_ASSERT(read.getState(env.js) == jsg::Promise::State::FULFILLED, @@ -784,22 +776,22 @@ KJ_TEST("After read BackingStore maintains identity") { std::unique_ptr backing = v8::ArrayBuffer::NewBackingStore(env.js.v8Isolate, 10); auto* backingPtr = backing.get(); - v8::Local originalArrayBuffer = - v8::ArrayBuffer::New(env.js.v8Isolate, kj::mv(backing)); - jsg::BufferSource source(env.js, originalArrayBuffer); + auto ab = jsg::JsArrayBuffer::create(env.js, kj::mv(backing)); + auto u8 = jsg::JsUint8Array::create(env.js, ab); return env.context .awaitJs(env.js, adapter ->read(env.js, ReadableStreamSourceJsAdapter::ReadOptions{ - .buffer = jsg::BufferSource(env.js, originalArrayBuffer), + .buffer = jsg::JsArrayBufferView(u8).addRef(env.js), .minBytes = 5, }) .then(env.js, [backingPtr](jsg::Lock& js, auto result) { auto handle = result.buffer.getHandle(js); - KJ_ASSERT(handle->IsArrayBuffer()); - auto backing = handle.template As()->GetBackingStore(); + KJ_ASSERT(handle.isUint8Array()); + v8::Local buf = handle.getBuffer(); + auto backing = buf->GetBackingStore(); KJ_ASSERT(backing.get() == backingPtr); return js.resolvedPromise(); })).attach(kj::mv(adapter)); @@ -838,10 +830,10 @@ KJ_TEST("Read all bytes") { return env.context .awaitJs(env.js, - adapter->readAllBytes(env.js).then( - env.js, [&adapter = *adapter](jsg::Lock& js, jsg::BufferSource result) { + adapter->readAllBytes(env.js).then(env.js, + [&adapter = *adapter](jsg::Lock& js, jsg::JsRef result) { // With exponential growth strategy: 1024 + 2048 + 4096 + 8192 = 15360 - KJ_ASSERT(result.size() == 15360); + KJ_ASSERT(result.getHandle(js).size() == 15360); KJ_ASSERT(adapter.isClosed(), "Adapter should be closed after readAllText()"); })).attach(kj::mv(adapter)); }); @@ -926,31 +918,31 @@ KJ_TEST("tee successful") { KJ_ASSERT(!branch2->isClosed(), "Branch2 should not be closed after tee"); KJ_ASSERT(branch2->isCanceled() == kj::none, "Branch2 should not be canceled after tee"); - auto backing1 = jsg::BackingStore::alloc(env.js, 11); - auto buffer1 = jsg::BufferSource(env.js, kj::mv(backing1)); + auto u81 = jsg::JsUint8Array::create(env.js, 11); + auto u82 = jsg::JsUint8Array::create(env.js, 11); auto read1 = branch1->read(env.js, ReadableStreamSourceJsAdapter::ReadOptions{ - .buffer = kj::mv(buffer1), + .buffer = jsg::JsArrayBufferView(u81).addRef(env.js), }); - auto backing2 = jsg::BackingStore::alloc(env.js, 11); - auto buffer2 = jsg::BufferSource(env.js, kj::mv(backing2)); auto read2 = branch2->read(env.js, ReadableStreamSourceJsAdapter::ReadOptions{ - .buffer = kj::mv(buffer2), + .buffer = jsg::JsArrayBufferView(u82).addRef(env.js), }); return env.context .awaitJs(env.js, kj::mv(read1) .then(env.js, [read2 = kj::mv(read2)](jsg::Lock& js, auto result1) mutable { + auto handle = result1.buffer.getHandle(js); KJ_ASSERT(!result1.done, "Stream should not be done yet"); - KJ_ASSERT(result1.buffer.asArrayPtr().size() == 11); - KJ_ASSERT(result1.buffer.asArrayPtr() == "hello world"_kjb); + KJ_ASSERT(handle.asArrayPtr().size() == 11); + KJ_ASSERT(handle.asArrayPtr() == "hello world"_kjb); return kj::mv(read2); }).then(env.js, [](jsg::Lock& js, auto result2) { + auto handle = result2.buffer.getHandle(js); KJ_ASSERT(!result2.done, "Stream should not be done yet"); - KJ_ASSERT(result2.buffer.asArrayPtr().size() == 11); - KJ_ASSERT(result2.buffer.asArrayPtr() == "hello world"_kjb); + KJ_ASSERT(handle.asArrayPtr().size() == 11); + KJ_ASSERT(handle.asArrayPtr() == "hello world"_kjb); return js.resolvedPromise(); })).attach(kj::mv(branch1), kj::mv(branch2)); }); @@ -974,10 +966,9 @@ jsg::Ref createFiniteBytesReadableStream( KJ_ASSERT_NONNULL(controller.template tryGet>())); auto& counter = *count; if (counter++ < 10) { - auto backing = jsg::BackingStore::alloc(js, chunkSize); - jsg::BufferSource buffer(js, kj::mv(backing)); - buffer.asArrayPtr().fill(96 + counter); // fill with 'a'...'j' - c->enqueue(js, buffer.getHandle(js)); + auto ab = jsg::JsArrayBuffer::create(js, chunkSize); + ab.asArrayPtr().fill(96 + counter); // fill with 'a'...'j' + c->enqueue(js, ab); } if (counter == 10) { c->close(js); @@ -1001,9 +992,7 @@ jsg::Ref createFiniteByobReadableStream(jsg::Lock& js, size_t ch KJ_ASSERT_NONNULL(controller.template tryGet>())); static int count = 0; if (count++ < 10) { - auto backing = jsg::BackingStore::alloc(js, chunkSize); - jsg::BufferSource buffer(js, kj::mv(backing)); - c->enqueue(js, kj::mv(buffer)); + c->enqueue(js, jsg::JsArrayBuffer::create(js, chunkSize)); } if (count == 10) { c->close(js); @@ -1587,10 +1576,9 @@ KJ_TEST("KjAdapter MinReadPolicy IMMEDIATE behavior") { controller.template tryGet>()); if (counter < 8) { // Return 256 bytes per chunk, 8 chunks total (2048 bytes) - auto backing = jsg::BackingStore::alloc(js, 256); - jsg::BufferSource buffer(js, kj::mv(backing)); - buffer.asArrayPtr().fill(97 + counter); // 'a', 'b', 'c', etc. - c->enqueue(js, buffer.getHandle(js)); + auto ab = jsg::JsArrayBuffer::create(js, 256); + ab.asArrayPtr().fill(97 + counter); // 'a', 'b', 'c', etc. + c->enqueue(js, ab); counter++; } else { c->close(js); @@ -1643,10 +1631,9 @@ KJ_TEST("KjAdapter MinReadPolicy OPPORTUNISTIC behavior") { if (counter < 8) { // Return 256 bytes per chunk, 8 chunks total (2048 bytes) - auto backing = jsg::BackingStore::alloc(js, 256); - jsg::BufferSource buffer(js, kj::mv(backing)); - buffer.asArrayPtr().fill(97 + counter); // 'a', 'b', 'c', etc. - c->enqueue(js, buffer.getHandle(js)); + auto ab = jsg::JsArrayBuffer::create(js, 256); + ab.asArrayPtr().fill(97 + counter); // 'a', 'b', 'c', etc. + c->enqueue(js, ab); counter++; } else { c->close(js); diff --git a/src/workerd/api/streams/readable-source-adapter.c++ b/src/workerd/api/streams/readable-source-adapter.c++ index 6e5e81b2032..0164c255697 100644 --- a/src/workerd/api/streams/readable-source-adapter.c++ +++ b/src/workerd/api/streams/readable-source-adapter.c++ @@ -15,13 +15,10 @@ namespace { // does that. It takes the original allocation and wraps it into a new ArrayBuffer // instance that is wrapped by a zero-length view of the same type as the original // TypedArray we were given. -jsg::BufferSource transferToEmptyBuffer(jsg::Lock& js, jsg::BufferSource buffer) { - KJ_DASSERT(!buffer.isDetached() && buffer.canDetach(js)); - auto backing = buffer.detach(js); - backing.limit(0); - auto buf = jsg::BufferSource(js, kj::mv(backing)); - KJ_DASSERT(buf.size() == 0); - return kj::mv(buf); +jsg::JsArrayBufferView transferToEmptyBuffer(jsg::Lock& js, jsg::JsArrayBufferView buffer) { + KJ_DASSERT(!buffer.isDetached() && buffer.isDetachable()); + auto backing = buffer.detachAndTake(js); + return backing.slice(js, 0, 0); } } // namespace @@ -168,11 +165,12 @@ jsg::Promise ReadableStreamSourceJsAd return js.rejectedPromise(js.exceptionToJs(exception.clone())); } + auto buffer = options.buffer.getHandle(js); if (state.is()) { // We are already in a closed state. This is a no-op, just return // an empty buffer. return js.resolvedPromise(ReadResult{ - .buffer = transferToEmptyBuffer(js, kj::mv(options.buffer)), + .buffer = transferToEmptyBuffer(js, buffer).addRef(js), .done = true, }); } @@ -185,7 +183,7 @@ jsg::Promise ReadableStreamSourceJsAd // Treat them as if the stream is closed. if (active.closePending) { return js.resolvedPromise(ReadResult{ - .buffer = transferToEmptyBuffer(js, kj::mv(options.buffer)), + .buffer = transferToEmptyBuffer(js, buffer).addRef(js), .done = true, }); } @@ -193,14 +191,10 @@ jsg::Promise ReadableStreamSourceJsAd // Ok, we are in a readable state, there are no pending closes. // Let's enqueue our read request. auto& ioContext = IoContext::current(); - - auto buffer = kj::mv(options.buffer); auto elementSize = buffer.getElementSize(); // The buffer size should always be a multiple of the element size and should - // always be at least as large as minBytes. This should be handled for us by - // the jsg::BufferSource, but just to be safe, we will double-check with a - // debug assert here. + // always be at least as large as minBytes. KJ_DASSERT(buffer.size() % elementSize == 0); auto minBytes = kj::min(options.minBytes.orDefault(elementSize), buffer.size()); @@ -231,41 +225,43 @@ jsg::Promise ReadableStreamSourceJsAd })); return ioContext .awaitIo(js, kj::mv(promise), - [buffer = kj::mv(buffer), self = selfRef.addRef()](jsg::Lock& js, - size_t bytesRead) mutable -> jsg::Promise { - // If the bytesRead is 0, that indicates the stream is closed. We will - // move the stream to a closed state and return the empty buffer. - if (bytesRead == 0) { - self->runIfAlive([](ReadableStreamSourceJsAdapter& self) { - KJ_IF_SOME(open, self.state.tryGetActiveUnsafe()) { - open.active->closePending = true; - } - }); - return js.resolvedPromise(ReadResult{ - .buffer = transferToEmptyBuffer(js, kj::mv(buffer)), - .done = true, - }); - } - KJ_DASSERT(bytesRead <= buffer.size()); - - // If bytesRead is not a multiple of the element size, that indicates - // that the source either read less than minBytes (and ended), or is - // simply unable to satisfy the element size requirement. We cannot - // provide a partial element to the caller, so reject the read. - if (bytesRead % buffer.getElementSize() != 0) { - return js.rejectedPromise( - js.typeError(kj::str("The underlying stream failed to provide a multiple of the " - "target element size ", - buffer.getElementSize()))); - } - - auto backing = buffer.detach(js); - backing.limit(bytesRead); - return js.resolvedPromise(ReadResult{ - .buffer = jsg::BufferSource(js, kj::mv(backing)), - .done = false, - }); - }) + JSG_VISITABLE_LAMBDA((buffer = buffer.addRef(js), self = selfRef.addRef()), (buffer), + (jsg::Lock & js, size_t bytesRead) mutable + ->jsg::Promise { + // If the bytesRead is 0, that indicates the stream is closed. We will + // move the stream to a closed state and return the empty buffer. + auto handle = buffer.getHandle(js); + if (bytesRead == 0) { + self->runIfAlive([](ReadableStreamSourceJsAdapter& self) { + KJ_IF_SOME(open, self.state.tryGetActiveUnsafe()) { + open.active->closePending = true; + } else { + } + }); + return js.resolvedPromise(ReadResult{ + .buffer = transferToEmptyBuffer(js, handle).addRef(js), + .done = true, + }); + } + KJ_DASSERT(bytesRead <= handle.size()); + + // If bytesRead is not a multiple of the element size, that indicates + // that the source either read less than minBytes (and ended), or is + // simply unable to satisfy the element size requirement. We cannot + // provide a partial element to the caller, so reject the read. + if (bytesRead % handle.getElementSize() != 0) { + return js.rejectedPromise(js.typeError( + kj::str("The underlying stream failed to provide a multiple of the " + "target element size ", + handle.getElementSize()))); + } + + auto backing = handle.detachAndTake(js); + return js.resolvedPromise(ReadResult{ + .buffer = backing.slice(js, 0, bytesRead).addRef(js), + .done = false, + }); + })) .catch_(js, [self = selfRef.addRef()]( jsg::Lock& js, jsg::Value exception) -> ReadableStreamSourceJsAdapter::ReadResult { @@ -329,7 +325,7 @@ jsg::Promise> ReadableStreamSourceJsAdapter::readAllTe // We are already in a closed state. This is a no-op. This really // should not have been called if closed but just in case, return // a resolved promise. - return js.resolvedPromise(jsg::JsRef(js, js.str())); + return js.resolvedPromise(js.str().addRef(js)); } auto& open = state.requireActiveUnsafe(); @@ -361,9 +357,9 @@ jsg::Promise> ReadableStreamSourceJsAdapter::readAllTe [&](ReadableStreamSourceJsAdapter& self) { self.state.transitionTo(); }); KJ_IF_SOME(result, holder->result) { KJ_DASSERT(result.size() == amount); - return jsg::JsRef(js, js.str(result)); + return js.str(result).addRef(js); } else { - return jsg::JsRef(js, js.str()); + return js.str().addRef(js); } }) .catch_(js, @@ -377,20 +373,20 @@ jsg::Promise> ReadableStreamSourceJsAdapter::readAllTe }); } -jsg::Promise ReadableStreamSourceJsAdapter::readAllBytes( +jsg::Promise> ReadableStreamSourceJsAdapter::readAllBytes( jsg::Lock& js, uint64_t limit) { KJ_IF_SOME(exception, state.tryGetErrorUnsafe()) { // Really should not have been called if errored but just in case, // return a rejected promise. - return js.rejectedPromise(js.exceptionToJs(exception.clone())); + return js.rejectedPromise>(js.exceptionToJs(exception.clone())); } if (state.is()) { // We are already in a closed state. This is a no-op. This really // should not have been called if closed but just in case, return // a resolved promise. - auto backing = jsg::BackingStore::alloc(js, 0); - return js.resolvedPromise(jsg::BufferSource(js, kj::mv(backing))); + auto ab = jsg::JsArrayBuffer::create(js, 0); + return js.resolvedPromise(ab.addRef(js)); } auto& open = state.requireActiveUnsafe(); @@ -398,7 +394,7 @@ jsg::Promise ReadableStreamSourceJsAdapter::readAllBytes( auto& active = *open.active; if (active.closePending) { - return js.rejectedPromise( + return js.rejectedPromise>( js.typeError("Close already pending, cannot read.")); } active.closePending = true; @@ -424,16 +420,16 @@ jsg::Promise ReadableStreamSourceJsAdapter::readAllBytes( KJ_DASSERT(result.size() == amount); // We have to copy the data into the backing store because of the // v8 sandboxing rules. - auto backing = jsg::BackingStore::alloc(js, amount); - backing.asArrayPtr().copyFrom(result); - return jsg::BufferSource(js, kj::mv(backing)); + auto ab = jsg::JsArrayBuffer::create(js, result); + return ab.addRef(js); } else { - auto backing = jsg::BackingStore::alloc(js, 0); - return jsg::BufferSource(js, kj::mv(backing)); + auto ab = jsg::JsArrayBuffer::create(js, 0); + return ab.addRef(js); } }) .catch_(js, - [self = selfRef.addRef()](jsg::Lock& js, jsg::Value&& exception) -> jsg::BufferSource { + [self = selfRef.addRef()]( + jsg::Lock& js, jsg::Value&& exception) -> jsg::JsRef { // Likewise, while nothing should be waiting on the ready promise, we // should still reject it just in case. auto error = jsg::JsValue(exception.getHandle(js)); @@ -589,11 +585,11 @@ using JsByteSource = kj::OneOf, kj::Maybe tryExtractJsByteSource(jsg::Lock& js, const jsg::JsValue& jsval) { KJ_IF_SOME(abView, jsval.tryCast()) { - return kj::Maybe(jsg::JsRef(js, abView)); + return kj::Maybe(abView.addRef(js)); } else KJ_IF_SOME(ab, jsval.tryCast()) { - return kj::Maybe(jsg::JsRef(js, ab)); + return kj::Maybe(ab.addRef(js)); } else KJ_IF_SOME(str, jsval.tryCast()) { - return kj::Maybe(jsg::JsRef(js, str)); + return kj::Maybe(str.addRef(js)); } return kj::none; } @@ -753,7 +749,7 @@ jsg::Promise> ReadableSourceKjAdap // Ok, we have some data. Let's make sure it is bytes. // We accept either an ArrayBuffer, ArrayBufferView, or string. - auto jsval = jsg::JsValue(value.getHandle(js)); + auto jsval = value.getHandle(js); KJ_IF_SOME(result, tryExtractJsByteSource(js, jsval)) { // Process the resulting data. KJ_IF_SOME(leftOver, copyFromSource(js, *context, result)) { @@ -1330,8 +1326,7 @@ jsg::Promise> ReadableSourceKjAdapter::readAllReadImpl(jsg::Lock& j auto leftover = readable.view.asBytes(); if (leftover.size() > limit) { auto error = js.rangeError("Memory limit would be exceeded before EOF."); - return active->reader->cancel(js, error).then( - js, [ex = jsg::JsRef(js, error)](jsg::Lock& js) { + return active->reader->cancel(js, error).then(js, [ex = error.addRef(js)](jsg::Lock& js) { return js.rejectedPromise>(ex.getHandle(js)); }); } @@ -1362,7 +1357,7 @@ jsg::Promise> ReadableSourceKjAdapter::readAllReadImpl(jsg::Lock& j } auto& value = KJ_ASSERT_NONNULL(result.value); - auto jsval = jsg::JsValue(value.getHandle(js)); + auto jsval = value.getHandle(js); kj::ArrayPtr bytes; kj::Maybe maybeOwnedString; @@ -1378,16 +1373,14 @@ jsg::Promise> ReadableSourceKjAdapter::readAllReadImpl(jsg::Lock& j } else { auto error = js.typeError("ReadableStream provided a non-bytes value. Only ArrayBuffer, " "ArrayBufferView, or string are supported."); - return active->reader->cancel(js, error).then( - js, [err = jsg::JsRef(js, error)](jsg::Lock& js) { + return active->reader->cancel(js, error).then(js, [err = error.addRef(js)](jsg::Lock& js) { return js.rejectedPromise>(err.getHandle(js)); }); } if (accumulated.size() + bytes.size() > limit) { auto error = js.rangeError("Memory limit would be exceeded before EOF."); - return active->reader->cancel(js, error).then( - js, [err = jsg::JsRef(js, error)](jsg::Lock& js) { + return active->reader->cancel(js, error).then(js, [err = error.addRef(js)](jsg::Lock& js) { return js.rejectedPromise>(err.getHandle(js)); }); } diff --git a/src/workerd/api/streams/readable-source-adapter.h b/src/workerd/api/streams/readable-source-adapter.h index e167798bc06..7bca8298cf2 100644 --- a/src/workerd/api/streams/readable-source-adapter.h +++ b/src/workerd/api/streams/readable-source-adapter.h @@ -159,7 +159,7 @@ class ReadableStreamSourceJsAdapter final { // is equal to the length of this buffer. The actual number of // bytes read is indicated by the resolved value of the promise // but will never exceed the length of this buffer. - jsg::BufferSource buffer; + jsg::JsRef buffer; // The optional minimum number of bytes to read. If not provided, // the read will complete as soon as at least the mininum number @@ -179,7 +179,7 @@ class ReadableStreamSourceJsAdapter final { // of the same type as that provided in ReadOptions. // If the read produced no data because the stream is // closed, the type array will be zero length. - jsg::BufferSource buffer; + jsg::JsRef buffer; // True if the stream is now closed and no further reads // are possible. If this is true, the buffer will be zero @@ -210,7 +210,8 @@ class ReadableStreamSourceJsAdapter final { // If there are pending reads when this is called, those reads // will be allowed to complete first, and then the stream will // be read to the end. - jsg::Promise readAllBytes(jsg::Lock& js, uint64_t limit = kj::maxValue); + jsg::Promise> readAllBytes( + jsg::Lock& js, uint64_t limit = kj::maxValue); // If the stream is still active, tries to get the total length, // if known. If the length is not known, the encoding does not diff --git a/src/workerd/api/streams/readable-source.c++ b/src/workerd/api/streams/readable-source.c++ index 9c2a4c736af..198a61ae4c1 100644 --- a/src/workerd/api/streams/readable-source.c++ +++ b/src/workerd/api/streams/readable-source.c++ @@ -825,7 +825,7 @@ class MemoryInputStream final: public ReadableStreamSource { return kj::none; } - kj::Promise> pumpTo(WritableStreamSink& output, bool end) override { + kj::Promise> pumpTo(WritableStreamSink& output, End end) override { // Explicitly NOT using KJ_CO_MAGIC BEGIN_DEFERRED_PROXYING here! // The backing memory may be tied to V8 heap (e.g., jsg::BackingStore, Blob data), // so we must complete all I/O before the IoContext can be released. diff --git a/src/workerd/api/streams/readable.c++ b/src/workerd/api/streams/readable.c++ index 774aed242fa..61e9454559c 100644 --- a/src/workerd/api/streams/readable.c++ +++ b/src/workerd/api/streams/readable.c++ @@ -39,12 +39,11 @@ void ReaderImpl::detach() { } } -jsg::Promise ReaderImpl::cancel( - jsg::Lock& js, jsg::Optional> maybeReason) { +jsg::Promise ReaderImpl::cancel(jsg::Lock& js, jsg::Optional maybeReason) { assertAttachedOrTerminal(); if (state.is()) { return js.rejectedPromise( - js.v8TypeError("This ReadableStream reader has been released."_kj)); + js.typeError("This ReadableStream reader has been released."_kj)); } if (state.is()) { return js.resolvedPromise(); @@ -74,36 +73,35 @@ jsg::Promise ReaderImpl::read( assertAttachedOrTerminal(); if (state.is()) { return js.rejectedPromise( - js.v8TypeError("This ReadableStream reader has been released."_kj)); + js.typeError("This ReadableStream reader has been released."_kj)); } if (state.is()) { - return js.rejectedPromise( - js.v8TypeError("This ReadableStream has been closed."_kj)); + return js.rejectedPromise(js.typeError("This ReadableStream has been closed."_kj)); } auto& attached = state.requireActiveUnsafe(); KJ_IF_SOME(options, byobOptions) { // Per the spec, we must perform these checks before disturbing the stream. size_t atLeast = options.atLeast.orDefault(1); + auto view = options.bufferView.getHandle(js); - if (options.byteLength == 0) { + if (view.size() == 0) { return js.rejectedPromise( - js.v8TypeError("You must call read() on a \"byob\" reader with a positive-sized " - "TypedArray object."_kj)); + js.typeError("You must call read() on a \"byob\" reader with a positive-sized " + "TypedArray object."_kj)); } if (atLeast == 0) { - return js.rejectedPromise(js.v8TypeError( + return js.rejectedPromise(js.typeError( kj::str("Requested invalid minimum number of bytes to read (", atLeast, ")."))); } // Both read() and readAtLeast() pass atLeast in element count. // Convert to bytes before validation and forwarding to the controller. - jsg::BufferSource source(js, options.bufferView.getHandle(js)); - auto elementSize = source.getElementSize(); + auto elementSize = view.getElementSize(); atLeast = atLeast * elementSize; - if (atLeast > options.byteLength) { - return js.rejectedPromise(js.v8TypeError(kj::str("Minimum bytes to read (", - atLeast, ") exceeds size of buffer (", options.byteLength, ")."))); + if (atLeast > view.size()) { + return js.rejectedPromise(js.typeError(kj::str( + "Minimum bytes to read (", atLeast, ") exceeds size of buffer (", view.size(), ")."))); } options.atLeast = atLeast; @@ -113,8 +111,6 @@ jsg::Promise ReaderImpl::read( } void ReaderImpl::releaseLock(jsg::Lock& js) { - // TODO(soon): Releasing the lock should cancel any pending reads. This is a recent - // modification to the spec that we have not yet implemented. assertAttachedOrTerminal(); // Closed and Released states are no-ops. KJ_IF_SOME(attached, state.tryGetActiveUnsafe()) { @@ -154,8 +150,8 @@ void ReadableStreamDefaultReader::attach( } jsg::Promise ReadableStreamDefaultReader::cancel( - jsg::Lock& js, jsg::Optional> maybeReason) { - return impl.cancel(js, kj::mv(maybeReason)); + jsg::Lock& js, jsg::Optional maybeReason) { + return impl.cancel(js, maybeReason); } void ReadableStreamDefaultReader::detach() { @@ -207,8 +203,8 @@ void ReadableStreamBYOBReader::attach( } jsg::Promise ReadableStreamBYOBReader::cancel( - jsg::Lock& js, jsg::Optional> maybeReason) { - return impl.cancel(js, kj::mv(maybeReason)); + jsg::Lock& js, jsg::Optional maybeReason) { + return impl.cancel(js, maybeReason); } void ReadableStreamBYOBReader::detach() { @@ -224,13 +220,11 @@ void ReadableStreamBYOBReader::lockToStream(jsg::Lock& js, ReadableStream& strea } jsg::Promise ReadableStreamBYOBReader::read(jsg::Lock& js, - v8::Local byobBuffer, + jsg::JsArrayBufferView byobBuffer, jsg::Optional maybeOptions) { static const ReadableStreamBYOBReaderReadOptions defaultOptions{}; auto options = ReadableStreamController::ByobOptions{ - .bufferView = js.v8Ref(byobBuffer), - .byteOffset = byobBuffer->ByteOffset(), - .byteLength = byobBuffer->ByteLength(), + .bufferView = byobBuffer.addRef(js), .atLeast = maybeOptions.orDefault(defaultOptions).min.orDefault(1), .detachBuffer = FeatureFlags::get(js).getStreamsByobReaderDetachesBuffer(), }; @@ -238,11 +232,9 @@ jsg::Promise ReadableStreamBYOBReader::read(jsg::Lock& js, } jsg::Promise ReadableStreamBYOBReader::readAtLeast( - jsg::Lock& js, int minElements, v8::Local byobBuffer) { + jsg::Lock& js, int minElements, jsg::JsArrayBufferView byobBuffer) { auto options = ReadableStreamController::ByobOptions{ - .bufferView = js.v8Ref(byobBuffer), - .byteOffset = byobBuffer->ByteOffset(), - .byteLength = byobBuffer->ByteLength(), + .bufferView = byobBuffer.addRef(js), .atLeast = minElements, .detachBuffer = true, }; @@ -316,11 +308,11 @@ jsg::Promise DrainingReader::read(jsg::Lock& js, size_t maxR return kj::mv(result); } return js.rejectedPromise( - js.v8TypeError("Unable to perform draining read on this stream."_kj)); + js.typeError("Unable to perform draining read on this stream."_kj)); } KJ_CASE_ONEOF(r, Released) { return js.rejectedPromise( - js.v8TypeError("This ReadableStream reader has been released."_kj)); + js.typeError("This ReadableStream reader has been released."_kj)); } KJ_CASE_ONEOF(c, StreamStates::Closed) { return js.resolvedPromise(DrainingReadResult{ @@ -332,8 +324,7 @@ jsg::Promise DrainingReader::read(jsg::Lock& js, size_t maxR KJ_UNREACHABLE; } -jsg::Promise DrainingReader::cancel( - jsg::Lock& js, jsg::Optional> maybeReason) { +jsg::Promise DrainingReader::cancel(jsg::Lock& js, jsg::Optional maybeReason) { KJ_SWITCH_ONEOF(state) { KJ_CASE_ONEOF(i, Initial) { KJ_FAIL_ASSERT("this reader was never attached"); @@ -344,7 +335,7 @@ jsg::Promise DrainingReader::cancel( } KJ_CASE_ONEOF(r, Released) { return js.rejectedPromise( - js.v8TypeError("This ReadableStream reader has been released."_kj)); + js.typeError("This ReadableStream reader has been released."_kj)); } KJ_CASE_ONEOF(c, StreamStates::Closed) { return js.resolvedPromise(); @@ -431,11 +422,10 @@ ReadableStreamController& ReadableStream::getController() { return *controller; } -jsg::Promise ReadableStream::cancel( - jsg::Lock& js, jsg::Optional> maybeReason) { +jsg::Promise ReadableStream::cancel(jsg::Lock& js, jsg::Optional maybeReason) { if (isLocked()) { return js.rejectedPromise( - js.v8TypeError("This ReadableStream is currently locked to a reader."_kj)); + js.typeError("This ReadableStream is currently locked to a reader."_kj)); } return getController().cancel(js, maybeReason); } @@ -496,12 +486,12 @@ jsg::Promise ReadableStream::pipeTo(jsg::Lock& js, jsg::Optional maybeOptions) { if (isLocked()) { return js.rejectedPromise( - js.v8TypeError("This ReadableStream is currently locked to a reader."_kj)); + js.typeError("This ReadableStream is currently locked to a reader."_kj)); } if (destination->getController().isLockedToWriter()) { return js.rejectedPromise( - js.v8TypeError("This WritableStream is currently locked to a writer"_kj)); + js.typeError("This WritableStream is currently locked to a writer"_kj)); } auto options = kj::mv(maybeOptions).orDefault({}); @@ -530,24 +520,25 @@ jsg::Optional ReadableStream::inspectLength() { return tryGetLength(StreamEncoding::IDENTITY); } -jsg::Promise> ReadableStream::nextFunction( +jsg::Promise>> ReadableStream::nextFunction( jsg::Lock& js, AsyncIteratorState& state) { return state.reader->read(js).then( js, [reader = state.reader.addRef()](jsg::Lock& js, ReadResult result) mutable { if (result.done) { reader->releaseLock(js); - return js.resolvedPromise(kj::Maybe(kj::none)); + return js.resolvedPromise(kj::Maybe>(kj::none)); } - return js.resolvedPromise>(kj::mv(result.value)); + return js.resolvedPromise>>(kj::mv(result.value)); }); } jsg::Promise ReadableStream::returnFunction( - jsg::Lock& js, AsyncIteratorState& state, jsg::Optional& value) { + jsg::Lock& js, AsyncIteratorState& state, jsg::Optional>& value) { if (state.reader.get() != nullptr) { auto reader = kj::mv(state.reader); if (!state.preventCancel) { - auto promise = reader->cancel(js, value.map([&](jsg::Value& v) { return v.getHandle(js); })); + auto promise = reader->cancel( + js, value.map([&](jsg::JsRef& v) { return v.getHandle(js); })); reader->releaseLock(js); auto result = promise.then(js, JSG_VISITABLE_LAMBDA((reader = kj::mv(reader)), (reader), (jsg::Lock& js) { @@ -566,7 +557,7 @@ jsg::Promise ReadableStream::returnFunction( return js.resolvedPromise(); } -jsg::Ref ReadableStream::detach(jsg::Lock& js, bool ignoreDisturbed) { +jsg::Ref ReadableStream::detach(jsg::Lock& js, IgnoreDisturbed ignoreDisturbed) { JSG_REQUIRE( !isDisturbed() || ignoreDisturbed, TypeError, "The ReadableStream has already been read."); JSG_REQUIRE(!isLocked(), TypeError, "The ReadableStream has been locked to a reader."); @@ -578,7 +569,7 @@ kj::Maybe ReadableStream::tryGetLength(StreamEncoding encoding) { } kj::Promise> ReadableStream::pumpTo( - jsg::Lock& js, kj::Own sink, bool end) { + jsg::Lock& js, kj::Own sink, End end) { JSG_REQUIRE( IoContext::hasCurrent(), Error, "Unable to consume this ReadableStream outside of a request"); JSG_REQUIRE(!isLocked(), TypeError, "The ReadableStream has been locked to a reader."); @@ -598,29 +589,37 @@ jsg::Ref ReadableStream::constructor(jsg::Lock& js, auto controller = newReadableStreamJsController(); auto stream = js.allocAccounted( sizeof(ReadableStream) + controller->jsgGetMemorySelfSize(), kj::mv(controller)); - stream->getController().setup(js, kj::mv(underlyingSource), kj::mv(queuingStrategy)); + + auto source = kj::heap( + js, kj::mv(underlyingSource).orDefault({}), kj::mv(queuingStrategy).orDefault({})); + + stream->getController().setup(js, kj::mv(source)); return kj::mv(stream); } jsg::Optional ByteLengthQueuingStrategy::size( - jsg::Lock& js, jsg::Optional> maybeValue) { + jsg::Lock& js, jsg::Optional maybeValue) { KJ_IF_SOME(value, maybeValue) { - if ((value)->IsArrayBuffer()) { - auto buffer = value.As(); - return buffer->ByteLength(); - } else if ((value)->IsArrayBufferView()) { - auto view = value.As(); - return view->ByteLength(); - } else { - // Per the WHATWG Streams spec, ByteLengthQueuingStrategy.size should return - // GetV(chunk, "byteLength"), which means getting the byteLength property - // from any object, not just ArrayBuffer/ArrayBufferView. - KJ_IF_SOME(obj, jsg::JsValue(value).tryCast()) { - auto byteLength = obj.get(js, "byteLength"_kj); - KJ_IF_SOME(num, byteLength.tryCast()) { - KJ_IF_SOME(val, num.value(js)) { - return static_cast(val); - } + KJ_IF_SOME(ab, value.tryCast()) { + return ab.size(); + } + KJ_IF_SOME(sab, value.tryCast()) { + return sab.size(); + } + KJ_IF_SOME(view, value.tryCast()) { + return view.size(); + } + KJ_IF_SOME(str, value.tryCast()) { + return str.utf8Length(js); + } + // Per the WHATWG Streams spec, ByteLengthQueuingStrategy.size should return + // GetV(chunk, "byteLength"), which means getting the byteLength property + // from any object, not just ArrayBuffer/ArrayBufferView. + KJ_IF_SOME(obj, value.tryCast()) { + auto byteLength = obj.get(js, "byteLength"_kj); + KJ_IF_SOME(num, byteLength.tryCast()) { + KJ_IF_SOME(val, num.value(js)) { + return static_cast(val); } } } @@ -733,7 +732,7 @@ class NoDeferredProxyReadableStream final: public ReadableStreamSource { return inner->tryRead(buffer, minBytes, maxBytes); } - kj::Promise> pumpTo(WritableStreamSink& output, bool end) override { + kj::Promise> pumpTo(WritableStreamSink& output, End end) override { // Move the deferred proxy part of the task over to the non-deferred part. To do this, // we use `ioctx.waitForDeferredProxy()`, which returns a single promise covering both parts // (and, importantly, registering pending events where needed). Then, we add a noop deferred @@ -826,7 +825,7 @@ void ReadableStream::serialize(jsg::Lock& js, jsg::Serializer& serializer) { auto sink = newSystemStream(kj::mv(kjStream), encoding, ioctx); ioctx.addTask( - ioctx.waitForDeferredProxy(pumpTo(js, kj::mv(sink), true)).catch_([](kj::Exception&& e) { + ioctx.waitForDeferredProxy(pumpTo(js, kj::mv(sink), End::YES)).catch_([](kj::Exception&& e) { // Errors in pumpTo() are automatically propagated to the source and destination. We don't // want to throw them from here since it'll cause an uncaught exception to be reported, even // if the application actually does handle it! diff --git a/src/workerd/api/streams/readable.h b/src/workerd/api/streams/readable.h index ad76d7d9304..a6c721dcfe0 100644 --- a/src/workerd/api/streams/readable.h +++ b/src/workerd/api/streams/readable.h @@ -22,7 +22,7 @@ class ReaderImpl final { void attach(ReadableStreamController& controller, jsg::Promise closedPromise); - jsg::Promise cancel(jsg::Lock& js, jsg::Optional> maybeReason); + jsg::Promise cancel(jsg::Lock& js, jsg::Optional maybeReason); void detach(); @@ -105,7 +105,7 @@ class ReadableStreamDefaultReader : public jsg::Object, jsg::Lock& js, jsg::Ref stream); jsg::MemoizedIdentity>& getClosed(); - jsg::Promise cancel(jsg::Lock& js, jsg::Optional> reason); + jsg::Promise cancel(jsg::Lock& js, jsg::Optional reason); jsg::Promise read(jsg::Lock& js); void releaseLock(jsg::Lock& js); @@ -156,26 +156,25 @@ class ReadableStreamBYOBReader: public jsg::Object, jsg::Ref stream); jsg::MemoizedIdentity>& getClosed(); - jsg::Promise cancel(jsg::Lock& js, jsg::Optional> reason); + jsg::Promise cancel(jsg::Lock& js, jsg::Optional reason); struct ReadableStreamBYOBReaderReadOptions { jsg::Optional min; JSG_STRUCT(min); }; - jsg::Promise read(jsg::Lock& js, v8::Local byobBuffer, + jsg::Promise read(jsg::Lock& js, jsg::JsArrayBufferView byobBuffer, jsg::Optional options = kj::none); - // Non-standard extension so that reads can specify a minimum number of elements to read. It's a - // struct so that we could eventually add things like timeouts if we need to. Since there's no - // existing spec that's a leading contender, this is behind a different method name to avoid - // conflicts with any changes to `read`. Fewer than `minElements` may be returned if EOF is hit - // or the underlying stream is closed/errors out. In all cases the read result is either - // {value: theChunk, done: false} or {value: undefined, done: true} as with read. - // TODO(soon): Like fetch() and Cache.match(), readAtLeast() returns a promise for a V8 object. + // Non-standard extension so that reads can specify a minimum number of elements to read. + // Note: The standard read() method now supports a `min` option via + // ReadableStreamBYOBReaderReadOptions (see above), which largely supersedes this method. + // readAtLeast() is retained for backward compatibility. Fewer than `minElements` may be + // returned if EOF is hit or the underlying stream is closed/errors out. In all cases the + // read result is either {value: theChunk, done: false} or {value: undefined, done: true}. jsg::Promise readAtLeast(jsg::Lock& js, int minElements, - v8::Local byobBuffer); + jsg::JsArrayBufferView byobBuffer); void releaseLock(jsg::Lock& js); @@ -238,7 +237,7 @@ class DrainingReader: public ReadableStreamController::Reader { jsg::Promise read(jsg::Lock& js, size_t maxRead = kj::maxValue); // Cancels the stream. - jsg::Promise cancel(jsg::Lock& js, jsg::Optional> maybeReason); + jsg::Promise cancel(jsg::Lock& js, jsg::Optional maybeReason); // Releases the lock on the stream. void releaseLock(jsg::Lock& js); @@ -272,14 +271,14 @@ class ReadableStream: public jsg::Object { bool preventCancel; }; - static jsg::Promise> nextFunction( + static jsg::Promise>> nextFunction( jsg::Lock& js, AsyncIteratorState& state); static jsg::Promise returnFunction( jsg::Lock& js, AsyncIteratorState& state, - jsg::Optional& value); + jsg::Optional>& value); public: explicit ReadableStream(IoContext& ioContext, @@ -304,7 +303,8 @@ class ReadableStream: public jsg::Object { jsg::Optional underlyingSource, jsg::Optional queuingStrategy); - static jsg::Ref from(jsg::Lock& js, jsg::AsyncGenerator generator); + static jsg::Ref from(jsg::Lock& js, + jsg::AsyncGenerator> generator); bool isLocked(); @@ -312,7 +312,7 @@ class ReadableStream: public jsg::Object { // results. `reason` will be passed to the underlying source's cancel algorithm -- if this // readable stream is one side of a transform stream, then its cancel algorithm causes the // transform's writable side to become errored with `reason`. - jsg::Promise cancel(jsg::Lock& js, jsg::Optional> reason); + jsg::Promise cancel(jsg::Lock& js, jsg::Optional reason); using Reader = kj::OneOf, jsg::Ref>; @@ -337,7 +337,7 @@ class ReadableStream: public jsg::Object { JSG_ASYNC_ITERATOR_WITH_OPTIONS(ReadableStreamAsyncIterator, values, - jsg::Value, + jsg::JsRef, AsyncIteratorState, nextFunction, returnFunction, @@ -446,7 +446,7 @@ class ReadableStream: public jsg::Object { // ReadableStream that will take over ownership of the internal state of this one, // leaving this ReadableStream locked and disturbed so that it is no longer usable. // The name "detach" here is used in the sense of "detaching the internal state". - jsg::Ref detach(jsg::Lock& js, bool ignoreDisturbed=false); + jsg::Ref detach(jsg::Lock& js, IgnoreDisturbed ignoreDisturbed = IgnoreDisturbed::NO); kj::Maybe tryGetLength(StreamEncoding encoding); @@ -456,7 +456,7 @@ class ReadableStream: public jsg::Object { // state of the readable. kj::Promise> pumpTo(jsg::Lock& js, kj::Own sink, - bool end); + End end); // Initializes signalling mechanism for EOF detection. Returns a promise that will resolve when // EOF is reached. @@ -492,7 +492,7 @@ struct QueuingStrategyInit { }; using QueuingStrategySizeFunction = - jsg::Optional(jsg::Optional>); + jsg::Optional(jsg::Optional); // Utility class defined by the streams spec that uses byteLength to calculate // backpressure changes. @@ -519,7 +519,7 @@ class ByteLengthQueuingStrategy: public jsg::Object { } private: - static jsg::Optional size(jsg::Lock& js, jsg::Optional>); + static jsg::Optional size(jsg::Lock& js, jsg::Optional); QueuingStrategyInit init; }; @@ -549,7 +549,7 @@ class CountQueuingStrategy: public jsg::Object { } private: - static jsg::Optional size(jsg::Lock& js, jsg::Optional>) { + static jsg::Optional size(jsg::Lock& js, jsg::Optional) { return 1; } diff --git a/src/workerd/api/streams/standard-test.c++ b/src/workerd/api/streams/standard-test.c++ index 3dec1d8871b..6e65713a4cc 100644 --- a/src/workerd/api/streams/standard-test.c++ +++ b/src/workerd/api/streams/standard-test.c++ @@ -15,18 +15,16 @@ void preamble(auto callback) { fixture.runInIoContext([&](const TestFixture::Environment& env) { callback(env.js); }); } -v8::Local toBytes(jsg::Lock& js, kj::String str) { - return jsg::BackingStore::from(js, str.asBytes().attach(kj::mv(str))).createHandle(js); +jsg::JsUint8Array toBytes(jsg::Lock& js, kj::String str) { + return jsg::JsUint8Array::create(js, str.asBytes().slice(0, str.size())); } -jsg::BufferSource toBufferSource(jsg::Lock& js, kj::String str) { - auto backing = jsg::BackingStore::from(js, str.asBytes().attach(kj::mv(str))).createHandle(js); - return jsg::BufferSource(js, kj::mv(backing)); +jsg::JsBufferSource toBufferSource(jsg::Lock& js, kj::String str) { + return jsg::JsBufferSource(jsg::JsUint8Array::create(js, str.asBytes().slice(0, str.size()))); } -jsg::BufferSource toBufferSource(jsg::Lock& js, kj::Array bytes) { - auto backing = jsg::BackingStore::from(js, kj::mv(bytes)).createHandle(js); - return jsg::BufferSource(js, kj::mv(backing)); +jsg::JsBufferSource toBufferSource(jsg::Lock& js, kj::Array bytes) { + return jsg::JsBufferSource(jsg::JsUint8Array::create(js, bytes)); } // ====================================================================================== @@ -37,30 +35,29 @@ KJ_TEST("ReadableStream read all text (value readable)") { uint checked = 0; auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ - .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { + + UnderlyingSource source; + source.pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { + // Because we're using a value-based stream, two enqueue operations will + // require at least three reads to complete: one for the first chunk, 'hello, ', + // one for the second chunk, 'world!', and one to signal close. + KJ_SWITCH_ONEOF(controller) { // Because we're using a value-based stream, two enqueue operations will // require at least three reads to complete: one for the first chunk, 'hello, ', // one for the second chunk, 'world!', and one to signal close. - KJ_SWITCH_ONEOF(controller) { - // Because we're using a value-based stream, two enqueue operations will - // require at least three reads to complete: one for the first chunk, 'hello, ', - // one for the second chunk, 'world!', and one to signal close. - KJ_CASE_ONEOF(c, jsg::Ref) { - checked++; - c->enqueue(js, toBytes(js, kj::str("Hello, "))); - c->enqueue(js, toBytes(js, kj::str("world!"))); - c->close(js); - return js.resolvedPromise(); - } - KJ_CASE_ONEOF(c, jsg::Ref) {} + KJ_CASE_ONEOF(c, jsg::Ref) { + checked++; + c->enqueue(js, toBytes(js, kj::str("Hello, "))); + c->enqueue(js, toBytes(js, kj::str("world!"))); + c->close(js); + return js.resolvedPromise(); } - KJ_UNREACHABLE; + KJ_CASE_ONEOF(c, jsg::Ref) {} } - // Setting a highWaterMark of 0 means the pull function above will not be called - // immediately on creation of the stream, but only when the first read in the - // readall call below happens. - }, StreamQueuingStrategy{.highWaterMark = 0}); + KJ_UNREACHABLE; + }; + + rs->getController().setup(js, kj::heap(js, kj::mv(source), StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on // Starts a read loop of javascript promises. @@ -93,30 +90,29 @@ KJ_TEST("ReadableStream read all text, rs ref held (value readable)") { uint checked = 0; auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ - .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { + UnderlyingSource source; + source.pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { + // Because we're using a value-based stream, two enqueue operations will + // require at least three reads to complete: one for the first chunk, 'hello, ', + // one for the second chunk, 'world!', and one to signal close. + KJ_SWITCH_ONEOF(controller) { // Because we're using a value-based stream, two enqueue operations will // require at least three reads to complete: one for the first chunk, 'hello, ', // one for the second chunk, 'world!', and one to signal close. - KJ_SWITCH_ONEOF(controller) { - // Because we're using a value-based stream, two enqueue operations will - // require at least three reads to complete: one for the first chunk, 'hello, ', - // one for the second chunk, 'world!', and one to signal close. - KJ_CASE_ONEOF(c, jsg::Ref) { - checked++; - c->enqueue(js, toBytes(js, kj::str("Hello, "))); - c->enqueue(js, toBytes(js, kj::str("world!"))); - c->close(js); - return js.resolvedPromise(); - } - KJ_CASE_ONEOF(c, jsg::Ref) {} + KJ_CASE_ONEOF(c, jsg::Ref) { + checked++; + c->enqueue(js, toBytes(js, kj::str("Hello, "))); + c->enqueue(js, toBytes(js, kj::str("world!"))); + c->close(js); + return js.resolvedPromise(); } - KJ_UNREACHABLE; + KJ_CASE_ONEOF(c, jsg::Ref) {} } - // Setting a highWaterMark of 0 means the pull function above will not be called - // immediately on creation of the stream, but only when the first read in the - // readall call below happens. - }, StreamQueuingStrategy{.highWaterMark = 0}); + KJ_UNREACHABLE; + }; + + rs->getController().setup(js, kj::heap(js, kj::mv(source), + StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on // Starts a read loop of javascript promises. @@ -145,31 +141,30 @@ KJ_TEST("ReadableStream read all text (byte readable)") { uint checked = 0; auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ - .type = kj::str("bytes"), - .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { + UnderlyingSource source; + source.type = kj::str("bytes"); + source.pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { + // Because we're using a value-based stream, two enqueue operations will + // require at least three reads to complete: one for the first chunk, 'hello, ', + // one for the second chunk, 'world!', and one to signal close. + KJ_SWITCH_ONEOF(controller) { // Because we're using a value-based stream, two enqueue operations will // require at least three reads to complete: one for the first chunk, 'hello, ', // one for the second chunk, 'world!', and one to signal close. - KJ_SWITCH_ONEOF(controller) { - // Because we're using a value-based stream, two enqueue operations will - // require at least three reads to complete: one for the first chunk, 'hello, ', - // one for the second chunk, 'world!', and one to signal close. - KJ_CASE_ONEOF(c, jsg::Ref) { - checked++; - c->enqueue(js, toBufferSource(js, kj::str("Hello, "))); - c->enqueue(js, toBufferSource(js, kj::str("world!"))); - c->close(js); - return js.resolvedPromise(); - } - KJ_CASE_ONEOF(c, jsg::Ref) {} + KJ_CASE_ONEOF(c, jsg::Ref) { + checked++; + c->enqueue(js, toBufferSource(js, kj::str("Hello, "))); + c->enqueue(js, toBufferSource(js, kj::str("world!"))); + c->close(js); + return js.resolvedPromise(); } - KJ_UNREACHABLE; + KJ_CASE_ONEOF(c, jsg::Ref) {} } - // Setting a highWaterMark of 0 means the pull function above will not be called - // immediately on creation of the stream, but only when the first read in the - // readall call below happens. - }, StreamQueuingStrategy{.highWaterMark = 0}); + KJ_UNREACHABLE; + }; + + rs->getController().setup(js, kj::heap(js, kj::mv(source), + StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on // Starts a read loop of javascript promises. @@ -202,36 +197,34 @@ KJ_TEST("ReadableStream read all bytes (value readable)") { uint checked = 0; auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ - .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { + UnderlyingSource source; + source.pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { + // Because we're using a value-based stream, two enqueue operations will + // require at least three reads to complete: one for the first chunk, 'hello, ', + // one for the second chunk, 'world!', and one to signal close. + KJ_SWITCH_ONEOF(controller) { // Because we're using a value-based stream, two enqueue operations will // require at least three reads to complete: one for the first chunk, 'hello, ', // one for the second chunk, 'world!', and one to signal close. - KJ_SWITCH_ONEOF(controller) { - // Because we're using a value-based stream, two enqueue operations will - // require at least three reads to complete: one for the first chunk, 'hello, ', - // one for the second chunk, 'world!', and one to signal close. - KJ_CASE_ONEOF(c, jsg::Ref) { - checked++; - c->enqueue(js, toBytes(js, kj::str("Hello, "))); - c->enqueue(js, toBytes(js, kj::str("world!"))); - c->close(js); - return js.resolvedPromise(); - } - KJ_CASE_ONEOF(c, jsg::Ref) {} + KJ_CASE_ONEOF(c, jsg::Ref) { + checked++; + c->enqueue(js, toBytes(js, kj::str("Hello, "))); + c->enqueue(js, toBytes(js, kj::str("world!"))); + c->close(js); + return js.resolvedPromise(); } - KJ_UNREACHABLE; + KJ_CASE_ONEOF(c, jsg::Ref) {} } - // Setting a highWaterMark of 0 means the pull function above will not be called - // immediately on creation of the stream, but only when the first read in the - // readall call below happens. - }, StreamQueuingStrategy{.highWaterMark = 0}); + KJ_UNREACHABLE; + }; + rs->getController().setup(js, kj::heap(js, kj::mv(source), + StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on // Starts a read loop of javascript promises. auto promise = rs->getController().readAllBytes(js, 20).then( - js, [&](jsg::Lock& js, jsg::BufferSource&& text) { - KJ_ASSERT(text.asArrayPtr() == "Hello, world!"_kjb); + js, [&](jsg::Lock& js, jsg::JsRef text) { + KJ_ASSERT(text.getHandle(js).asArrayPtr() == "Hello, world!"_kjb); checked++; }); @@ -258,37 +251,35 @@ KJ_TEST("ReadableStream read all bytes (byte readable)") { uint checked = 0; auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ - .type = kj::str("bytes"), - .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { + UnderlyingSource source; + source.type = kj::str("bytes"); + source.pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { + // Because we're using a value-based stream, two enqueue operations will + // require at least three reads to complete: one for the first chunk, 'hello, ', + // one for the second chunk, 'world!', and one to signal close. + KJ_SWITCH_ONEOF(controller) { // Because we're using a value-based stream, two enqueue operations will // require at least three reads to complete: one for the first chunk, 'hello, ', // one for the second chunk, 'world!', and one to signal close. - KJ_SWITCH_ONEOF(controller) { - // Because we're using a value-based stream, two enqueue operations will - // require at least three reads to complete: one for the first chunk, 'hello, ', - // one for the second chunk, 'world!', and one to signal close. - KJ_CASE_ONEOF(c, jsg::Ref) { - checked++; - c->enqueue(js, toBufferSource(js, kj::str("Hello, "))); - c->enqueue(js, toBufferSource(js, kj::str("world!"))); - c->close(js); - return js.resolvedPromise(); - } - KJ_CASE_ONEOF(c, jsg::Ref) {} + KJ_CASE_ONEOF(c, jsg::Ref) { + checked++; + c->enqueue(js, toBufferSource(js, kj::str("Hello, "))); + c->enqueue(js, toBufferSource(js, kj::str("world!"))); + c->close(js); + return js.resolvedPromise(); } - KJ_UNREACHABLE; + KJ_CASE_ONEOF(c, jsg::Ref) {} } - // Setting a highWaterMark of 0 means the pull function above will not be called - // immediately on creation of the stream, but only when the first read in the - // readall call below happens. - }, StreamQueuingStrategy{.highWaterMark = 0}); + KJ_UNREACHABLE; + }; + rs->getController().setup(js, kj::heap(js, kj::mv(source), + StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on // Starts a read loop of javascript promises. auto promise = rs->getController().readAllBytes(js, 20).then( - js, [&](jsg::Lock& js, jsg::BufferSource&& text) { - KJ_ASSERT(text.asArrayPtr() == "Hello, world!"_kjb); + js, [&](jsg::Lock& js, jsg::JsRef text) { + KJ_ASSERT(text.getHandle(js).asArrayPtr() == "Hello, world!"_kjb); checked++; }); @@ -319,7 +310,7 @@ KJ_TEST("ReadableStream read all bytes (value readable, more reads)") { kj::str("o"), kj::str(","), kj::str(" "), kj::str("w"), kj::str("o"), kj::str("r"), kj::str("l"), kj::str("d"), kj::str("!")); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { // Because we're using a value-based stream, two enqueue operations will // require at least three reads to complete: one for the first chunk, 'hello, ', @@ -344,13 +335,13 @@ KJ_TEST("ReadableStream read all bytes (value readable, more reads)") { // Setting a highWaterMark of 0 means the pull function above will not be called // immediately on creation of the stream, but only when the first read in the // readall call below happens. - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on // Starts a read loop of javascript promises. auto promise = rs->getController().readAllBytes(js, 20).then( - js, [&](jsg::Lock& js, jsg::BufferSource&& text) { - KJ_ASSERT(text.asArrayPtr() == "Hello, world!"_kjb); + js, [&](jsg::Lock& js, jsg::JsRef text) { + KJ_ASSERT(text.getHandle(js).asArrayPtr() == "Hello, world!"_kjb); checked++; }); @@ -381,7 +372,7 @@ KJ_TEST("ReadableStream read all bytes (byte readable, more reads)") { kj::str("o"), kj::str(","), kj::str(" "), kj::str("w"), kj::str("o"), kj::str("r"), kj::str("l"), kj::str("d"), kj::str("!")); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .type = kj::str("bytes"), .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { // Because we're using a value-based stream, two enqueue operations will @@ -407,13 +398,13 @@ KJ_TEST("ReadableStream read all bytes (byte readable, more reads)") { // Setting a highWaterMark of 0 means the pull function above will not be called // immediately on creation of the stream, but only when the first read in the // readall call below happens. - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on // Starts a read loop of javascript promises. auto promise = rs->getController().readAllBytes(js, 20).then( - js, [&](jsg::Lock& js, jsg::BufferSource&& text) { - KJ_ASSERT(text.asArrayPtr() == "Hello, world!"_kjb); + js, [&](jsg::Lock& js, jsg::JsRef text) { + KJ_ASSERT(text.getHandle(js).asArrayPtr() == "Hello, world!"_kjb); checked++; }); @@ -447,7 +438,7 @@ KJ_TEST("ReadableStream read all bytes (byte readable, large data)") { chunks[1].asPtr().fill('B'); chunks[2].asPtr().fill('C'); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .type = kj::str("bytes"), .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { // Because we're using a value-based stream, two enqueue operations will @@ -473,14 +464,15 @@ KJ_TEST("ReadableStream read all bytes (byte readable, large data)") { // Setting a highWaterMark of 0 means the pull function above will not be called // immediately on creation of the stream, but only when the first read in the // readall call below happens. - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on // Starts a read loop of javascript promises. auto promise = rs->getController() .readAllBytes(js, (BASE * 7) + 1) - .then(js, [&](jsg::Lock& js, jsg::BufferSource&& text) { + .then(js, [&](jsg::Lock& js, jsg::JsRef buf) { kj::byte check[BASE * 7]{}; + auto text = buf.getHandle(js); kj::arrayPtr(check).first(BASE).fill('A'); kj::arrayPtr(check).slice(BASE).first(BASE * 2).fill('B'); kj::arrayPtr(check).slice(BASE * 3).fill('C'); @@ -515,17 +507,14 @@ KJ_TEST("ReadableStream read all bytes (value readable, wrong type)") { uint checked = 0; auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { // Because we're using a value-based stream, two enqueue operations will // require at least three reads to complete: one for the first chunk, 'hello, ', // one for the second chunk, 'world!', and one to signal close. KJ_SWITCH_ONEOF(controller) { - // Because we're using a value-based stream, two enqueue operations will - // require at least three reads to complete: one for the first chunk, 'hello, ', - // one for the second chunk, 'world!', and one to signal close. KJ_CASE_ONEOF(c, jsg::Ref) { - c->enqueue(js, js.str("wrong type"_kjc)); + c->enqueue(js, js.num(1)); checked++; return js.resolvedPromise(); } @@ -541,13 +530,12 @@ KJ_TEST("ReadableStream read all bytes (value readable, wrong type)") { // Setting a highWaterMark of 0 means the pull function above will not be called // immediately on creation of the stream, but only when the first read in the // readall call below happens. - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on // Starts a read loop of javascript promises. - auto promise = rs->getController().readAllBytes(js, 20).then(js, - [](jsg::Lock& js, jsg::BufferSource&& text) { KJ_UNREACHABLE; }, - [&](jsg::Lock& js, jsg::Value&& exception) { + auto promise = rs->getController().readAllBytes(js, 20).then( + js, [](auto&, auto) { KJ_UNREACHABLE; }, [&](jsg::Lock& js, jsg::Value&& exception) { KJ_ASSERT(kj::str(exception.getHandle(js)) == "TypeError: This ReadableStream did not return bytes."); checked++; @@ -575,7 +563,7 @@ KJ_TEST("ReadableStream read all bytes (value readable, to many bytes)") { uint checked = 0; auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { // Because we're using a value-based stream, two enqueue operations will // require at least three reads to complete: one for the first chunk, 'hello, ', @@ -596,13 +584,12 @@ KJ_TEST("ReadableStream read all bytes (value readable, to many bytes)") { // Setting a highWaterMark of 0 means the pull function above will not be called // immediately on creation of the stream, but only when the first read in the // readall call below happens. - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on // Starts a read loop of javascript promises. - auto promise = rs->getController().readAllBytes(js, 20).then(js, - [](jsg::Lock& js, jsg::BufferSource&& text) { KJ_UNREACHABLE; }, - [&](jsg::Lock& js, jsg::Value&& exception) { + auto promise = rs->getController().readAllBytes(js, 20).then( + js, [](auto&, auto) { KJ_UNREACHABLE; }, [&](jsg::Lock& js, jsg::Value&& exception) { KJ_ASSERT(kj::str(exception.getHandle(js)) == "TypeError: Memory limit exceeded before EOF."); checked++; }); @@ -629,7 +616,7 @@ KJ_TEST("ReadableStream read all bytes (byte readable, to many bytes)") { uint checked = 0; auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .type = kj::str("bytes"), .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { // Because we're using a value-based stream, two enqueue operations will @@ -651,13 +638,12 @@ KJ_TEST("ReadableStream read all bytes (byte readable, to many bytes)") { // Setting a highWaterMark of 0 means the pull function above will not be called // immediately on creation of the stream, but only when the first read in the // readall call below happens. - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on // Starts a read loop of javascript promises. - auto promise = rs->getController().readAllBytes(js, 20).then(js, - [](jsg::Lock& js, jsg::BufferSource&& text) { KJ_UNREACHABLE; }, - [&](jsg::Lock& js, jsg::Value&& exception) { + auto promise = rs->getController().readAllBytes(js, 20).then( + js, [](auto&, auto) { KJ_UNREACHABLE; }, [&](jsg::Lock& js, jsg::Value&& exception) { KJ_ASSERT(kj::str(exception.getHandle(js)) == "TypeError: Memory limit exceeded before EOF."); checked++; }); @@ -684,7 +670,7 @@ KJ_TEST("ReadableStream read all bytes (byte readable, failed read)") { uint checked = 0; auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .type = kj::str("bytes"), .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { checked++; @@ -693,13 +679,12 @@ KJ_TEST("ReadableStream read all bytes (byte readable, failed read)") { // Setting a highWaterMark of 0 means the pull function above will not be called // immediately on creation of the stream, but only when the first read in the // readall call below happens. - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on // Starts a read loop of javascript promises. - auto promise = rs->getController().readAllBytes(js, 20).then(js, - [](jsg::Lock& js, jsg::BufferSource&& text) { KJ_UNREACHABLE; }, - [&](jsg::Lock& js, jsg::Value&& exception) { + auto promise = rs->getController().readAllBytes(js, 20).then( + js, [](auto&, auto) { KJ_UNREACHABLE; }, [&](jsg::Lock& js, jsg::Value&& exception) { KJ_ASSERT(kj::str(exception.getHandle(js)) == "Error: boom"); checked++; }); @@ -726,7 +711,7 @@ KJ_TEST("ReadableStream read all bytes (value readable, failed read)") { uint checked = 0; auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { checked++; return js.rejectedPromise(js.error("boom")); @@ -734,13 +719,12 @@ KJ_TEST("ReadableStream read all bytes (value readable, failed read)") { // Setting a highWaterMark of 0 means the pull function above will not be called // immediately on creation of the stream, but only when the first read in the // readall call below happens. - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on // Starts a read loop of javascript promises. - auto promise = rs->getController().readAllBytes(js, 20).then(js, - [](jsg::Lock& js, jsg::BufferSource&& text) { KJ_UNREACHABLE; }, - [&](jsg::Lock& js, jsg::Value&& exception) { + auto promise = rs->getController().readAllBytes(js, 20).then( + js, [](auto&, auto) { KJ_UNREACHABLE; }, [&](jsg::Lock& js, jsg::Value&& exception) { KJ_ASSERT(kj::str(exception.getHandle(js)) == "Error: boom"); checked++; }); @@ -767,7 +751,7 @@ KJ_TEST("ReadableStream read all bytes (byte readable, failed start)") { uint checked = 0; auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .type = kj::str("bytes"), .start = [&](jsg::Lock& js, UnderlyingSource::Controller controller) -> jsg::Promise { checked++; @@ -776,13 +760,12 @@ KJ_TEST("ReadableStream read all bytes (byte readable, failed start)") { // Setting a highWaterMark of 0 means the pull function above will not be called // immediately on creation of the stream, but only when the first read in the // readall call below happens. - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on // Starts a read loop of javascript promises. - auto promise = rs->getController().readAllBytes(js, 20).then(js, - [](jsg::Lock& js, jsg::BufferSource&& text) { KJ_UNREACHABLE; }, - [&](jsg::Lock& js, jsg::Value&& exception) { + auto promise = rs->getController().readAllBytes(js, 20).then( + js, [](auto&, auto) { KJ_UNREACHABLE; }, [&](jsg::Lock& js, jsg::Value&& exception) { KJ_ASSERT(kj::str(exception.getHandle(js)) == "Error: boom"); checked++; }); @@ -809,7 +792,7 @@ KJ_TEST("ReadableStream read all bytes (byte readable, failed start 2)") { uint checked = 0; auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .type = kj::str("bytes"), .start = [&](jsg::Lock& js, UnderlyingSource::Controller controller) -> jsg::Promise { checked++; @@ -818,13 +801,12 @@ KJ_TEST("ReadableStream read all bytes (byte readable, failed start 2)") { // Setting a highWaterMark of 0 means the pull function above will not be called // immediately on creation of the stream, but only when the first read in the // readall call below happens. - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on // Starts a read loop of javascript promises. - auto promise = rs->getController().readAllBytes(js, 20).then(js, - [](jsg::Lock& js, jsg::BufferSource&& text) { KJ_UNREACHABLE; }, - [&](jsg::Lock& js, jsg::Value&& exception) { + auto promise = rs->getController().readAllBytes(js, 20).then( + js, [](auto&, auto) { KJ_UNREACHABLE; }, [&](jsg::Lock& js, jsg::Value&& exception) { KJ_ASSERT(kj::str(exception.getHandle(js)) == "Error: boom"); checked++; }); @@ -852,7 +834,9 @@ KJ_TEST("ReadableStream read all bytes (byte readable, failed start 2)") { KJ_TEST("DrainingReader basic creation and locking (value stream)") { preamble([](jsg::Lock& js) { auto rs = js.alloc(newReadableStreamJsController()); - rs->getController().setup(js, UnderlyingSource{}, StreamQueuingStrategy{.highWaterMark = 0}); + rs->getController().setup(js, + kj::heap( + js, UnderlyingSource{}, StreamQueuingStrategy{.highWaterMark = 0})); // Stream should not be locked initially KJ_ASSERT(!rs->isLocked()); @@ -876,7 +860,9 @@ KJ_TEST("DrainingReader basic creation and locking (value stream)") { KJ_TEST("DrainingReader cannot be created on locked stream") { preamble([](jsg::Lock& js) { auto rs = js.alloc(newReadableStreamJsController()); - rs->getController().setup(js, UnderlyingSource{}, StreamQueuingStrategy{.highWaterMark = 0}); + rs->getController().setup(js, + kj::heap( + js, UnderlyingSource{}, StreamQueuingStrategy{.highWaterMark = 0})); // Create first reader to lock the stream KJ_IF_SOME(reader1, DrainingReader::create(js, *rs)) { @@ -898,7 +884,7 @@ KJ_TEST("DrainingReader read drains buffered data (value stream)") { uint pullCount = 0; auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { KJ_SWITCH_ONEOF(controller) { KJ_CASE_ONEOF(c, jsg::Ref) { @@ -917,7 +903,7 @@ KJ_TEST("DrainingReader read drains buffered data (value stream)") { } KJ_UNREACHABLE; } - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on KJ_IF_SOME(reader, DrainingReader::create(js, *rs)) { @@ -947,7 +933,7 @@ KJ_TEST("DrainingReader read drains buffered data (byte stream)") { uint pullCount = 0; auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .type = kj::str("bytes"), .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { KJ_SWITCH_ONEOF(controller) { @@ -965,7 +951,7 @@ KJ_TEST("DrainingReader read drains buffered data (byte stream)") { } KJ_UNREACHABLE; } - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on KJ_IF_SOME(reader, DrainingReader::create(js, *rs)) { @@ -994,7 +980,7 @@ KJ_TEST("DrainingReader read on closed stream returns done") { preamble([](jsg::Lock& js) { auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .start = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { KJ_SWITCH_ONEOF(controller) { KJ_CASE_ONEOF(c, jsg::Ref) { @@ -1005,7 +991,7 @@ KJ_TEST("DrainingReader read on closed stream returns done") { } KJ_UNREACHABLE; } - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on js.runMicrotasks(); @@ -1031,7 +1017,9 @@ KJ_TEST("DrainingReader read on closed stream returns done") { KJ_TEST("DrainingReader read after releaseLock rejects") { preamble([](jsg::Lock& js) { auto rs = js.alloc(newReadableStreamJsController()); - rs->getController().setup(js, UnderlyingSource{}, StreamQueuingStrategy{.highWaterMark = 0}); + rs->getController().setup(js, + kj::heap( + js, UnderlyingSource{}, StreamQueuingStrategy{.highWaterMark = 0})); KJ_IF_SOME(reader, DrainingReader::create(js, *rs)) { reader->releaseLock(js); @@ -1065,7 +1053,7 @@ KJ_TEST("DrainingReader sync data then async pull waits") { auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { KJ_SWITCH_ONEOF(controller) { KJ_CASE_ONEOF(c, jsg::Ref) { @@ -1089,7 +1077,7 @@ KJ_TEST("DrainingReader sync data then async pull waits") { } KJ_UNREACHABLE; } - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on KJ_IF_SOME(reader, DrainingReader::create(js, *rs)) { @@ -1145,7 +1133,7 @@ KJ_TEST("DrainingReader with fully async pull") { auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { KJ_SWITCH_ONEOF(controller) { KJ_CASE_ONEOF(c, jsg::Ref) { @@ -1160,7 +1148,7 @@ KJ_TEST("DrainingReader with fully async pull") { } KJ_UNREACHABLE; } - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on KJ_IF_SOME(reader, DrainingReader::create(js, *rs)) { @@ -1201,7 +1189,7 @@ KJ_TEST("DrainingReader byte stream with async pull") { auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .type = kj::str("bytes"), .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { KJ_SWITCH_ONEOF(controller) { @@ -1221,7 +1209,7 @@ KJ_TEST("DrainingReader byte stream with async pull") { } KJ_UNREACHABLE; } - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on KJ_IF_SOME(reader, DrainingReader::create(js, *rs)) { @@ -1254,7 +1242,7 @@ KJ_TEST("DrainingReader multiple sync chunks then close") { uint pullCount = 0; auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { KJ_SWITCH_ONEOF(controller) { KJ_CASE_ONEOF(c, jsg::Ref) { @@ -1270,7 +1258,7 @@ KJ_TEST("DrainingReader multiple sync chunks then close") { } KJ_UNREACHABLE; } - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on KJ_IF_SOME(reader, DrainingReader::create(js, *rs)) { @@ -1301,7 +1289,7 @@ KJ_TEST("DrainingReader read from teed branches") { preamble([](jsg::Lock& js) { auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .pull = [](jsg::Lock& js, UnderlyingSource::Controller controller) { KJ_SWITCH_ONEOF(controller) { KJ_CASE_ONEOF(c, jsg::Ref) { @@ -1314,7 +1302,7 @@ KJ_TEST("DrainingReader read from teed branches") { } KJ_UNREACHABLE; } - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on // Tee the stream into two branches @@ -1371,7 +1359,7 @@ KJ_TEST("DrainingReader read from byte stream with BYOB support") { preamble([](jsg::Lock& js) { auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .type = kj::str("bytes"), .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { KJ_SWITCH_ONEOF(controller) { @@ -1391,7 +1379,7 @@ KJ_TEST("DrainingReader read from byte stream with BYOB support") { } KJ_UNREACHABLE; } - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on // Use DrainingReader (which uses default reader) to drain the BYOB-capable byte stream @@ -1425,7 +1413,7 @@ KJ_TEST("DrainingReader error during pull in value stream") { preamble([](jsg::Lock& js) { auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .pull = [](jsg::Lock& js, UnderlyingSource::Controller controller) { KJ_SWITCH_ONEOF(controller) { KJ_CASE_ONEOF(c, jsg::Ref) { @@ -1437,7 +1425,7 @@ KJ_TEST("DrainingReader error during pull in value stream") { } KJ_UNREACHABLE; } - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on KJ_IF_SOME(reader, DrainingReader::create(js, *rs)) { @@ -1464,7 +1452,7 @@ KJ_TEST("DrainingReader error during pull in byte stream") { preamble([](jsg::Lock& js) { auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .type = kj::str("bytes"), .pull = [](jsg::Lock& js, UnderlyingSource::Controller controller) { KJ_SWITCH_ONEOF(controller) { @@ -1477,7 +1465,7 @@ KJ_TEST("DrainingReader error during pull in byte stream") { } KJ_UNREACHABLE; } - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on KJ_IF_SOME(reader, DrainingReader::create(js, *rs)) { @@ -1507,7 +1495,7 @@ KJ_TEST("DrainingReader read from stream with transform-like pattern") { auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .start = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { KJ_SWITCH_ONEOF(controller) { KJ_CASE_ONEOF(c, jsg::Ref) { @@ -1523,7 +1511,7 @@ KJ_TEST("DrainingReader read from stream with transform-like pattern") { // No-op pull - data comes from external enqueue calls (like transform writes) return js.resolvedPromise(); } - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on js.runMicrotasks(); @@ -1583,7 +1571,7 @@ KJ_TEST("DrainingReader cancel while read is pending (value stream)") { auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { // Return a pending promise to keep the read waiting auto prp = js.newPromiseAndResolver(); @@ -1595,7 +1583,7 @@ KJ_TEST("DrainingReader cancel while read is pending (value stream)") { KJ_ASSERT(kj::str(reason) == "canceled by reader"); return js.resolvedPromise(); } - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on KJ_IF_SOME(reader, DrainingReader::create(js, *rs)) { @@ -1643,7 +1631,7 @@ KJ_TEST("DrainingReader cancel while read is pending (byte stream)") { auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .type = kj::str("bytes"), .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { // Return a pending promise to keep the read waiting @@ -1656,7 +1644,7 @@ KJ_TEST("DrainingReader cancel while read is pending (byte stream)") { KJ_ASSERT(kj::str(reason) == "canceled by reader"); return js.resolvedPromise(); } - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on KJ_IF_SOME(reader, DrainingReader::create(js, *rs)) { @@ -1700,7 +1688,7 @@ KJ_TEST("DrainingReader cancel while read is pending with buffered data") { auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { KJ_SWITCH_ONEOF(controller) { KJ_CASE_ONEOF(c, jsg::Ref) { @@ -1720,7 +1708,7 @@ KJ_TEST("DrainingReader cancel while read is pending with buffered data") { cancelCalled = true; return js.resolvedPromise(); } - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on KJ_IF_SOME(reader, DrainingReader::create(js, *rs)) { @@ -1784,7 +1772,7 @@ KJ_TEST("DrainingReader cancel while read pending - UAF safety (value stream)") auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { pullCalled = true; // Return a pending promise - this keeps the read waiting @@ -1796,7 +1784,7 @@ KJ_TEST("DrainingReader cancel while read pending - UAF safety (value stream)") cancelCalled = true; return js.resolvedPromise(); } - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on KJ_IF_SOME(reader, DrainingReader::create(js, *rs)) { @@ -1855,7 +1843,7 @@ KJ_TEST("DrainingReader cancel while read pending - UAF safety (byte stream)") { auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .type = kj::str("bytes"), .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { pullCalled = true; @@ -1867,7 +1855,7 @@ KJ_TEST("DrainingReader cancel while read pending - UAF safety (byte stream)") { cancelCalled = true; return js.resolvedPromise(); } - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on KJ_IF_SOME(reader, DrainingReader::create(js, *rs)) { @@ -1927,9 +1915,10 @@ KJ_TEST("ReadableStream handles execution termination during read") { // Set up a stream where the pull callback terminates execution rs->getController().setup(js, - UnderlyingSource{ - .pull = - [&](jsg::Lock& js, UnderlyingSource::Controller controller) { + kj::heap(js, + UnderlyingSource{ + .pull = + [&](jsg::Lock& js, UnderlyingSource::Controller controller) { // Terminate execution - this simulates CPU time limit exceeded js.terminateNextExecution(); @@ -1940,8 +1929,8 @@ KJ_TEST("ReadableStream handles execution termination during read") { // We shouldn't get here - termination should have been triggered return js.resolvedPromise(); }, - }, - StreamQueuingStrategy{.highWaterMark = 0}); + }, + StreamQueuingStrategy{.highWaterMark = 0})); // Start a read - this will call pull which terminates execution auto promise = rs->getController().readAllText(js, 100); @@ -1992,22 +1981,22 @@ KJ_TEST("WritableStream close during abort algorithm returns rejected promise") // We capture a raw pointer to the writer so the abort callback can call close(). WritableStreamDefaultWriter* writerPtr = nullptr; + auto sink = kj::heap(js, + UnderlyingSink{ + .abort = [&](jsg::Lock& js, v8::Local reason) -> jsg::Promise { + abortCalled = true; + // Re-entrantly call close() on the writer during the abort algorithm. + // At this point, WritableImpl has already transitioned to Errored state + // but WritableStreamJsController hasn't been updated yet. + // This should return a rejected promise instead of crashing. + writerPtr->close(js).then(js, [](jsg::Lock& js) {}, + [&](jsg::Lock& js, jsg::Value reason) { closeRejected = true; }); + return js.resolvedPromise(); + }}, + StreamQueuingStrategy{}); + // clang-format off - ws->getController().setup(js, UnderlyingSink{ - .abort = [&](jsg::Lock& js, v8::Local reason) -> jsg::Promise { - abortCalled = true; - // Re-entrantly call close() on the writer during the abort algorithm. - // At this point, WritableImpl has already transitioned to Errored state - // but WritableStreamJsController hasn't been updated yet. - // This should return a rejected promise instead of crashing. - writerPtr->close(js).then(js, - [](jsg::Lock& js) {}, - [&](jsg::Lock& js, jsg::Value reason) { - closeRejected = true; - }); - return js.resolvedPromise(); - } - }, StreamQueuingStrategy{}); + ws->getController().setup(js, kj::mv(sink)); // clang-format on auto writer = js.alloc(); @@ -2046,7 +2035,7 @@ KJ_TEST("DrainingReader: pull that synchronously closes does not UAF (value stre preamble([](jsg::Lock& js) { auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { KJ_SWITCH_ONEOF(controller) { KJ_CASE_ONEOF(c, jsg::Ref) { @@ -2060,7 +2049,7 @@ KJ_TEST("DrainingReader: pull that synchronously closes does not UAF (value stre } KJ_UNREACHABLE; } - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on KJ_IF_SOME(reader, DrainingReader::create(js, *rs)) { @@ -2083,7 +2072,7 @@ KJ_TEST("DrainingReader: pull that synchronously closes does not UAF (byte strea preamble([](jsg::Lock& js) { auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .type = kj::str("bytes"), .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { KJ_SWITCH_ONEOF(controller) { @@ -2095,7 +2084,7 @@ KJ_TEST("DrainingReader: pull that synchronously closes does not UAF (byte strea } KJ_UNREACHABLE; } - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on KJ_IF_SOME(reader, DrainingReader::create(js, *rs)) { @@ -2118,18 +2107,18 @@ KJ_TEST("DrainingReader: pull that synchronously errors does not UAF (value stre preamble([](jsg::Lock& js) { auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { KJ_SWITCH_ONEOF(controller) { KJ_CASE_ONEOF(c, jsg::Ref) { - c->error(js, js.v8TypeError("test error"_kj)); + c->error(js, js.typeError("test error"_kj)); return js.resolvedPromise(); } KJ_CASE_ONEOF(c, jsg::Ref) {} } KJ_UNREACHABLE; } - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on KJ_IF_SOME(reader, DrainingReader::create(js, *rs)) { @@ -2152,7 +2141,7 @@ KJ_TEST("DrainingReader: pull enqueues then closes on next pull (value stream)") uint pullCount = 0; auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { KJ_SWITCH_ONEOF(controller) { KJ_CASE_ONEOF(c, jsg::Ref) { @@ -2169,7 +2158,7 @@ KJ_TEST("DrainingReader: pull enqueues then closes on next pull (value stream)") } KJ_UNREACHABLE; } - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on KJ_IF_SOME(reader, DrainingReader::create(js, *rs)) { @@ -2213,7 +2202,7 @@ KJ_TEST("DrainingReader: pull that synchronously cancels does not hang (value st preamble([](jsg::Lock& js) { auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { KJ_SWITCH_ONEOF(controller) { KJ_CASE_ONEOF(c, jsg::Ref) { @@ -2225,7 +2214,7 @@ KJ_TEST("DrainingReader: pull that synchronously cancels does not hang (value st } KJ_UNREACHABLE; } - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on KJ_IF_SOME(reader, DrainingReader::create(js, *rs)) { @@ -2249,7 +2238,7 @@ KJ_TEST("DrainingReader: pull that synchronously cancels does not hang (byte str preamble([](jsg::Lock& js) { auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .type = kj::str("bytes"), .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { KJ_SWITCH_ONEOF(controller) { @@ -2261,7 +2250,7 @@ KJ_TEST("DrainingReader: pull that synchronously cancels does not hang (byte str } KJ_UNREACHABLE; } - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on KJ_IF_SOME(reader, DrainingReader::create(js, *rs)) { @@ -2288,7 +2277,7 @@ KJ_TEST("DrainingReader: pull enqueues then cancels on next pull (value stream)" uint pullCount = 0; auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { KJ_SWITCH_ONEOF(controller) { KJ_CASE_ONEOF(c, jsg::Ref) { @@ -2305,7 +2294,7 @@ KJ_TEST("DrainingReader: pull enqueues then cancels on next pull (value stream)" } KJ_UNREACHABLE; } - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on KJ_IF_SOME(reader, DrainingReader::create(js, *rs)) { @@ -2350,7 +2339,7 @@ KJ_TEST("DrainingReader: pending error in endOperation rejects read (value strea preamble([](jsg::Lock& js) { auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .pull = [](jsg::Lock& js, UnderlyingSource::Controller controller) { KJ_SWITCH_ONEOF(controller) { KJ_CASE_ONEOF(c, jsg::Ref) { @@ -2360,13 +2349,13 @@ KJ_TEST("DrainingReader: pending error in endOperation rejects read (value strea // and calls doError(), which defers the error because beginOperation() is // active. When wrapDrainingRead's endOperation() fires, it applies the // pending error and should throw rather than returning the data. - return js.rejectedPromise(js.v8TypeError("pull failed"_kj)); + return js.rejectedPromise(js.typeError("pull failed"_kj)); } KJ_CASE_ONEOF(c, jsg::Ref) {} } KJ_UNREACHABLE; } - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on KJ_IF_SOME(reader, DrainingReader::create(js, *rs)) { @@ -2389,19 +2378,19 @@ KJ_TEST("DrainingReader: pending error in endOperation rejects read (byte stream preamble([](jsg::Lock& js) { auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .type = kj::str("bytes"), .pull = [](jsg::Lock& js, UnderlyingSource::Controller controller) { KJ_SWITCH_ONEOF(controller) { KJ_CASE_ONEOF(c, jsg::Ref) {} KJ_CASE_ONEOF(c, jsg::Ref) { c->enqueue(js, toBufferSource(js, kj::str("should-be-discarded"))); - return js.rejectedPromise(js.v8TypeError("pull failed"_kj)); + return js.rejectedPromise(js.typeError("pull failed"_kj)); } } KJ_UNREACHABLE; } - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on KJ_IF_SOME(reader, DrainingReader::create(js, *rs)) { @@ -2437,7 +2426,7 @@ KJ_TEST("DrainingReader: controller closes promptly after drainingRead done (val preamble([](jsg::Lock& js) { auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { KJ_SWITCH_ONEOF(controller) { KJ_CASE_ONEOF(c, jsg::Ref) { @@ -2452,7 +2441,7 @@ KJ_TEST("DrainingReader: controller closes promptly after drainingRead done (val } KJ_UNREACHABLE; } - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on KJ_IF_SOME(reader, DrainingReader::create(js, *rs)) { @@ -2482,7 +2471,7 @@ KJ_TEST("DrainingReader: controller closes promptly after drainingRead done (byt preamble([](jsg::Lock& js) { auto rs = js.alloc(newReadableStreamJsController()); // clang-format off - rs->getController().setup(js, UnderlyingSource{ + rs->getController().setup(js, kj::heap(js, UnderlyingSource{ .type = kj::str("bytes"), .pull = [&](jsg::Lock& js, UnderlyingSource::Controller controller) { KJ_SWITCH_ONEOF(controller) { @@ -2496,7 +2485,7 @@ KJ_TEST("DrainingReader: controller closes promptly after drainingRead done (byt } KJ_UNREACHABLE; } - }, StreamQueuingStrategy{.highWaterMark = 0}); + }, StreamQueuingStrategy{.highWaterMark = 0})); // clang-format on KJ_IF_SOME(reader, DrainingReader::create(js, *rs)) { diff --git a/src/workerd/api/streams/standard.c++ b/src/workerd/api/streams/standard.c++ index 2a4b4b5e136..0f39d819426 100644 --- a/src/workerd/api/streams/standard.c++ +++ b/src/workerd/api/streams/standard.c++ @@ -7,7 +7,9 @@ #include "readable.h" #include "writable.h" +#include #include +#include #include #include #include @@ -62,9 +64,9 @@ class ReadableLockImpl { bool lock(); void onClose(jsg::Lock& js); - void onError(jsg::Lock& js, v8::Local reason); + void onError(jsg::Lock& js, jsg::JsValue reason); - kj::Maybe tryPipeLock(Controller& self); + kj::Maybe tryPipeLock(Controller& self) KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT; void visitForGc(jsg::GcVisitor& visitor); @@ -95,14 +97,14 @@ class ReadableLockImpl { return inner.state.template is(); } - kj::Maybe> tryGetErrored(jsg::Lock& js) override { + kj::Maybe tryGetErrored(jsg::Lock& js) override KJ_WARN_UNUSED_RESULT { KJ_IF_SOME(errored, inner.state.template tryGetUnsafe()) { return errored.getHandle(js); } return kj::none; } - void cancel(jsg::Lock& js, v8::Local reason) override { + void cancel(jsg::Lock& js, jsg::JsValue reason) override { // Cancel here returns a Promise but we do not need to propagate it. // We can safely drop it on the floor here. auto promise KJ_UNUSED = inner.cancel(js, reason); @@ -112,20 +114,21 @@ class ReadableLockImpl { inner.doClose(js); } - void error(jsg::Lock& js, v8::Local reason) override { + void error(jsg::Lock& js, jsg::JsValue reason) override { inner.doError(js, reason); } - void release(jsg::Lock& js, kj::Maybe> maybeError = kj::none) override { + void release(jsg::Lock& js, kj::Maybe maybeError = kj::none) override { KJ_IF_SOME(error, maybeError) { cancel(js, error); } inner.lock.state.template transitionTo(); } - kj::Maybe> tryPumpTo(WritableStreamSink& sink, bool end) override; + kj::Maybe> tryPumpTo( + WritableStreamSink& sink, End end) override KJ_WARN_UNUSED_RESULT; - jsg::Promise read(jsg::Lock& js) override; + jsg::Promise read(jsg::Lock& js) override KJ_WARN_UNUSED_RESULT; private: Controller& inner; @@ -187,7 +190,8 @@ class WritableLockImpl { kj::Maybe> maybeSignal; - kj::Maybe> checkSignal(jsg::Lock& js, Controller& self); + kj::Maybe> checkSignal( + jsg::Lock& js, Controller& self) KJ_WARN_UNUSED_RESULT; struct Flags { uint8_t preventAbort : 1 = 0; @@ -213,7 +217,7 @@ class WritableLockImpl { using LockState = StateMachine; LockState state = LockState::template create(); - inline kj::Maybe tryGetPipe() { + inline kj::Maybe tryGetPipe() KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT { KJ_IF_SOME(locked, state.template tryGetUnsafe()) { return locked; } @@ -334,7 +338,7 @@ void ReadableLockImpl::onClose(jsg::Lock& js) { } template -void ReadableLockImpl::onError(jsg::Lock& js, v8::Local reason) { +void ReadableLockImpl::onError(jsg::Lock& js, jsg::JsValue reason) { KJ_IF_SOME(locked, state.template tryGetUnsafe()) { try { maybeRejectPromise(js, locked.getClosedFulfiller(), reason); @@ -352,7 +356,7 @@ void ReadableLockImpl::onError(jsg::Lock& js, v8::Local r template kj::Maybe> ReadableLockImpl::PipeLocked::tryPumpTo( - WritableStreamSink& sink, bool end) { + WritableStreamSink& sink, End end) { // We return nullptr here because this controller does not support kj's pumpTo. return kj::none; } @@ -429,7 +433,7 @@ void WritableLockImpl::releaseWriter( // Per spec (WritableStreamDefaultWriterRelease), both the ready and closed // promises must be rejected when the writer is released. - auto releaseReason = js.v8TypeError("This WritableStream writer has been released."_kjc); + auto releaseReason = js.typeError("This WritableStream writer has been released."_kjc); if (FeatureFlags::get(js).getWritableStreamSpecCompliantWriter()) { if (locked.getReadyFulfiller() != kj::none) { maybeRejectPromise(js, locked.getReadyFulfiller(), releaseReason); @@ -515,16 +519,18 @@ kj::Maybe> WritableLockImpl::PipeLocked::checkSig if (signal->getAborted(js)) { auto reason = signal->getReason(js); if (!flags.preventCancel) { - source.release(js, v8::Local(reason)); + source.release(js, reason); } else { source.release(js); } if (!flags.preventAbort) { return self.abort(js, reason).then(js, JSG_VISITABLE_LAMBDA((this, reason = reason.addRef(js), ref = self.addRef()), (reason, ref), (jsg::Lock& js) { - return rejectedMaybeHandledPromise(js, reason.getHandle(js), flags.pipeThrough); + return rejectedMaybeHandledPromise( + js, reason.getHandle(js), flags.pipeThrough ? MarkAsHandled::YES : MarkAsHandled::NO); })); } - return rejectedMaybeHandledPromise(js, reason, flags.pipeThrough); + return rejectedMaybeHandledPromise( + js, reason, flags.pipeThrough ? MarkAsHandled::YES : MarkAsHandled::NO); } } return kj::none; @@ -611,21 +617,33 @@ jsg::Promise maybeRunAlgorithmAsync( // rare cases. For those we return a rejected promise but do not call the // onFailure case since such errors are generally indicative of a fatal // condition in the isolate (e.g. out of memory, other fatal exception, etc). - return js.tryCatch([&] { + JSG_TRY(js) { KJ_IF_SOME(ioContext, IoContext::tryCurrent()) { - return js - .tryCatch([&] { return algorithm(js, kj::fwd(args)...); }, - [&](jsg::Value&& exception) { return js.rejectedPromise(kj::mv(exception)); }) - .then(js, ioContext.addFunctor(kj::mv(onSuccess)), - ioContext.addFunctor(kj::mv(onFailure))); + auto getInnerPromise = [&]() -> jsg::Promise { + JSG_TRY(js) { + return algorithm(js, kj::fwd(args)...); + } + JSG_CATCH(exception) { + return js.rejectedPromise(kj::mv(exception)); + } + }; + return getInnerPromise().then( + js, ioContext.addFunctor(kj::mv(onSuccess)), ioContext.addFunctor(kj::mv(onFailure))); } else { - return js - .tryCatch([&] { return algorithm(js, kj::fwd(args)...); }, - [&](jsg::Value&& exception) { - return js.rejectedPromise(kj::mv(exception)); - }).then(js, kj::mv(onSuccess), kj::mv(onFailure)); + auto getInnerPromise = [&]() -> jsg::Promise { + JSG_TRY(js) { + return algorithm(js, kj::fwd(args)...); + } + JSG_CATCH(exception) { + return js.rejectedPromise(kj::mv(exception)); + } + }; + return getInnerPromise().then(js, kj::mv(onSuccess), kj::mv(onFailure)); } - }, [&](jsg::Value&& exception) { return js.rejectedPromise(kj::mv(exception)); }); + } + JSG_CATCH(exception) { + return js.rejectedPromise(kj::mv(exception)); + }; } // If the algorithm does not exist, we handle it as a success but ensure @@ -637,10 +655,68 @@ jsg::Promise maybeRunAlgorithmAsync( } } -int getHighWaterMark( - const UnderlyingSource& underlyingSource, const StreamQueuingStrategy& queuingStrategy) { - bool isBytes = underlyingSource.type.map([](auto& s) { return s == "bytes"; }).orDefault(false); - return queuingStrategy.highWaterMark.orDefault(isBytes ? 0 : 1); +// Like maybeRunAlgorithm but uses a pre-built PersistentContinuation instead of per-call +// lambdas. Skips OpaqueWrappable allocation, v8::Function::New, and addFunctor wrapping — +// the pre-built functions from the continuation are reused directly via thenRef(). +template +jsg::Promise maybeRunAlgorithmWithPersistentContinuation(jsg::Lock& js, + auto& maybeAlgorithm, + jsg::PersistentContinuation& continuation, + auto&&... args) { + KJ_IF_SOME(algorithm, maybeAlgorithm) { + JSG_TRY(js) { + auto innerPromise = [&]() -> jsg::Promise { + JSG_TRY(js) { + return algorithm(js, kj::fwd(args)...); + } + JSG_CATCH(exception) { + return js.rejectedPromise(kj::mv(exception)); + } + }(); + return innerPromise.thenRef(js, continuation); + } + JSG_CATCH(exception) { + return js.rejectedPromise(kj::mv(exception)); + } + } + + // Algorithm absent — invoke the success callback directly. + auto& funcPair = jsg::unwrapOpaqueRef(js.v8Isolate, continuation.data.getHandle(js)); + if constexpr (jsg::isVoid()) { + funcPair.thenFunc(js); + return js.resolvedPromise(); + } else { + return funcPair.thenFunc(js); + } +} + +// Async variant: defers to a microtask when the algorithm is absent. +template +jsg::Promise maybeRunAlgorithmAsyncWithPersistentContinuation(jsg::Lock& js, + auto& maybeAlgorithm, + jsg::PersistentContinuation& continuation, + auto&&... args) { + KJ_IF_SOME(algorithm, maybeAlgorithm) { + JSG_TRY(js) { + auto innerPromise = [&]() -> jsg::Promise { + JSG_TRY(js) { + return algorithm(js, kj::fwd(args)...); + } + JSG_CATCH(exception) { + return js.rejectedPromise(kj::mv(exception)); + } + }(); + return innerPromise.thenRef(js, continuation); + } + JSG_CATCH(exception) { + return js.rejectedPromise(kj::mv(exception)); + } + } + + // Algorithm absent — defer success to a microtask. + // When Output is void, thenRef returns Promise from the resolved promise chain, + // which is what we want. + return js.resolvedPromise().thenRef(js, continuation); } } // namespace @@ -659,7 +735,7 @@ jsg::Promise deferControllerStateChange(jsg::Lock& js, // methods, as well as the methods can trigger JavaScript errors to be thrown // synchronously in some cases. We want to make sure non-fatal errors cause the // stream to error and only fatal cases bubble up. - return js.tryCatch([&] { + JSG_TRY(js) { controller.state.beginOperation(); auto result = readCallback(); endOperation = false; @@ -682,15 +758,17 @@ jsg::Promise deferControllerStateChange(jsg::Lock& js, } return kj::mv(result); - }, [&](jsg::Value exception) -> jsg::Promise { + } + JSG_CATCH(exception) { if (endOperation) { // Clear any pending state since we're erroring controller.state.clearPendingState(); (void)controller.state.endOperation(); } - controller.doError(js, exception.getHandle(js)); - return js.rejectedPromise(kj::mv(exception)); - }); + auto handle = jsg::JsValue(exception.getHandle(js)); + controller.doError(js, handle); + return js.rejectedPromise(handle); + }; } // The ReadableStreamJsController provides the implementation of custom @@ -732,12 +810,15 @@ class ReadableStreamJsController final: public ReadableStreamController { explicit ReadableStreamJsController(StreamStates::Errored errored); explicit ReadableStreamJsController(jsg::Lock& js, ValueReadable& consumer); explicit ReadableStreamJsController(jsg::Lock& js, ByteReadable& consumer); + ~ReadableStreamJsController() noexcept(false); - jsg::Ref addRef() override; + bool isInternal() const override; - void setup(jsg::Lock& js, - jsg::Optional maybeUnderlyingSource, - jsg::Optional maybeQueuingStrategy) override; + kj::Maybe> tryReleaseSource() override KJ_WARN_UNUSED_RESULT; + + jsg::Ref addRef() override KJ_WARN_UNUSED_RESULT; + + void setup(jsg::Lock& js, kj::Own source) override; // Signals that this ReadableStream is no longer interested in the underlying // data source. Whether this cancels the underlying data source also depends @@ -746,11 +827,12 @@ class ReadableStreamJsController final: public ReadableStreamController { // is still pending, the ReadableStream will be no longer usable and any // data still in the queue will be dropped. Pending read requests will be // rejected if a reason is given, or resolved with no data otherwise. - jsg::Promise cancel(jsg::Lock& js, jsg::Optional> reason) override; + jsg::Promise cancel( + jsg::Lock& js, jsg::Optional reason) override KJ_WARN_UNUSED_RESULT; void doClose(jsg::Lock& js); - void doError(jsg::Lock& js, v8::Local reason); + void doError(jsg::Lock& js, jsg::JsValue reason); bool canCloseOrEnqueue(); bool hasBackpressure(); @@ -767,44 +849,50 @@ class ReadableStreamJsController final: public ReadableStreamController { bool lockReader(jsg::Lock& js, Reader& reader) override; - kj::Maybe> isErrored(jsg::Lock& js); + kj::Maybe isErrored(jsg::Lock& js) KJ_WARN_UNUSED_RESULT; kj::Maybe getDesiredSize(); - jsg::Promise pipeTo( - jsg::Lock& js, WritableStreamController& destination, PipeToOptions options) override; + jsg::Promise pipeTo(jsg::Lock& js, + WritableStreamController& destination, + PipeToOptions options) override KJ_WARN_UNUSED_RESULT; kj::Promise> pumpTo( - jsg::Lock& js, kj::Own, bool end) override; + jsg::Lock& js, kj::Own, End end) override KJ_WARN_UNUSED_RESULT; kj::Maybe> read( - jsg::Lock& js, kj::Maybe byobOptions) override; + jsg::Lock& js, kj::Maybe byobOptions) override KJ_WARN_UNUSED_RESULT; kj::Maybe> drainingRead( - jsg::Lock& js, size_t maxRead = kj::maxValue) override; + jsg::Lock& js, size_t maxRead = kj::maxValue) override KJ_WARN_UNUSED_RESULT; // See the comment for releaseReader in common.h for details on the use of maybeJs void releaseReader(Reader& reader, kj::Maybe maybeJs) override; void setOwnerRef(ReadableStream& stream) override; - Tee tee(jsg::Lock& js) override; + Tee tee(jsg::Lock& js) override KJ_WARN_UNUSED_RESULT; - kj::Maybe tryPipeLock() override; + kj::Maybe tryPipeLock() override KJ_WARN_UNUSED_RESULT; void visitForGc(jsg::GcVisitor& visitor) override; - kj::Maybe> getController(); + kj::Maybe> getController() KJ_WARN_UNUSED_RESULT; - jsg::Promise readAllBytes(jsg::Lock& js, uint64_t limit) override; - jsg::Promise readAllText(jsg::Lock& js, uint64_t limit) override; + jsg::Promise> readAllBytes( + jsg::Lock& js, uint64_t limit) override KJ_WARN_UNUSED_RESULT; + jsg::Promise readAllText( + jsg::Lock& js, uint64_t limit) override KJ_WARN_UNUSED_RESULT; kj::Maybe tryGetLength(StreamEncoding encoding) override; - kj::Own detach(jsg::Lock& js, bool ignoreDisturbed) override; + StreamEncoding getPreferredEncoding() override; + + kj::Own detach( + jsg::Lock& js, IgnoreDisturbed ignoreDisturbed) override KJ_WARN_UNUSED_RESULT; void setPendingClosure() override { - KJ_UNIMPLEMENTED("only implemented for WritableStreamInternalController"); + flags.pendingClosure = true; } kj::StringPtr jsgGetMemoryName() const override; @@ -846,15 +934,61 @@ class ReadableStreamJsController final: public ReadableStreamController { State state = State::create(); kj::Maybe expectedLength = kj::none; - bool canceling = false; + + struct Flags { + uint8_t canceling : 1 = 0; + // Used by Sockets code to signal that the owning object is closing. + // When set, read/tee/pumpTo/readAll operations reject immediately. + uint8_t pendingClosure : 1 = 0; + uint8_t disturbed : 1 = 0; + }; + Flags flags{}; // The lock state is separate because a closed or errored stream can still be locked. ReadableLockImpl lock; - bool disturbed = false; + // WeakRef for persistent continuation safety. + kj::Rc> weakSelf = + kj::rc>(kj::Badge{}, *this); + + // Persistent continuation for wrapDrainingRead's .then() — reused across + // draining read cycles to avoid per-read OpaqueWrappable/v8::Function allocations. + struct WrapDrainingReadCallbacks { + kj::Rc> weakSelf; + + DrainingReadResult thenFunc(jsg::Lock& js, DrainingReadResult result) KJ_WARN_UNUSED_RESULT { + KJ_IF_SOME(self, weakSelf->tryGet()) { + if (self.state.endOperation()) { + if (self.state.template is()) { + self.lock.onClose(js); + } else if (self.state.template is()) { + KJ_IF_SOME(err, self.state.template tryGetUnsafe()) { + self.lock.onError(js, err.getHandle(js)); + js.throwException(err.addRef(js)); + } + } + } + return kj::mv(result); + } + // Controller is gone — just return what we have. + return kj::mv(result); + } + + DrainingReadResult catchFunc(jsg::Lock& js, jsg::Value exception) KJ_WARN_UNUSED_RESULT { + KJ_IF_SOME(self, weakSelf->tryGet()) { + self.state.clearPendingState(); + (void)self.state.endOperation(); + } + js.throwException(kj::mv(exception)); + KJ_UNREACHABLE; + } + }; + using WrapDrainingReadContinuationType = jsg:: + PersistentContinuation; + kj::Maybe wrapDrainingReadContinuation; template - jsg::Promise readAll(jsg::Lock& js, uint64_t limit); + jsg::Promise readAll(jsg::Lock& js, uint64_t limit) KJ_WARN_UNUSED_RESULT; friend ReadableLockImpl; friend ReadableLockImpl::PipeLocked; @@ -886,28 +1020,29 @@ class WritableStreamJsController final: public WritableStreamController { KJ_DISALLOW_COPY_AND_MOVE(WritableStreamJsController); - jsg::Promise abort(jsg::Lock& js, jsg::Optional> reason) override; + jsg::Promise abort( + jsg::Lock& js, jsg::Optional reason) override KJ_WARN_UNUSED_RESULT; - jsg::Ref addRef() override; + jsg::Ref addRef() override KJ_WARN_UNUSED_RESULT; - jsg::Promise close(jsg::Lock& js, bool markAsHandled = false) override; + jsg::Promise close(jsg::Lock& js, + MarkAsHandled markAsHandled = MarkAsHandled::NO) override KJ_WARN_UNUSED_RESULT; - jsg::Promise flush(jsg::Lock& js, bool markAsHandled = false) override { - KJ_UNIMPLEMENTED("expected WritableStreamInternalController implementation to be enough"); - } + jsg::Promise flush(jsg::Lock& js, + MarkAsHandled markAsHandled = MarkAsHandled::NO) override KJ_WARN_UNUSED_RESULT; void doClose(jsg::Lock& js); - void doError(jsg::Lock& js, v8::Local reason); + void doError(jsg::Lock& js, jsg::JsValue reason); // Error through the underlying controller if available, going through the proper // error transition (Erroring -> Errored). - void errorIfNeeded(jsg::Lock& js, v8::Local reason); + void errorIfNeeded(jsg::Lock& js, jsg::JsValue reason); kj::Maybe getDesiredSize() override; - kj::Maybe> isErroring(jsg::Lock& js) override; - kj::Maybe> isErroredOrErroring(jsg::Lock& js); + kj::Maybe isErroring(jsg::Lock& js) override KJ_WARN_UNUSED_RESULT; + kj::Maybe isErroredOrErroring(jsg::Lock& js) KJ_WARN_UNUSED_RESULT; bool isLocked() const; @@ -923,7 +1058,7 @@ class WritableStreamJsController final: public WritableStreamController { bool lockWriter(jsg::Lock& js, Writer& writer) override; - void maybeRejectReadyPromise(jsg::Lock& js, v8::Local reason); + void maybeRejectReadyPromise(jsg::Lock& js, jsg::JsValue reason); void maybeResolveReadyPromise(jsg::Lock& js); @@ -935,16 +1070,14 @@ class WritableStreamJsController final: public WritableStreamController { void setOwnerRef(WritableStream& stream) override; - void setup(jsg::Lock& js, - jsg::Optional maybeUnderlyingSink, - jsg::Optional maybeQueuingStrategy) override; + void setup(jsg::Lock& js, kj::Own sink) override; kj::Maybe> tryPipeFrom( jsg::Lock& js, jsg::Ref source, PipeToOptions options) override; - void updateBackpressure(jsg::Lock& js, bool backpressure); + void updateBackpressure(jsg::Lock& js, UpdateBackpressure backpressure); - jsg::Promise write(jsg::Lock& js, jsg::Optional> value) override; + jsg::Promise write(jsg::Lock& js, jsg::Optional value) override; void visitForGc(jsg::GcVisitor& visitor) override; @@ -952,11 +1085,19 @@ class WritableStreamJsController final: public WritableStreamController { bool isErrored() override; inline bool isByteOriented() const override { + // When backed by an internal sink, report as byte-oriented. + KJ_IF_SOME(controller, state.tryGetUnsafe()) { + return controller->isInternal(); + } return false; } void setPendingClosure() override { - KJ_UNIMPLEMENTED("only implemented for WritableStreamInternalController"); + flags.pendingClosure = true; + } + + void setClosureWaitable(jsg::Promise waitable) override { + maybeClosureWaitable = kj::mv(waitable); } kj::StringPtr jsgGetMemoryName() const override; @@ -966,6 +1107,70 @@ class WritableStreamJsController final: public WritableStreamController { private: jsg::Promise pipeLoop(jsg::Lock& js); + // Pipe loop continuation callbacks — used by pipeLoop for the read and write + // dispatch. These are created lazily on the first pipe operation and reused + // for each chunk flowing through the pipe. + + // Called when a read from the source succeeds during piping. + jsg::Promise onPipeReadSuccess(jsg::Lock& js, ReadResult result) KJ_WARN_UNUSED_RESULT; + // Called when a read from the source fails during piping. + jsg::Promise onPipeReadFailure(jsg::Lock& js, jsg::Value reason) KJ_WARN_UNUSED_RESULT; + // Called when a write to this destination succeeds during piping. + jsg::Promise onPipeWriteSuccess(jsg::Lock& js) KJ_WARN_UNUSED_RESULT; + // Called when a write to this destination fails during piping. + jsg::Promise onPipeWriteFailure(jsg::Lock& js, jsg::Value reason) KJ_WARN_UNUSED_RESULT; + + struct PipeReadContinuationCallbacks { + kj::Rc> weakCtrl; + + jsg::Promise thenFunc(jsg::Lock& js, ReadResult result) KJ_WARN_UNUSED_RESULT { + KJ_IF_SOME(ctrl, weakCtrl->tryGet()) { + return ctrl.onPipeReadSuccess(js, kj::mv(result)); + } + return js.resolvedPromise(); + } + + jsg::Promise catchFunc(jsg::Lock& js, jsg::Value reason) KJ_WARN_UNUSED_RESULT { + KJ_IF_SOME(ctrl, weakCtrl->tryGet()) { + return ctrl.onPipeReadFailure(js, kj::mv(reason)); + } + return js.rejectedPromise(kj::mv(reason)); + } + }; + using PipeReadContinuationType = + jsg::PersistentContinuation>; + + struct PipeWriteContinuationCallbacks { + kj::Rc> weakCtrl; + + jsg::Promise thenFunc(jsg::Lock& js) KJ_WARN_UNUSED_RESULT { + KJ_IF_SOME(ctrl, weakCtrl->tryGet()) { + return ctrl.onPipeWriteSuccess(js); + } + return js.resolvedPromise(); + } + + jsg::Promise catchFunc(jsg::Lock& js, jsg::Value reason) KJ_WARN_UNUSED_RESULT { + KJ_IF_SOME(ctrl, weakCtrl->tryGet()) { + return ctrl.onPipeWriteFailure(js, kj::mv(reason)); + } + return js.rejectedPromise(kj::mv(reason)); + } + }; + using PipeWriteContinuationType = + jsg::PersistentContinuation>; + + kj::Maybe pipeReadContinuation; + kj::Maybe pipeWriteContinuation; + + PipeReadContinuationType& getPipeReadContinuation( + jsg::Lock& js) KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT; + PipeWriteContinuationType& getPipeWriteContinuation( + jsg::Lock& js) KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT; + + kj::Rc> weakSelf = + kj::rc>(kj::Badge{}, *this); + kj::Maybe ioContext; kj::Maybe owner; @@ -990,6 +1195,18 @@ class WritableStreamJsController final: public WritableStreamController { WritableLockImpl lock; kj::Maybe> maybeAbortPromise; + struct Flags { + // Used by Sockets code to signal that the owning object is closing. + uint8_t pendingClosure : 1 = 0; + // Set during setup() when the underlying sink is internal (InternalUnderlyingSinkImpl). + uint8_t internalBacked : 1 = 0; + // Used by Sockets code to ensure the connection is established before close. + uint8_t waitingOnClosureWritableAlready : 1 = 0; + }; + Flags flags{}; + + kj::Maybe> maybeClosureWaitable; + friend WritableLockImpl; }; @@ -1003,21 +1220,48 @@ kj::Own newWritableStreamJsController() { template ReadableImpl::ReadableImpl( - UnderlyingSource underlyingSource, StreamQueuingStrategy queuingStrategy) - : state(State::template create(getHighWaterMark(underlyingSource, queuingStrategy))), - algorithms(kj::mv(underlyingSource), kj::mv(queuingStrategy)) {} + jsg::Lock& js, kj::Own source, kj::Rc> weakController) + : state(State::template create(source->getHighWaterMark())), + underlyingSource(kj::mv(source)), + weakController(kj::mv(weakController)) {} + +template +kj::Maybe ReadableImpl::tryTeeSource(uint64_t limit) { + if (underlyingSource.get() == nullptr) return kj::none; + if (flags.pulling) return kj::none; + return underlyingSource->tryTee(limit); +} + +template +kj::Maybe> ReadableImpl::tryReleaseSource() { + if (underlyingSource.get() == nullptr) return kj::none; + if (flags.pulling) return kj::none; + return underlyingSource->tryReleaseSource(); +} + +template +bool ReadableImpl::isInternal() const { + if (underlyingSource.get() == nullptr) return false; + return underlyingSource->isInternal(); +} + +template +StreamEncoding ReadableImpl::getPreferredEncoding() { + if (underlyingSource.get() == nullptr) return StreamEncoding::IDENTITY; + return underlyingSource->getPreferredEncoding(); +} + +template +kj::Maybe ReadableImpl::tryGetLength(StreamEncoding encoding) { + if (underlyingSource.get() == nullptr) return kj::none; + return underlyingSource->tryGetLength(encoding); +} template void ReadableImpl::start(jsg::Lock& js, jsg::Ref self) { KJ_ASSERT(!flags.started && !flags.starting); flags.starting = true; - // Per the streams spec, the size function should be called with `undefined` as `this`, - // not as a method on the strategy object. - KJ_IF_SOME(sizeFunc, algorithms.size) { - sizeFunc.setReceiver(jsg::Value(js.v8Isolate, js.v8Undefined())); - } - auto onSuccess = JSG_VISITABLE_LAMBDA((this, self = self.addRef()), (self), (jsg::Lock& js) { flags.started = true; flags.starting = false; @@ -1028,11 +1272,12 @@ void ReadableImpl::start(jsg::Lock& js, jsg::Ref self) { (this, self = self.addRef()), (self), (jsg::Lock& js, jsg::Value reason) { flags.started = true; flags.starting = false; - doError(js, kj::mv(reason)); + doError(js, jsg::JsValue(reason.getHandle(js))); }); - maybeRunAlgorithm(js, algorithms.start, kj::mv(onSuccess), kj::mv(onFailure), kj::mv(self)); - algorithms.start = kj::none; + maybeRunAlgorithm( + js, underlyingSource->start(), kj::mv(onSuccess), kj::mv(onFailure), kj::mv(self)); + underlyingSource->clearStart(); } template @@ -1042,7 +1287,7 @@ size_t ReadableImpl::consumerCount() { template jsg::Promise ReadableImpl::cancel( - jsg::Lock& js, jsg::Ref self, v8::Local reason) { + jsg::Lock& js, jsg::Ref self, jsg::JsValue reason) { if (state.template is()) { // We are already closed. There's nothing to cancel. // This shouldn't happen but we handle the case anyway, just to be safe. @@ -1095,7 +1340,7 @@ bool ReadableImpl::canCloseOrEnqueue() { // that they called cancel. What we do want to do here, tho, is close the implementation // and trigger the cancel algorithm. template -void ReadableImpl::doCancel(jsg::Lock& js, jsg::Ref self, v8::Local reason) { +void ReadableImpl::doCancel(jsg::Lock& js, jsg::Ref self, jsg::JsValue reason) { state.template transitionTo(); auto onSuccess = JSG_VISITABLE_LAMBDA((this, self = self.addRef()), (self), (jsg::Lock& js) { @@ -1113,13 +1358,13 @@ void ReadableImpl::doCancel(jsg::Lock& js, jsg::Ref self, v8::Local< // no longer cares and has gone away. doClose(js); KJ_IF_SOME(pendingCancel, maybePendingCancel) { - maybeRejectPromise(js, pendingCancel.fulfiller, reason.getHandle(js)); + maybeRejectPromise(js, pendingCancel.fulfiller, jsg::JsValue(reason.getHandle(js))); } else { // Else block to avert dangling else compiler warning. } }); - maybeRunAlgorithm(js, algorithms.cancel, kj::mv(onSuccess), kj::mv(onFailure), reason); + maybeRunAlgorithm(js, underlyingSource->cancel(), kj::mv(onSuccess), kj::mv(onFailure), reason); } template @@ -1135,11 +1380,10 @@ void ReadableImpl::close(jsg::Lock& js) { JSG_REQUIRE(canCloseOrEnqueue(), TypeError, "This ReadableStream is closed."); auto& queue = state.template getUnsafe(); - if (queue.hasPartiallyFulfilledRead()) { - auto error = - js.v8Ref(js.v8TypeError("This ReadableStream was closed with a partial read pending.")); - doError(js, error.addRef(js)); - js.throwException(kj::mv(error)); + if (queue.hasPartiallyFulfilledRead(js)) { + auto error = js.typeError("This ReadableStream was closed with a partial read pending."); + doError(js, error); + js.throwException(error); return; } @@ -1153,20 +1397,24 @@ template void ReadableImpl::doClose(jsg::Lock& js) { // The state should have already been set to closed. KJ_ASSERT(state.template is()); - algorithms.clear(); + if (underlyingSource.get() != nullptr) { + underlyingSource->clear(); + } } template -void ReadableImpl::doError(jsg::Lock& js, jsg::Value reason) { +void ReadableImpl::doError(jsg::Lock& js, jsg::JsValue reason) { // If already closed or errored, do nothing if (state.isInactive()) { return; } auto& queue = state.template getUnsafe(); - queue.error(js, reason.addRef(js)); - state.template transitionTo(kj::mv(reason)); - algorithms.clear(); + queue.error(js, reason); + state.template transitionTo(reason.addRef(js)); + if (underlyingSource.get() != nullptr) { + underlyingSource->clear(); + } } template @@ -1188,8 +1436,46 @@ bool ReadableImpl::shouldCallPull() { [this](Queue& q) { return q.wantsRead() || getDesiredSize().orDefault(0) > 0; }, false); } +template +ReadableImpl::PullContinuationType& ReadableImpl::getPullContinuation(jsg::Lock& js) { + KJ_IF_SOME(pc, pullContinuation) { + return pc; + } + pullContinuation.emplace( + PullContinuationType::create(js, PullContinuationCallbacks{this, weakController.addRef()})); + return KJ_ASSERT_NONNULL(pullContinuation); +} + +template +void ReadableImpl::onPullSuccess(jsg::Lock& js) { + // pullSelf may have been cleared by clearAlgorithms() during teardown. + // If so, the stream is being destroyed and we should bail out. + auto maybeSelf = kj::mv(pullSelf); + pullSelf = kj::none; + flags.pulling = false; + KJ_IF_SOME(self, maybeSelf) { + if (flags.pullAgain) { + flags.pullAgain = false; + pullIfNeeded(js, kj::mv(self)); + } + } +} + +template +void ReadableImpl::onPullFailure(jsg::Lock& js, jsg::Value reason) { + // pullSelf may have been cleared by clearAlgorithms() during teardown. + // Keep it alive on the stack through doError — doError can trigger a cascade + // (via queue.error → consumer callbacks) that may drop the last Ref. + auto self = kj::mv(pullSelf); + pullSelf = kj::none; + flags.pulling = false; + doError(js, jsg::JsValue(reason.getHandle(js))); +} + template void ReadableImpl::pullIfNeeded(jsg::Lock& js, jsg::Ref self) { + // If algorithms have been cleared (e.g. during teardown), don't pull. + if (underlyingSource.get() == nullptr) return; // Determining if we need to pull is fairly complicated. All of the following // must hold true: if (!shouldCallPull()) { @@ -1203,25 +1489,15 @@ void ReadableImpl::pullIfNeeded(jsg::Lock& js, jsg::Ref self) { KJ_ASSERT(!flags.pullAgain); flags.pulling = true; - auto onSuccess = JSG_VISITABLE_LAMBDA((this, self = self.addRef()), (self), (jsg::Lock& js) { - flags.pulling = false; - if (flags.pullAgain) { - flags.pullAgain = false; - pullIfNeeded(js, kj::mv(self)); - } - }); - - auto onFailure = JSG_VISITABLE_LAMBDA( - (this, self = self.addRef()), (self), (jsg::Lock& js, jsg::Value reason) { - flags.pulling = false; - doError(js, kj::mv(reason)); - }); - - maybeRunAlgorithm(js, algorithms.pull, kj::mv(onSuccess), kj::mv(onFailure), self.addRef()); + pullSelf = self.addRef(); + auto& pc = getPullContinuation(js); + maybeRunAlgorithmWithPersistentContinuation(js, underlyingSource->pull(), pc, kj::mv(self)); } template void ReadableImpl::forcePullIfNeeded(jsg::Lock& js, jsg::Ref self) { + // If algorithms have been cleared (e.g. during teardown), don't pull. + if (underlyingSource.get() == nullptr) return; // Like pullIfNeeded but bypasses the shouldCallPull() check. Used for draining reads // which need to pull all available data regardless of backpressure settings. if (!canCloseOrEnqueue()) { @@ -1235,22 +1511,9 @@ void ReadableImpl::forcePullIfNeeded(jsg::Lock& js, jsg::Ref self) { KJ_ASSERT(!flags.pullAgain); flags.pulling = true; - auto onSuccess = JSG_VISITABLE_LAMBDA((this, self = self.addRef()), (self), (jsg::Lock& js) { - flags.pulling = false; - if (flags.pullAgain) { - flags.pullAgain = false; - // After a force pull, we go back to normal pullIfNeeded behavior. - pullIfNeeded(js, kj::mv(self)); - } - }); - - auto onFailure = JSG_VISITABLE_LAMBDA( - (this, self = self.addRef()), (self), (jsg::Lock& js, jsg::Value reason) { - flags.pulling = false; - doError(js, kj::mv(reason)); - }); - - maybeRunAlgorithm(js, algorithms.pull, kj::mv(onSuccess), kj::mv(onFailure), self.addRef()); + pullSelf = self.addRef(); + auto& pc = getPullContinuation(js); + maybeRunAlgorithmWithPersistentContinuation(js, underlyingSource->pull(), pc, kj::mv(self)); } template @@ -1259,7 +1522,16 @@ void ReadableImpl::visitForGc(jsg::GcVisitor& visitor) { KJ_IF_SOME(pendingCancel, maybePendingCancel) { visitor.visit(pendingCancel.fulfiller, pendingCancel.promise); } - visitor.visit(algorithms); + if (underlyingSource.get() != nullptr) { + visitor.visit(*underlyingSource); + } + KJ_IF_SOME(pc, pullContinuation) { + pc.visitForGc(visitor); + } + // Note: pullSelf is intentionally NOT traced here. It is a Ref back to the + // controller (Self) that contains this ReadableImpl, which would create an infinite + // GC tracing cycle. The controller is already reachable through its owning + // ReadableStream's ref chain, so it won't be collected while pullSelf exists. } template @@ -1272,25 +1544,54 @@ kj::Own::Consumer> ReadableImpl::getConsumer( // ====================================================================================== template -WritableImpl::WritableImpl( - jsg::Lock& js, WritableStream& owner, jsg::Ref abortSignal) +WritableImpl::WritableImpl(jsg::Lock& js, + WritableStream& owner, + kj::Own sink, + jsg::Ref abortSignal, + kj::Rc> weakController) : owner(owner.addWeakRef()), - signal(kj::mv(abortSignal)) { - flags.pedanticWpt = FeatureFlags::get(js).getPedanticWpt(); + signal(kj::mv(abortSignal)), + underlyingSink(kj::mv(sink)), + weakController(kj::mv(weakController)) { + auto featureFlags = FeatureFlags::get(js); + flags.pedanticWpt = featureFlags.getPedanticWpt(); + flags.specCompliantWriter = featureFlags.getWritableStreamSpecCompliantWriter(); +} + +template +bool WritableImpl::isInternal() const { + if (underlyingSink.get() == nullptr) return false; + return underlyingSink->isInternal(); +} + +template +kj::Maybe> WritableImpl::tryReleaseSink() { + if (underlyingSink.get() == nullptr) return kj::none; + if (inFlightWrite != kj::none || inFlightBatchWrite != kj::none || inFlightClose != kj::none || + !writeRequests.empty()) { + return kj::none; + } + return underlyingSink->tryReleaseSink(); +} + +template +kj::Maybe WritableImpl::tryGetSink() { + if (underlyingSink.get() == nullptr) return kj::none; + return underlyingSink->tryGetSink(); } template jsg::Promise WritableImpl::abort( - jsg::Lock& js, jsg::Ref self, v8::Local reason) { + jsg::Lock& js, jsg::Ref self, jsg::JsValue reason) { // Per the spec, the signal.reason should be a DOMException with name 'AbortError' // when no reason is provided, but the stored error should remain as the original reason. auto signalReason = [&]() -> jsg::JsValue { - if (reason->IsUndefined() && FeatureFlags::get(js).getPedanticWpt()) { + if (reason.isUndefined() && flags.pedanticWpt) { auto ex = js.domException( kj::str("AbortError"), kj::str("This writable stream has been aborted."), kj::none); return jsg::JsValue(KJ_ASSERT_NONNULL(ex.tryGetHandle(js))); } - return jsg::JsValue(reason); + return reason; }(); signal->triggerAbort(js, signalReason); @@ -1308,12 +1609,13 @@ jsg::Promise WritableImpl::abort( bool wasAlreadyErroring = false; if (state.template is()) { wasAlreadyErroring = true; - reason = js.v8Undefined(); + reason = js.undefined(); } KJ_DEFER(if (!wasAlreadyErroring) { startErroring(js, kj::mv(self), reason); }); - maybePendingAbort = kj::heap(js, reason, wasAlreadyErroring); + maybePendingAbort = + kj::heap(js, reason, wasAlreadyErroring ? Reject::YES : Reject::NO); return KJ_ASSERT_NONNULL(maybePendingAbort)->whenResolved(js); } @@ -1328,13 +1630,151 @@ kj::Maybe WritableImpl::tryGetOwner() { } template -ssize_t WritableImpl::getDesiredSize() { - return highWaterMark - amountBuffered; +kj::Maybe WritableImpl::getDesiredSize() { + return static_cast(underlyingSink->getHighWaterMark() - amountBuffered); +} + +template +WritableImpl::WriteContinuationType& WritableImpl::getWriteContinuation(jsg::Lock& js) { + KJ_IF_SOME(wc, writeContinuation) { + return wc; + } + writeContinuation.emplace( + WriteContinuationType::create(js, WriteContinuationCallbacks{this, weakController.addRef()})); + return KJ_ASSERT_NONNULL(writeContinuation); +} + +template +jsg::Promise WritableImpl::onWriteSuccess(jsg::Lock& js) { + // Read per-write state from inFlightWrite — no per-write captures needed. + auto& write = KJ_ASSERT_NONNULL(inFlightWrite); + amountBuffered -= write.size; + auto self = kj::mv(KJ_ASSERT_NONNULL(inFlightSelf)); + inFlightSelf = kj::none; + finishInFlightWrite(js, self.addRef()); + KJ_ASSERT(isWritable() || state.template is()); + if (!isCloseQueuedOrInFlight() && isWritable()) { + updateBackpressure(js); + } + if (state.template is() || writeRequests.empty()) { + // In this case, we know advanceQueueIfNeeded won't recurse further, so we can + // avoid the extra microtask hop. + advanceQueueIfNeeded(js, kj::mv(self)); + return js.resolvedPromise(); + } + // Here, however, let's avoid potentially deep recursion by hopping to a new + // microtask to continue processing the queue. + inFlightSelf = kj::mv(self); + return js.resolvedPromise().thenRef(js, getDrainContinuation(js)); +} + +template +jsg::Promise WritableImpl::onWriteFailure(jsg::Lock& js, jsg::Value reason) { + auto& write = KJ_ASSERT_NONNULL(inFlightWrite); + amountBuffered -= write.size; + auto self = kj::mv(KJ_ASSERT_NONNULL(inFlightSelf)); + inFlightSelf = kj::none; + finishInFlightWrite(js, kj::mv(self), jsg::JsValue(reason.getHandle(js))); + return js.resolvedPromise(); +} + +template +WritableImpl::DrainContinuationType& WritableImpl::getDrainContinuation(jsg::Lock& js) { + KJ_IF_SOME(dc, drainContinuation) { + return dc; + } + drainContinuation.emplace( + DrainContinuationType::create(js, DrainContinuationCallbacks{this, weakController.addRef()})); + return KJ_ASSERT_NONNULL(drainContinuation); +} + +template +void WritableImpl::onDrainNext(jsg::Lock& js) { + auto self = kj::mv(KJ_ASSERT_NONNULL(inFlightSelf)); + inFlightSelf = kj::none; + if (isWritable() || state.template is()) { + advanceQueueIfNeeded(js, kj::mv(self)); + } +} + +template +WritableImpl::WritevContinuationType& WritableImpl::getWritevContinuation( + jsg::Lock& js) { + KJ_IF_SOME(wvc, writevContinuation) { + return wvc; + } + writevContinuation.emplace(WritevContinuationType::create( + js, WritevContinuationCallbacks{this, weakController.addRef()})); + return KJ_ASSERT_NONNULL(writevContinuation); +} + +template +jsg::Promise WritableImpl::onWritevSuccess(jsg::Lock& js) { + auto& batch = KJ_ASSERT_NONNULL(inFlightBatchWrite); + amountBuffered -= batch.totalSize; + auto self = kj::mv(KJ_ASSERT_NONNULL(inFlightSelf)); + inFlightSelf = kj::none; + // Resolve all the individual write promises. + for (auto& resolver: batch.resolvers) { + resolver.resolve(js); + } + inFlightBatchWrite = kj::none; + KJ_ASSERT(isWritable() || state.template is()); + if (!isCloseQueuedOrInFlight() && isWritable()) { + updateBackpressure(js); + } + if (state.template is() || writeRequests.empty()) { + advanceQueueIfNeeded(js, kj::mv(self)); + return js.resolvedPromise(); + } + inFlightSelf = kj::mv(self); + return js.resolvedPromise().thenRef(js, getDrainContinuation(js)); +} + +template +jsg::Promise WritableImpl::onWritevFailure(jsg::Lock& js, jsg::Value reason) { + auto& batch = KJ_ASSERT_NONNULL(inFlightBatchWrite); + amountBuffered -= batch.totalSize; + auto self = kj::mv(KJ_ASSERT_NONNULL(inFlightSelf)); + inFlightSelf = kj::none; + auto jsReason = jsg::JsValue(reason.getHandle(js)); + // Reject all the individual write promises. + for (auto& resolver: batch.resolvers) { + resolver.reject(js, jsReason); + } + inFlightBatchWrite = kj::none; + KJ_ASSERT(isWritable() || state.template is()); + dealWithRejection(js, kj::mv(self), jsReason); + return js.resolvedPromise(); +} + +template +WritableImpl::CloseContinuationType& WritableImpl::getCloseContinuation(jsg::Lock& js) { + KJ_IF_SOME(cc, closeContinuation) { + return cc; + } + closeContinuation.emplace( + CloseContinuationType::create(js, CloseContinuationCallbacks{this, weakController.addRef()})); + return KJ_ASSERT_NONNULL(closeContinuation); +} + +template +void WritableImpl::onCloseSuccess(jsg::Lock& js) { + auto self = kj::mv(KJ_ASSERT_NONNULL(inFlightSelf)); + inFlightSelf = kj::none; + finishInFlightClose(js, kj::mv(self)); +} + +template +void WritableImpl::onCloseFailure(jsg::Lock& js, jsg::Value reason) { + auto self = kj::mv(KJ_ASSERT_NONNULL(inFlightSelf)); + inFlightSelf = kj::none; + finishInFlightClose(js, kj::mv(self), jsg::JsValue(reason.getHandle(js))); } template void WritableImpl::advanceQueueIfNeeded(jsg::Lock& js, jsg::Ref self) { - if (!flags.started || inFlightWrite != kj::none) { + if (!flags.started || inFlightWrite != kj::none || inFlightBatchWrite != kj::none) { return; } KJ_ASSERT(isWritable() || state.template is()); @@ -1343,89 +1783,98 @@ void WritableImpl::advanceQueueIfNeeded(jsg::Lock& js, jsg::Ref self return finishErroring(js, kj::mv(self)); } + KJ_ASSERT(inFlightWrite == kj::none); + KJ_ASSERT(inFlightBatchWrite == kj::none); + + // Drain any leading flush entries — these are sync points that resolve immediately + // once all preceding writes have completed (which they have, since inFlightWrite is none). + while (!writeRequests.empty() && writeRequests.front().flush) { + dequeueWriteRequest().resolver.resolve(js); + } + if (writeRequests.empty()) { if (closeRequest != kj::none) { KJ_ASSERT(inFlightClose == kj::none); KJ_ASSERT_NONNULL(closeRequest); inFlightClose = kj::mv(closeRequest); + inFlightSelf = kj::mv(self); - auto onSuccess = JSG_VISITABLE_LAMBDA((this, self = self.addRef()), (self), - (jsg::Lock& js) { finishInFlightClose(js, kj::mv(self)); }); - - auto onFailure = JSG_VISITABLE_LAMBDA( - (this, self = self.addRef()), (self), (jsg::Lock& js, jsg::Value reason) { - finishInFlightClose(js, kj::mv(self), reason.getHandle(js)); - }); - + auto& cc = getCloseContinuation(js); // Per the spec, the close algorithm should always run asynchronously, even if // there's no user-provided close handler. This ensures that releaseLock() can // reject the closed promise before the close completes. // The original maybeRunAlgorithm would call the onSuccess continuation // synchronously if algorithms.close is not specified. maybeRunAlgorithmAsync // always defers to a microtask. - if (FeatureFlags::get(js).getPedanticWpt()) { - maybeRunAlgorithmAsync(js, algorithms.close, kj::mv(onSuccess), kj::mv(onFailure)); + if (flags.pedanticWpt) { + maybeRunAlgorithmAsyncWithPersistentContinuation(js, underlyingSink->close(), cc); } else { - maybeRunAlgorithm(js, algorithms.close, kj::mv(onSuccess), kj::mv(onFailure)); + maybeRunAlgorithmWithPersistentContinuation(js, underlyingSink->close(), cc); } } return; } - KJ_ASSERT(inFlightWrite == kj::none); - auto req = dequeueWriteRequest(); - auto value = req.value.addRef(js); - auto size = req.size; - inFlightWrite = kj::mv(req); - - auto onSuccess = - JSG_VISITABLE_LAMBDA((this, self = self.addRef(), size), (self), (jsg::Lock& js) { - amountBuffered -= size; - finishInFlightWrite(js, self.addRef()); - KJ_ASSERT(isWritable() || state.template is()); - if (!isCloseQueuedOrInFlight() && isWritable()) { - updateBackpressure(js); - } - if (state.template is() || writeRequests.empty()) { - // In this case, we know advanceQueueIfNeeded won't recurse further, so we can - // avoid the extra microtask hop. - advanceQueueIfNeeded(js, kj::mv(self)); - return js.resolvedPromise(); + // If the underlying sink supports writev and there are multiple entries queued, + // batch them into a single writev call. Flush entries ride along — their resolvers + // are included in the batch but no chunk is added for them. + if (writeRequests.size() > 1) { + if (underlyingSink->writev() != kj::none) { + auto count = writeRequests.size(); + auto resolvers = kj::heapArrayBuilder::Resolver>(count); + auto values = kj::heapArrayBuilder>(count); + size_t totalSize = 0; + + while (!writeRequests.empty()) { + auto req = dequeueWriteRequest(); + if (req.flush) { + // Flush marker — include resolver but no chunk. + resolvers.add(kj::mv(req.resolver)); + } else { + values.add(kj::mv(req.value)); + resolvers.add(kj::mv(req.resolver)); + totalSize += req.size; } - // Here, however, let's avoid potentially deep recursion by hopping to a new - // microtask to continue processing the queue. - return js.resolvedPromise().then( - js, JSG_VISITABLE_LAMBDA((this, self = kj::mv(self)), (self), (jsg::Lock & js) mutable { - if (isWritable() || state.template is()) { - advanceQueueIfNeeded(js, kj::mv(self)); - } - })); - }); + } - auto onFailure = JSG_VISITABLE_LAMBDA( - (this, self = self.addRef(), size), (self), (jsg::Lock& js, jsg::Value reason) { - amountBuffered -= size; - finishInFlightWrite(js, kj::mv(self), reason.getHandle(js)); - return js.resolvedPromise(); - }); + inFlightBatchWrite = BatchWriteRequest{ + .resolvers = resolvers.finish(), + .totalSize = totalSize, + }; + inFlightSelf = kj::mv(self); + + auto& wvc = getWritevContinuation(js); + // writev is a non-standard extension — always dispatch synchronously (no pedanticWpt). + maybeRunAlgorithmWithPersistentContinuation(js, underlyingSink->writev(), wvc, + values.finish(), KJ_ASSERT_NONNULL(inFlightSelf).addRef()); + return; + } + } + + // Single write dispatch. + auto req = dequeueWriteRequest(); + auto value = req.value.getHandle(js); + inFlightWrite = kj::mv(req); + inFlightSelf = kj::mv(self); + auto& wc = getWriteContinuation(js); // Per the spec, the write algorithm should always run asynchronously, even if // there's no user-provided write handler. This ensures that backpressure changes // from the write don't resolve the ready promise synchronously, preserving correct // microtask ordering (e.g., ready rejects before closed on releaseLock). - if (FeatureFlags::get(js).getPedanticWpt()) { - maybeRunAlgorithmAsync(js, algorithms.write, kj::mv(onSuccess), kj::mv(onFailure), - value.getHandle(js), self.addRef()); + if (flags.pedanticWpt) { + maybeRunAlgorithmAsyncWithPersistentContinuation( + js, underlyingSink->write(), wc, value, KJ_ASSERT_NONNULL(inFlightSelf).addRef()); } else { - maybeRunAlgorithm(js, algorithms.write, kj::mv(onSuccess), kj::mv(onFailure), - value.getHandle(js), self.addRef()); + maybeRunAlgorithmWithPersistentContinuation( + js, underlyingSink->write(), wc, value, KJ_ASSERT_NONNULL(inFlightSelf).addRef()); } } template jsg::Promise WritableImpl::close(jsg::Lock& js, jsg::Ref self) { if (state.template is()) { - return js.rejectedPromise(js.v8TypeError("This WritableStream has been closed."_kj)); + return js.rejectedPromise(js.typeError("This WritableStream has been closed."_kj)); } KJ_IF_SOME(errored, state.template tryGetUnsafe()) { return js.rejectedPromise(errored.addRef(js)); @@ -1449,7 +1898,7 @@ jsg::Promise WritableImpl::close(jsg::Lock& js, jsg::Ref self) template void WritableImpl::dealWithRejection( - jsg::Lock& js, jsg::Ref self, v8::Local reason) { + jsg::Lock& js, jsg::Ref self, jsg::JsValue reason) { if (isWritable()) { return startErroring(js, kj::mv(self), reason); } @@ -1473,7 +1922,9 @@ void WritableImpl::doClose(jsg::Lock& js) { KJ_ASSERT(writeRequests.empty()); // State should have already been transitioned to Closed KJ_ASSERT(state.template is()); - algorithms.clear(); + if (underlyingSink.get() != nullptr) { + underlyingSink->clear(); + } KJ_IF_SOME(owner, tryGetOwner()) { owner.doClose(js); @@ -1481,15 +1932,18 @@ void WritableImpl::doClose(jsg::Lock& js) { } template -void WritableImpl::doError(jsg::Lock& js, v8::Local reason) { +void WritableImpl::doError(jsg::Lock& js, jsg::JsValue reason) { KJ_ASSERT(closeRequest == kj::none); KJ_ASSERT(inFlightClose == kj::none); KJ_ASSERT(inFlightWrite == kj::none); + KJ_ASSERT(inFlightBatchWrite == kj::none); KJ_ASSERT(maybePendingAbort == kj::none); KJ_ASSERT(writeRequests.empty()); // State should have already been transitioned to Errored KJ_ASSERT(state.template is()); - algorithms.clear(); + if (underlyingSink.get() != nullptr) { + underlyingSink->clear(); + } KJ_IF_SOME(owner, tryGetOwner()) { owner.doError(js, reason); @@ -1497,9 +1951,11 @@ void WritableImpl::doError(jsg::Lock& js, v8::Local reason) { } template -void WritableImpl::error(jsg::Lock& js, jsg::Ref self, v8::Local reason) { +void WritableImpl::error(jsg::Lock& js, jsg::Ref self, jsg::JsValue reason) { if (isWritable()) { - algorithms.clear(); + if (underlyingSink.get() != nullptr) { + underlyingSink->clear(); + } startErroring(js, kj::mv(self), reason); } } @@ -1509,6 +1965,7 @@ void WritableImpl::finishErroring(jsg::Lock& js, jsg::Ref self) { auto erroring = kj::mv(KJ_ASSERT_NONNULL(state.template tryGetUnsafe())); auto reason = erroring.reason.getHandle(js); KJ_ASSERT(inFlightWrite == kj::none); + KJ_ASSERT(inFlightBatchWrite == kj::none); KJ_ASSERT(inFlightClose == kj::none); state.template transitionTo(kj::mv(erroring.reason)); @@ -1533,11 +1990,11 @@ void WritableImpl::finishErroring(jsg::Lock& js, jsg::Ref self) { auto onFailure = JSG_VISITABLE_LAMBDA( (this, self = self.addRef()), (self), (jsg::Lock& js, jsg::Value reason) { auto& pendingAbort = KJ_ASSERT_NONNULL(maybePendingAbort); - pendingAbort->fail(js, reason.getHandle(js)); + pendingAbort->fail(js, jsg::JsValue(reason.getHandle(js))); rejectCloseAndClosedPromiseIfNeeded(js); }); - maybeRunAlgorithm(js, algorithms.abort, kj::mv(onSuccess), kj::mv(onFailure), reason); + maybeRunAlgorithm(js, underlyingSink->abort(), kj::mv(onSuccess), kj::mv(onFailure), reason); return; } rejectCloseAndClosedPromiseIfNeeded(js); @@ -1545,8 +2002,10 @@ void WritableImpl::finishErroring(jsg::Lock& js, jsg::Ref self) { template void WritableImpl::finishInFlightClose( - jsg::Lock& js, jsg::Ref self, kj::Maybe> maybeReason) { - algorithms.clear(); + jsg::Lock& js, jsg::Ref self, kj::Maybe maybeReason) { + if (underlyingSink.get() != nullptr) { + underlyingSink->clear(); + } KJ_ASSERT_NONNULL(inFlightClose); KJ_ASSERT(isWritable() || state.template is()); @@ -1576,12 +2035,16 @@ void WritableImpl::finishInFlightClose( template void WritableImpl::finishInFlightWrite( - jsg::Lock& js, jsg::Ref self, kj::Maybe> maybeReason) { + jsg::Lock& js, jsg::Ref self, kj::Maybe maybeReason) { auto& write = KJ_ASSERT_NONNULL(inFlightWrite); KJ_IF_SOME(reason, maybeReason) { write.resolver.reject(js, reason); inFlightWrite = kj::none; + // If the state already transitioned to Errored (e.g., via a side-channel + // error from TransformStreamDefaultController::error() during the write), + // skip dealWithRejection since error handling already completed. + if (state.template is()) return; KJ_ASSERT(isWritable() || state.template is()); return dealWithRejection(js, kj::mv(self), reason); } @@ -1597,37 +2060,26 @@ bool WritableImpl::isCloseQueuedOrInFlight() { template void WritableImpl::rejectCloseAndClosedPromiseIfNeeded(jsg::Lock& js) { - algorithms.clear(); + if (underlyingSink.get() != nullptr) { + underlyingSink->clear(); + } auto reason = KJ_ASSERT_NONNULL(state.template tryGetUnsafe()).getHandle(js); maybeRejectPromise(js, closeRequest, reason); - PendingAbort::dequeue(maybePendingAbort); + auto _ KJ_UNUSED = PendingAbort::dequeue(maybePendingAbort); doError(js, reason); } template -void WritableImpl::setup(jsg::Lock& js, - jsg::Ref self, - UnderlyingSink underlyingSink, - StreamQueuingStrategy queuingStrategy) { +void WritableImpl::setup(jsg::Lock& js, jsg::Ref self) { KJ_ASSERT(!flags.started && !flags.starting); flags.starting = true; - highWaterMark = queuingStrategy.highWaterMark.orDefault(1); - auto startAlgorithm = kj::mv(underlyingSink.start); - algorithms.write = kj::mv(underlyingSink.write); - algorithms.close = kj::mv(underlyingSink.close); - algorithms.abort = kj::mv(underlyingSink.abort); - algorithms.size = kj::mv(queuingStrategy.size); - // Per the streams spec, the size function should be called with `undefined` as `this`, - // not as a method on the strategy object. - KJ_IF_SOME(sizeFunc, algorithms.size) { - sizeFunc.setReceiver(jsg::Value(js.v8Isolate, js.v8Undefined())); - } - auto onSuccess = JSG_VISITABLE_LAMBDA((this, self = self.addRef()), (self), (jsg::Lock& js) { KJ_ASSERT(isWritable() || state.template is()); + this->underlyingSink->clearStart(); + if (isWritable()) { // Only resolve the ready promise if an abort is not pending. // It will have been rejected already. @@ -1645,7 +2097,7 @@ void WritableImpl::setup(jsg::Lock& js, auto onFailure = JSG_VISITABLE_LAMBDA( (this, self = self.addRef()), (self), (jsg::Lock& js, jsg::Value reason) { - auto handle = reason.getHandle(js); + auto handle = jsg::JsValue(reason.getHandle(js)); KJ_ASSERT(isWritable() || state.template is()); KJ_IF_SOME(owner, tryGetOwner()) { owner.maybeRejectReadyPromise(js, handle); @@ -1657,20 +2109,21 @@ void WritableImpl::setup(jsg::Lock& js, dealWithRejection(js, kj::mv(self), handle); }); - flags.backpressure = getDesiredSize() <= 0; + flags.backpressure = getDesiredSize().orDefault(0) <= 0; - maybeRunAlgorithm(js, startAlgorithm, kj::mv(onSuccess), kj::mv(onFailure), self.addRef()); + maybeRunAlgorithm( + js, this->underlyingSink->start(), kj::mv(onSuccess), kj::mv(onFailure), self.addRef()); } template -void WritableImpl::startErroring( - jsg::Lock& js, jsg::Ref self, v8::Local reason) { +void WritableImpl::startErroring(jsg::Lock& js, jsg::Ref self, jsg::JsValue reason) { KJ_ASSERT(isWritable()); KJ_IF_SOME(owner, tryGetOwner()) { owner.maybeRejectReadyPromise(js, reason); } - state.template transitionTo(js.v8Ref(reason)); - if (inFlightWrite == kj::none && inFlightClose == kj::none && flags.started) { + state.template transitionTo(js, reason); + if (inFlightWrite == kj::none && inFlightBatchWrite == kj::none && inFlightClose == kj::none && + flags.started) { finishErroring(js, kj::mv(self)); } } @@ -1679,32 +2132,34 @@ template void WritableImpl::updateBackpressure(jsg::Lock& js) { KJ_ASSERT(isWritable()); KJ_ASSERT(!isCloseQueuedOrInFlight()); - bool bp = getDesiredSize() <= 0; + bool bp = getDesiredSize().orDefault(0) <= 0; if (bp != flags.backpressure) { flags.backpressure = bp; KJ_IF_SOME(owner, tryGetOwner()) { - owner.updateBackpressure(js, flags.backpressure); + owner.updateBackpressure( + js, flags.backpressure ? UpdateBackpressure::YES : UpdateBackpressure::NO); } } } template jsg::Promise WritableImpl::write( - jsg::Lock& js, jsg::Ref self, v8::Local value) { + jsg::Lock& js, jsg::Ref self, jsg::JsValue value) { size_t size = 1; - KJ_IF_SOME(sizeFunc, algorithms.size) { - kj::Maybe failure; + KJ_IF_SOME(sizeFunc, underlyingSink->size()) { + kj::Maybe failure; JSG_TRY(js) { size = sizeFunc(js, value); } JSG_CATCH(exception) { - startErroring(js, self.addRef(), exception.getHandle(js)); - failure = kj::mv(exception); + auto handle = jsg::JsValue(exception.getHandle(js)); + startErroring(js, self.addRef(), handle); + failure = handle; } KJ_IF_SOME(exception, failure) { - return js.rejectedPromise(kj::mv(exception)); + return js.rejectedPromise(exception); } } @@ -1713,11 +2168,11 @@ jsg::Promise WritableImpl::write( // releaseLock() was called from within strategy.size(), the write must be // rejected. This check must occur before any state checks, as the stream // state may still appear writable even after the writer was released. - if (FeatureFlags::get(js).getWritableStreamSpecCompliantWriter()) { + if (flags.specCompliantWriter) { KJ_IF_SOME(owner, tryGetOwner()) { if (!owner.isLockedToWriter()) { return js.rejectedPromise( - js.v8TypeError("This WritableStream writer has been released."_kjc)); + js.typeError("This WritableStream writer has been released."_kjc)); } } } @@ -1727,7 +2182,7 @@ jsg::Promise WritableImpl::write( } if (isCloseQueuedOrInFlight() || state.template is()) { - return js.rejectedPromise(js.v8TypeError("This ReadableStream is closed."_kj)); + return js.rejectedPromise(js.typeError("This WritableStream is closed."_kj)); } KJ_IF_SOME(erroring, state.template tryGetUnsafe()) { @@ -1737,26 +2192,110 @@ jsg::Promise WritableImpl::write( KJ_ASSERT(isWritable()); auto prp = js.newPromiseAndResolver(); + amountBuffered += size; + + // Fast path: if no write is in flight and the queue is empty, dispatch immediately + // without round-tripping the value through a JsRef (avoids a V8 global handle). + if (flags.started && inFlightWrite == kj::none && inFlightBatchWrite == kj::none && + writeRequests.empty()) { + inFlightWrite = WriteRequest{ + .resolver = kj::mv(prp.resolver), + .value = {}, // Not stored — value is passed directly to the algorithm below. + .size = size, + }; + inFlightSelf = kj::mv(self); + updateBackpressure(js); + + auto& wc = getWriteContinuation(js); + if (flags.pedanticWpt) { + maybeRunAlgorithmAsyncWithPersistentContinuation( + js, underlyingSink->write(), wc, value, KJ_ASSERT_NONNULL(inFlightSelf).addRef()); + } else { + maybeRunAlgorithmWithPersistentContinuation( + js, underlyingSink->write(), wc, value, KJ_ASSERT_NONNULL(inFlightSelf).addRef()); + } + return kj::mv(prp.promise); + } + + // Slow path: queue the write for later dispatch via advanceQueueIfNeeded. writeRequests.push_back(WriteRequest{ .resolver = kj::mv(prp.resolver), - .value = js.v8Ref(value), + .value = value.addRef(js), .size = size, }); - amountBuffered += size; updateBackpressure(js); advanceQueueIfNeeded(js, kj::mv(self)); return kj::mv(prp.promise); } +template +jsg::Promise WritableImpl::flush( + jsg::Lock& js, jsg::Ref self, MarkAsHandled markAsHandled) { + KJ_IF_SOME(error, state.template tryGetUnsafe()) { + return rejectedMaybeHandledPromise(js, error.getHandle(js), markAsHandled); + } + + if (state.template is()) { + return rejectedMaybeHandledPromise( + js, js.typeError("This WritableStream has been closed."_kj), markAsHandled); + } + + auto prp = js.newPromiseAndResolver(); + if (markAsHandled) { + prp.promise.markAsHandled(js); + } + + // If nothing is in flight and the queue is empty, resolve immediately. + if (inFlightWrite == kj::none && inFlightBatchWrite == kj::none && writeRequests.empty()) { + prp.resolver.resolve(js); + return kj::mv(prp.promise); + } + + // Enqueue a flush marker. When advanceQueueIfNeeded encounters this entry, + // it resolves the promise immediately without dispatching a write algorithm. + writeRequests.push_back(WriteRequest{ + .resolver = kj::mv(prp.resolver), + .value = {}, + .size = 0, + .flush = true, + }); + return kj::mv(prp.promise); +} + template void WritableImpl::visitForGc(jsg::GcVisitor& visitor) { state.visitForGc(visitor); - visitor.visit(inFlightWrite, inFlightClose, closeRequest, algorithms, signal); + visitor.visit(inFlightWrite, inFlightClose, closeRequest, signal); + + // The underlyingSink might not yet be set the first time visitForGc is called. + if (underlyingSink.get() != nullptr) { + underlyingSink->visitForGc(visitor); + } KJ_IF_SOME(pendingAbort, maybePendingAbort) { visitor.visit(*pendingAbort); } visitor.visitAll(writeRequests); + KJ_IF_SOME(batch, inFlightBatchWrite) { + batch.visitForGc(visitor); + } + KJ_IF_SOME(wc, writeContinuation) { + wc.visitForGc(visitor); + } + KJ_IF_SOME(wvc, writevContinuation) { + wvc.visitForGc(visitor); + } + KJ_IF_SOME(dc, drainContinuation) { + dc.visitForGc(visitor); + } + KJ_IF_SOME(cc, closeContinuation) { + cc.visitForGc(visitor); + } + // Note: inFlightSelf is intentionally NOT traced here. It is a Ref back to the + // controller (Self) that contains this WritableImpl, which would create an infinite + // GC tracing cycle (WritableImpl → Self → WritableImpl → ...). The controller is + // already reachable through its owning WritableStream's ref chain, so it won't be + // collected while inFlightSelf exists. } template @@ -1770,6 +2309,12 @@ void WritableImpl::cancelPendingWrites(jsg::Lock& js, jsg::JsValue reason) write.resolver.reject(js, reason); } writeRequests.clear(); + KJ_IF_SOME(batch, inFlightBatchWrite) { + for (auto& resolver: batch.resolvers) { + resolver.reject(js, reason); + } + inFlightBatchWrite = kj::none; + } } // ====================================================================================== @@ -1788,6 +2333,21 @@ struct ReadableState { consumer(kj::mv(consumer)), owner(owner) {} + ReadableState(ReadableState&&) = default; + ReadableState& operator=(ReadableState&&) = default; + KJ_DISALLOW_COPY(ReadableState); + + ~ReadableState() noexcept(false) { + // Break the pullSelf ref cycle (controller → ReadableImpl → pullSelf → controller) + // so the controller can be freed when this Ref is dropped. We only clear pullSelf + // here — the full clearAlgorithms (which moves underlyingSource) is deferred to + // the controller's destructor, because the controller may still need the source + // for in-progress operations. + if (controller.get() != nullptr) { + controller->breakPullCycle(); + } + } + ReadableState(Controller controller, Queue::ConsumerImpl::StateListener& listener, ReadableStreamJsController& owner) @@ -1804,8 +2364,11 @@ struct ValueReadable final: private api::ValueQueue::ConsumerImpl::StateListener using State = ReadableState; kj::Maybe state; - bool reading = false; - bool pendingCancel = false; + struct Flags { + uint8_t reading : 1 = 0; + uint8_t pendingCancel : 1 = 0; + }; + Flags flags{}; JSG_MEMORY_INFO(ValueReadable) { KJ_IF_SOME(s, state) { @@ -1846,16 +2409,16 @@ struct ValueReadable final: private api::ValueQueue::ConsumerImpl::StateListener jsg::Promise read(jsg::Lock& js) { KJ_IF_SOME(s, state) { auto prp = js.newPromiseAndResolver(); - reading = true; + flags.reading = true; s.consumer->read(js, ValueQueue::ReadRequest{ .resolver = kj::mv(prp.resolver), }); - reading = false; - if (pendingCancel) { + flags.reading = false; + if (flags.pendingCancel) { // If we were canceled while reading, we need to drop our state now. state = kj::none; - pendingCancel = false; + flags.pendingCancel = false; } return kj::mv(prp.promise); } @@ -1883,7 +2446,7 @@ struct ValueReadable final: private api::ValueQueue::ConsumerImpl::StateListener }); } - jsg::Promise cancel(jsg::Lock& js, jsg::Optional> maybeReason) { + jsg::Promise cancel(jsg::Lock& js, jsg::Optional maybeReason) { // When a ReadableStream is canceled, the expected behavior is that the underlying // controller is notified and the cancel algorithm on the underlying source is // called. When there are multiple ReadableStreams sharing consumption of a @@ -1891,7 +2454,7 @@ struct ValueReadable final: private api::ValueQueue::ConsumerImpl::StateListener // the underlying controller only when the last reader is canceled. // Here, we rely on the controller implementing the correct behavior since it owns // the queue that knows about all of the attached consumers. - if (pendingCancel) return js.resolvedPromise(); + if (flags.pendingCancel) return js.resolvedPromise(); KJ_IF_SOME(s, state) { // Check if there's a pending draining read before calling cancel, since cancel // will resolve the pending read and we need to know if we should defer destruction. @@ -1902,8 +2465,8 @@ struct ValueReadable final: private api::ValueQueue::ConsumerImpl::StateListener // finish before dropping our state. For draining reads, the promise callbacks // capture 'this' (the Consumer) to clear hasPendingDrainingRead. If we destroy // the state now, those callbacks will UAF. - if (reading || hasPendingDrainingRead) { - pendingCancel = true; + if (flags.reading || hasPendingDrainingRead) { + flags.pendingCancel = true; } else { state = kj::none; } @@ -1923,13 +2486,13 @@ struct ValueReadable final: private api::ValueQueue::ConsumerImpl::StateListener } } - void onConsumerError(jsg::Lock& js, jsg::Value reason) override { + void onConsumerError(jsg::Lock& js, jsg::JsValue reason) override { // Called by the consumer when a state change to errored happens. // We need to notify the owner. Note that the owner may drop this // readable in doClose so it is not safe to access anything on this // after calling doError. KJ_IF_SOME(s, state) { - s.owner.doError(js, reason.getHandle(js)); + s.owner.doError(js, reason); } } @@ -1998,7 +2561,10 @@ struct ByteReadable final: private api::ByteQueue::ConsumerImpl::StateListener { using State = ReadableState; kj::Maybe state; kj::Maybe autoAllocateChunkSize; - bool pendingCancel = false; + struct Flags { + uint8_t pendingCancel : 1 = 0; + }; + Flags flags{}; JSG_MEMORY_INFO(ByteReadable) { KJ_IF_SOME(s, state) { @@ -2046,48 +2612,52 @@ struct ByteReadable final: private api::ByteQueue::ConsumerImpl::StateListener { auto prp = js.newPromiseAndResolver(); KJ_IF_SOME(byob, byobOptions) { - jsg::BufferSource source(js, byob.bufferView.getHandle(js)); + auto view = byob.bufferView.getHandle(js); + auto elementSize = view.getElementSize(); // If atLeast is not given, then by default it is the element size of the view // that we were given. If atLeast is given, we make sure that it is aligned // with the element size. No matter what, atLeast cannot be less than 1. - auto atLeast = kj::max(source.getElementSize(), byob.atLeast.orDefault(1)); - atLeast = kj::max(1, atLeast - (atLeast % source.getElementSize())); + auto atLeast = kj::max(elementSize, byob.atLeast.orDefault(1)); + atLeast = kj::max(1, atLeast - (atLeast % elementSize)); s.consumer->read(js, ByteQueue::ReadRequest(kj::mv(prp.resolver), { - .store = jsg::BufferSource(js, source.detach(js)), + .store = view.detachAndTake(js).addRef(js), .atLeast = atLeast, .type = ByteQueue::ReadRequest::Type::BYOB, })); } else KJ_IF_SOME(chunkSize, autoAllocateChunkSize) { - // autoAllocateChunkSize is set, so we allocate a buffer and do a BYOB read. + // autoAllocateChunkSize is set, so we allocate a buffer and do a BYOB-style read. // This makes the buffer available to the underlying source via controller.byobRequest. - KJ_IF_SOME(store, jsg::BufferSource::tryAlloc(js, chunkSize)) { + // The type stays BYOB for queue processing, but autoAllocated is set so that close + // semantics follow the default reader spec ({done: true, value: undefined}). + KJ_IF_SOME(store, jsg::JsUint8Array::tryCreate(js, chunkSize)) { // Ensure that the handle is created here so that the size of the buffer // is accounted for in the isolate memory tracking. s.consumer->read(js, ByteQueue::ReadRequest(kj::mv(prp.resolver), { - .store = kj::mv(store), + .store = jsg::JsArrayBufferView(store).addRef(js), .type = ByteQueue::ReadRequest::Type::BYOB, + .autoAllocated = true, })); } else { - prp.resolver.reject(js, js.v8Error("Failed to allocate buffer for read.")); + prp.resolver.reject(js, js.error("Failed to allocate buffer for read.")); } } else { // autoAllocateChunkSize is not set. Per spec, we do a DEFAULT read which means // the underlying source's pull method won't get a byobRequest. It must use // controller.enqueue() to provide data instead. constexpr size_t kDefaultReadSize = 16384; // 16KB default buffer - KJ_IF_SOME(store, jsg::BufferSource::tryAlloc(js, kDefaultReadSize)) { + KJ_IF_SOME(store, jsg::JsUint8Array::tryCreate(js, kDefaultReadSize)) { s.consumer->read(js, ByteQueue::ReadRequest(kj::mv(prp.resolver), { - .store = kj::mv(store), + .store = jsg::JsArrayBufferView(store).addRef(js), .type = ByteQueue::ReadRequest::Type::DEFAULT, })); } else { - prp.resolver.reject(js, js.v8Error("Failed to allocate buffer for read.")); + prp.resolver.reject(js, js.error("Failed to allocate buffer for read.")); } } @@ -2098,11 +2668,9 @@ struct ByteReadable final: private api::ByteQueue::ConsumerImpl::StateListener { KJ_IF_SOME(byob, byobOptions) { // If a BYOB buffer was given, we need to give it back wrapped in a TypedArray // whose size is set to zero. - jsg::BufferSource source(js, byob.bufferView.getHandle(js)); - auto store = source.detach(js); - store.consume(store.size()); + auto view = byob.bufferView.getHandle(js).detachAndTake(js); return js.resolvedPromise(ReadResult{ - .value = js.v8Ref(store.createHandle(js)), + .value = jsg::JsValue(view.slice(js, 0, 0)).addRef(js), .done = true, }); } else { @@ -2133,8 +2701,8 @@ struct ByteReadable final: private api::ByteQueue::ConsumerImpl::StateListener { // the underlying controller only when the last reader is canceled. // Here, we rely on the controller implementing the correct behavior since it owns // the queue that knows about all of the attached consumers. - jsg::Promise cancel(jsg::Lock& js, jsg::Optional> maybeReason) { - if (pendingCancel) return js.resolvedPromise(); + jsg::Promise cancel(jsg::Lock& js, jsg::Optional maybeReason) { + if (flags.pendingCancel) return js.resolvedPromise(); KJ_IF_SOME(s, state) { // Check if there's a pending draining read before calling cancel, since cancel // will resolve the pending read and we need to know if we should defer destruction. @@ -2146,7 +2714,7 @@ struct ByteReadable final: private api::ByteQueue::ConsumerImpl::StateListener { // Consumer) to clear hasPendingDrainingRead. If we destroy the state now, those // callbacks will UAF. if (hasPendingDrainingRead) { - pendingCancel = true; + flags.pendingCancel = true; } else { state = kj::none; } @@ -2164,11 +2732,11 @@ struct ByteReadable final: private api::ByteQueue::ConsumerImpl::StateListener { } } - void onConsumerError(jsg::Lock& js, jsg::Value reason) override { + void onConsumerError(jsg::Lock& js, jsg::JsValue reason) override { // Note that the owner may drop this readable in doClose so it // is not safe to access anything on this after calling doError. KJ_IF_SOME(s, state) { - s.owner.doError(js, reason.getHandle(js)); + s.owner.doError(js, reason); }; } @@ -2236,9 +2804,42 @@ struct ByteReadable final: private api::ByteQueue::ConsumerImpl::StateListener { // ======================================================================================= ReadableStreamDefaultController::ReadableStreamDefaultController( - UnderlyingSource underlyingSource, StreamQueuingStrategy queuingStrategy) - : ioContext(tryGetIoContext()), - impl(kj::mv(underlyingSource), kj::mv(queuingStrategy)) {} + jsg::Lock& js, kj::Own source) + : weakSelf(kj::rc>( + kj::Badge{}, *this)), + ioContext(tryGetIoContext()), + impl(js, kj::mv(source), weakSelf.addRef()) {} + +ReadableStreamDefaultController::~ReadableStreamDefaultController() noexcept(false) { + weakSelf->invalidate(); + clearAlgorithms(); +} + +kj::Maybe ReadableStreamDefaultController::tryTeeSource(uint64_t limit) { + return impl.tryTeeSource(limit); +} + +kj::Maybe> ReadableStreamDefaultController::tryReleaseSource() { + return impl.tryReleaseSource(); +} + +bool ReadableStreamDefaultController::isInternal() const { + return impl.isInternal(); +} + +StreamEncoding ReadableStreamDefaultController::getPreferredEncoding() { + return impl.getPreferredEncoding(); +} + +kj::Maybe ReadableStreamDefaultController::tryGetLength(StreamEncoding encoding) { + return impl.tryGetLength(encoding); +} + +void ReadableStreamDefaultController::clearAlgorithms() { + auto _ KJ_UNUSED = kj::mv(impl.underlyingSource); + impl.pullContinuation = kj::none; + impl.pullSelf = kj::none; +} kj::Maybe ReadableStreamDefaultController::getMaybeErrorState( jsg::Lock& js) { @@ -2269,16 +2870,15 @@ void ReadableStreamDefaultController::visitForGc(jsg::GcVisitor& visitor) { } jsg::Promise ReadableStreamDefaultController::cancel( - jsg::Lock& js, jsg::Optional> maybeReason) { - return impl.cancel(js, JSG_THIS, maybeReason.orDefault([&] { return js.v8Undefined(); })); + jsg::Lock& js, jsg::Optional maybeReason) { + return impl.cancel(js, JSG_THIS, maybeReason.orDefault([&] { return js.undefined(); })); } void ReadableStreamDefaultController::close(jsg::Lock& js) { impl.close(js); } -void ReadableStreamDefaultController::enqueue( - jsg::Lock& js, jsg::Optional> chunk) { +void ReadableStreamDefaultController::enqueue(jsg::Lock& js, jsg::Optional chunk) { // Hold a strong reference to prevent this controller from being freed if the // user-provided size algorithm (below) re-enters JS and errors the controller // through a side-channel (e.g. TransformStreamDefaultController::error() @@ -2290,23 +2890,26 @@ void ReadableStreamDefaultController::enqueue( size_t size = 1; bool errored = false; - KJ_IF_SOME(sizeFunc, impl.algorithms.size) { - js.tryCatch([&] { size = sizeFunc(js, value); }, [&](jsg::Value exception) { - impl.doError(js, kj::mv(exception)); + KJ_IF_SOME(sizeFunc, impl.underlyingSource->size()) { + JSG_TRY(js) { + size = sizeFunc(js, value); + } + JSG_CATCH(exception) { + impl.doError(js, jsg::JsValue(exception.getHandle(js))); errored = true; - }); + }; } // Re-check canCloseOrEnqueue: the size callback may have errored us without // throwing (e.g. by calling transformController.error()), in which case // `errored` is still false but the impl state has transitioned to Errored. if (!errored && impl.canCloseOrEnqueue()) { - impl.enqueue(js, kj::rc(js.v8Ref(value), size), kj::mv(self)); + impl.enqueue(js, kj::rc(js, value, size), kj::mv(self)); } } -void ReadableStreamDefaultController::error(jsg::Lock& js, v8::Local reason) { - impl.doError(js, js.v8Ref(reason)); +void ReadableStreamDefaultController::error(jsg::Lock& js, jsg::JsValue reason) { + impl.doError(js, reason); } // When a consumer receives a read request, but does not have the data available to @@ -2327,19 +2930,28 @@ kj::Own ReadableStreamDefaultController::getConsumer( // ====================================================================================== +namespace { +jsg::JsRef getViewRef(jsg::Lock& js, kj::Maybe maybeView) { + KJ_IF_SOME(view, maybeView) { + return view.addRef(js); + } + KJ_FAIL_ASSERT("BYOB read request's view is expected to be present when updating the view"); +} +} // namespace + ReadableStreamBYOBRequest::Impl::Impl(jsg::Lock& js, kj::Own readRequest, kj::Rc> controller) : readRequest(kj::mv(readRequest)), controller(kj::mv(controller)), - view(js.v8Ref(this->readRequest->getView(js))), + view(getViewRef(js, this->readRequest->getView(js))), originalBufferByteLength(this->readRequest->getOriginalBufferByteLength(js)), - originalByteOffsetPlusBytesFilled(this->readRequest->getOriginalByteOffsetPlusBytesFilled()) { -} + originalByteOffsetPlusBytesFilled( + this->readRequest->getOriginalByteOffsetPlusBytesFilled(js)) {} void ReadableStreamBYOBRequest::Impl::updateView(jsg::Lock& js) { - jsg::check(view.getHandle(js)->Buffer()->Detach(v8::Local())); - view = js.v8Ref(readRequest->getView(js)); + view.getHandle(js).detachInPlace(js); + view = getViewRef(js, readRequest->getView(js)); } void ReadableStreamBYOBRequest::visitForGc(jsg::GcVisitor& visitor) { @@ -2361,9 +2973,9 @@ kj::Maybe ReadableStreamBYOBRequest::getAtLeast() { return kj::none; } -kj::Maybe> ReadableStreamBYOBRequest::getView(jsg::Lock& js) { +kj::Maybe ReadableStreamBYOBRequest::getView(jsg::Lock& js) { KJ_IF_SOME(impl, maybeImpl) { - return impl.view.addRef(js); + return impl.view.getHandle(js); } return kj::none; } @@ -2373,7 +2985,7 @@ void ReadableStreamBYOBRequest::invalidate(jsg::Lock& js) { // If the user code happened to have retained a reference to the view or // the buffer, we need to detach it so that those references cannot be used // to modify or observe modifications. - jsg::check(impl.view.getHandle(js)->Buffer()->Detach(v8::Local())); + impl.view.getHandle(js).detachInPlace(js); impl.controller->runIfAlive( [](ReadableByteStreamController& controller) { controller.maybeByobRequest = kj::none; }); } @@ -2383,9 +2995,9 @@ void ReadableStreamBYOBRequest::invalidate(jsg::Lock& js) { void ReadableStreamBYOBRequest::respond(jsg::Lock& js, int bytesWritten) { auto& impl = JSG_REQUIRE_NONNULL( maybeImpl, TypeError, "This ReadableStreamBYOBRequest has been invalidated."); + auto handle = impl.view.getHandle(js); JSG_REQUIRE(impl.controller->isValid(), Error, "The ReadableStreamBYOBRequest is invalid."); - JSG_REQUIRE(impl.view.getHandle(js)->ByteLength() > 0, TypeError, - "Cannot respond with a zero-length or detached view"); + JSG_REQUIRE(handle.size() > 0, TypeError, "Cannot respond with a zero-length or detached view"); impl.controller->runIfAlive([&](ReadableByteStreamController& controller) { if (!controller.canCloseOrEnqueue()) { JSG_REQUIRE(bytesWritten == 0, TypeError, @@ -2397,8 +3009,7 @@ void ReadableStreamBYOBRequest::respond(jsg::Lock& js, int bytesWritten) { if (impl.readRequest->isInvalidated() && controller.impl.consumerCount() >= 1) { // While this particular request may be invalidated, there are still // other branches we can push the data to. Let's do so. - jsg::BufferSource source(js, impl.view.getHandle(js)); - auto entry = kj::rc(jsg::BufferSource(js, source.detach(js))); + auto entry = kj::rc(js, jsg::JsBufferSource(handle.detachAndTake(js))); controller.impl.enqueue(js, kj::mv(entry), controller.getSelf()); } else { JSG_REQUIRE(bytesWritten > 0, TypeError, @@ -2421,7 +3032,7 @@ void ReadableStreamBYOBRequest::respond(jsg::Lock& js, int bytesWritten) { }); } -void ReadableStreamBYOBRequest::respondWithNewView(jsg::Lock& js, jsg::BufferSource view) { +void ReadableStreamBYOBRequest::respondWithNewView(jsg::Lock& js, jsg::JsBufferSource view) { auto& impl = JSG_REQUIRE_NONNULL( maybeImpl, TypeError, "This ReadableStreamBYOBRequest has been invalidated."); JSG_REQUIRE(impl.controller->isValid(), Error, "The ReadableStreamBYOBRequest is invalid."); @@ -2436,22 +3047,16 @@ void ReadableStreamBYOBRequest::respondWithNewView(jsg::Lock& js, jsg::BufferSou // 2. The underlying buffer must not be detached (TypeError) // 3. The buffer byte length must not be zero (RangeError) // 4. The buffer byte length must match the original (RangeError) - auto handle = view.getHandle(js); - auto buffer = handle->IsArrayBuffer() ? handle.As() - : handle.As()->Buffer(); - JSG_REQUIRE( - !buffer->WasDetached(), TypeError, "The underlying ArrayBuffer has been detached."); - - JSG_REQUIRE(view.canDetach(js), TypeError, "Unable to use non-detachable ArrayBuffer."); + JSG_REQUIRE(!view.isDetached(), TypeError, "The underlying ArrayBuffer has been detached."); + JSG_REQUIRE(view.isDetachable(), TypeError, "Unable to use non-detachable ArrayBuffer."); // Use the stored values since the ByobRequest may have been invalidated during close. - auto actualBufferByteLength = buffer->ByteLength(); + auto actualBufferByteLength = view.underlyingArrayBufferSize(js); JSG_REQUIRE( actualBufferByteLength != 0, RangeError, "The underlying ArrayBuffer is zero-length."); JSG_REQUIRE(actualBufferByteLength == impl.originalBufferByteLength, RangeError, "The underlying ArrayBuffer is not the correct length."); // The view's byte offset must match the original byte offset plus bytes filled. - auto viewByteOffset = - handle->IsArrayBuffer() ? 0 : handle.As()->ByteOffset(); + auto viewByteOffset = view.getOffset(); JSG_REQUIRE(viewByteOffset == impl.originalByteOffsetPlusBytesFilled, RangeError, "The view has an invalid byte offset."); } else { @@ -2464,12 +3069,12 @@ void ReadableStreamBYOBRequest::respondWithNewView(jsg::Lock& js, jsg::BufferSou if (impl.readRequest->isInvalidated() && controller.impl.consumerCount() >= 1) { // While this particular request may be invalidated, there are still // other branches we can push the data to. Let's do so. - auto entry = kj::rc(jsg::BufferSource(js, view.detach(js))); + auto entry = kj::rc(js, view.detachAndTake(js)); controller.impl.enqueue(js, kj::mv(entry), controller.getSelf()); } else { JSG_REQUIRE(view.size() > 0, TypeError, "The view byte length must be more than zero while the stream is open."); - if (impl.readRequest->respondWithNewView(js, kj::mv(view))) { + if (impl.readRequest->respondWithNewView(js, view)) { // The read request was fulfilled, we need to invalidate. shouldInvalidate = true; } else { @@ -2488,9 +3093,9 @@ void ReadableStreamBYOBRequest::respondWithNewView(jsg::Lock& js, jsg::BufferSou }); } -bool ReadableStreamBYOBRequest::isPartiallyFulfilled() { +bool ReadableStreamBYOBRequest::isPartiallyFulfilled(jsg::Lock& js) { KJ_IF_SOME(impl, maybeImpl) { - return impl.readRequest->isPartiallyFulfilled(); + return impl.readRequest->isPartiallyFulfilled(js); } return false; } @@ -2498,14 +3103,41 @@ bool ReadableStreamBYOBRequest::isPartiallyFulfilled() { // ====================================================================================== ReadableByteStreamController::ReadableByteStreamController( - UnderlyingSource underlyingSource, StreamQueuingStrategy queuingStrategy) + jsg::Lock& js, kj::Own source) : weakSelf(kj::rc>( kj::Badge{}, *this)), ioContext(tryGetIoContext()), - impl(kj::mv(underlyingSource), kj::mv(queuingStrategy)) {} + impl(js, kj::mv(source), weakSelf.addRef()) {} ReadableByteStreamController::~ReadableByteStreamController() noexcept(false) { weakSelf->invalidate(); + clearAlgorithms(); +} + +kj::Maybe ReadableByteStreamController::tryTeeSource(uint64_t limit) { + return impl.tryTeeSource(limit); +} + +kj::Maybe> ReadableByteStreamController::tryReleaseSource() { + return impl.tryReleaseSource(); +} + +bool ReadableByteStreamController::isInternal() const { + return impl.isInternal(); +} + +StreamEncoding ReadableByteStreamController::getPreferredEncoding() { + return impl.getPreferredEncoding(); +} + +kj::Maybe ReadableByteStreamController::tryGetLength(StreamEncoding encoding) { + return impl.tryGetLength(encoding); +} + +void ReadableByteStreamController::clearAlgorithms() { + auto _ KJ_UNUSED = kj::mv(impl.underlyingSource); + impl.pullContinuation = kj::none; + impl.pullSelf = kj::none; } void ReadableByteStreamController::start(jsg::Lock& js) { @@ -2524,12 +3156,19 @@ kj::Maybe ReadableByteStreamController::getDesiredSize() { return impl.getDesiredSize(); } +kj::Maybe ReadableByteStreamController::getMaybeErrorState(jsg::Lock& js) { + KJ_IF_SOME(errored, impl.state.tryGetUnsafe()) { + return errored.addRef(js); + } + return kj::none; +} + void ReadableByteStreamController::visitForGc(jsg::GcVisitor& visitor) { visitor.visit(maybeByobRequest, impl); } jsg::Promise ReadableByteStreamController::cancel( - jsg::Lock& js, jsg::Optional> maybeReason) { + jsg::Lock& js, jsg::Optional maybeReason) { KJ_IF_SOME(byobRequest, maybeByobRequest) { if (impl.consumerCount() == 1) { byobRequest->invalidate(js); @@ -2540,7 +3179,7 @@ jsg::Promise ReadableByteStreamController::cancel( void ReadableByteStreamController::close(jsg::Lock& js) { KJ_IF_SOME(byobRequest, maybeByobRequest) { - JSG_REQUIRE(!byobRequest->isPartiallyFulfilled(), TypeError, + JSG_REQUIRE(!byobRequest->isPartiallyFulfilled(js), TypeError, "This ReadableStream was closed with a partial read pending."); } else if (FeatureFlags::get(js).getPedanticWpt()) { // If maybeByobRequest is not set, check if there's a pending byob request. @@ -2549,37 +3188,37 @@ void ReadableByteStreamController::close(jsg::Lock& js) { // respondWithNewView() error handling in the closed state. // Only do this if the queue doesn't have a partially fulfilled read. KJ_IF_SOME(queue, impl.state.tryGetUnsafe()) { - if (!queue.hasPartiallyFulfilledRead()) { - getByobRequest(js); + if (!queue.hasPartiallyFulfilledRead(js)) { + auto _ KJ_UNUSED = getByobRequest(js); } } } impl.close(js); } -void ReadableByteStreamController::enqueue(jsg::Lock& js, jsg::BufferSource chunk) { +void ReadableByteStreamController::enqueue(jsg::Lock& js, jsg::JsBufferSource chunk) { // Hold a strong reference up front. Operations below (invalidate, detach) touch // the JS heap and C++ argument evaluation order is unspecified, so JSG_THIS as a // function argument would not reliably precede chunk.detach(js). auto self = JSG_THIS; JSG_REQUIRE(chunk.size() > 0, TypeError, "Cannot enqueue a zero-length ArrayBuffer."); - JSG_REQUIRE(chunk.canDetach(js), TypeError, "The provided ArrayBuffer must be detachable."); + JSG_REQUIRE(chunk.isDetachable(), TypeError, "The provided ArrayBuffer must be detachable."); JSG_REQUIRE(impl.canCloseOrEnqueue(), TypeError, "This ReadableByteStreamController is closed."); KJ_IF_SOME(byobRequest, maybeByobRequest) { KJ_IF_SOME(view, byobRequest->getView(js)) { - JSG_REQUIRE(view.getHandle(js)->ByteLength() > 0, TypeError, - "The byobRequest.view is zero-length or was detached"); + JSG_REQUIRE( + view.size() > 0, TypeError, "The byobRequest.view is zero-length or was detached"); } byobRequest->invalidate(js); } - impl.enqueue(js, kj::rc(jsg::BufferSource(js, chunk.detach(js))), kj::mv(self)); + impl.enqueue(js, kj::rc(js, chunk.detachAndTake(js)), kj::mv(self)); } -void ReadableByteStreamController::error(jsg::Lock& js, v8::Local reason) { - impl.doError(js, js.v8Ref(reason)); +void ReadableByteStreamController::error(jsg::Lock& js, jsg::JsValue reason) { + impl.doError(js, reason); } kj::Maybe> ReadableByteStreamController::getByobRequest( @@ -2619,6 +3258,10 @@ kj::Own ReadableByteStreamController::getConsumer( ReadableStreamJsController::ReadableStreamJsController(): ioContext(tryGetIoContext()) {} +ReadableStreamJsController::~ReadableStreamJsController() noexcept(false) { + weakSelf->invalidate(); +} + ReadableStreamJsController::ReadableStreamJsController(StreamStates::Closed closed) : ioContext(tryGetIoContext()) { state.transitionTo(); @@ -2639,18 +3282,88 @@ ReadableStreamJsController::ReadableStreamJsController(jsg::Lock& js, ByteReadab state.transitionTo>(consumer.clone(js, *this)); } +bool ReadableStreamJsController::isInternal() const { + KJ_SWITCH_ONEOF(state) { + KJ_CASE_ONEOF(initial, Initial) { + return false; + } + KJ_CASE_ONEOF(closed, StreamStates::Closed) { + return false; + } + KJ_CASE_ONEOF(errored, StreamStates::Errored) { + return false; + } + KJ_CASE_ONEOF(consumer, kj::Own) { + KJ_IF_SOME(s, consumer->state) { + return s.controller->isInternal(); + } + return false; + } + KJ_CASE_ONEOF(consumer, kj::Own) { + KJ_IF_SOME(s, consumer->state) { + return s.controller->isInternal(); + } + return false; + } + } + KJ_UNREACHABLE; +} + +kj::Maybe> ReadableStreamJsController::tryReleaseSource() { + if (isLockedToReader()) return kj::none; + if (flags.disturbed) return kj::none; + auto tryRelease = [&](auto& consumer) -> kj::Maybe> { + KJ_IF_SOME(s, consumer->state) { + KJ_IF_SOME(source, s.controller->tryReleaseSource()) { + flags.disturbed = true; + state.transitionTo(); + return kj::mv(source); + } + } + return kj::none; + }; + KJ_SWITCH_ONEOF(state) { + KJ_CASE_ONEOF(initial, Initial) { + return kj::none; + } + KJ_CASE_ONEOF(closed, StreamStates::Closed) { + // Return a NullSource for closed streams (zero-length, immediate EOF). + class NullSource final: public ReadableStreamSource { + public: + kj::Promise tryRead(void*, size_t, size_t) override { + return static_cast(0); + } + kj::Maybe tryGetLength(StreamEncoding) override { + return static_cast(0); + } + }; + return kj::heap(); + } + KJ_CASE_ONEOF(errored, StreamStates::Errored) { + return kj::none; + } + KJ_CASE_ONEOF(consumer, kj::Own) { + return tryRelease(consumer); + } + KJ_CASE_ONEOF(consumer, kj::Own) { + return tryRelease(consumer); + } + } + KJ_UNREACHABLE; +} + jsg::Ref ReadableStreamJsController::addRef() { return KJ_REQUIRE_NONNULL(owner).addRef(); } jsg::Promise ReadableStreamJsController::cancel( - jsg::Lock& js, jsg::Optional> maybeReason) { - disturbed = true; + jsg::Lock& js, jsg::Optional maybeReason) { + flags.disturbed = true; const auto doCancel = [&](auto& consumer) { - auto reason = js.v8Ref(maybeReason.orDefault([&] { return js.v8Undefined(); })); + auto reason = maybeReason.orDefault([&] { return js.undefined(); }); KJ_DEFER(doClose(js)); - return consumer->cancel(js, reason.getHandle(js)); + return consumer->cancel(js, reason); }; // Check for pending state first (deferred close/error during a read operation) @@ -2673,13 +3386,13 @@ jsg::Promise ReadableStreamJsController::cancel( return js.rejectedPromise(errored.addRef(js)); } KJ_CASE_ONEOF(consumer, kj::Own) { - if (canceling) return js.resolvedPromise(); - canceling = true; + if (flags.canceling) return js.resolvedPromise(); + flags.canceling = true; return doCancel(consumer); } KJ_CASE_ONEOF(consumer, kj::Own) { - if (canceling) return js.resolvedPromise(); - canceling = true; + if (flags.canceling) return js.resolvedPromise(); + flags.canceling = true; return doCancel(consumer); } } @@ -2712,13 +3425,13 @@ void ReadableStreamJsController::doClose(jsg::Lock& js) { // erroring. We detach ourselves from the underlying controller by releasing the ValueReadable // or ByteReadable in the state and changing that to errored. // We also clean up other state here. -void ReadableStreamJsController::doError(jsg::Lock& js, v8::Local reason) { +void ReadableStreamJsController::doError(jsg::Lock& js, jsg::JsValue reason) { // If already in a terminal state, nothing to do. if (state.isTerminal()) return; // deferTransitionTo will defer if an operation is in progress, otherwise transition immediately. // Returns true if transition happened immediately. - if (state.deferTransitionTo(js.v8Ref(reason))) { + if (state.deferTransitionTo(reason.addRef(js))) { lock.onError(js, reason); } // If deferred, lock.onError will be called when the pending state is applied @@ -2741,7 +3454,7 @@ bool ReadableStreamJsController::isClosed() const { } bool ReadableStreamJsController::isDisturbed() { - return disturbed; + return flags.disturbed; } bool ReadableStreamJsController::isLockedToReader() const { @@ -2757,30 +3470,34 @@ jsg::Promise ReadableStreamJsController::pipeTo( KJ_DASSERT(!isLockedToReader()); KJ_DASSERT(!destination.isLockedToWriter()); - disturbed = true; + flags.disturbed = true; KJ_IF_SOME(promise, destination.tryPipeFrom(js, addRef(), kj::mv(options))) { return kj::mv(promise); } return js.rejectedPromise( - js.v8TypeError("This ReadableStream cannot be piped to this WritableStream"_kj)); + js.typeError("This ReadableStream cannot be piped to this WritableStream"_kj)); } kj::Maybe> ReadableStreamJsController::read( jsg::Lock& js, kj::Maybe maybeByobOptions) { - disturbed = true; + if (flags.pendingClosure) { + return js.rejectedPromise( + js.typeError("This ReadableStream belongs to an object that is closing."_kj)); + } + flags.disturbed = true; KJ_IF_SOME(byobOptions, maybeByobOptions) { byobOptions.detachBuffer = true; auto view = byobOptions.bufferView.getHandle(js); - if (!view->Buffer()->IsDetachable()) { + if (!view.isDetachable()) { return js.rejectedPromise( - js.v8TypeError("Unabled to use non-detachable ArrayBuffer."_kj)); + js.typeError("Unable to use non-detachable ArrayBuffer."_kj)); } - if (view->ByteLength() == 0 || view->Buffer()->ByteLength() == 0) { + if (view.size() == 0 && !isInternal()) { return js.rejectedPromise( - js.v8TypeError("Unable to use a zero-length ArrayBuffer."_kj)); + js.typeError("Unable to use a zero-length ArrayBuffer."_kj)); } // Check for pending error first (deferred error during a prior read operation) @@ -2792,11 +3509,9 @@ kj::Maybe> ReadableStreamJsController::read( // If it is a BYOB read, then the spec requires that we return an empty // view of the same type provided, that uses the same backing memory // as that provided, but with zero-length. - auto source = jsg::BufferSource(js, byobOptions.bufferView.getHandle(js)); - auto store = source.detach(js); - store.consume(store.size()); + auto view = byobOptions.bufferView.getHandle(js).detachAndTake(js); return js.resolvedPromise(ReadResult{ - .value = js.v8Ref(store.createHandle(js)), + .value = jsg::JsValue(view.slice(js, 0, 0)).addRef(js), .done = true, }); } @@ -2842,7 +3557,11 @@ kj::Maybe> ReadableStreamJsController::read( kj::Maybe> ReadableStreamJsController::drainingRead( jsg::Lock& js, size_t maxRead) { - disturbed = true; + if (flags.pendingClosure) { + return js.rejectedPromise( + js.typeError("This ReadableStream belongs to an object that is closing."_kj)); + } + flags.disturbed = true; // Check for pending state first (deferred close/error during a prior read operation) if (state.pendingStateIs()) { @@ -2872,27 +3591,12 @@ kj::Maybe> ReadableStreamJsController::draining auto wrapDrainingRead = [this](jsg::Lock& js, jsg::Promise promise) -> jsg::Promise { - return promise.then(js, [this](jsg::Lock& js, DrainingReadResult result) { - if (state.endOperation()) { - // A pending state was applied. Call the appropriate callback. - if (state.template is()) { - lock.onClose(js); - } else if (state.template is()) { - KJ_IF_SOME(err, state.template tryGetUnsafe()) { - lock.onError(js, err.getHandle(js)); - // The error was applied during this operation — the data we collected - // may be invalid. Discard it and propagate the error rather than - // silently returning possibly-corrupt data. - js.throwException(err.addRef(js)); - } - } - } - return kj::mv(result); - }, [this](jsg::Lock& js, jsg::Value exception) -> DrainingReadResult { - state.clearPendingState(); - (void)state.endOperation(); - js.throwException(kj::mv(exception)); - }); + KJ_IF_SOME(wdr, wrapDrainingReadContinuation) { + return promise.thenRef(js, wdr); + } + wrapDrainingReadContinuation.emplace( + WrapDrainingReadContinuationType::create(js, WrapDrainingReadCallbacks{weakSelf.addRef()})); + return promise.thenRef(js, KJ_ASSERT_NONNULL(wrapDrainingReadContinuation)); }; KJ_SWITCH_ONEOF(state) { @@ -2921,8 +3625,9 @@ kj::Maybe> ReadableStreamJsController::draining JSG_CATCH(exception) { state.clearPendingState(); (void)state.endOperation(); - doError(js, exception.getHandle(js)); - return js.rejectedPromise(kj::mv(exception)); + auto handle = jsg::JsValue(exception.getHandle(js)); + doError(js, handle); + return js.rejectedPromise(handle); }; } KJ_CASE_ONEOF(consumer, kj::Own) { @@ -2934,8 +3639,9 @@ kj::Maybe> ReadableStreamJsController::draining JSG_CATCH(exception) { state.clearPendingState(); (void)state.endOperation(); - doError(js, exception.getHandle(js)); - return js.rejectedPromise(kj::mv(exception)); + auto handle = jsg::JsValue(exception.getHandle(js)); + doError(js, handle); + return js.rejectedPromise(handle); }; } } @@ -2947,9 +3653,11 @@ void ReadableStreamJsController::releaseReader(Reader& reader, kj::Maybe(); - disturbed = true; + flags.disturbed = true; // This will leave this stream locked, disturbed, and closed. @@ -2971,6 +3679,23 @@ ReadableStreamController::Tee ReadableStreamJsController::tee(jsg::Lock& js) { }; } + auto trySourceTee = [&](auto& consumer) -> kj::Maybe { + auto& s = KJ_ASSERT_NONNULL(consumer->state); + auto limit = IoContext::current().getLimitEnforcer().getBufferingLimit(); + KJ_IF_SOME(tee, s.controller->tryTeeSource(limit)) { + state.transitionTo(); + auto stream1 = js.alloc(newReadableStreamJsController()); + auto stream2 = js.alloc(newReadableStreamJsController()); + stream1->getController().setup(js, kj::mv(tee.branch1)); + stream2->getController().setup(js, kj::mv(tee.branch2)); + return Tee{ + .branch1 = kj::mv(stream1), + .branch2 = kj::mv(stream2), + }; + } + return kj::none; + }; + KJ_SWITCH_ONEOF(state) { KJ_CASE_ONEOF(initial, Initial) { // Stream not yet set up, treat as closed. @@ -2998,6 +3723,9 @@ ReadableStreamController::Tee ReadableStreamJsController::tee(jsg::Lock& js) { }; } KJ_CASE_ONEOF(consumer, kj::Own) { + KJ_IF_SOME(tee, trySourceTee(consumer)) { + return kj::mv(tee); + } KJ_DEFER(state.transitionTo()); // We create two additional streams that clone this stream's consumer state, // then close this stream's consumer. @@ -3007,6 +3735,9 @@ ReadableStreamController::Tee ReadableStreamJsController::tee(jsg::Lock& js) { }; } KJ_CASE_ONEOF(consumer, kj::Own) { + KJ_IF_SOME(tee, trySourceTee(consumer)) { + return kj::mv(tee); + } KJ_DEFER(state.transitionTo()); // We create two additional streams that clone this stream's consumer state, // then close this stream's consumer. @@ -3024,16 +3755,11 @@ void ReadableStreamJsController::setOwnerRef(ReadableStream& stream) { owner = &stream; } -void ReadableStreamJsController::setup(jsg::Lock& js, - jsg::Optional maybeUnderlyingSource, - jsg::Optional maybeQueuingStrategy) { - auto underlyingSource = kj::mv(maybeUnderlyingSource).orDefault({}); - auto queuingStrategy = kj::mv(maybeQueuingStrategy).orDefault({}); - auto type = underlyingSource.type.map([](kj::StringPtr s) { return s; }).orDefault(""_kj); +void ReadableStreamJsController::setup(jsg::Lock& js, kj::Own source) { - expectedLength = underlyingSource.expectedLength; + expectedLength = source->getExpectedLength(); - if (type == "bytes") { + if (source->isBytes()) { // Per spec, autoAllocateChunkSize should only be set if the user explicitly provides it. // If not set, the underlying source's pull method won't receive a byobRequest for // non-BYOB reads and must use controller.enqueue() instead. @@ -3048,19 +3774,17 @@ void ReadableStreamJsController::setup(jsg::Lock& js, kj::Maybe autoAllocateChunkSize; if (useSpecCompliantBehavior) { // Spec-compliant: only set if user explicitly provides it - autoAllocateChunkSize = - underlyingSource.autoAllocateChunkSize.map([](int size) { return size; }); + autoAllocateChunkSize = source->getAutoAllocateChunkSize(); } else { // Legacy behavior: apply a default autoAllocateChunkSize if not provided. auto defaultChunkSize = UnderlyingSource::DEFAULT_AUTO_ALLOCATE_CHUNK_SIZE; if (util::Autogate::isEnabled(util::AutogateKey::UPDATED_AUTO_ALLOCATE_CHUNK_SIZE)) { defaultChunkSize = UnderlyingSource::DEFAULT_AUTO_ALLOCATE_CHUNK_SIZE_2; } - autoAllocateChunkSize = underlyingSource.autoAllocateChunkSize.orDefault(defaultChunkSize); + autoAllocateChunkSize = source->getAutoAllocateChunkSize().orDefault(defaultChunkSize); } - auto controller = - js.alloc(kj::mv(underlyingSource), kj::mv(queuingStrategy)); + auto controller = js.alloc(js, kj::mv(source)); KJ_IF_SOME(chunkSize, autoAllocateChunkSize) { JSG_REQUIRE(chunkSize > 0, TypeError, "The autoAllocateChunkSize option cannot be zero."); @@ -3075,10 +3799,7 @@ void ReadableStreamJsController::setup(jsg::Lock& js, sizeof(ByteReadable) + sizeof(ReadableByteStreamController)))); controller->start(js); } else { - JSG_REQUIRE( - type == "", TypeError, kj::str("\"", type, "\" is not a valid type of ReadableStream.")); - auto controller = js.alloc( - kj::mv(underlyingSource), kj::mv(queuingStrategy)); + auto controller = js.alloc(js, kj::mv(source)); state.transitionTo>( kj::heap(controller.addRef(), *this) .attach(js.getExternalMemoryAdjustment( @@ -3114,6 +3835,9 @@ void ReadableStreamJsController::visitForGc(jsg::GcVisitor& visitor) { } } visitor.visit(lock); + KJ_IF_SOME(wdr, wrapDrainingReadContinuation) { + wdr.visitForGc(visitor); + } } kj::Maybe ReadableStreamJsController::getDesiredSize() { @@ -3142,14 +3866,17 @@ kj::Maybe ReadableStreamJsController::getDesiredSize() { KJ_UNREACHABLE; } -kj::Maybe> ReadableStreamJsController::isErrored(jsg::Lock& js) { +kj::Maybe ReadableStreamJsController::isErrored(jsg::Lock& js) { // Check for pending error first KJ_IF_SOME(pendingError, state.tryGetPendingStateUnsafe()) { return pendingError.getHandle(js); } // Pending Closed means not errored, so we can just check current state - return state.tryGetUnsafe().map( - [&](jsg::Value& reason) { return reason.getHandle(js); }); + KJ_IF_SOME(err, state.tryGetUnsafe()) { + return err.getHandle(js); + } + + return kj::none; } bool ReadableStreamJsController::canCloseOrEnqueue() { @@ -3219,20 +3946,25 @@ class AllReader { using PartList = kj::Array>; AllReader(jsg::Ref stream, uint64_t limit) - : state(State::create>(kj::mv(stream))), + : weakSelf(kj::rc>(kj::Badge{}, *this)), + state(State::create>(kj::mv(stream))), limit(limit) {} + ~AllReader() noexcept(false) { + weakSelf->invalidate(); + } KJ_DISALLOW_COPY_AND_MOVE(AllReader); - jsg::Promise allBytes(jsg::Lock& js) { - return loop(js).then(js, [this](auto& js, PartList&& partPtrs) -> jsg::BufferSource { - auto out = jsg::BackingStore::alloc(js, runningTotal); + jsg::Promise> allBytes(jsg::Lock& js) KJ_WARN_UNUSED_RESULT { + return loop(js).then( + js, [this](auto& js, PartList&& partPtrs) -> jsg::JsRef { + auto out = jsg::JsArrayBuffer::create(js, runningTotal); copyInto(out.asArrayPtr(), partPtrs.asPtr()); - return jsg::BufferSource(js, kj::mv(out)); + return out.addRef(js); }); } - jsg::Promise allText( - jsg::Lock& js, ReadAllTextOption option = ReadAllTextOption::NULL_TERMINATE) { + jsg::Promise allText(jsg::Lock& js, + ReadAllTextOption option = ReadAllTextOption::NULL_TERMINATE) KJ_WARN_UNUSED_RESULT { return loop(js).then(js, [this, option](auto& js, PartList&& partPtrs) { // Strip UTF-8 BOM if requested if ((option & ReadAllTextOption::STRIP_BOM) && partPtrs.size() > 0 && @@ -3253,9 +3985,14 @@ class AllReader { void visitForGc(jsg::GcVisitor& visitor) { state.visitForGc(visitor); + KJ_IF_SOME(lc, loopContinuation) { + lc.visitForGc(visitor); + } } private: + kj::Rc> weakSelf; + // State machine for AllReader: // Closed is terminal, Errored is implicitly terminal via ErrorState. // jsg::Ref is the active state (still reading). @@ -3267,69 +4004,127 @@ class AllReader { jsg::Ref>; State state; uint64_t limit; - kj::Vector parts; + kj::Vector, jsg::DOMString>> parts; uint64_t runningTotal = 0; - jsg::Promise loop(jsg::Lock& js) { + // Persistent continuation for the read loop. Reused across iterations to + // avoid per-read OpaqueWrappable and v8::Function allocations. + struct LoopCallbacks { + kj::Rc> weakSelf; + + jsg::Promise thenFunc(jsg::Lock& js, ReadResult result) KJ_WARN_UNUSED_RESULT { + KJ_IF_SOME(self, weakSelf->tryGet()) { + return self.onLoopSuccess(js, kj::mv(result)); + } + return js.rejectedPromise(js.error("AllReader was destroyed")); + } + + jsg::Promise catchFunc(jsg::Lock& js, jsg::Value exception) KJ_WARN_UNUSED_RESULT { + KJ_IF_SOME(self, weakSelf->tryGet()) { + return self.onLoopFailure(js, kj::mv(exception)); + } + return js.rejectedPromise(kj::mv(exception)); + } + }; + using LoopContinuationType = + jsg::PersistentContinuation>; + kj::Maybe loopContinuation; + + jsg::Promise loop(jsg::Lock& js) KJ_WARN_UNUSED_RESULT { KJ_SWITCH_ONEOF(state) { KJ_CASE_ONEOF(closed, StreamStates::Closed) { - return js.resolvedPromise(KJ_MAP(p, parts) { return p.asArrayPtr(); }); + return js.resolvedPromise(KJ_MAP(p, parts) { + KJ_SWITCH_ONEOF(p) { + KJ_CASE_ONEOF(str, jsg::DOMString) { + return str.asBytes().slice(0, str.size()); + } + KJ_CASE_ONEOF(buf, jsg::JsRef) { + return buf.getHandle(js).asArrayPtr(); + } + } + KJ_UNREACHABLE; + }); } KJ_CASE_ONEOF(errored, StreamStates::Errored) { return js.template rejectedPromise(errored.getHandle(js)); } KJ_CASE_ONEOF(readable, jsg::Ref) { - // Note that these nested lambda retain references to `this` and `readable` - // and are passed into to promise returned by this method. It is the responsibility - // of the caller to ensure that the AllReader instance is kept alive until the - // promise is settled. - auto onSuccess = JSG_VISITABLE_LAMBDA((this, readable = readable.addRef()), (readable), - (jsg::Lock & js, ReadResult result) mutable->jsg::Promise { - if (result.done) { - state.template transitionTo(); - return loop(js); - } + KJ_IF_SOME(lc, loopContinuation) { + return KJ_ASSERT_NONNULL(readable->getController().read(js, kj::none)).thenRef(js, lc); + } + loopContinuation.emplace( + LoopContinuationType::create(js, LoopCallbacks{weakSelf.addRef()})); + return KJ_ASSERT_NONNULL(readable->getController().read(js, kj::none)) + .thenRef(js, KJ_ASSERT_NONNULL(loopContinuation)); + } + } + KJ_UNREACHABLE; + } - // If we're not done, the result value must be interpretable as - // bytes for the read to make any sense. - auto handle = KJ_ASSERT_NONNULL(result.value).getHandle(js); - if (!handle->IsArrayBufferView() && !handle->IsArrayBuffer()) { - auto error = js.v8TypeError("This ReadableStream did not return bytes."); - state.template transitionTo(js.v8Ref(error)); - return readable->getController().cancel(js, error).then( - js, [&](jsg::Lock& js) { return loop(js); }); - } + // Helper to cancel the readable and loop back to return the error. + jsg::Promise cancelAndLoop( + jsg::Lock& js, jsg::Ref readable, jsg::JsValue error) KJ_WARN_UNUSED_RESULT { + state.template transitionTo(error.addRef(js)); + return readable->getController().cancel(js, error).then( + js, [weak = weakSelf.addRef()](jsg::Lock& js) -> jsg::Promise { + KJ_IF_SOME(self, weak->tryGet()) { + return self.loop(js); + } + return js.rejectedPromise(js.error("AllReader was destroyed")); + }); + } - jsg::BufferSource bufferSource(js, handle); + jsg::Promise onLoopSuccess(jsg::Lock& js, ReadResult result) KJ_WARN_UNUSED_RESULT { + KJ_IF_SOME(readable, state.tryGetActiveUnsafe()) { + if (result.done) { + state.template transitionTo(); + return loop(js); + } - if (bufferSource.size() == 0) { - // Weird but allowed, we'll skip it. - return loop(js); - } + auto handle = KJ_ASSERT_NONNULL(result.value).getHandle(js); - if ((runningTotal + bufferSource.size()) > limit) { - auto error = js.v8TypeError("Memory limit exceeded before EOF."); - state.template transitionTo(js.v8Ref(error)); - return readable->getController().cancel(js, error).then( - js, [&](jsg::Lock& js) { return loop(js); }); - } + KJ_IF_SOME(str, handle.tryCast()) { + auto kjstr = str.toDOMString(js); + if (kjstr.size() == 0) return loop(js); + if ((runningTotal + kjstr.size()) > limit) { + return cancelAndLoop( + js, readable.addRef(), js.typeError("Memory limit exceeded before EOF.")); + } + runningTotal += kjstr.size(); + parts.add(kj::mv(kjstr)); + return loop(js); + } else { + } + + if (!handle.isArrayBufferView() && !handle.isSharedArrayBuffer() && !handle.isArrayBuffer()) { + return cancelAndLoop( + js, readable.addRef(), js.typeError("This ReadableStream did not return bytes.")); + } - runningTotal += bufferSource.size(); - parts.add(bufferSource.copy(js)); - return loop(js); - }); + jsg::JsBufferSource bufferSource(handle); - auto onFailure = [this](auto& js, jsg::Value exception) -> jsg::Promise { - // In this case the stream should already be errored. - state.template transitionTo(js.v8Ref(exception.getHandle(js))); - return loop(js); - }; + if (bufferSource.size() == 0) { + return loop(js); + } - return maybeAddFunctor(js, KJ_ASSERT_NONNULL(readable->getController().read(js, kj::none)), - kj::mv(onSuccess), kj::mv(onFailure)); + if ((runningTotal + bufferSource.size()) > limit) { + return cancelAndLoop( + js, readable.addRef(), js.typeError("Memory limit exceeded before EOF.")); } + + runningTotal += bufferSource.size(); + parts.add(bufferSource.addRef(js)); + return loop(js); + } else { + // State already terminal — return terminal result. + return loop(js); } - KJ_UNREACHABLE; + } + + jsg::Promise onLoopFailure(jsg::Lock& js, jsg::Value exception) KJ_WARN_UNUSED_RESULT { + auto handle = jsg::JsValue(exception.getHandle(js)); + state.template transitionTo(handle.addRef(js)); + return loop(js); } void copyInto(kj::ArrayPtr out, kj::ArrayPtr> in) { @@ -3341,234 +4136,44 @@ class AllReader { } }; -// PumpToReader implements the original JS promise-loop approach to pumping data from -// a ReadableStream to a WritableStreamSink. It reads one chunk at a time using the -// standard read() API, writes each chunk to the sink, and loops until done or errored. -// This is the fallback path used when the ENABLE_DRAINING_READ_ON_STANDARD_STREAMS -// autogate is not enabled. -class PumpToReader { - public: - PumpToReader(jsg::Ref stream, kj::Own sink, bool end) - : ioContext(IoContext::current()), - state(State::create>(kj::mv(stream))), - sink(kj::mv(sink)), - self(kj::refcounted>(kj::Badge{}, *this)), - end(end) {} - KJ_DISALLOW_COPY_AND_MOVE(PumpToReader); +// pumpToCoroutine uses a DrainingReader to efficiently pull all synchronously available +// data from the stream in each iteration, then writes it to the sink using vectored +// I/O. This minimizes isolate lock acquisitions by batching: each time the lock is +// held, the stream's internal queue is fully drained and the JS pull callback is +// pumped synchronously as many times as possible. +// +// The pump loop is a kj coroutine. Dropping the returned kj::Promise drops the +// coroutine frame, which destroys the DrainingReader (releasing the stream lock) +// and the sink. No WeakRef/IoOwn dance is needed because ownership is clear. +// The coroutine that implements the pump loop takes ownership of the DrainingReader +// and sink. The jsg::Ref is not passed into the coroutine because +// jsg::Ref is disallowed in coroutine parameters; instead, the DrainingReader holds +// a reference to the stream internally. +kj::Promise pumpToImpl(IoContext& ioContext, + kj::Own reader, + kj::Own sink, + End end) { - ~PumpToReader() noexcept(false) { - self->invalidate(); - // Ensure that if a write promise is pending it is proactively canceled. - canceler.cancel("PumpToReader was destroyed"); - } + bool writeFailed = false; - kj::Promise pumpTo(jsg::Lock& js) { - ioContext.requireCurrentOrThrowJs(); - KJ_SWITCH_ONEOF(state) { - KJ_CASE_ONEOF(stream, jsg::Ref) { - auto readable = stream.addRef(); - state.template transitionTo(); - return ioContext.awaitJs( - js, pumpLoop(js, ioContext, kj::mv(readable), ioContext.addObject(self->addRef()))); - } - KJ_CASE_ONEOF(pumping, Pumping) { - return KJ_EXCEPTION(FAILED, "pumping is already in progress"); - } - KJ_CASE_ONEOF(closed, StreamStates::Closed) { - return KJ_EXCEPTION(FAILED, "stream has already been consumed"); - } - KJ_CASE_ONEOF(errored, kj::Exception) { - return errored.clone(); - } - } - KJ_UNREACHABLE; - } - - private: - struct Pumping { - static constexpr kj::StringPtr NAME KJ_UNUSED = "pumping"_kj; - }; - IoContext& ioContext; - - using State = StateMachine, - ErrorState, - Pumping, - StreamStates::Closed, - kj::Exception, - jsg::Ref>; - State state; - kj::Own sink; - kj::Own> self; - kj::Canceler canceler; - bool end; - - bool isErroredOrClosed() { - return state.isTerminal(); - } - - jsg::Promise pumpLoop(jsg::Lock& js, - IoContext& ioContext, - jsg::Ref readable, - IoOwn> pumpToReader) { - ioContext.requireCurrentOrThrowJs(); - - KJ_SWITCH_ONEOF(state) { - KJ_CASE_ONEOF(ready, jsg::Ref) { - KJ_UNREACHABLE; - } - KJ_CASE_ONEOF(closed, StreamStates::Closed) { - return end ? ioContext.awaitIoLegacy(js, sink->end().attach(kj::mv(sink))) - : js.resolvedPromise(); - } - KJ_CASE_ONEOF(errored, kj::Exception) { - if (end) { - sink->abort(errored.clone()); - } - return js.rejectedPromise(errored.clone()); - } - KJ_CASE_ONEOF(pumping, Pumping) { - using Result = kj::OneOf, StreamStates::Closed, jsg::Value>; - - return KJ_ASSERT_NONNULL(readable->getController().read(js, kj::none)) - .then(js, - ioContext.addFunctor([byteStream = readable->getController().isByteOriented()]( - auto& js, ReadResult result) mutable -> Result { - if (result.done) { - return StreamStates::Closed(); - } - - auto handle = KJ_ASSERT_NONNULL(result.value).getHandle(js); - if (!handle->IsArrayBufferView() && !handle->IsArrayBuffer()) { - return js.v8Ref(js.v8TypeError("This ReadableStream did not return bytes.")); - } - - jsg::BufferSource bufferSource(js, handle); - if (bufferSource.size() == 0) { - return Pumping{}; - } - - if (byteStream) { - jsg::BackingStore backing = bufferSource.detach(js); - return backing.asArrayPtr().attach(kj::mv(backing)); - } - return bufferSource.asArrayPtr().attach(kj::mv(bufferSource)); - }), - [](auto& js, jsg::Value exception) mutable -> Result { return kj::mv(exception); }) - .then(js, ioContext.addFunctor( JSG_VISITABLE_LAMBDA((readable = kj::mv(readable), pumpToReader = kj::mv(pumpToReader)), (readable), (jsg::Lock & js, Result result) mutable { - KJ_IF_SOME(reader, pumpToReader->tryGet()) { - reader.ioContext.requireCurrentOrThrowJs(); - auto& ioContext = IoContext::current(); - KJ_SWITCH_ONEOF(result) { - KJ_CASE_ONEOF(bytes, kj::Array) { - auto promise = reader.sink->write(bytes).attach(kj::mv(bytes)); - return ioContext.awaitIo(js, reader.canceler.wrap(kj::mv(promise))) - .then(js, - [](jsg::Lock& js) -> kj::Maybe { - return kj::Maybe(kj::none); - }, - [](jsg::Lock& js, jsg::Value exception) mutable -> kj::Maybe { - return kj::mv(exception); - }) - .then(js, - ioContext.addFunctor(JSG_VISITABLE_LAMBDA( - (readable = readable.addRef(), pumpToReader = kj::mv(pumpToReader)), - (readable), - (jsg::Lock & js, kj::Maybe maybeException) mutable { - KJ_IF_SOME(reader, pumpToReader->tryGet()) { - auto& ioContext = reader.ioContext; - ioContext.requireCurrentOrThrowJs(); - KJ_IF_SOME(exception, maybeException) { - if (!reader.isErroredOrClosed()) { - reader.state.transitionTo( - js.exceptionToKj(kj::mv(exception))); - } - } else { - // Else block to avert dangling else compiler warning. - } - return reader.pumpLoop( - js, ioContext, readable.addRef(), kj::mv(pumpToReader)); - } else { - return readable->getController().cancel(js, - maybeException.map( - [&](jsg::Value& ex) { return ex.getHandle(js); })); - } - }))); - } - KJ_CASE_ONEOF(pumping, Pumping) {} - KJ_CASE_ONEOF(closed, StreamStates::Closed) { - if (!reader.isErroredOrClosed()) { - reader.state.transitionTo(); - } - } - KJ_CASE_ONEOF(exception, jsg::Value) { - if (!reader.isErroredOrClosed()) { - reader.state.transitionTo(js.exceptionToKj(kj::mv(exception))); - } - } - } - return reader.pumpLoop(js, ioContext, readable.addRef(), kj::mv(pumpToReader)); - } else { - KJ_SWITCH_ONEOF(result) { - KJ_CASE_ONEOF(bytes, kj::Array) { - return readable->getController().cancel(js, kj::none); - } - KJ_CASE_ONEOF(pumping, Pumping) { - return readable->getController().cancel(js, kj::none); - } - KJ_CASE_ONEOF(closed, StreamStates::Closed) { - return js.resolvedPromise(); - } - KJ_CASE_ONEOF(exception, jsg::Value) { - return readable->getController().cancel(js, exception.getHandle(js)); - } - } - } - KJ_UNREACHABLE; - }))); - } - } - KJ_UNREACHABLE; - } -}; - -// pumpToCoroutine uses a DrainingReader to efficiently pull all synchronously available -// data from the stream in each iteration, then writes it to the sink using vectored -// I/O. This minimizes isolate lock acquisitions by batching: each time the lock is -// held, the stream's internal queue is fully drained and the JS pull callback is -// pumped synchronously as many times as possible. -// -// The pump loop is a kj coroutine. Dropping the returned kj::Promise drops the -// coroutine frame, which destroys the DrainingReader (releasing the stream lock) -// and the sink. No WeakRef/IoOwn dance is needed because ownership is clear. -// The coroutine that implements the pump loop takes ownership of the DrainingReader -// and sink. The jsg::Ref is not passed into the coroutine because -// jsg::Ref is disallowed in coroutine parameters; instead, the DrainingReader holds -// a reference to the stream internally. -kj::Promise pumpToImpl(IoContext& ioContext, - kj::Own reader, - kj::Own sink, - bool end) { - - bool writeFailed = false; - - KJ_TRY { - while (true) { - // Perform a draining read to get all synchronously available data if possible - // or fall back to a regular read if not. - DrainingReadResult result = co_await ioContext.run([&reader](jsg::Lock& js) mutable { - auto& ioContext = IoContext::current(); - // Use a 256KB limit to allow periodic yielding to the event loop, - // preventing a fast producer from monopolizing the thread. - constexpr size_t kMaxReadPerCycle = 256 * 1024; - return ioContext.awaitJs(js, reader->read(js, kMaxReadPerCycle)); - }); - - // Write all the chunks we received using vectored write for efficiency. - if (result.chunks.size() > 0) { - KJ_ON_SCOPE_FAILURE(writeFailed = true); - auto pieces = - KJ_MAP(chunk, result.chunks) -> kj::ArrayPtr { return chunk.asPtr(); }; - co_await sink->write(pieces); + KJ_TRY { + while (true) { + // Perform a draining read to get all synchronously available data if possible + // or fall back to a regular read if not. + DrainingReadResult result = co_await ioContext.run([&reader](jsg::Lock& js) mutable { + auto& ioContext = IoContext::current(); + // Use a 256KB limit to allow periodic yielding to the event loop, + // preventing a fast producer from monopolizing the thread. + constexpr size_t kMaxReadPerCycle = 256 * 1024; + return ioContext.awaitJs(js, reader->read(js, kMaxReadPerCycle)); + }); + + // Write all the chunks we received using vectored write for efficiency. + if (result.chunks.size() > 0) { + KJ_ON_SCOPE_FAILURE(writeFailed = true); + auto pieces = + KJ_MAP(chunk, result.chunks) -> kj::ArrayPtr { return chunk.asPtr(); }; + co_await sink->write(pieces); } // If the stream is done, end the output if needed and exit. @@ -3598,11 +4203,15 @@ kj::Promise pumpToImpl(IoContext& ioContext, template jsg::Promise ReadableStreamJsController::readAll(jsg::Lock& js, uint64_t limit) { + if (flags.pendingClosure) { + return js.rejectedPromise( + js.typeError("This ReadableStream belongs to an object that is closing."_kj)); + } if (isLockedToReader()) { - return js.rejectedPromise(KJ_EXCEPTION( - FAILED, "jsg.TypeError: This ReadableStream is currently locked to a reader.")); + return js.rejectedPromise( + js.typeError("This ReadableStream is currently locked to a reader.")); } - disturbed = true; + flags.disturbed = true; bool stripBom = false; KJ_IF_SOME(flags, FeatureFlags::tryGet(js)) { @@ -3612,13 +4221,37 @@ jsg::Promise ReadableStreamJsController::readAll(jsg::Lock& js, uint64_t limi // This operation leaves the stream locked and disturbed. The loop will read until // the stream is closed or errored. If the limit is reached, the loop will error. + // Try to extract the underlying kj source for direct readAll. + auto trySourceReadAll = [&](auto& consumer) -> kj::Maybe> { + auto& s = KJ_ASSERT_NONNULL(consumer->state); + KJ_IF_SOME(source, s.controller->tryReleaseSource()) { + state.transitionTo(); + auto& context = IoContext::current(); + if constexpr (kj::isSameType>()) { + return context.awaitIoLegacy(js, source->readAllBytes(limit).attach(kj::mv(source))) + .then( + js, [](jsg::Lock& js, kj::Array bytes) -> jsg::JsRef { + auto ab = jsg::JsArrayBuffer::create(js, bytes); + return ab.addRef(js); + }); + } else { + auto option = ReadAllTextOption::NULL_TERMINATE; + if (stripBom) { + option |= ReadAllTextOption::STRIP_BOM; + } + return context.awaitIoLegacy(js, source->readAllText(limit, option).attach(kj::mv(source))); + } + } + return kj::none; + }; + const auto readAll = [this, limit, stripBom](auto& js) -> jsg::Promise { KJ_ASSERT(lock.lock()); // The AllReader will hold a traceable reference to the ReadableStream. auto reader = kj::heap(addRef(), limit); auto promise = ([&js, &reader, stripBom]() -> jsg::Promise { - if constexpr (kj::isSameType()) { + if constexpr (kj::isSameType>()) { (void)stripBom; // Unused in this branch. return reader->allBytes(js); } else { @@ -3647,17 +4280,17 @@ jsg::Promise ReadableStreamJsController::readAll(jsg::Lock& js, uint64_t limi KJ_SWITCH_ONEOF(state) { KJ_CASE_ONEOF(initial, Initial) { // Stream not yet set up, treat as closed. - if constexpr (kj::isSameType()) { - auto backing = jsg::BackingStore::alloc(js, 0); - return js.resolvedPromise(jsg::BufferSource(js, kj::mv(backing))); + if constexpr (kj::isSameType>()) { + auto ab = jsg::JsArrayBuffer::create(js, 0); + return js.resolvedPromise(ab.addRef(js)); } else { return js.resolvedPromise(T()); } } KJ_CASE_ONEOF(closed, StreamStates::Closed) { - if constexpr (kj::isSameType()) { - auto backing = jsg::BackingStore::alloc(js, 0); - return js.resolvedPromise(jsg::BufferSource(js, kj::mv(backing))); + if constexpr (kj::isSameType>()) { + auto ab = jsg::JsArrayBuffer::create(js, 0); + return js.resolvedPromise(ab.addRef(js)); } else { return js.resolvedPromise(T()); } @@ -3666,18 +4299,24 @@ jsg::Promise ReadableStreamJsController::readAll(jsg::Lock& js, uint64_t limi return js.rejectedPromise(errored.addRef(js)); } KJ_CASE_ONEOF(valueReadable, kj::Own) { + KJ_IF_SOME(result, trySourceReadAll(valueReadable)) { + return kj::mv(result); + } return readAll(js); } KJ_CASE_ONEOF(byteReadable, kj::Own) { + KJ_IF_SOME(result, trySourceReadAll(byteReadable)) { + return kj::mv(result); + } return readAll(js); } } KJ_UNREACHABLE; } -jsg::Promise ReadableStreamJsController::readAllBytes( +jsg::Promise> ReadableStreamJsController::readAllBytes( jsg::Lock& js, uint64_t limit) { - return readAll(js, limit); + return readAll>(js, limit); } jsg::Promise ReadableStreamJsController::readAllText(jsg::Lock& js, uint64_t limit) { @@ -3685,13 +4324,13 @@ jsg::Promise ReadableStreamJsController::readAllText(jsg::Lock& js, } kj::Own ReadableStreamJsController::detach( - jsg::Lock& js, bool ignored /* unused */) { + jsg::Lock& js, IgnoreDisturbed ignoreDisturbed) { KJ_ASSERT(!isLockedToReader()); - KJ_ASSERT(!isDisturbed()); + KJ_ASSERT(!isDisturbed() || ignoreDisturbed); KJ_ASSERT(!state.hasOperationInProgress(), "Unable to detach with read pending"); auto controller = kj::heap(); controller->expectedLength = expectedLength; - disturbed = true; + flags.disturbed = true; // Clones this streams state into a new ReadableStreamController, leaving this stream // locked, disturbed, and closed. @@ -3726,34 +4365,116 @@ kj::Own ReadableStreamJsController::detach( } kj::Maybe ReadableStreamJsController::tryGetLength(StreamEncoding encoding) { - return expectedLength; + // When backed by an internal source, delegate for encoding-aware length. + auto tryGetFromController = [&](auto& consumer) -> kj::Maybe { + KJ_IF_SOME(s, consumer->state) { + return s.controller->tryGetLength(encoding); + } + return kj::none; + }; + + KJ_SWITCH_ONEOF(state) { + KJ_CASE_ONEOF(initial, Initial) { + return expectedLength; + } + KJ_CASE_ONEOF(closed, StreamStates::Closed) { + return expectedLength; + } + KJ_CASE_ONEOF(errored, StreamStates::Errored) { + return expectedLength; + } + KJ_CASE_ONEOF(consumer, kj::Own) { + KJ_IF_SOME(len, tryGetFromController(consumer)) return len; + return expectedLength; + } + KJ_CASE_ONEOF(consumer, kj::Own) { + KJ_IF_SOME(len, tryGetFromController(consumer)) return len; + return expectedLength; + } + } + KJ_UNREACHABLE; +} + +StreamEncoding ReadableStreamJsController::getPreferredEncoding() { + KJ_SWITCH_ONEOF(state) { + KJ_CASE_ONEOF(initial, Initial) { + return StreamEncoding::IDENTITY; + } + KJ_CASE_ONEOF(closed, StreamStates::Closed) { + return StreamEncoding::IDENTITY; + } + KJ_CASE_ONEOF(errored, StreamStates::Errored) { + return StreamEncoding::IDENTITY; + } + KJ_CASE_ONEOF(consumer, kj::Own) { + KJ_IF_SOME(s, consumer->state) { + return s.controller->getPreferredEncoding(); + } + return StreamEncoding::IDENTITY; + } + KJ_CASE_ONEOF(consumer, kj::Own) { + KJ_IF_SOME(s, consumer->state) { + return s.controller->getPreferredEncoding(); + } + return StreamEncoding::IDENTITY; + } + } + KJ_UNREACHABLE; } kj::Promise> ReadableStreamJsController::pumpTo( - jsg::Lock& js, kj::Own sink, bool end) { + jsg::Lock& js, kj::Own sink, End end) { KJ_ASSERT(IoContext::hasCurrent(), "Unable to consume this ReadableStream outside of a request"); + KJ_REQUIRE(!flags.pendingClosure, "This ReadableStream belongs to an object that is closing."); KJ_REQUIRE(!isLockedToReader(), "This ReadableStream is currently locked to a reader."); - disturbed = true; + flags.disturbed = true; // This operation will leave the ReadableStream locked and disturbed. It will consume // the stream until it either closed or errors. - // - // When the ENABLE_DRAINING_READ_ON_STANDARD_STREAMS autogate is enabled, uses the new - // pumpToImpl coroutine with DrainingReader for batched reads and vectored writes. - // Otherwise, falls back to the original PumpToReader JS promise loop that reads one - // chunk at a time. - const auto handlePump = [&] { - if (util::Autogate::isEnabled(util::AutogateKey::ENABLE_DRAINING_READ_ON_STANDARD_STREAMS)) { - auto reader = KJ_ASSERT_NONNULL(DrainingReader::create(js, *this->addRef()), - "Failed to create DrainingReader — stream should not be locked"); - auto& ioContext = IoContext::current(); - return addNoopDeferredProxy(pumpToImpl(ioContext, kj::mv(reader), kj::mv(sink), end)); - } else { - KJ_ASSERT(lock.lock()); - auto reader = kj::heap(addRef(), kj::mv(sink), end); - return addNoopDeferredProxy(reader->pumpTo(js).attach(kj::mv(reader))); + // Try to extract the underlying kj source for deferred-proxy-capable pumping. + auto trySourcePump = [&](auto& consumer) -> kj::Maybe>> { + auto& s = KJ_ASSERT_NONNULL(consumer->state); + KJ_IF_SOME(source, s.controller->tryReleaseSource()) { + // Source extracted — close this stream and pump at the kj level. + state.transitionTo(); + // Use the same Holder pattern as ReadableStreamInternalController::pumpTo + // to ensure cancellation propagates if the pump is dropped. + struct Holder: public kj::Refcounted { + kj::Own sink; + kj::Own source; + bool done = false; + Holder(kj::Own sink, kj::Own source) + : sink(kj::mv(sink)), + source(kj::mv(source)) {} + ~Holder() noexcept(false) { + if (!done) { + source->cancel(KJ_EXCEPTION(DISCONNECTED, "pump canceled")); + } + } + }; + auto holder = kj::rc(kj::mv(sink), kj::mv(source)); + return holder->source->pumpTo(*holder->sink, end) + .then( + [holder = holder.addRef()](DeferredProxy proxy) mutable -> DeferredProxy { + proxy.proxyTask = proxy.proxyTask.attach(holder.addRef()); + holder->done = true; + return kj::mv(proxy); + }, [holder = holder.addRef()](kj::Exception&& ex) mutable { + holder->sink->abort(ex.clone()); + holder->source->cancel(ex.clone()); + holder->done = true; + return kj::mv(ex); + }); } + return kj::none; + }; + + const auto handlePump = [&] { + auto reader = KJ_ASSERT_NONNULL(DrainingReader::create(js, *this->addRef()), + "Failed to create DrainingReader — stream should not be locked"); + auto& ioContext = IoContext::current(); + return addNoopDeferredProxy(pumpToImpl(ioContext, kj::mv(reader), kj::mv(sink), end)); }; KJ_SWITCH_ONEOF(state) { @@ -3768,9 +4489,15 @@ kj::Promise> ReadableStreamJsController::pumpTo( return js.exceptionToKj(errored.addRef(js)); } KJ_CASE_ONEOF(readable, kj::Own) { + KJ_IF_SOME(result, trySourcePump(readable)) { + return kj::mv(result); + } return handlePump(); } KJ_CASE_ONEOF(readable, kj::Own) { + KJ_IF_SOME(result, trySourcePump(readable)) { + return kj::mv(result); + } return handlePump(); } } @@ -3780,13 +4507,28 @@ kj::Promise> ReadableStreamJsController::pumpTo( // ====================================================================================== -WritableStreamDefaultController::WritableStreamDefaultController( - jsg::Lock& js, WritableStream& owner, jsg::Ref abortSignal) - : ioContext(tryGetIoContext()), - impl(js, owner, kj::mv(abortSignal)) {} +WritableStreamDefaultController::WritableStreamDefaultController(jsg::Lock& js, + WritableStream& owner, + kj::Own underlyingSink, + jsg::Ref abortSignal) + : weakSelf(kj::rc>( + kj::Badge{}, *this)), + ioContext(tryGetIoContext()), + impl(js, owner, kj::mv(underlyingSink), kj::mv(abortSignal), weakSelf.addRef()) {} + +bool WritableStreamDefaultController::isInternal() const { + return impl.isInternal(); +} + +kj::Maybe> WritableStreamDefaultController::tryReleaseSink() { + return impl.tryReleaseSink(); +} + +kj::Maybe WritableStreamDefaultController::tryGetSink() { + return impl.tryGetSink(); +} -jsg::Promise WritableStreamDefaultController::abort( - jsg::Lock& js, v8::Local reason) { +jsg::Promise WritableStreamDefaultController::abort(jsg::Lock& js, jsg::JsValue reason) { return impl.abort(js, JSG_THIS, reason); } @@ -3798,12 +4540,11 @@ jsg::Promise WritableStreamDefaultController::close(jsg::Lock& js) { return impl.close(js, JSG_THIS); } -void WritableStreamDefaultController::error( - jsg::Lock& js, jsg::Optional> reason) { +void WritableStreamDefaultController::error(jsg::Lock& js, jsg::Optional reason) { impl.error(js, JSG_THIS, reason.orDefault(js.undefined())); } -kj::Maybe WritableStreamDefaultController::getDesiredSize() { +kj::Maybe WritableStreamDefaultController::getDesiredSize() { // Per the spec, desiredSize should be null when the stream is erroring. if (impl.flags.pedanticWpt && isErroring()) { return kj::none; @@ -3815,33 +4556,46 @@ jsg::Ref WritableStreamDefaultController::getSignal() { return impl.signal.addRef(); } -kj::Maybe> WritableStreamDefaultController::isErroring(jsg::Lock& js) { +kj::Maybe WritableStreamDefaultController::isErroring(jsg::Lock& js) { KJ_IF_SOME(erroring, impl.state.tryGetUnsafe()) { return erroring.reason.getHandle(js); } return kj::none; } -void WritableStreamDefaultController::setup( - jsg::Lock& js, UnderlyingSink underlyingSink, StreamQueuingStrategy queuingStrategy) { - impl.setup(js, JSG_THIS, kj::mv(underlyingSink), kj::mv(queuingStrategy)); +void WritableStreamDefaultController::setup(jsg::Lock& js) { + impl.setup(js, JSG_THIS); } -jsg::Promise WritableStreamDefaultController::write( - jsg::Lock& js, v8::Local value) { +jsg::Promise WritableStreamDefaultController::write(jsg::Lock& js, jsg::JsValue value) { return impl.write(js, JSG_THIS, value); } +jsg::Promise WritableStreamDefaultController::flush( + jsg::Lock& js, MarkAsHandled markAsHandled) { + return impl.flush(js, JSG_THIS, markAsHandled); +} + void WritableStreamDefaultController::cancelPendingWrites(jsg::Lock& js, jsg::JsValue reason) { impl.cancelPendingWrites(js, reason); } void WritableStreamDefaultController::clearAlgorithms() { - impl.algorithms.clear(); + // Free the underlying sink to break circular references. + auto _ = kj::mv(impl.underlyingSink); + // Release V8 handles from persistent continuations. + impl.writeContinuation = kj::none; + impl.writevContinuation = kj::none; + impl.drainContinuation = kj::none; + impl.closeContinuation = kj::none; + impl.inFlightSelf = kj::none; } WritableStreamDefaultController::~WritableStreamDefaultController() noexcept(false) { - // Clear algorithms in destructor to break circular references + // Invalidate the weak ref BEFORE clearing algorithms. This prevents + // persistent continuation callbacks from dereferencing this (now being + // destroyed) WritableImpl. + weakSelf->invalidate(); clearAlgorithms(); } @@ -3849,6 +4603,7 @@ WritableStreamDefaultController::~WritableStreamDefaultController() noexcept(fal WritableStreamJsController::WritableStreamJsController(): ioContext(tryGetIoContext()) {} WritableStreamJsController::~WritableStreamJsController() noexcept(false) { + weakSelf->invalidate(); // Clear algorithms to break circular references during destruction KJ_IF_SOME(controller, state.tryGetUnsafe()) { controller->clearAlgorithms(); @@ -3873,7 +4628,7 @@ WritableStreamJsController::WritableStreamJsController(StreamStates::Errored err } jsg::Promise WritableStreamJsController::abort( - jsg::Lock& js, jsg::Optional> reason) { + jsg::Lock& js, jsg::Optional reason) { // The spec requires that if abort is called multiple times, it is supposed to return the same // promise each time. That's a bit cumbersome here with jsg::Promise so we intentionally just // return a continuation branch off the same promise. @@ -3915,20 +4670,42 @@ bool WritableStreamJsController::isErrored() { return state.isErrored(); } -jsg::Promise WritableStreamJsController::close(jsg::Lock& js, bool markAsHandled) { +jsg::Promise WritableStreamJsController::close(jsg::Lock& js, MarkAsHandled markAsHandled) { + // If there's a closure waitable (Sockets), wait for it before closing. + KJ_IF_SOME(closureWaitable, maybeClosureWaitable) { + if (flags.waitingOnClosureWritableAlready) { + return closureWaitable.whenResolved(js); + } + flags.waitingOnClosureWritableAlready = true; + auto promise = closureWaitable.then( + js, [markAsHandled, weak = weakSelf.addRef()](jsg::Lock& js) -> jsg::Promise { + KJ_IF_SOME(self, weak->tryGet()) { + return self.close(js, markAsHandled); + } + return js.resolvedPromise(); + }, [](jsg::Lock& js, jsg::Value) { + // Ignore rejection — reported in Socket's closed/opened promises instead. + return js.resolvedPromise(); + }); + maybeClosureWaitable = promise.whenResolved(js); + return kj::mv(promise); + } + KJ_SWITCH_ONEOF(state) { KJ_CASE_ONEOF(initial, Initial) { return rejectedMaybeHandledPromise( - js, js.v8TypeError("This WritableStream has been closed."_kj), markAsHandled); + js, js.typeError("This WritableStream has been closed."_kj), markAsHandled); } KJ_CASE_ONEOF(closed, StreamStates::Closed) { + // Internal-backed streams silently succeed on double-close (matching internal controller). + if (flags.internalBacked) return js.resolvedPromise(); return rejectedMaybeHandledPromise( - js, js.v8TypeError("This WritableStream has been closed."_kj), markAsHandled); + js, js.typeError("This WritableStream has been closed."_kj), markAsHandled); } KJ_CASE_ONEOF(errored, StreamStates::Errored) { if (FeatureFlags::get(js).getPedanticWpt()) { return rejectedMaybeHandledPromise( - js, js.v8TypeError("This WritableStream has been errored."_kj), markAsHandled); + js, js.typeError("This WritableStream has been errored."_kj), markAsHandled); } return rejectedMaybeHandledPromise(js, errored.getHandle(js), markAsHandled); } @@ -3939,6 +4716,26 @@ jsg::Promise WritableStreamJsController::close(jsg::Lock& js, bool markAsH KJ_UNREACHABLE; } +jsg::Promise WritableStreamJsController::flush(jsg::Lock& js, MarkAsHandled markAsHandled) { + KJ_SWITCH_ONEOF(state) { + KJ_CASE_ONEOF(initial, Initial) { + return rejectedMaybeHandledPromise( + js, js.typeError("This WritableStream has been closed."_kj), markAsHandled); + } + KJ_CASE_ONEOF(closed, StreamStates::Closed) { + return rejectedMaybeHandledPromise( + js, js.typeError("This WritableStream has been closed."_kj), markAsHandled); + } + KJ_CASE_ONEOF(errored, StreamStates::Errored) { + return rejectedMaybeHandledPromise(js, errored.getHandle(js), markAsHandled); + } + KJ_CASE_ONEOF(controller, Controller) { + return controller->flush(js, markAsHandled); + } + } + KJ_UNREACHABLE; +} + void WritableStreamJsController::doClose(jsg::Lock& js) { // If already in a terminal state, nothing to do. if (state.isTerminal()) return; @@ -3957,7 +4754,7 @@ void WritableStreamJsController::doClose(jsg::Lock& js) { } } -void WritableStreamJsController::doError(jsg::Lock& js, v8::Local reason) { +void WritableStreamJsController::doError(jsg::Lock& js, jsg::JsValue reason) { // If already in a terminal state, nothing to do. if (state.isTerminal()) return; @@ -3966,7 +4763,7 @@ void WritableStreamJsController::doError(jsg::Lock& js, v8::Local rea controller->clearAlgorithms(); } - state.transitionTo(js.v8Ref(reason)); + state.transitionTo(reason.addRef(js)); KJ_IF_SOME(locked, lock.state.tryGetUnsafe()) { maybeRejectPromise(js, locked.getClosedFulfiller(), reason); maybeResolvePromise(js, locked.getReadyFulfiller()); @@ -3983,7 +4780,7 @@ void WritableStreamJsController::doError(jsg::Lock& js, v8::Local rea } } -void WritableStreamJsController::errorIfNeeded(jsg::Lock& js, v8::Local reason) { +void WritableStreamJsController::errorIfNeeded(jsg::Lock& js, jsg::JsValue reason) { // Error through the underlying controller if available, which goes through the proper // error transition (Erroring -> Errored). This allows close() to be called while the // stream is "erroring" and reject with the stored error. @@ -4005,13 +4802,13 @@ kj::Maybe WritableStreamJsController::getDesiredSize() { return kj::none; } KJ_CASE_ONEOF(controller, Controller) { - return controller->getDesiredSize().map([](ssize_t size) -> int { return size; }); + return controller->getDesiredSize(); } } KJ_UNREACHABLE; } -kj::Maybe> WritableStreamJsController::isErroring(jsg::Lock& js) { +kj::Maybe WritableStreamJsController::isErroring(jsg::Lock& js) { KJ_IF_SOME(controller, state.tryGetUnsafe()) { return controller->isErroring(js); } @@ -4022,7 +4819,7 @@ bool WritableStreamDefaultController::isErroring() const { return impl.state.is(); } -kj::Maybe> WritableStreamJsController::isErroredOrErroring(jsg::Lock& js) { +kj::Maybe WritableStreamJsController::isErroredOrErroring(jsg::Lock& js) { KJ_IF_SOME(err, state.tryGetErrorUnsafe()) { return err.getHandle(js); } @@ -4066,8 +4863,7 @@ bool WritableStreamJsController::lockWriter(jsg::Lock& js, Writer& writer) { return lock.lockWriter(js, *this, writer); } -void WritableStreamJsController::maybeRejectReadyPromise( - jsg::Lock& js, v8::Local reason) { +void WritableStreamJsController::maybeRejectReadyPromise(jsg::Lock& js, jsg::JsValue reason) { KJ_IF_SOME(writerLock, lock.state.tryGetUnsafe()) { if (writerLock.getReadyFulfiller() != kj::none) { maybeRejectPromise(js, writerLock.getReadyFulfiller(), reason); @@ -4091,37 +4887,46 @@ void WritableStreamJsController::releaseWriter(Writer& writer, kj::Maybe> WritableStreamJsController::removeSink(jsg::Lock& js) { + JSG_REQUIRE( + !lock.isLockedToWriter(), TypeError, "This WritableStream is currently locked to a writer."); + KJ_IF_SOME(controller, state.tryGetUnsafe()) { + KJ_IF_SOME(sink, controller->tryReleaseSink()) { + lock.state.transitionTo(); + state.transitionTo(); + return kj::mv(sink); + } + } return kj::none; } void WritableStreamJsController::detach(jsg::Lock& js) { - KJ_UNIMPLEMENTED("WritableStreamJsController::detach is not implemented"); + JSG_REQUIRE( + !lock.isLockedToWriter(), TypeError, "This WritableStream is currently locked to a writer."); + KJ_IF_SOME(controller, state.tryGetUnsafe()) { + // Try to release the underlying sink. If it can't be released (e.g., in-flight + // writes), the sink stays with the controller but the stream is still detached. + // This differs from the readable side which asserts no pending operations — + // the writable side is more lenient because detach is used in serialization + // contexts where the stream may still be draining. + auto maybeSink KJ_UNUSED = controller->tryReleaseSink(); + } + lock.state.transitionTo(); + state.transitionTo(); } void WritableStreamJsController::setOwnerRef(WritableStream& stream) { owner = stream; } -void WritableStreamJsController::setup(jsg::Lock& js, - jsg::Optional maybeUnderlyingSink, - jsg::Optional maybeQueuingStrategy) { - auto underlyingSink = kj::mv(maybeUnderlyingSink).orDefault({}); - auto queuingStrategy = kj::mv(maybeQueuingStrategy).orDefault({}); - - if (FeatureFlags::get(js).getPedanticWpt()) { - // Per the spec, the type property for WritableStream's underlying sink must be undefined. - // If it's anything else, throw a RangeError. - JSG_REQUIRE(underlyingSink.type == kj::none, RangeError, - "Invalid underlying sink type. Only undefined is valid."); - } - +void WritableStreamJsController::setup(jsg::Lock& js, kj::Own sink) { + flags.internalBacked = sink->isInternal(); // We account for the memory usage of the WritableStreamDefaultController and AbortSignal together // because their lifetimes are identical and memory accounting itself has a memory overhead. auto controller = js.allocAccounted( sizeof(WritableStreamDefaultController) + sizeof(AbortSignal), js, KJ_ASSERT_NONNULL(owner), - js.alloc()); + kj::mv(sink), js.alloc()); auto& controllerRef = *controller; state.transitionTo(kj::mv(controller)); - controllerRef.setup(js, kj::mv(underlyingSink), kj::mv(queuingStrategy)); + controllerRef.setup(js); } kj::Maybe> WritableStreamJsController::tryPipeFrom( @@ -4137,12 +4942,159 @@ kj::Maybe> WritableStreamJsController::tryPipeFrom( // This method will return a JavaScript promise that is resolved when the pipe operation // completes, or is rejected if the pipe operation is aborted or errored. + auto preventAbort = options.preventAbort.orDefault(false); + auto preventClose = options.preventClose.orDefault(false); + auto preventCancel = options.preventCancel.orDefault(false); + auto pipeThrough = options.pipeThrough; + + // If both source and destination are backed by internal kj streams, use the + // kj-level pump directly, bypassing the JS pipe loop. The source is extracted + // (consumed) but the sink is accessed by reference — the controller retains + // ownership so the writable remains usable if preventClose is true. + KJ_IF_SOME(controller, state.tryGetUnsafe()) { + if (controller->isInternal() && source->getController().isInternal()) { + auto& sinkRef = KJ_ASSERT_NONNULL(controller->tryGetSink()); + auto kjSource = KJ_ASSERT_NONNULL(source->getController().tryReleaseSource()); + lock.pipeLock(KJ_ASSERT_NONNULL(owner), source.addRef(), options); + auto end = preventClose ? End::NO : End::YES; + auto& ioCtx = IoContext::current(); + struct Holder: public kj::Refcounted { + kj::Own source; + bool done = false; + explicit Holder(kj::Own source): source(kj::mv(source)) {} + ~Holder() noexcept(false) { + if (!done) { + source->cancel(KJ_EXCEPTION(DISCONNECTED, "pipe canceled")); + } + } + }; + auto holder = kj::rc(kj::mv(kjSource)); + auto promise = holder->source->pumpTo(sinkRef, end) + .then([holder = holder.addRef()](DeferredProxy proxy) mutable { + holder->done = true; + return kj::mv(proxy.proxyTask); + }, [holder = holder.addRef()](kj::Exception&& ex) mutable -> kj::Promise { + holder->source->cancel(ex.clone()); + holder->done = true; + kj::throwFatalException(kj::mv(ex)); + }); + KJ_IF_SOME(signal, options.signal) { + promise = signal->wrap(js, kj::mv(promise)); + } + auto result = ioCtx.awaitIo(js, kj::mv(promise)) + .then(js, + [weak = weakSelf.addRef(), preventClose](jsg::Lock& js) { + weak->runIfAlive([&](auto& self) { + KJ_IF_SOME(l, self.lock.tryGetPipe()) { + l.source.release(js); + } + self.lock.releasePipeLock(); + if (!preventClose) { + self.doClose(js); + } + }); + return js.resolvedPromise(); + }, + [weak = weakSelf.addRef(), preventAbort, preventCancel, pipeThrough]( + jsg::Lock& js, jsg::Value reason) -> jsg::Promise { + auto handle = jsg::JsValue(reason.getHandle(js)); + weak->runIfAlive([&](auto& self) { + KJ_IF_SOME(l, self.lock.tryGetPipe()) { + if (!preventCancel) { + l.source.release(js, handle); + } else { + l.source.release(js); + } + if (!preventAbort) { + self.doError(js, handle); + } + } + self.lock.releasePipeLock(); + }); + return rejectedMaybeHandledPromise( + js, handle, pipeThrough ? MarkAsHandled::YES : MarkAsHandled::NO); + }); + if (pipeThrough) { + result.markAsHandled(js); + } + return kj::mv(result); + } + } + // Let's also acquire the destination pipe lock. lock.pipeLock(KJ_ASSERT_NONNULL(owner), kj::mv(source), options); return pipeLoop(js).then(js, JSG_VISITABLE_LAMBDA((ref = addRef()), (ref), (auto& js){})); } +WritableStreamJsController::PipeReadContinuationType& WritableStreamJsController:: + getPipeReadContinuation(jsg::Lock& js) { + KJ_IF_SOME(prc, pipeReadContinuation) { + return prc; + } + pipeReadContinuation.emplace( + PipeReadContinuationType::create(js, PipeReadContinuationCallbacks{weakSelf.addRef()})); + return KJ_ASSERT_NONNULL(pipeReadContinuation); +} + +WritableStreamJsController::PipeWriteContinuationType& WritableStreamJsController:: + getPipeWriteContinuation(jsg::Lock& js) { + KJ_IF_SOME(pwc, pipeWriteContinuation) { + return pwc; + } + pipeWriteContinuation.emplace( + PipeWriteContinuationType::create(js, PipeWriteContinuationCallbacks{weakSelf.addRef()})); + return KJ_ASSERT_NONNULL(pipeWriteContinuation); +} + +jsg::Promise WritableStreamJsController::onPipeReadSuccess(jsg::Lock& js, ReadResult result) { + auto maybePipeLock = lock.tryGetPipe(); + if (maybePipeLock == kj::none) return js.resolvedPromise(); + auto& pipeLock = KJ_REQUIRE_NONNULL(maybePipeLock); + + KJ_IF_SOME(promise, pipeLock.checkSignal(js, *this)) { + lock.releasePipeLock(); + return kj::mv(promise); + } else { + } // Trailing else() to squash compiler warning + + if (result.done) { + // We'll handle the close at the start of the next iteration. + return pipeLoop(js); + } + + auto promise = write( + js, result.value.map([&](jsg::JsRef& value) { return value.getHandle(js); })); + + return promise.thenRef(js, getPipeWriteContinuation(js)); +} + +jsg::Promise WritableStreamJsController::onPipeReadFailure(jsg::Lock& js, jsg::Value reason) { + // The read failed. We will handle the error at the start of the next iteration. + return pipeLoop(js); +} + +jsg::Promise WritableStreamJsController::onPipeWriteSuccess(jsg::Lock& js) { + return pipeLoop(js); +} + +jsg::Promise WritableStreamJsController::onPipeWriteFailure( + jsg::Lock& js, jsg::Value reason) { + auto jsReason = jsg::JsValue(reason.getHandle(js)); + bool pipeThrough = false; + KJ_IF_SOME(pipeLock, lock.tryGetPipe()) { + pipeThrough = pipeLock.flags.pipeThrough; + if (!pipeLock.flags.preventCancel) { + pipeLock.source.release(js, jsReason); + } else { + pipeLock.source.release(js); + } + } else { + } // Trailing else() to squash compiler warning + return rejectedMaybeHandledPromise( + js, jsReason, pipeThrough ? MarkAsHandled::YES : MarkAsHandled::NO); +} + jsg::Promise WritableStreamJsController::pipeLoop(jsg::Lock& js) { auto maybePipeLock = lock.tryGetPipe(); if (maybePipeLock == kj::none) return js.resolvedPromise(); @@ -4165,8 +5117,9 @@ jsg::Promise WritableStreamJsController::pipeLoop(jsg::Lock& js) { lock.releasePipeLock(); if (!preventAbort) { auto onSuccess = JSG_VISITABLE_LAMBDA( - (pipeThrough, reason = js.v8Ref(errored)), (reason), (jsg::Lock& js) { - return rejectedMaybeHandledPromise(js, reason.getHandle(js), pipeThrough); + (pipeThrough, reason = errored.addRef(js)), (reason), (jsg::Lock& js) { + return rejectedMaybeHandledPromise( + js, reason.getHandle(js), pipeThrough ? MarkAsHandled::YES : MarkAsHandled::NO); }); auto promise = abort(js, errored); KJ_IF_SOME(ioContext, IoContext::tryCurrent()) { @@ -4175,7 +5128,8 @@ jsg::Promise WritableStreamJsController::pipeLoop(jsg::Lock& js) { return promise.then(js, kj::mv(onSuccess)); } } - return rejectedMaybeHandledPromise(js, errored, pipeThrough); + return rejectedMaybeHandledPromise( + js, errored, pipeThrough ? MarkAsHandled::YES : MarkAsHandled::NO); } KJ_IF_SOME(errored, state.tryGetUnsafe()) { @@ -4186,7 +5140,8 @@ jsg::Promise WritableStreamJsController::pipeLoop(jsg::Lock& js) { } else { source.release(js); } - return rejectedMaybeHandledPromise(js, reason, pipeThrough); + return rejectedMaybeHandledPromise( + js, reason, pipeThrough ? MarkAsHandled::YES : MarkAsHandled::NO); } KJ_IF_SOME(erroring, isErroring(js)) { @@ -4196,7 +5151,8 @@ jsg::Promise WritableStreamJsController::pipeLoop(jsg::Lock& js) { } else { source.release(js); } - return rejectedMaybeHandledPromise(js, erroring, pipeThrough); + return rejectedMaybeHandledPromise( + js, erroring, pipeThrough ? MarkAsHandled::YES : MarkAsHandled::NO); } if (source.isClosed()) { @@ -4214,14 +5170,15 @@ jsg::Promise WritableStreamJsController::pipeLoop(jsg::Lock& js) { if (state.is()) { lock.releasePipeLock(); - auto reason = js.v8TypeError("This destination writable stream is closed."_kj); + auto reason = js.typeError("This destination writable stream is closed."_kj); if (!preventCancel) { source.release(js, reason); } else { source.release(js); } - return rejectedMaybeHandledPromise(js, reason, pipeThrough); + return rejectedMaybeHandledPromise( + js, reason, pipeThrough ? MarkAsHandled::YES : MarkAsHandled::NO); } // Assuming we get by that, we perform a read on the source. If the read errors, @@ -4234,59 +5191,11 @@ jsg::Promise WritableStreamJsController::pipeLoop(jsg::Lock& js) { // source (again, depending on options). If the write operation is successful, // we call pipeLoop again to move on to the next iteration. - auto onSuccess = JSG_VISITABLE_LAMBDA((this, ref = addRef(), preventCancel, pipeThrough), (ref), - (jsg::Lock & js, ReadResult result)->jsg::Promise { - auto maybePipeLock = lock.tryGetPipe(); - if (maybePipeLock == kj::none) return js.resolvedPromise(); - auto& pipeLock = KJ_REQUIRE_NONNULL(maybePipeLock); - - KJ_IF_SOME(promise, pipeLock.checkSignal(js, *this)) { - lock.releasePipeLock(); - return kj::mv(promise); - } else { - } // Trailing else() is squash compiler warning + return pipeLock.source.read(js).thenRef(js, getPipeReadContinuation(js)); +} - if (result.done) { - // We'll handle the close at the start of the next iteration. - return pipeLoop(js); - } - - auto onSuccess = JSG_VISITABLE_LAMBDA( - (this, ref=addRef()), (ref) , (jsg::Lock& js) { - return pipeLoop(js); - } ); - - auto onFailure = JSG_VISITABLE_LAMBDA( - (this, ref=addRef(), preventCancel, pipeThrough), - (ref) , (jsg::Lock& js, jsg::Value value) { - // The write failed. We need to release the source if the pipe lock still exists. - auto reason = value.getHandle(js); - KJ_IF_SOME(pipeLock, lock.tryGetPipe()) { - if (!preventCancel) { - pipeLock.source.release(js, reason); - } else { - pipeLock.source.release(js); - } - } else {} // Trailing else() to squash compiler warning - return rejectedMaybeHandledPromise(js, reason, pipeThrough); - } ); - - auto promise = - write(js, result.value.map([&](jsg::Value& value) { return value.getHandle(js); })); - - return maybeAddFunctor(js, kj::mv(promise), kj::mv(onSuccess), kj::mv(onFailure)); - }); - - auto onFailure = - JSG_VISITABLE_LAMBDA((this, ref = addRef()), (ref), (jsg::Lock& js, jsg::Value value) { - // The read failed. We will handle the error at the start of the next iteration. - return pipeLoop(js); - }); - - return maybeAddFunctor(js, pipeLock.source.read(js), kj::mv(onSuccess), kj::mv(onFailure)); -} - -void WritableStreamJsController::updateBackpressure(jsg::Lock& js, bool backpressure) { +void WritableStreamJsController::updateBackpressure( + jsg::Lock& js, UpdateBackpressure backpressure) { KJ_IF_SOME(writerLock, lock.state.tryGetUnsafe()) { if (backpressure) { // Per the spec, when backpressure is updated and is true, we replace the existing @@ -4303,13 +5212,17 @@ void WritableStreamJsController::updateBackpressure(jsg::Lock& js, bool backpres } jsg::Promise WritableStreamJsController::write( - jsg::Lock& js, jsg::Optional> value) { + jsg::Lock& js, jsg::Optional value) { + if (flags.pendingClosure) { + return js.rejectedPromise( + js.typeError("This WritableStream belongs to an object that is closing."_kj)); + } KJ_SWITCH_ONEOF(state) { KJ_CASE_ONEOF(initial, Initial) { - return js.rejectedPromise(js.v8TypeError("This WritableStream has been closed."_kj)); + return js.rejectedPromise(js.typeError("This WritableStream has been closed."_kj)); } KJ_CASE_ONEOF(closed, StreamStates::Closed) { - return js.rejectedPromise(js.v8TypeError("This WritableStream has been closed."_kj)); + return js.rejectedPromise(js.typeError("This WritableStream has been closed."_kj)); } KJ_CASE_ONEOF(errored, StreamStates::Errored) { return js.rejectedPromise(errored.addRef(js)); @@ -4324,77 +5237,117 @@ jsg::Promise WritableStreamJsController::write( void WritableStreamJsController::visitForGc(jsg::GcVisitor& visitor) { state.visitForGc(visitor); visitor.visit(maybeAbortPromise, lock); + KJ_IF_SOME(prc, pipeReadContinuation) { + prc.visitForGc(visitor); + } + KJ_IF_SOME(pwc, pipeWriteContinuation) { + pwc.visitForGc(visitor); + } } // ======================================================================================= -TransformStreamDefaultController::TransformStreamDefaultController(jsg::Lock& js) - : ioContext(tryGetIoContext()), - startPromise(js.newPromiseAndResolver()) {} +TransformStreamDefaultController::TransformStreamDefaultController( + jsg::Lock& js, kj::Own transformer) + : weakSelf(kj::rc>( + kj::Badge{}, *this)), + ioContext(tryGetIoContext()), + startPromise(js.newPromiseAndResolver()), + transformer(kj::mv(transformer)) { + flags.fixupBackpressure = FeatureFlags::get(js).getFixupTransformStreamBackpressure(); +} + +TransformStreamDefaultController::~TransformStreamDefaultController() noexcept(false) { + weakSelf->invalidate(); + // Clear the persistent continuation to break the reference cycle: + // controller → transformContinuation → OpaqueWrappable → Ref + transformContinuation = kj::none; +} kj::Maybe TransformStreamDefaultController::getDesiredSize() { - KJ_IF_SOME(readableController, tryGetReadableController()) { - return readableController.getDesiredSize(); + KJ_IF_SOME(result, withReadableController([](auto& ctrl) { return ctrl.getDesiredSize(); })) { + return result; } return kj::none; } -void TransformStreamDefaultController::enqueue(jsg::Lock& js, v8::Local chunk) { - auto& readableController = JSG_REQUIRE_NONNULL(tryGetReadableController(), TypeError, - "The readable side of this TransformStream is no longer readable."); - // Hold a strong reference to the readable controller for the duration of this - // method. The readableController.enqueue() call below invokes the user-provided - // size algorithm, which can re-enter JS and call error() on this transform - // controller, dropping the jsg::Ref held by this->readable and the one held by - // the ReadableStreamJsController's ValueReadable. Without this ref the - // ReadableStreamDefaultController would be freed while its enqueue() method is - // still on the stack. - auto readableControllerRef = kj::addRef(readableController); - - JSG_REQUIRE(readableController.canCloseOrEnqueue(), TypeError, +void TransformStreamDefaultController::enqueue(jsg::Lock& js, jsg::JsValue chunk) { + JSG_REQUIRE(readable != kj::none, TypeError, "The readable side of this TransformStream is no longer readable."); - js.tryCatch([&] { readableController.enqueue(js, chunk); }, [&](jsg::Value exception) { - errorWritableAndUnblockWrite(js, exception.getHandle(js)); - js.throwException(kj::mv(exception)); - }); - - // If the controller was errored during the enqueue (e.g. by the size callback - // calling error()), skip the backpressure update — the stream is already torn down. - if (!readableController.canCloseOrEnqueue()) { - return; - } - bool newBackpressure = readableController.hasBackpressure(); - if (newBackpressure != backpressure) { - KJ_ASSERT(newBackpressure); - // Unfortunately the original implementation forgot to actually set the backpressure - // here so the backpressure signaling failed to work correctly. This is unfortunate - // because applying the backpressure here could break existing code, so we need to - // put the fix behind a compat flag. Doh! - if (FeatureFlags::get(js).getFixupTransformStreamBackpressure()) { - setBackpressure(js, true); + // Dispatch enqueue + backpressure update through the controller variant. + // Both ReadableStreamDefaultController and ReadableByteStreamController share + // canCloseOrEnqueue/hasBackpressure/error, but enqueue signatures differ: + // DefaultController::enqueue(js, Optional) + // ByteController::enqueue(js, JsBufferSource) + auto& rc = KJ_ASSERT_NONNULL(readable); + KJ_SWITCH_ONEOF(rc) { + KJ_CASE_ONEOF(defaultCtrl, jsg::Ref) { + auto ref = defaultCtrl.addRef(); + JSG_REQUIRE(ref->canCloseOrEnqueue(), TypeError, + "The readable side of this TransformStream is no longer readable."); + JSG_TRY(js) { + ref->enqueue(js, chunk); + } + JSG_CATCH(exception) { + auto handle = jsg::JsValue(exception.getHandle(js)); + errorWritableAndUnblockWrite(js, handle); + js.throwException(handle); + }; + if (!ref->canCloseOrEnqueue()) return; + bool newBackpressure = ref->hasBackpressure(); + if (newBackpressure != flags.backpressure) { + KJ_ASSERT(newBackpressure); + if (flags.fixupBackpressure) { + setBackpressure(js, UpdateBackpressure::YES); + } + } + } + KJ_CASE_ONEOF(byteCtrl, jsg::Ref) { + auto ref = byteCtrl.addRef(); + JSG_REQUIRE(ref->canCloseOrEnqueue(), TypeError, + "The readable side of this TransformStream is no longer readable."); + JSG_REQUIRE(chunk.isArrayBuffer() || chunk.isSharedArrayBuffer() || chunk.isArrayBufferView(), + TypeError, "This chunk type is not supported by a byte stream controller."_kj); + JSG_TRY(js) { + ref->enqueue(js, jsg::JsBufferSource(chunk)); + } + JSG_CATCH(exception) { + auto handle = jsg::JsValue(exception.getHandle(js)); + errorWritableAndUnblockWrite(js, handle); + js.throwException(handle); + }; + if (!ref->canCloseOrEnqueue()) return; + bool newBackpressure = ref->hasBackpressure(); + if (newBackpressure != flags.backpressure) { + KJ_ASSERT(newBackpressure); + if (flags.fixupBackpressure) { + setBackpressure(js, UpdateBackpressure::YES); + } + } } } } -void TransformStreamDefaultController::error(jsg::Lock& js, v8::Local reason) { - KJ_IF_SOME(readableController, tryGetReadableController()) { - readableController.error(js, reason); - readable = kj::none; - } +void TransformStreamDefaultController::error(jsg::Lock& js, jsg::JsValue reason) { + withReadableController([&](auto& ctrl) -> bool { + ctrl.error(js, reason); + return true; + }); + // Do NOT clear `readable` here — dropping the Ref can destroy `this`. errorWritableAndUnblockWrite(js, reason); } void TransformStreamDefaultController::terminate(jsg::Lock& js) { - KJ_IF_SOME(readableController, tryGetReadableController()) { - readableController.close(js); - readable = kj::none; - } - errorWritableAndUnblockWrite(js, js.v8TypeError("The transform stream has been terminated"_kj)); + withReadableController([&](auto& ctrl) -> bool { + ctrl.close(js); + return true; + }); + // Do NOT clear `readable` here — dropping the Ref can destroy `this`. + errorWritableAndUnblockWrite(js, js.typeError("The transform stream has been terminated"_kj)); } -jsg::Promise TransformStreamDefaultController::write( - jsg::Lock& js, v8::Local chunk) { +jsg::Promise TransformStreamDefaultController::write(jsg::Lock& js, jsg::JsValue chunk) { KJ_IF_SOME(writableController, tryGetWritableController()) { KJ_IF_SOME(error, writableController.isErroredOrErroring(js)) { return js.rejectedPromise(error); @@ -4402,10 +5355,9 @@ jsg::Promise TransformStreamDefaultController::write( KJ_ASSERT(writableController.isWritable()); - if (backpressure) { - auto chunkRef = js.v8Ref(chunk); + if (flags.backpressure) { return KJ_ASSERT_NONNULL(maybeBackpressureChange).promise.whenResolved(js).then(js, - JSG_VISITABLE_LAMBDA((chunkRef = kj::mv(chunkRef), ref=JSG_THIS), + JSG_VISITABLE_LAMBDA((chunkRef = chunk.addRef(js), ref=JSG_THIS), (chunkRef, ref), (jsg::Lock& js) mutable -> jsg::Promise { KJ_IF_SOME(writableController, ref->tryGetWritableController()) { KJ_IF_SOME(error, writableController.isErroring(js)) { @@ -4421,41 +5373,69 @@ jsg::Promise TransformStreamDefaultController::write( } return performTransform(js, chunk); } else { - return js.rejectedPromise( - KJ_EXCEPTION(FAILED, "jsg.TypeError: Writing to the TransformStream failed.")); + return js.rejectedPromise(js.typeError("Writing to the TransformStream failed.")); + } +} + +jsg::Promise TransformStreamDefaultController::writev( + jsg::Lock& js, kj::Array> chunks) { + KJ_IF_SOME(writableController, tryGetWritableController()) { + KJ_IF_SOME(error, writableController.isErroredOrErroring(js)) { + return js.rejectedPromise(error); + } + + KJ_ASSERT(writableController.isWritable()); + + if (flags.backpressure) { + return KJ_ASSERT_NONNULL(maybeBackpressureChange).promise.whenResolved(js).then(js, + JSG_VISITABLE_LAMBDA((chunks = kj::mv(chunks), ref=JSG_THIS), + (chunks, ref), (jsg::Lock& js) mutable -> jsg::Promise { + KJ_IF_SOME(writableController, ref->tryGetWritableController()) { + KJ_IF_SOME(error, writableController.isErroring(js)) { + return js.rejectedPromise(error); + } else { + // Else block to avert dangling else compiler warning. + } + } else { + // Else block to avert dangling else compiler warning. + } + return ref->performTransformv(js, kj::mv(chunks)); + })); + } + return performTransformv(js, kj::mv(chunks)); + } else { + return js.rejectedPromise(js.typeError("Writing to the TransformStream failed.")); } } -jsg::Promise TransformStreamDefaultController::abort( - jsg::Lock& js, v8::Local reason) { +jsg::Promise TransformStreamDefaultController::abort(jsg::Lock& js, jsg::JsValue reason) { if (FeatureFlags::get(js).getPedanticWpt()) { // If a finish operation is already in progress, return the existing promise // or handle the case where we're being called synchronously from within another // finish operation. - if (algorithms.finishStarted) { - KJ_IF_SOME(finish, algorithms.maybeFinish) { + if (flags.finishStarted) { + KJ_IF_SOME(finish, maybeFinish) { return finish.whenResolved(js); } - // finishStarted is true but maybeFinish is not set yet - this means we're being + // flags.finishStarted is true but maybeFinish is not set yet - this means we're being // called synchronously from within another finish operation (like cancel). // We need to error the stream with the abort reason so that both the current // operation and this abort reject with the abort reason. error(js, reason); - return js.rejectedPromise(js.v8Ref(reason)); + return js.rejectedPromise(reason); } // Mark that we're starting a finish operation before running the algorithm. - algorithms.finishStarted = true; + flags.finishStarted = true; } else { - KJ_IF_SOME(finish, algorithms.maybeFinish) { + KJ_IF_SOME(finish, maybeFinish) { return finish.whenResolved(js); } } - return algorithms.maybeFinish - .emplace(maybeRunAlgorithm(js, algorithms.cancel, - JSG_VISITABLE_LAMBDA( - (this, ref = JSG_THIS, reason = jsg::JsRef(js, jsg::JsValue(reason))), (ref, reason), + return maybeFinish + .emplace(maybeRunAlgorithm(js, transformer->cancel(), + JSG_VISITABLE_LAMBDA((this, ref = JSG_THIS, reason = reason.addRef(js)), (ref, reason), (jsg::Lock & js)->jsg::Promise { // If the readable side is errored, return a rejected promise with the stored error { @@ -4471,24 +5451,25 @@ jsg::Promise TransformStreamDefaultController::abort( }), JSG_VISITABLE_LAMBDA((this, ref = JSG_THIS), (ref), (jsg::Lock & js, jsg::Value reason)->jsg::Promise { - error(js, reason.getHandle(js)); - return js.rejectedPromise(kj::mv(reason)); + auto handle = jsg::JsValue(reason.getHandle(js)); + error(js, handle); + return js.rejectedPromise(handle); }), - jsg::JsValue(reason))) + reason)) .whenResolved(js); } jsg::Promise TransformStreamDefaultController::close(jsg::Lock& js) { - auto flags = FeatureFlags::get(js); - if (flags.getPedanticWpt()) { + auto featureFlags = FeatureFlags::get(js); + if (featureFlags.getPedanticWpt()) { // If a finish operation is already in progress (e.g., from cancel or abort), // we should not run flush. Per the WHATWG streams spec, close/flush should // coordinate with cancel to avoid calling both. - if (algorithms.finishStarted) { - KJ_IF_SOME(finish, algorithms.maybeFinish) { + if (flags.finishStarted) { + KJ_IF_SOME(finish, maybeFinish) { return finish.whenResolved(js); } - // finishStarted is true but maybeFinish is not set yet - this means we're being + // flags.finishStarted is true but maybeFinish is not set yet - this means we're being // called synchronously from within another finish operation. If the stream was // errored during that operation, return a rejected promise with the error. KJ_IF_SOME(writableController, tryGetWritableController()) { @@ -4504,7 +5485,7 @@ jsg::Promise TransformStreamDefaultController::close(jsg::Lock& js) { // Mark that we're starting a finish operation before running the algorithm, // since the algorithm may synchronously call other finish operations. - algorithms.finishStarted = true; + flags.finishStarted = true; } auto onSuccess = @@ -4522,51 +5503,49 @@ jsg::Promise TransformStreamDefaultController::close(jsg::Lock& js) { // complete once all of the queued data is read or the stream // errors. Only close if the stream can still be closed (e.g., // it wasn't closed by a cancel operation from within flush). - { - KJ_IF_SOME(readableController, ref->tryGetReadableController()) { - if (readableController.canCloseOrEnqueue()) { - readableController.close(js); - } - } else { - // Else block to avert dangling else compiler warning. - } - } + ref->withReadableController([&](auto& ctrl) -> bool { + if (ctrl.canCloseOrEnqueue()) { + ctrl.close(js); + } + return true; + }); return js.resolvedPromise(); }); auto onFailure = JSG_VISITABLE_LAMBDA( (ref = JSG_THIS), (ref), (jsg::Lock & js, jsg::Value reason)->jsg::Promise { - ref->error(js, reason.getHandle(js)); - return js.rejectedPromise(kj::mv(reason)); + auto handle = jsg::JsValue(reason.getHandle(js)); + ref->error(js, handle); + return js.rejectedPromise(handle); }); - if (flags.getPedanticWpt()) { - return algorithms.maybeFinish - .emplace( - maybeRunAlgorithm(js, algorithms.flush, kj::mv(onSuccess), kj::mv(onFailure), JSG_THIS)) + if (featureFlags.getPedanticWpt()) { + return maybeFinish + .emplace(maybeRunAlgorithm( + js, transformer->flush(), kj::mv(onSuccess), kj::mv(onFailure), JSG_THIS)) .whenResolved(js); } - return maybeRunAlgorithm(js, algorithms.flush, kj::mv(onSuccess), kj::mv(onFailure), JSG_THIS); + return maybeRunAlgorithm( + js, transformer->flush(), kj::mv(onSuccess), kj::mv(onFailure), JSG_THIS); } jsg::Promise TransformStreamDefaultController::pull(jsg::Lock& js) { - KJ_ASSERT(backpressure); - setBackpressure(js, false); + KJ_ASSERT(!!flags.backpressure); + setBackpressure(js, UpdateBackpressure::NO); return KJ_ASSERT_NONNULL(maybeBackpressureChange).promise.whenResolved(js); } -jsg::Promise TransformStreamDefaultController::cancel( - jsg::Lock& js, v8::Local reason) { +jsg::Promise TransformStreamDefaultController::cancel(jsg::Lock& js, jsg::JsValue reason) { if (FeatureFlags::get(js).getPedanticWpt()) { // If a finish operation is already in progress, return the existing promise // or check for errors if we're being called synchronously from within another // finish operation. - if (algorithms.finishStarted) { - KJ_IF_SOME(finish, algorithms.maybeFinish) { + if (flags.finishStarted) { + KJ_IF_SOME(finish, maybeFinish) { return finish.whenResolved(js); } - // finishStarted is true but maybeFinish is not set yet - check if the stream + // flags.finishStarted is true but maybeFinish is not set yet - check if the stream // was errored during that operation. KJ_IF_SOME(err, getReadableErrorState(js)) { return js.rejectedPromise(kj::mv(err)); @@ -4575,13 +5554,12 @@ jsg::Promise TransformStreamDefaultController::cancel( } // Mark that we're starting a finish operation before running the algorithm. - algorithms.finishStarted = true; + flags.finishStarted = true; } - return algorithms.maybeFinish - .emplace(maybeRunAlgorithm(js, algorithms.cancel, - JSG_VISITABLE_LAMBDA( - (this, ref = JSG_THIS, reason = jsg::JsRef(js, jsg::JsValue(reason))), (ref, reason), + return maybeFinish + .emplace(maybeRunAlgorithm(js, transformer->cancel(), + JSG_VISITABLE_LAMBDA((this, ref = JSG_THIS, reason = reason.addRef(js)), (ref, reason), (jsg::Lock & js)->jsg::Promise { // If the stream was errored during the cancel algorithm (e.g., by controller.error() // or by a parallel abort()), we should reject with that error. @@ -4601,59 +5579,77 @@ jsg::Promise TransformStreamDefaultController::cancel( JSG_VISITABLE_LAMBDA((this, ref = JSG_THIS), (ref), (jsg::Lock & js, jsg::Value reason)->jsg::Promise { readable = kj::none; - errorWritableAndUnblockWrite(js, reason.getHandle(js)); - return js.rejectedPromise(kj::mv(reason)); + auto handle = jsg::JsValue(reason.getHandle(js)); + errorWritableAndUnblockWrite(js, handle); + return js.rejectedPromise(handle); }), - jsg::JsValue(reason))) + reason)) .whenResolved(js); } +TransformStreamDefaultController::TransformContinuationType& TransformStreamDefaultController:: + getTransformContinuation(jsg::Lock& js, jsg::Ref self) { + KJ_IF_SOME(tc, transformContinuation) { + return tc; + } + transformContinuation.emplace( + TransformContinuationType::create(js, TransformContinuationCallbacks{kj::mv(self)})); + return KJ_ASSERT_NONNULL(transformContinuation); +} + jsg::Promise TransformStreamDefaultController::performTransform( - jsg::Lock& js, v8::Local chunk) { - if (algorithms.transform != kj::none) { - return maybeRunAlgorithm(js, algorithms.transform, - [](jsg::Lock& js) -> jsg::Promise { return js.resolvedPromise(); }, - JSG_VISITABLE_LAMBDA((ref = JSG_THIS), (ref), - (jsg::Lock & js, jsg::Value reason)->jsg::Promise { - ref->error(js, reason.getHandle(js)); - return js.rejectedPromise(kj::mv(reason)); - }), - chunk, JSG_THIS); + jsg::Lock& js, jsg::JsValue chunk) { + if (transformer->transform() != kj::none) { + auto& tc = getTransformContinuation(js, JSG_THIS); + return maybeRunAlgorithmWithPersistentContinuation( + js, transformer->transform(), tc, chunk, JSG_THIS); } // If we got here, there is no transform algorithm. Per the spec, the default // behavior then is to just pass along the value untransformed. - return js.tryCatch([&] { + JSG_TRY(js) { enqueue(js, chunk); return js.resolvedPromise(); - }, [&](jsg::Value exception) { return js.rejectedPromise(kj::mv(exception)); }); + } + JSG_CATCH(exception) { + return js.rejectedPromise(kj::mv(exception)); + }; +} + +jsg::Promise TransformStreamDefaultController::performTransformv( + jsg::Lock& js, kj::Array> chunks) { + KJ_ASSERT(transformer->transformv() != kj::none); + auto& tc = getTransformContinuation(js, JSG_THIS); + return maybeRunAlgorithmWithPersistentContinuation( + js, transformer->transformv(), tc, kj::mv(chunks), JSG_THIS); } -void TransformStreamDefaultController::setBackpressure(jsg::Lock& js, bool newBackpressure) { - KJ_ASSERT(newBackpressure != backpressure); +void TransformStreamDefaultController::setBackpressure( + jsg::Lock& js, UpdateBackpressure newBackpressure) { + KJ_ASSERT(!!newBackpressure != !!flags.backpressure); KJ_IF_SOME(prp, maybeBackpressureChange) { prp.resolver.resolve(js); } maybeBackpressureChange = js.newPromiseAndResolver(); KJ_ASSERT_NONNULL(maybeBackpressureChange).promise.markAsHandled(js); - backpressure = newBackpressure; + flags.backpressure = !!newBackpressure; } void TransformStreamDefaultController::errorWritableAndUnblockWrite( - jsg::Lock& js, v8::Local reason) { - algorithms.clear(); + jsg::Lock& js, jsg::JsValue reason) { + transformer->clear(); KJ_IF_SOME(writableController, tryGetWritableController()) { - if (FeatureFlags::get(js).getPedanticWpt()) { - // Use errorIfNeeded which goes through the proper error transition (Erroring -> Errored). - // This allows close() to be called while the stream is "erroring" and reject with the - // stored error, which is the expected behavior per the WHATWG streams spec. - writableController.errorIfNeeded(js, reason); - } else if (writableController.isWritable()) { - writableController.doError(js, reason); - } - writable = kj::none; + // Always use errorIfNeeded which goes through the proper error transition + // (Writable -> Erroring -> Errored). This respects in-flight write/close + // operations by deferring finishErroring until the operation completes, + // preventing destruction of the controller while promise reactions are + // still pending. + writableController.errorIfNeeded(js, reason); + // NOTE: Do NOT clear `writable` here. Dropping the Ref can trigger a + // destruction chain that destroys `this` (the TransformStreamDefaultController) + // while we are still inside a member function. Let GC handle cleanup. } - if (backpressure) { - setBackpressure(js, false); + if (flags.backpressure) { + setBackpressure(js, UpdateBackpressure::NO); } } @@ -4661,13 +5657,28 @@ void TransformStreamDefaultController::visitForGc(jsg::GcVisitor& visitor) { KJ_IF_SOME(backpressureChange, maybeBackpressureChange) { visitor.visit(backpressureChange.promise, backpressureChange.resolver); } - visitor.visit(writable, readable, startPromise.resolver, startPromise.promise, algorithms); + visitor.visit(writable, startPromise.resolver, startPromise.promise, maybeFinish); + KJ_IF_SOME(rc, readable) { + KJ_SWITCH_ONEOF(rc) { + KJ_CASE_ONEOF(defaultCtrl, jsg::Ref) { + visitor.visit(defaultCtrl); + } + KJ_CASE_ONEOF(byteCtrl, jsg::Ref) { + visitor.visit(byteCtrl); + } + } + } + + if (transformer.get() != nullptr) { + visitor.visit(*transformer); + } + KJ_IF_SOME(tc, transformContinuation) { + tc.visitForGc(visitor); + } } -void TransformStreamDefaultController::init(jsg::Lock& js, - jsg::Ref& readable, - jsg::Ref& writable, - jsg::Optional maybeTransformer) { +void TransformStreamDefaultController::init( + jsg::Lock& js, jsg::Ref& readable, jsg::Ref& writable) { KJ_ASSERT(this->readable == kj::none); KJ_ASSERT(this->writable == kj::none); @@ -4679,33 +5690,18 @@ void TransformStreamDefaultController::init(jsg::Lock& js, // to push data into it. auto& readableController = static_cast(readable->getController()); auto readableRef = KJ_ASSERT_NONNULL(readableController.getController()); - this->readable = KJ_ASSERT_NONNULL(readableRef.tryGet()).addRef(); - - auto transformer = kj::mv(maybeTransformer).orDefault({}); - - // TODO(someday): The stream standard includes placeholders for supporting byte-oriented - // TransformStreams but does not yet define them. For now, we are limiting our implementation - // here to only support value-based transforms. - JSG_REQUIRE(transformer.readableType == kj::none, TypeError, - "transformer.readableType must be undefined."); - JSG_REQUIRE(transformer.writableType == kj::none, TypeError, - "transformer.writableType must be undefined."); - - KJ_IF_SOME(transform, transformer.transform) { - algorithms.transform = kj::mv(transform); - } - - KJ_IF_SOME(flush, transformer.flush) { - algorithms.flush = kj::mv(flush); - } - - KJ_IF_SOME(cancel, transformer.cancel) { - algorithms.cancel = kj::mv(cancel); + KJ_SWITCH_ONEOF(readableRef) { + KJ_CASE_ONEOF(defaultCtrl, DefaultController) { + this->readable = defaultCtrl.addRef(); + } + KJ_CASE_ONEOF(byteCtrl, ByobController) { + this->readable = byteCtrl.addRef(); + } } - setBackpressure(js, true); + setBackpressure(js, UpdateBackpressure::YES); - maybeRunAlgorithm(js, transformer.start, + maybeRunAlgorithm(js, transformer->start(), JSG_VISITABLE_LAMBDA( (ref = JSG_THIS), (ref), (jsg::Lock& js) { ref->startPromise.resolver.resolve(js); }), JSG_VISITABLE_LAMBDA((ref = JSG_THIS), (ref), @@ -4715,14 +5711,6 @@ void TransformStreamDefaultController::init(jsg::Lock& js, JSG_THIS); } -kj::Maybe TransformStreamDefaultController:: - tryGetReadableController() { - KJ_IF_SOME(controller, readable) { - return *controller; - } - return kj::none; -} - kj::Maybe TransformStreamDefaultController:: tryGetWritableController() { KJ_IF_SOME(w, writable) { @@ -4731,9 +5719,14 @@ kj::Maybe TransformStreamDefaultController:: return kj::none; } -kj::Maybe TransformStreamDefaultController::getReadableErrorState(jsg::Lock& js) { - KJ_IF_SOME(controller, tryGetReadableController()) { - return controller.getMaybeErrorState(js); +kj::Maybe TransformStreamDefaultController::getReadableErrorState(jsg::Lock& js) { + KJ_IF_SOME(result, withReadableController([&](auto& ctrl) -> kj::Maybe { + KJ_IF_SOME(err, ctrl.getMaybeErrorState(js)) { + return err.getHandle(js); + } + return kj::none; + })) { + return result; } return kj::none; } @@ -4763,10 +5756,7 @@ void WritableImpl::jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const { KJ_CASE_ONEOF(writable, Writable) {} } - tracker.trackField("abortAlgorithm", algorithms.abort); - tracker.trackField("closeAlgorithm", algorithms.close); - tracker.trackField("writeAlgorithm", algorithms.write); - tracker.trackField("sizeAlgorithm", algorithms.size); + tracker.trackField("sink", underlyingSink); for (auto& request: writeRequests) { tracker.trackField("pendingWrite", request); @@ -4858,10 +5848,7 @@ void ReadableImpl::jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const { } } - tracker.trackField("startAlgorithm", algorithms.start); - tracker.trackField("pullAlgorithm", algorithms.pull); - tracker.trackField("cancelAlgorithm", algorithms.cancel); - tracker.trackField("sizeAlgorithm", algorithms.size); + tracker.trackField("source", underlyingSource); tracker.trackField("pendingCancel", maybePendingCancel); } @@ -4875,68 +5862,700 @@ void ReadableStreamBYOBRequest::visitForMemoryInfo(jsg::MemoryTracker& tracker) void TransformStreamDefaultController::visitForMemoryInfo(jsg::MemoryTracker& tracker) const { tracker.trackField("startPromise", startPromise); tracker.trackField("maybeBackpressureChange", maybeBackpressureChange); - tracker.trackField("transformAlgorithm", algorithms.transform); - tracker.trackField("flushAlgorithm", algorithms.flush); + tracker.trackField("transformer", transformer); tracker.trackField("writable", writable); - tracker.trackField("readable", readable); + KJ_IF_SOME(rc, readable) { + KJ_SWITCH_ONEOF(rc) { + KJ_CASE_ONEOF(defaultCtrl, jsg::Ref) { + tracker.trackField("readable", defaultCtrl); + } + KJ_CASE_ONEOF(byteCtrl, jsg::Ref) { + tracker.trackField("readable", byteCtrl); + } + } + } } // ====================================================================================== +namespace { + +class FromUnderlyingSourceImpl final: public UnderlyingSourceImpl { + public: + FromUnderlyingSourceImpl(jsg::AsyncGenerator> generator) + : generator(kj::mv(generator)) { + pull_ = [ref = addWeakRef()](jsg::Lock& js, auto controller) mutable { + auto& self = JSG_REQUIRE_NONNULL(ref->tryGet(), Error, "The ReadableStream has been closed"); + auto& generator = self.getGenerator(); + auto& c = controller.template get(); + return generator.next(js).thenRef(js, self.getNextContinuation(js, c.addRef())); + }; + + cancel_ = [ref = addWeakRef()](jsg::Lock& js, auto reason) mutable { + auto& self = JSG_REQUIRE_NONNULL(ref->tryGet(), Error, "The ReadableStream has been closed"); + auto& generator = self.getGenerator(); + return generator.return_(js, reason.addRef(js)) + .then(js, [generator = kj::mv(generator)](auto& lock, auto) { + // The generator might produce a value on return and might even want to continue, + // but the stream has been canceled at this point, so we stop here. + }); + }; + } + + ~FromUnderlyingSourceImpl() noexcept(false) { + weakSelf->invalidate(); + } + + jsg::AsyncGenerator>& getGenerator() { + return generator; + } + + void visitForGc(jsg::GcVisitor& visitor) { + generator.visitForGc(visitor); + } + + private: + jsg::AsyncGenerator> generator; + kj::Rc> weakSelf = + kj::rc>(kj::Badge{}, *this); + + jsg::Promise nextSuccess( + jsg::Lock& js, DefaultController controller, kj::Maybe> value) { + KJ_IF_SOME(v, value) { + auto handle = v.getHandle(js); + // Per the ReadableStream.from spec, if the value is a promise, + // the stream should wait for it to resolve and enqueue the + // resolved value... + // ... yes, this means that ReadableStream.from where the inputs + // are promises will be slow, but that's the spec. + KJ_IF_SOME(promise, handle.tryCast()) { + return js.toPromise(promise).thenRef( + js, getNextFinishContinuation(js, controller.addRef())); + } else { + } + controller->enqueue(js, v.getHandle(js)); + } else { + controller->close(js); + } + return js.resolvedPromise(); + } + + jsg::Promise nextFailure(jsg::Lock& js, DefaultController controller, jsg::Value reason) { + auto handle = jsg::JsValue(reason.getHandle(js)); + controller->error(js, handle); + return js.rejectedPromise(handle); + } + + void nextFinish(jsg::Lock& js, DefaultController controller, jsg::Value val) { + controller->enqueue(js, jsg::JsValue(val.getHandle(js))); + } + + struct NextCallbacks { + kj::Rc> weakSelf; + DefaultController controller; + jsg::Promise thenFunc(jsg::Lock& js, kj::Maybe> value) { + auto& self = + JSG_REQUIRE_NONNULL(weakSelf->tryGet(), Error, "The ReadableStream has been closed"); + return self.nextSuccess(js, controller.addRef(), kj::mv(value)); + } + + jsg::Promise catchFunc(jsg::Lock& js, jsg::Value reason) { + auto& self = + JSG_REQUIRE_NONNULL(weakSelf->tryGet(), Error, "The ReadableStream has been closed"); + return self.nextFailure(js, controller.addRef(), kj::mv(reason)); + } + }; + using NextContinuationType = jsg::PersistentContinuation>, + jsg::Promise>; + kj::Maybe nextContinuation; + + NextContinuationType& getNextContinuation(jsg::Lock& js, DefaultController controller) { + KJ_IF_SOME(nc, nextContinuation) { + return nc; + } + return nextContinuation.emplace( + NextContinuationType::create(js, NextCallbacks{addWeakRef(), kj::mv(controller)})); + } + + struct NextFinishCallbacks { + kj::Rc> weakSelf; + DefaultController controller; + void operator()(jsg::Lock& js, jsg::Value val) { + auto& self = + JSG_REQUIRE_NONNULL(weakSelf->tryGet(), Error, "The ReadableStream has been closed"); + self.nextFinish(js, controller.addRef(), kj::mv(val)); + } + }; + + using NextFinishContinuationType = + jsg::PersistentThenContinuation; + kj::Maybe nextFinishContinuation; + + NextFinishContinuationType& getNextFinishContinuation( + jsg::Lock& js, DefaultController controller) { + KJ_IF_SOME(nc, nextFinishContinuation) { + return nc; + } + return nextFinishContinuation.emplace(NextFinishContinuationType::create( + js, NextFinishCallbacks{addWeakRef(), kj::mv(controller)})); + } + + kj::Rc> addWeakRef() { + return weakSelf.addRef(); + } +}; + +} // namespace + jsg::Ref ReadableStream::from( - jsg::Lock& js, jsg::AsyncGenerator generator) { + jsg::Lock& js, jsg::AsyncGenerator> generator) { - // AsyncGenerator is not a refcounted type, so we need to wrap it in a refcounted - // struct so that we can keep it alive through the various promise branches below. - auto rcGenerator = - kj::rc>>(kj::mv(generator)); + auto controller = newReadableStreamJsController(); + auto stream = js.allocAccounted( + sizeof(ReadableStream) + controller->jsgGetMemorySelfSize(), kj::mv(controller)); + auto sourceImpl = kj::heap(kj::mv(generator)); + sourceImpl->setOwner(*stream); + stream->getController().setup(js, kj::mv(sourceImpl)); + return kj::mv(stream); +} - // clang-format off - return constructor(js, UnderlyingSource{ - .pull = [generator = rcGenerator.addRef()](jsg::Lock& js, auto controller) mutable { - auto& c = controller.template get(); - return generator->getWrapped().next(js).then(js, - JSG_VISITABLE_LAMBDA((controller = c.addRef(), generator = generator.addRef()), - (controller), - (jsg::Lock& js, kj::Maybe value) { - KJ_IF_SOME(v, value) { - auto handle = v.getHandle(js); - // Per the ReadableStream.from spec, if the value is a promise, - // the stream should wait for it to resolve and enqueue the - // resolved value... - // ... yes, this means that ReadableStream.from where the inputs - // are promises will be slow, but that's the spec. - if (handle->IsPromise()) { - return js.toPromise(handle.As()).then(js, - JSG_VISITABLE_LAMBDA( - (controller=controller.addRef()), - (controller), - (jsg::Lock& js, jsg::Value val) mutable { - controller->enqueue(js, val.getHandle(js)); - return js.resolvedPromise(); - })); - } - controller->enqueue(js, v.getHandle(js)); +// ======================================================================================= + +namespace { + +class InternalUnderlyingSourceImpl final: public UnderlyingSourceImpl { + private: + struct Closed {}; + + public: + InternalUnderlyingSourceImpl(IoContext& context, kj::Own in) + : inner(context.addObject(kj::heap(kj::mv(in)))) { + + isBytes_ = true; + // Note: `in` was moved above, so we access the source through `inner`. + auto& active = *inner.get>(); + expectedLength_ = active.source->tryGetLength(StreamEncoding::IDENTITY); + autoAllocateChunkSize_ = UnderlyingSource::DEFAULT_AUTO_ALLOCATE_CHUNK_SIZE_2; + start_ = kj::none; + size_ = kj::none; + highWaterMark_ = 0; + + pull_ = [selfRef = addWeakRef()](jsg::Lock& js, auto controller) { + auto& ioContext = IoContext::current(); + KJ_IF_SOME(self, selfRef->tryGet()) { + KJ_SWITCH_ONEOF(self.inner) { + KJ_CASE_ONEOF(active, IoOwn) { + auto& a = *active; + // Because this is a byte stream, the controller must be a ReadableByteStreamController + jsg::Ref& byteController = KJ_ASSERT_NONNULL( + controller.template tryGet>()); + + // Even tho this is a byte stream that will use BYOB reads, we need to allocate an + // intermediate buffer to read into since it is filled outside of the isolate lock. + // When the read completes, we will copy that data into the BYOB buffer. Even if + // a default reader is used, because we are using autoAllocateChunkSize, the pull + // will look like a BYOB read to the underlying source. + + auto request = KJ_ASSERT_NONNULL(byteController->getByobRequest(js)); + auto atLeast = request->getAtLeast().orDefault(1); + auto view = KJ_ASSERT_NONNULL(request->getView(js)); + auto buf = kj::heapArray(view.size()); + + auto promise = a.canceler.wrap(a.source->tryRead(buf.begin(), atLeast, buf.size())); + return ioContext.awaitIo(js, kj::mv(promise), + JSG_VISITABLE_LAMBDA( + (request = kj::mv(request), view = view.addRef(js), + byteController = kj::mv(byteController), buf = kj::mv(buf), atLeast, + maybeOwnerRef = self.owner.map([](ReadableStream& s) { return s.addRef(); })), + (request, view, byteController, maybeOwnerRef), (jsg::Lock& js, size_t amount) { + // Nothing read, signal EOF by closing the stream. + if (amount == 0) { + request->respond(js, 0); + byteController->close(js); + KJ_IF_SOME(ownerRef, maybeOwnerRef) { + ownerRef->signalEof(js); } else { - controller->close(js); } return js.resolvedPromise(); - }), - JSG_VISITABLE_LAMBDA((controller = c.addRef(), generator = generator.addRef()), - (controller), (jsg::Lock& js, jsg::Value reason) { - controller->error(js, reason.getHandle(js)); - return js.rejectedPromise(kj::mv(reason)); + } + + // Copy the data into the BYOB buffer and respond with the amount read. + view.getHandle(js).asArrayPtr().first(amount).copyFrom(buf.first(amount)); + request->respond(js, amount); + + if (amount < atLeast) { + byteController->close(js); + } + return js.resolvedPromise(); })); - }, - .cancel = [generator = rcGenerator.addRef()](jsg::Lock& js, auto reason) mutable { - return generator->getWrapped().return_(js, js.v8Ref(reason)) - .then(js, [generator = kj::mv(generator)](auto& lock, auto) { - // The generator might produce a value on return and might even want to continue, - // but the stream has been canceled at this point, so we stop here. + } + KJ_CASE_ONEOF(closed, Closed) { + return js.rejectedPromise(js.typeError("The ReadableStream is closed")); + } + KJ_CASE_ONEOF(errored, Errored) { + return js.rejectedPromise(js.exceptionToJs(errored.clone())); + } + } + KJ_UNREACHABLE; + } + + return js.rejectedPromise(js.typeError("The ReadableStream is closed")); + }; + + cancel_ = [selfRef = addWeakRef()](jsg::Lock& js, jsg::JsValue reason) { + KJ_IF_SOME(self, selfRef->tryGet()) { + KJ_IF_SOME(active, self.inner.tryGet>()) { + auto& a = *active; + auto exception = js.exceptionToKj(reason); + a.canceler.cancel(exception.clone()); + a.source->cancel(exception.clone()); + self.inner = kj::mv(exception); + } + } + + return js.resolvedPromise(); + }; + } + + InternalUnderlyingSourceImpl(Closed closed, bool isBytes): inner(closed) { + isBytes_ = isBytes; + start_ = [](jsg::Lock& js, auto controller) { + // Use the controller variant to call error() on it + KJ_SWITCH_ONEOF(controller) { + KJ_CASE_ONEOF(byteCtrl, jsg::Ref) { + byteCtrl->close(js); + } + KJ_CASE_ONEOF(defaultCtrl, jsg::Ref) { + defaultCtrl->close(js); + } + } + return js.resolvedPromise(); + }; + } + InternalUnderlyingSourceImpl(kj::Exception error, bool isBytes): inner(error.clone()) { + isBytes_ = isBytes; + start_ = [error = kj::mv(error)](jsg::Lock& js, auto controller) mutable { + // Use the controller variant to call error() on it + KJ_SWITCH_ONEOF(controller) { + KJ_CASE_ONEOF(byteCtrl, jsg::Ref) { + byteCtrl->error(js, js.exceptionToJsValue(kj::mv(error)).getHandle(js)); + } + KJ_CASE_ONEOF(defaultCtrl, jsg::Ref) { + defaultCtrl->error(js, js.exceptionToJsValue(kj::mv(error)).getHandle(js)); + } + } + return js.resolvedPromise(); + }; + } + + ~InternalUnderlyingSourceImpl() noexcept(false) { + weakRef->invalidate(); + } + + KJ_DISALLOW_COPY_AND_MOVE(InternalUnderlyingSourceImpl); + + bool isInternal() const override { + return true; + } + + void setOwner(ReadableStream& stream) override { + owner = stream; + } + + StreamEncoding getPreferredEncoding() override { + KJ_IF_SOME(active, inner.tryGet>()) { + return active->source->getPreferredEncoding(); + } + return StreamEncoding::IDENTITY; + } + + kj::Maybe tryGetLength(StreamEncoding encoding) override { + KJ_IF_SOME(active, inner.tryGet>()) { + return active->source->tryGetLength(encoding); + } + return kj::none; + } + + kj::Maybe> tryReleaseSource() override KJ_WARN_UNUSED_RESULT { + KJ_IF_SOME(active, inner.tryGet>()) { + auto& a = *active; + KJ_REQUIRE(a.canceler.isEmpty(), "Cannot release source while there are pending operations"); + auto sink = kj::mv(active->source); + inner.template init(); + return kj::mv(sink); + } + return kj::none; + } + + kj::Maybe tryTee(uint64_t limit) override KJ_WARN_UNUSED_RESULT { + auto& ioContext = IoContext::current(); + KJ_SWITCH_ONEOF(inner) { + KJ_CASE_ONEOF(active, IoOwn) { + auto& a = *active; + KJ_REQUIRE(a.canceler.isEmpty(), "Cannot tee while there are pending operations"); + auto source = kj::mv(a.source); + // Try the source's own optimized tee first. + KJ_IF_SOME(tee, source->tryTee(limit)) { + inner.init(); + return Tee{ + .branch1 = kj::heap(ioContext, kj::mv(tee.branches[0])), + .branch2 = kj::heap(ioContext, kj::mv(tee.branches[1])), + }; + } + // Fall back to kj::newTee. + auto tee = kj::newTee(kj::heap(kj::mv(source)), limit); + inner.init(); + return Tee{ + .branch1 = kj::heap( + ioContext, kj::heap(newTeeErrorAdapter(kj::mv(tee.branches[0])))), + .branch2 = kj::heap( + ioContext, kj::heap(newTeeErrorAdapter(kj::mv(tee.branches[1])))), + }; + } + KJ_CASE_ONEOF(closed, Closed) { + return Tee{ + .branch1 = kj::heap(Closed{}, isBytes_), + .branch2 = kj::heap(Closed{}, isBytes_), + }; + } + KJ_CASE_ONEOF(errored, Errored) { + return Tee{ + .branch1 = kj::heap(errored.clone(), isBytes_), + .branch2 = kj::heap(errored.clone(), isBytes_), + }; + } + } + KJ_UNREACHABLE; + } + + private: + struct Active { + kj::Own source; + kj::Canceler canceler; + Active(kj::Own source): source(kj::mv(source)) {} + }; + using Errored = kj::Exception; + kj::OneOf, Closed, Errored> inner; + kj::Maybe owner; + + kj::Rc> weakRef = + kj::rc>( + kj::Badge(), *this); + + kj::Rc> addWeakRef() KJ_WARN_UNUSED_RESULT { + return weakRef.addRef(); + } +}; + +class InternalUnderlyingSinkImpl final: public UnderlyingSinkImpl { + public: + InternalUnderlyingSinkImpl(IoContext& context, + kj::Own out, + kj::Maybe> observer = kj::none, + kj::Maybe maybeHighWaterMark = kj::none) + : observer(kj::mv(observer)), + inner(context.addObject(kj::heap(kj::mv(out)))) { + + // Not really necessary to explicitly set this to none but doing so to be + // extra clear that there is no start algorithm for this underlying sink. + start_ = kj::none; + + // Use byte-counting for backpressure to match internal controller behavior. + // Without this, the default is 1 per chunk regardless of chunk size. + size_ = jsg::Function([](jsg::Lock&, jsg::JsValue chunk) -> uint64_t { + KJ_IF_SOME(ab, chunk.tryCast()) { + return ab.size(); + } + KJ_IF_SOME(sab, chunk.tryCast()) { + return sab.size(); + } + KJ_IF_SOME(view, chunk.tryCast()) { + return view.size(); + } + // For strings and unknown types, fall back to 1. + return 1; + }); + + // When no HWM is provided, use a large default to effectively disable backpressure, + // matching internal controller behavior where maybeHighWaterMark=kj::none means + // desiredSize is always positive. + highWaterMark_ = maybeHighWaterMark.orDefault(kj::maxValue); + + write_ = [selfRef = addWeakRef()](jsg::Lock& js, jsg::JsValue chunk, + jsg::Ref controller) { + KJ_IF_SOME(self, selfRef->tryGet()) { + KJ_SWITCH_ONEOF(self.inner) { + KJ_CASE_ONEOF(closed, Closed) { + return js.rejectedPromise(js.typeError("The WritableStream is closed")); + } + KJ_CASE_ONEOF(errored, Errored) { + return js.rejectedPromise(js.exceptionToJs(errored.clone())); + } + KJ_CASE_ONEOF(active, IoOwn) { + // Match internal controller behavior: undefined/null writes are no-ops, + // and zero-length writes are skipped. + if (chunk.isUndefined() || chunk.isNull()) { + return js.resolvedPromise(); + } + + auto& i = *active; + + KJ_IF_SOME(ab, chunk.tryCast()) { + if (ab.size() == 0) return js.resolvedPromise(); + return self.write(js, i, ab.copy()); + } + KJ_IF_SOME(sab, chunk.tryCast()) { + if (sab.size() == 0) return js.resolvedPromise(); + return self.write(js, i, sab.copy()); + } + KJ_IF_SOME(view, chunk.tryCast()) { + if (view.size() == 0) return js.resolvedPromise(); + auto buf = kj::heapArray(view.asArrayPtr()); + return self.write(js, i, kj::mv(buf)); + } + KJ_IF_SOME(str, chunk.tryCast()) { + auto kjstr = str.toDOMString(js); + if (kjstr.size() == 0) return js.resolvedPromise(); + return self.write( + js, i, kjstr.asBytes().slice(0, kjstr.size()).attach(kj::mv(kjstr))); + } + + return js.rejectedPromise(js.typeError( + "Chunk must be an ArrayBuffer, ArrayBufferView, SharedArrayBuffer, or string."_kj)); + } + } + } + + return js.rejectedPromise(js.typeError("The WritableStream is closed")); + }; + + writev_ = [selfRef = addWeakRef()](jsg::Lock& js, kj::Array> chunks, + jsg::Ref controller) { + KJ_IF_SOME(self, selfRef->tryGet()) { + KJ_SWITCH_ONEOF(self.inner) { + KJ_CASE_ONEOF(closed, Closed) { + return js.rejectedPromise(js.typeError("The WritableStream is closed")); + } + KJ_CASE_ONEOF(errored, Errored) { + return js.rejectedPromise(js.exceptionToJs(errored.clone())); + } + KJ_CASE_ONEOF(active, IoOwn) { + auto& i = *active; + auto bytesBuilder = kj::heapArrayBuilder>(chunks.size()); + for (auto& chunk: chunks) { + KJ_IF_SOME(bytes, chunkToBytes(js, chunk.getHandle(js))) { + bytesBuilder.add(kj::mv(bytes)); + } else { + return js.rejectedPromise( + js.typeError("Chunk must be an ArrayBuffer, ArrayBufferView, " + "SharedArrayBuffer, or string."_kj)); + } + } + return self.writev(js, i, bytesBuilder.finish()); + } + } + } + + return js.rejectedPromise(js.typeError("The WritableStream is closed")); + }; + + close_ = [selfRef = addWeakRef()](jsg::Lock& js) { + KJ_IF_SOME(self, selfRef->tryGet()) { + KJ_SWITCH_ONEOF(self.inner) { + KJ_CASE_ONEOF(closed, Closed) { + return js.rejectedPromise(js.typeError("The WritableStream is closed")); + } + KJ_CASE_ONEOF(errored, Errored) { + return js.rejectedPromise(js.exceptionToJs(errored.clone())); + } + KJ_CASE_ONEOF(active, IoOwn) { + return self.close(js, *active); + } + } + } + + return js.rejectedPromise(js.typeError("The WritableStream is closed")); + }; + + abort_ = [selfRef = addWeakRef()](jsg::Lock& js, jsg::JsValue reason) { + KJ_IF_SOME(self, selfRef->tryGet()) { + KJ_IF_SOME(active, self.inner.tryGet>()) { + auto exception = js.exceptionToKj(reason); + active->canceler.cancel(exception.clone()); + self.inner = kj::mv(exception); + } + } + + return js.resolvedPromise(); + }; + } + + ~InternalUnderlyingSinkImpl() noexcept(false) { + weakRef->invalidate(); + } + + KJ_DISALLOW_COPY_AND_MOVE(InternalUnderlyingSinkImpl); + + bool isInternal() const override { + return true; + } + + kj::Maybe> tryReleaseSink() override KJ_WARN_UNUSED_RESULT { + KJ_IF_SOME(active, inner.tryGet>()) { + auto& a = *active; + KJ_REQUIRE(a.canceler.isEmpty(), "Cannot release sink while there are pending operations"); + auto sink = kj::mv(active->out); + inner.template init(); + return kj::mv(sink); + } + return kj::none; + } + + kj::Maybe tryGetSink() override KJ_WARN_UNUSED_RESULT { + KJ_IF_SOME(active, inner.tryGet>()) { + return *active->out; + } + return kj::none; + } + + private: + struct Active { + kj::Own out; + kj::Canceler canceler; + Active(kj::Own out): out(kj::mv(out)) {} + }; + struct Closed {}; + using Errored = kj::Exception; + kj::Maybe> observer; + kj::OneOf, Closed, Errored> inner; + kj::Rc> weakRef = + kj::rc>(kj::Badge(), *this); + + // Convert a JS chunk to a byte array. Returns kj::none if the chunk type is unsupported. + static kj::Maybe> chunkToBytes(jsg::Lock& js, jsg::JsValue chunk) { + KJ_IF_SOME(ab, chunk.tryCast()) { + return ab.copy(); + } + KJ_IF_SOME(sab, chunk.tryCast()) { + return sab.copy(); + } + KJ_IF_SOME(view, chunk.tryCast()) { + return kj::heapArray(view.asArrayPtr()); + } + KJ_IF_SOME(str, chunk.tryCast()) { + auto kjstr = str.toDOMString(js); + auto bytes = kj::heapArray(kjstr.size()); + memcpy(bytes.begin(), kjstr.begin(), kjstr.size()); + return kj::mv(bytes); + } + return kj::none; + } + + jsg::Promise write( + jsg::Lock& js, Active& active, kj::Array chunk) KJ_WARN_UNUSED_RESULT { + auto& ioContext = IoContext::current(); + auto size = chunk.size(); + KJ_IF_SOME(o, observer) { + o->onChunkEnqueued(size); + } + auto writeOp = [&out = *active.out, &canceler = active.canceler, + chunk = kj::mv(chunk)]() mutable { + return canceler.wrap(out.write(chunk).attach(kj::mv(chunk))); + }; + auto promise = ([&]() -> kj::Promise { + KJ_IF_SOME(lock, ioContext.waitForOutputLocksIfNecessary()) { + return lock.then(kj::mv(writeOp)); + } + return writeOp(); + })() + .catch_([selfRef = addWeakRef()](auto exception) -> kj::Promise { + selfRef->runIfAlive([&](auto& self) { self.inner = exception.clone(); }); + return kj::mv(exception); + }).then([selfRef = addWeakRef(), size]() { + selfRef->runIfAlive([&](auto& self) { + KJ_IF_SOME(o, self.observer) { + o->onChunkDequeued(size); + } }); - }, - }, StreamQueuingStrategy{ .highWaterMark = 0 }); - // clang-format on + }); + return ioContext.awaitIo(js, kj::mv(promise)); + } + + jsg::Promise writev( + jsg::Lock& js, Active& active, kj::Array> chunks) KJ_WARN_UNUSED_RESULT { + auto& ioContext = IoContext::current(); + size_t totalSize = 0; + // Build the pieces array for the kj sink's vectorized write. + auto pieces = kj::heapArray>(chunks.size()); + for (size_t i = 0; i < chunks.size(); i++) { + pieces[i] = chunks[i]; + totalSize += chunks[i].size(); + } + KJ_IF_SOME(o, observer) { + o->onChunkEnqueued(totalSize); + } + auto writeOp = [&out = *active.out, &canceler = active.canceler, pieces = kj::mv(pieces), + chunks = kj::mv(chunks)]() mutable { + return canceler.wrap(out.write(pieces).attach(kj::mv(pieces), kj::mv(chunks))); + }; + auto promise = ([&]() -> kj::Promise { + KJ_IF_SOME(lock, ioContext.waitForOutputLocksIfNecessary()) { + return lock.then(kj::mv(writeOp)); + } + return writeOp(); + })() + .catch_([selfRef = addWeakRef()](auto exception) -> kj::Promise { + selfRef->runIfAlive([&](auto& self) { self.inner = exception.clone(); }); + return kj::mv(exception); + }).then([selfRef = addWeakRef(), totalSize]() { + selfRef->runIfAlive([&](auto& self) { + KJ_IF_SOME(o, self.observer) { + o->onChunkDequeued(totalSize); + } + }); + }); + return ioContext.awaitIo(js, kj::mv(promise)); + } + + jsg::Promise close(jsg::Lock& js, Active& active) KJ_WARN_UNUSED_RESULT { + auto& ioContext = IoContext::current(); + auto promise = active.canceler.wrap(active.out->end()).then([selfRef = addWeakRef()]() { + selfRef->runIfAlive([&](auto& self) { self.inner.template init(); }); + }); + return ioContext.awaitIo(js, kj::mv(promise)); + } + + kj::Rc> addWeakRef() KJ_WARN_UNUSED_RESULT { + return weakRef.addRef(); + } +}; + +} // namespace + +jsg::Ref newInternalWritableStream(jsg::Lock& js, + IoContext& ioContext, + kj::Own sink, + kj::Maybe> observer, + kj::Maybe maybeHighWaterMark) { + auto controller = newWritableStreamJsController(); + auto stream = js.allocAccounted( + sizeof(WritableStream) + controller->jsgGetMemorySelfSize(), kj::mv(controller)); + auto sinkImpl = kj::heap( + ioContext, kj::mv(sink), kj::mv(observer), maybeHighWaterMark); + stream->getController().setup(js, kj::mv(sinkImpl)); + return kj::mv(stream); +} + +jsg::Ref newInternalReadableStream( + jsg::Lock& js, IoContext& ioContext, kj::Own source) { + auto controller = newReadableStreamJsController(); + auto stream = js.allocAccounted( + sizeof(ReadableStream) + controller->jsgGetMemorySelfSize(), kj::mv(controller)); + auto sourceImpl = kj::heap(ioContext, kj::mv(source)); + sourceImpl->setOwner(*stream); + stream->getController().setup(js, kj::mv(sourceImpl)); + return kj::mv(stream); } } // namespace workerd::api diff --git a/src/workerd/api/streams/standard.h b/src/workerd/api/streams/standard.h index e7e2499971d..914c9514f0c 100644 --- a/src/workerd/api/streams/standard.h +++ b/src/workerd/api/streams/standard.h @@ -12,6 +12,10 @@ #include #include +namespace workerd { +class ByteStreamObserver; +} // namespace workerd + namespace workerd::api { // ======================================================================================= @@ -137,20 +141,22 @@ class ReadableImpl { using Entry = Self::QueueType::Entry; using StateListener = Self::QueueType::ConsumerImpl::StateListener; - ReadableImpl(UnderlyingSource underlyingSource, StreamQueuingStrategy queuingStrategy); + ReadableImpl( + jsg::Lock& js, kj::Own source, kj::Rc> weakController); // Invokes the start algorithm to initialize the underlying source. void start(jsg::Lock& js, jsg::Ref self); // If the readable is not already closed or errored, initiates a cancellation. - jsg::Promise cancel(jsg::Lock& js, jsg::Ref self, v8::Local maybeReason); + jsg::Promise cancel( + jsg::Lock& js, jsg::Ref self, jsg::JsValue maybeReason) KJ_WARN_UNUSED_RESULT; // True if the readable is not closed, not errored, and close has not already been requested. bool canCloseOrEnqueue(); // Invokes the cancel algorithm to let the underlying source know that the // readable has been canceled. - void doCancel(jsg::Lock& js, jsg::Ref self, v8::Local reason); + void doCancel(jsg::Lock& js, jsg::Ref self, jsg::JsValue reason); // Close the queue if we are in a state where we can be closed. void close(jsg::Lock& js); @@ -162,14 +168,14 @@ class ReadableImpl { // If it isn't already errored or closed, errors the queue, causing all consumers to be errored // and detached. - void doError(jsg::Lock& js, jsg::Value reason); + void doError(jsg::Lock& js, jsg::JsValue reason); // When a negative number is returned, indicates that we are above the highwatermark // and backpressure should be signaled. kj::Maybe getDesiredSize(); - // Invokes the pull algorithm only if we're in a state where the queue the - // queue is below the watermark and we actually need data right now. + // Invokes the pull algorithm only if we're in a state where the queue is + // below the watermark and we actually need data right now. void pullIfNeeded(jsg::Lock& js, jsg::Ref self); // Like pullIfNeeded but bypasses the shouldCallPull() check. Used for draining reads @@ -199,34 +205,13 @@ class ReadableImpl { size_t jsgGetMemorySelfSize() const; void jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const; - private: - struct Algorithms { - kj::Maybe> start; - kj::Maybe> pull; - kj::Maybe> cancel; - kj::Maybe> size; - - Algorithms(UnderlyingSource underlyingSource, StreamQueuingStrategy queuingStrategy) - : start(kj::mv(underlyingSource.start)), - pull(kj::mv(underlyingSource.pull)), - cancel(kj::mv(underlyingSource.cancel)), - size(kj::mv(queuingStrategy.size)) {} - - Algorithms(Algorithms&& other) = default; - Algorithms& operator=(Algorithms&& other) = default; - - void clear() { - start = kj::none; - pull = kj::none; - cancel = kj::none; - size = kj::none; - } - - void visitForGc(jsg::GcVisitor& visitor) { - visitor.visit(start, pull, cancel, size); - } - }; + kj::Maybe tryTeeSource(uint64_t limit) KJ_WARN_UNUSED_RESULT; + kj::Maybe> tryReleaseSource() KJ_WARN_UNUSED_RESULT; + bool isInternal() const; + StreamEncoding getPreferredEncoding(); + kj::Maybe tryGetLength(StreamEncoding encoding); + private: using Queue = Self::QueueType; // State machine for ReadableImpl: @@ -241,9 +226,8 @@ class ReadableImpl { StreamStates::Errored, Queue>; State state; - Algorithms algorithms; - size_t highWaterMark = 1; + kj::Own underlyingSource; struct PendingCancel { kj::Maybe::Resolver> fulfiller; @@ -255,6 +239,41 @@ class ReadableImpl { }; kj::Maybe maybePendingCancel; + // Weak reference to the owning controller. Shared with persistent continuation + // callbacks so they can detect when the controller has been destroyed and bail + // out instead of dereferencing a dangling pointer. + kj::Rc> weakController; + + // Persistent pull continuation — lazily initialized on first pull dispatch. + // Reused across all subsequent pulls to avoid per-pull OpaqueWrappable, + // v8::Function, and lambda heap allocations. + struct PullContinuationCallbacks { + // Raw pointer — safe to dereference only after verifying weakController is alive, + // which proves the owning controller (and thus this impl) still exists. + ReadableImpl* impl; + kj::Rc> weakController; + + void thenFunc(jsg::Lock& js) { + if (weakController->tryGet() == kj::none) return; + impl->onPullSuccess(js); + } + + void catchFunc(jsg::Lock& js, jsg::Value reason) { + if (weakController->tryGet() == kj::none) return; + impl->onPullFailure(js, kj::mv(reason)); + } + }; + using PullContinuationType = jsg::PersistentContinuation; + + kj::Maybe pullContinuation; + + PullContinuationType& getPullContinuation(jsg::Lock& js) KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT; + void onPullSuccess(jsg::Lock& js); + void onPullFailure(jsg::Lock& js, jsg::Value reason); + + // Keeps the controller alive via jsg::Ref during a pull operation. + kj::Maybe> pullSelf; + struct Flags { uint8_t pullAgain : 1 = 0; uint8_t pulling : 1 = 0; @@ -277,8 +296,9 @@ class WritableImpl { struct WriteRequest { jsg::Promise::Resolver resolver; - jsg::Value value; + jsg::JsRef value; size_t size; + bool flush = false; // True if this is a flush sync point (no data to write). void visitForGc(jsg::GcVisitor& visitor) { visitor.visit(resolver, value); @@ -290,48 +310,50 @@ class WritableImpl { } }; - WritableImpl(jsg::Lock& js, WritableStream& owner, jsg::Ref abortSignal); + WritableImpl(jsg::Lock& js, + WritableStream& owner, + kj::Own sink, + jsg::Ref abortSignal, + kj::Rc> weakController); - jsg::Promise abort(jsg::Lock& js, jsg::Ref self, v8::Local reason); + jsg::Promise abort( + jsg::Lock& js, jsg::Ref self, jsg::JsValue reason) KJ_WARN_UNUSED_RESULT; void advanceQueueIfNeeded(jsg::Lock& js, jsg::Ref self); - jsg::Promise close(jsg::Lock& js, jsg::Ref self); + jsg::Promise close(jsg::Lock& js, jsg::Ref self) KJ_WARN_UNUSED_RESULT; - void dealWithRejection(jsg::Lock& js, jsg::Ref self, v8::Local reason); + void dealWithRejection(jsg::Lock& js, jsg::Ref self, jsg::JsValue reason); - WriteRequest dequeueWriteRequest(); + WriteRequest dequeueWriteRequest() KJ_WARN_UNUSED_RESULT; void doClose(jsg::Lock& js); - void doError(jsg::Lock& js, v8::Local reason); + void doError(jsg::Lock& js, jsg::JsValue reason); - void error(jsg::Lock& js, jsg::Ref self, v8::Local reason); + void error(jsg::Lock& js, jsg::Ref self, jsg::JsValue reason); void finishErroring(jsg::Lock& js, jsg::Ref self); void finishInFlightClose( - jsg::Lock& js, jsg::Ref self, kj::Maybe> reason = kj::none); + jsg::Lock& js, jsg::Ref self, kj::Maybe reason = kj::none); void finishInFlightWrite( - jsg::Lock& js, jsg::Ref self, kj::Maybe> reason = kj::none); + jsg::Lock& js, jsg::Ref self, kj::Maybe reason = kj::none); - ssize_t getDesiredSize(); + kj::Maybe getDesiredSize(); bool isCloseQueuedOrInFlight(); void rejectCloseAndClosedPromiseIfNeeded(jsg::Lock& js); - kj::Maybe tryGetOwner(); + kj::Maybe tryGetOwner() KJ_WARN_UNUSED_RESULT; - void setup(jsg::Lock& js, - jsg::Ref self, - UnderlyingSink underlyingSink, - StreamQueuingStrategy queuingStrategy); + void setup(jsg::Lock& js, jsg::Ref self); // Puts the writable into an erroring state. This allows any in flight write or // close to complete before actually transitioning the writable. - void startErroring(jsg::Lock& js, jsg::Ref self, v8::Local reason); + void startErroring(jsg::Lock& js, jsg::Ref self, jsg::JsValue reason); // Notifies the Writer of the current backpressure state. If the amount of data queued // is equal to or above the highwatermark, then backpressure is applied. @@ -339,10 +361,21 @@ class WritableImpl { // Writes a chunk to the Writable, possibly queuing the chunk in the internal buffer // if there are already other writes pending. - jsg::Promise write(jsg::Lock& js, jsg::Ref self, v8::Local value); + jsg::Promise write( + jsg::Lock& js, jsg::Ref self, jsg::JsValue value) KJ_WARN_UNUSED_RESULT; + + // Inserts a flush sync point into the write queue. The returned promise resolves + // when all preceding writes have completed. If nothing is in flight, resolves + // immediately. Flush entries are represented as WriteRequests with empty value + // and size 0. + jsg::Promise flush( + jsg::Lock& js, jsg::Ref self, MarkAsHandled markAsHandled) KJ_WARN_UNUSED_RESULT; // True if the writable is in a state where new chunks can be written bool isWritable() const; + bool isInternal() const; + kj::Maybe> tryReleaseSink() KJ_WARN_UNUSED_RESULT; + kj::Maybe tryGetSink() KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT; void cancelPendingWrites(jsg::Lock& js, jsg::JsValue reason); @@ -353,32 +386,6 @@ class WritableImpl { void jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const; private: - struct Algorithms { - kj::Maybe> abort; - kj::Maybe> close; - kj::Maybe> write; - kj::Maybe> size; - - Algorithms() {}; - ~Algorithms() { - // Clear all algorithm references to break circular references - clear(); - } - Algorithms(Algorithms&& other) = default; - Algorithms& operator=(Algorithms&& other) = default; - - void clear() { - abort = kj::none; - close = kj::none; - size = kj::none; - write = kj::none; - } - - void visitForGc(jsg::GcVisitor& visitor) { - visitor.visit(write, close, abort, size); - } - }; - struct Writable { static constexpr kj::StringPtr NAME KJ_UNUSED = "writable"_kj; }; @@ -408,23 +415,153 @@ class WritableImpl { kj::Maybe>> owner; jsg::Ref signal; State state = State::template create(); - Algorithms algorithms; - size_t highWaterMark = 1; + kj::Own underlyingSink; + size_t amountBuffered = 0; RingBuffer writeRequests; kj::Maybe inFlightWrite; + + // Batch write: when writev is used, multiple WriteRequests are in flight simultaneously. + // Each has its own resolver that gets resolved/rejected when the batch completes. + struct BatchWriteRequest { + kj::Array::Resolver> resolvers; + size_t totalSize; + + void visitForGc(jsg::GcVisitor& visitor) { + for (auto& resolver: resolvers) { + visitor.visit(resolver); + } + } + + JSG_MEMORY_INFO(BatchWriteRequest) { + for (auto& resolver: resolvers) { + tracker.trackField("resolver", resolver); + } + } + }; + kj::Maybe inFlightBatchWrite; + kj::Maybe::Resolver> inFlightClose; kj::Maybe::Resolver> closeRequest; kj::Maybe> maybePendingAbort; + // Keeps the controller alive via jsg::Ref during an in-flight write operation. + // Set when a write is dispatched, cleared when the write continuation fires. + kj::Maybe> inFlightSelf; + + // Weak reference to the owning controller. Shared with persistent continuation + // callbacks so they can detect when the controller has been destroyed. + kj::Rc> weakController; + + // Persistent write continuation — lazily initialized on first write dispatch. + // Reused across all subsequent writes to avoid per-write OpaqueWrappable, + // v8::Function, and lambda heap allocations. + struct WriteContinuationCallbacks { + // Raw pointer — safe only after verifying weakController is alive. + WritableImpl* impl; + kj::Rc> weakController; + + jsg::Promise thenFunc(jsg::Lock& js) KJ_WARN_UNUSED_RESULT { + if (weakController->tryGet() == kj::none) return js.resolvedPromise(); + return impl->onWriteSuccess(js); + } + + jsg::Promise catchFunc(jsg::Lock& js, jsg::Value reason) KJ_WARN_UNUSED_RESULT { + if (weakController->tryGet() == kj::none) return js.rejectedPromise(kj::mv(reason)); + return impl->onWriteFailure(js, kj::mv(reason)); + } + }; + using WriteContinuationType = + jsg::PersistentContinuation>; + + kj::Maybe writeContinuation; + + WriteContinuationType& getWriteContinuation(jsg::Lock& js) KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT; + jsg::Promise onWriteSuccess(jsg::Lock& js) KJ_WARN_UNUSED_RESULT; + jsg::Promise onWriteFailure(jsg::Lock& js, jsg::Value reason) KJ_WARN_UNUSED_RESULT; + + // Persistent writev continuation — used when the underlying sink supports batch writes. + struct WritevContinuationCallbacks { + // Raw pointer — safe only after verifying weakController is alive. + WritableImpl* impl; + kj::Rc> weakController; + + jsg::Promise thenFunc(jsg::Lock& js) KJ_WARN_UNUSED_RESULT { + if (weakController->tryGet() == kj::none) return js.resolvedPromise(); + return impl->onWritevSuccess(js); + } + + jsg::Promise catchFunc(jsg::Lock& js, jsg::Value reason) KJ_WARN_UNUSED_RESULT { + if (weakController->tryGet() == kj::none) return js.rejectedPromise(kj::mv(reason)); + return impl->onWritevFailure(js, kj::mv(reason)); + } + }; + using WritevContinuationType = + jsg::PersistentContinuation>; + + kj::Maybe writevContinuation; + + WritevContinuationType& getWritevContinuation( + jsg::Lock& js) KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT; + jsg::Promise onWritevSuccess(jsg::Lock& js) KJ_WARN_UNUSED_RESULT; + jsg::Promise onWritevFailure(jsg::Lock& js, jsg::Value reason) KJ_WARN_UNUSED_RESULT; + + // Persistent close continuation — same pattern as write continuation. + struct CloseContinuationCallbacks { + // Raw pointer — safe only after verifying weakController is alive. + WritableImpl* impl; + kj::Rc> weakController; + + void thenFunc(jsg::Lock& js) { + if (weakController->tryGet() == kj::none) return; + impl->onCloseSuccess(js); + } + + void catchFunc(jsg::Lock& js, jsg::Value reason) { + if (weakController->tryGet() == kj::none) { + // throwException is [[noreturn]]. + js.throwException(kj::mv(reason)); + } + impl->onCloseFailure(js, kj::mv(reason)); + } + }; + using CloseContinuationType = jsg::PersistentContinuation; + + kj::Maybe closeContinuation; + + // Persistent drain continuation — used for the microtask hop when the write queue + // has more entries after a write completes. Avoids per-drain OpaqueWrappable and + // v8::Function allocations. + struct DrainContinuationCallbacks { + // Raw pointer — safe only after verifying weakController is alive. + WritableImpl* impl; + kj::Rc> weakController; + + void operator()(jsg::Lock& js) { + if (weakController->tryGet() == kj::none) return; + impl->onDrainNext(js); + } + }; + using DrainContinuationType = + jsg::PersistentThenContinuation; + + kj::Maybe drainContinuation; + + DrainContinuationType& getDrainContinuation(jsg::Lock& js) KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT; + void onDrainNext(jsg::Lock& js); + + CloseContinuationType& getCloseContinuation(jsg::Lock& js) KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT; + void onCloseSuccess(jsg::Lock& js); + void onCloseFailure(jsg::Lock& js, jsg::Value reason); struct Flags { uint8_t started : 1 = 0; uint8_t starting : 1 = 0; uint8_t backpressure : 1 = 0; uint8_t pedanticWpt : 1 = 0; + uint8_t specCompliantWriter : 1 = 0; }; Flags flags{}; @@ -441,12 +578,13 @@ class ReadableStreamDefaultController: public jsg::Object { using QueueType = ValueQueue; using ReadableImpl = ReadableImpl; - ReadableStreamDefaultController( - UnderlyingSource underlyingSource, StreamQueuingStrategy queuingStrategy); + ReadableStreamDefaultController(jsg::Lock& js, kj::Own source); + ~ReadableStreamDefaultController() noexcept(false); void start(jsg::Lock& js); - jsg::Promise cancel(jsg::Lock& js, jsg::Optional> maybeReason); + jsg::Promise cancel( + jsg::Lock& js, jsg::Optional maybeReason) KJ_WARN_UNUSED_RESULT; void close(jsg::Lock& js); @@ -454,9 +592,9 @@ class ReadableStreamDefaultController: public jsg::Object { bool hasBackpressure(); kj::Maybe getDesiredSize(); - void enqueue(jsg::Lock& js, jsg::Optional> chunk); + void enqueue(jsg::Lock& js, jsg::Optional chunk); - void error(jsg::Lock& js, v8::Local reason); + void error(jsg::Lock& js, jsg::JsValue reason); void pull(jsg::Lock& js); @@ -470,7 +608,7 @@ class ReadableStreamDefaultController: public jsg::Object { } kj::Own getConsumer( - kj::Maybe stateListener); + kj::Maybe stateListener) KJ_WARN_UNUSED_RESULT; JSG_RESOURCE_TYPE(ReadableStreamDefaultController) { JSG_READONLY_PROTOTYPE_PROPERTY(desiredSize, getDesiredSize); @@ -489,7 +627,24 @@ class ReadableStreamDefaultController: public jsg::Object { kj::Maybe getMaybeErrorState(jsg::Lock& js); + // Clear algorithms and persistent continuations to break circular references. + void clearAlgorithms(); + + // Break the pullSelf ref cycle without clearing the underlying source. + // Called from ReadableState destructor to allow the controller to be freed + // while keeping the source available for any in-progress operations. + void breakPullCycle() { + impl.pullSelf = kj::none; + } + + kj::Maybe tryTeeSource(uint64_t limit) KJ_WARN_UNUSED_RESULT; + kj::Maybe> tryReleaseSource() KJ_WARN_UNUSED_RESULT; + bool isInternal() const; + StreamEncoding getPreferredEncoding(); + kj::Maybe tryGetLength(StreamEncoding encoding); + private: + kj::Rc> weakSelf; kj::Maybe ioContext; ReadableImpl impl; @@ -522,13 +677,13 @@ class ReadableStreamBYOBRequest: public jsg::Object { // added to support the readAtLeast extension on the ReadableStreamBYOBReader. kj::Maybe getAtLeast(); - kj::Maybe> getView(jsg::Lock& js); + kj::Maybe getView(jsg::Lock& js) KJ_WARN_UNUSED_RESULT; void invalidate(jsg::Lock& js); void respond(jsg::Lock& js, int bytesWritten); - void respondWithNewView(jsg::Lock& js, jsg::BufferSource view); + void respondWithNewView(jsg::Lock& js, jsg::JsBufferSource view); JSG_RESOURCE_TYPE(ReadableStreamBYOBRequest) { JSG_READONLY_PROTOTYPE_PROPERTY(view, getView); @@ -540,7 +695,7 @@ class ReadableStreamBYOBRequest: public jsg::Object { JSG_READONLY_PROTOTYPE_PROPERTY(atLeast, getAtLeast); } - bool isPartiallyFulfilled(); + bool isPartiallyFulfilled(jsg::Lock& js); void visitForMemoryInfo(jsg::MemoryTracker& tracker) const; @@ -548,7 +703,7 @@ class ReadableStreamBYOBRequest: public jsg::Object { struct Impl { kj::Own readRequest; kj::Rc> controller; - jsg::V8Ref view; + jsg::JsRef view; size_t originalBufferByteLength; size_t originalByteOffsetPlusBytesFilled; @@ -574,29 +729,30 @@ class ReadableByteStreamController: public jsg::Object { using QueueType = ByteQueue; using ReadableImpl = ReadableImpl; - ReadableByteStreamController( - UnderlyingSource underlyingSource, StreamQueuingStrategy queuingStrategy); + ReadableByteStreamController(jsg::Lock& js, kj::Own source); ~ReadableByteStreamController() noexcept(false); - jsg::Ref getSelf() { + jsg::Ref getSelf() KJ_WARN_UNUSED_RESULT { return JSG_THIS; } void start(jsg::Lock& js); - jsg::Promise cancel(jsg::Lock& js, jsg::Optional> maybeReason); + jsg::Promise cancel( + jsg::Lock& js, jsg::Optional maybeReason) KJ_WARN_UNUSED_RESULT; void close(jsg::Lock& js); - void enqueue(jsg::Lock& js, jsg::BufferSource chunk); + void enqueue(jsg::Lock& js, jsg::JsBufferSource chunk); - void error(jsg::Lock& js, v8::Local reason); + void error(jsg::Lock& js, jsg::JsValue reason); bool canCloseOrEnqueue(); bool hasBackpressure(); kj::Maybe getDesiredSize(); - kj::Maybe> getByobRequest(jsg::Lock& js); + kj::Maybe> getByobRequest( + jsg::Lock& js) KJ_WARN_UNUSED_RESULT; void pull(jsg::Lock& js); @@ -610,7 +766,7 @@ class ReadableByteStreamController: public jsg::Object { } kj::Own getConsumer( - kj::Maybe stateListener); + kj::Maybe stateListener) KJ_WARN_UNUSED_RESULT; JSG_RESOURCE_TYPE(ReadableByteStreamController) { JSG_READONLY_PROTOTYPE_PROPERTY(byobRequest, getByobRequest); @@ -625,6 +781,22 @@ class ReadableByteStreamController: public jsg::Object { tracker.trackField("maybeByobRequest", maybeByobRequest); } + // Clear algorithms and persistent continuations to break circular references. + void clearAlgorithms(); + + // Break the pullSelf ref cycle without clearing the underlying source. + void breakPullCycle() { + impl.pullSelf = kj::none; + } + + kj::Maybe getMaybeErrorState(jsg::Lock& js); + + kj::Maybe tryTeeSource(uint64_t limit); + kj::Maybe> tryReleaseSource() KJ_WARN_UNUSED_RESULT; + bool isInternal() const; + StreamEncoding getPreferredEncoding(); + kj::Maybe tryGetLength(StreamEncoding encoding); + private: kj::Rc> weakSelf; kj::Maybe ioContext; @@ -647,39 +819,43 @@ class WritableStreamDefaultController: public jsg::Object { public: using WritableImpl = WritableImpl; - explicit WritableStreamDefaultController( - jsg::Lock& js, WritableStream& owner, jsg::Ref abortSignal); + explicit WritableStreamDefaultController(jsg::Lock& js, + WritableStream& owner, + kj::Own sink, + jsg::Ref abortSignal); ~WritableStreamDefaultController() noexcept(false); - jsg::Promise abort(jsg::Lock& js, v8::Local reason); + jsg::Promise abort(jsg::Lock& js, jsg::JsValue reason) KJ_WARN_UNUSED_RESULT; - jsg::Promise close(jsg::Lock& js); + jsg::Promise close(jsg::Lock& js) KJ_WARN_UNUSED_RESULT; - void error(jsg::Lock& js, jsg::Optional> reason); + void error(jsg::Lock& js, jsg::Optional reason); - kj::Maybe getDesiredSize(); + kj::Maybe getDesiredSize(); - jsg::Ref getSignal(); + jsg::Ref getSignal() KJ_WARN_UNUSED_RESULT; - kj::Maybe> isErroring(jsg::Lock& js); + kj::Maybe isErroring(jsg::Lock& js) KJ_WARN_UNUSED_RESULT; // Returns true if the stream is in the erroring state. Unlike the overload // that takes a lock, this method does not require a lock since it doesn't // return the error reason. bool isErroring() const; - bool isStarted() { + bool isStarted() const { return impl.flags.started; } - bool hasBackpressure() { + bool hasBackpressure() const { return impl.flags.backpressure; } - void setup(jsg::Lock& js, UnderlyingSink underlyingSink, StreamQueuingStrategy queuingStrategy); + void setup(jsg::Lock& js); + + jsg::Promise write(jsg::Lock& js, jsg::JsValue value) KJ_WARN_UNUSED_RESULT; - jsg::Promise write(jsg::Lock& js, v8::Local value); + jsg::Promise flush(jsg::Lock& js, MarkAsHandled markAsHandled) KJ_WARN_UNUSED_RESULT; JSG_RESOURCE_TYPE(WritableStreamDefaultController) { JSG_READONLY_PROTOTYPE_PROPERTY(signal, getSignal); @@ -693,7 +869,12 @@ class WritableStreamDefaultController: public jsg::Object { // Clear algorithms to break circular references during destruction void clearAlgorithms(); + bool isInternal() const; + kj::Maybe> tryReleaseSink() KJ_WARN_UNUSED_RESULT; + kj::Maybe tryGetSink() KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT; + private: + kj::Rc> weakSelf; kj::Maybe ioContext; WritableImpl impl; @@ -712,25 +893,23 @@ class WritableStreamDefaultController: public jsg::Object { // long after both the readable and writable sides have been GC'ed. class TransformStreamDefaultController: public jsg::Object { public: - TransformStreamDefaultController(jsg::Lock& js); + TransformStreamDefaultController(jsg::Lock& js, kj::Own transformer); + ~TransformStreamDefaultController() noexcept(false); - void init(jsg::Lock& js, - jsg::Ref& readable, - jsg::Ref& writable, - jsg::Optional maybeTransformer); + void init(jsg::Lock& js, jsg::Ref& readable, jsg::Ref& writable); // The startPromise is used by both the readable and writable sides in their respective // start algorithms. The promise itself is resolved within the init function when the // transformers own start algorithm completes. - inline jsg::Promise getStartPromise(jsg::Lock& js) { + jsg::Promise getStartPromise(jsg::Lock& js) KJ_WARN_UNUSED_RESULT { return startPromise.promise.whenResolved(js); } kj::Maybe getDesiredSize(); - void enqueue(jsg::Lock& js, v8::Local chunk); + void enqueue(jsg::Lock& js, jsg::JsValue chunk); - void error(jsg::Lock& js, v8::Local reason); + void error(jsg::Lock& js, jsg::JsValue reason); void terminate(jsg::Lock& js); @@ -745,64 +924,109 @@ class TransformStreamDefaultController: public jsg::Object { }); } - jsg::Promise write(jsg::Lock& js, v8::Local chunk); - jsg::Promise abort(jsg::Lock& js, v8::Local reason); - jsg::Promise close(jsg::Lock& js); - jsg::Promise pull(jsg::Lock& js); - jsg::Promise cancel(jsg::Lock& js, v8::Local reason); + jsg::Promise write(jsg::Lock& js, jsg::JsValue chunk) KJ_WARN_UNUSED_RESULT; + jsg::Promise writev( + jsg::Lock& js, kj::Array> chunks) KJ_WARN_UNUSED_RESULT; + jsg::Promise abort(jsg::Lock& js, jsg::JsValue reason) KJ_WARN_UNUSED_RESULT; + jsg::Promise close(jsg::Lock& js) KJ_WARN_UNUSED_RESULT; + jsg::Promise pull(jsg::Lock& js) KJ_WARN_UNUSED_RESULT; + jsg::Promise cancel(jsg::Lock& js, jsg::JsValue reason) KJ_WARN_UNUSED_RESULT; void visitForMemoryInfo(jsg::MemoryTracker& tracker) const; private: - struct Algorithms { - kj::Maybe> transform; - kj::Maybe> flush; - kj::Maybe> cancel; - - kj::Maybe> maybeFinish = kj::none; - // This flag is set to true at the start of a finish operation (close/cancel/abort) - // before the algorithm runs. This is needed because emplace() evaluates its argument - // before setting maybeFinish, so if the algorithm calls another finish operation - // synchronously, maybeFinish wouldn't be set yet. - bool finishStarted = false; - - Algorithms() {}; - Algorithms(Algorithms&& other) = default; - Algorithms& operator=(Algorithms&& other) = default; - - inline void clear() { - transform = kj::none; - flush = kj::none; - cancel = kj::none; - } - - inline void visitForGc(jsg::GcVisitor& visitor) { - visitor.visit(transform, flush, cancel, maybeFinish); - } - }; - - void errorWritableAndUnblockWrite(jsg::Lock& js, v8::Local reason); - jsg::Promise performTransform(jsg::Lock& js, v8::Local chunk); - void setBackpressure(jsg::Lock& js, bool newBackpressure); + void errorWritableAndUnblockWrite(jsg::Lock& js, jsg::JsValue reason); + jsg::Promise performTransform(jsg::Lock& js, jsg::JsValue chunk) KJ_WARN_UNUSED_RESULT; + jsg::Promise performTransformv( + jsg::Lock& js, kj::Array> chunks) KJ_WARN_UNUSED_RESULT; + void setBackpressure(jsg::Lock& js, UpdateBackpressure newBackpressure); + kj::Rc> weakSelf; kj::Maybe ioContext; jsg::PromiseResolverPair startPromise; + kj::Own transformer; + + // Dispatch a callable over whichever readable controller variant is stored. + // Returns kj::none when the readable has been released (e.g. after cancel). + template + auto withReadableController( + F&& f) -> kj::Maybe()))>; - kj::Maybe tryGetReadableController(); - kj::Maybe tryGetWritableController(); + kj::Maybe tryGetWritableController() + KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT; - kj::Maybe getReadableErrorState(jsg::Lock& js); + kj::Maybe getReadableErrorState(jsg::Lock& js) KJ_WARN_UNUSED_RESULT; - // Currently, JS-backed transform streams only support value-oriented streams. - // In the future, that may change and this will need to become a kj::OneOf - // that includes a ReadableByteStreamController. - kj::Maybe> readable; + using ReadableController = + kj::OneOf, jsg::Ref>; + kj::Maybe readable; kj::Maybe> writable; - Algorithms algorithms; - bool backpressure = false; + kj::Maybe> maybeFinish; + + struct Flags { + uint8_t finishStarted : 1 = 0; + uint8_t backpressure : 1 = 0; + uint8_t fixupBackpressure : 1 = 0; + }; + Flags flags{}; kj::Maybe> maybeBackpressureChange; + // Persistent transform continuation — used by performTransform() for the hot + // write-through path. The success callback is a no-op, the failure callback + // errors the stream. + struct TransformContinuationCallbacks { + // Strong reference keeps the controller alive through the callback. + // This creates a cycle (controller → continuation → wrappable → callbacks → Ref), + // which is broken when the PersistentContinuation is cleared (destructor). + jsg::Ref ref; + + jsg::Promise thenFunc(jsg::Lock& js) KJ_WARN_UNUSED_RESULT { + return js.resolvedPromise(); + } + + jsg::Promise catchFunc(jsg::Lock& js, jsg::Value reason) KJ_WARN_UNUSED_RESULT { + auto handle = jsg::JsValue(reason.getHandle(js)); + ref->error(js, handle); + return js.rejectedPromise(handle); + } + }; + using TransformContinuationType = + jsg::PersistentContinuation>; + + kj::Maybe transformContinuation; + + TransformContinuationType& getTransformContinuation(jsg::Lock& js, + jsg::Ref self) KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT; + void visitForGc(jsg::GcVisitor& visitor); }; +template +auto TransformStreamDefaultController::withReadableController( + F&& f) -> kj::Maybe()))> { + KJ_IF_SOME(rc, readable) { + KJ_SWITCH_ONEOF(rc) { + KJ_CASE_ONEOF(defaultCtrl, jsg::Ref) { + return f(*defaultCtrl); + } + KJ_CASE_ONEOF(byteCtrl, jsg::Ref) { + return f(*byteCtrl); + } + } + KJ_UNREACHABLE; + } + return kj::none; +} + +// ======================================================================================= + +jsg::Ref newInternalWritableStream(jsg::Lock& js, + IoContext& ioContext, + kj::Own sink, + kj::Maybe> observer = kj::none, + kj::Maybe maybeHighWaterMark = kj::none) KJ_WARN_UNUSED_RESULT; +jsg::Ref newInternalReadableStream(jsg::Lock& js, + IoContext& ioContext, + kj::Own source) KJ_WARN_UNUSED_RESULT; + } // namespace workerd::api diff --git a/src/workerd/api/streams/transform.c++ b/src/workerd/api/streams/transform.c++ index 812b6250d8b..7ff7040c6ac 100644 --- a/src/workerd/api/streams/transform.c++ +++ b/src/workerd/api/streams/transform.c++ @@ -22,81 +22,116 @@ jsg::Function maybeAddFunctor(auto t) { } } // namespace +jsg::Ref TransformStream::constructorNoCheck(jsg::Lock& js, + jsg::Optional maybeTransformer, + jsg::Optional maybeWritableStrategy, + jsg::Optional maybeReadableStrategy, + ReadableIsBytes readableIsBytes) { + // The standard implementation. Here the TransformStream is backed by readable + // and writable streams using the JavaScript-backed controllers. Data that is + // written to the writable side passes through the transform function that is + // given in maybeTransformer. If no transform function is given, then any value + // written is passed through unchanged. + // + // Per the standard specification, any JavaScript value can be written to and + // read from the transform stream, and the readable side does *not* support BYOB + // reads. + // + // Persistent references to the TransformStreamDefaultController are held by both + // the readable and writable sides. The actual TransformStream object can be dropped + // and allowed to be garbage collected. + + // ReadableIsBytes is a non-standard extension that signals that the readable side + // of the transform stream should use a ReadableByteStreamController and support + // BYOB reads. + + auto transformer = kj::mv(maybeTransformer).orDefault(Transformer{}); + auto expectedLength = transformer.expectedLength; + + auto transformerImpl = kj::heap(js, kj::mv(transformer)); + bool hasTransformv = transformerImpl->transformv() != kj::none; + auto controller = js.alloc(js, kj::mv(transformerImpl)); + + // By default, let's signal backpressure on the readable side by setting the highWaterMark + // to zero if a strategy is not given. This effectively means that writes/reads will be + // one to one as long as the writer is respecting backpressure signals. If buffering + // occurs, it will happen in the writable side of the transform stream. + auto readableStrategy = kj::mv(maybeReadableStrategy) + .orDefault(StreamQueuingStrategy{ + .highWaterMark = 0, + }); + auto writableStrategy = kj::mv(maybeWritableStrategy).orDefault(StreamQueuingStrategy{}); + + auto readableController = newReadableStreamJsController(); + auto readable = js.alloc(kj::mv(readableController)); + readable->getController().setup(js, + kj::heap(js, + UnderlyingSource{ + .type = readableIsBytes ? kj::Maybe(kj::str("bytes")) : kj::none, + .autoAllocateChunkSize = readableIsBytes + ? kj::Maybe(UnderlyingSource::DEFAULT_AUTO_ALLOCATE_CHUNK_SIZE_2) + : kj::none, + .start = maybeAddFunctor( + JSG_VISITABLE_LAMBDA((controller = controller.addRef()), (controller), + (jsg::Lock & js, auto c) mutable { return controller->getStartPromise(js); })), + .pull = maybeAddFunctor( + JSG_VISITABLE_LAMBDA((controller = controller.addRef()), (controller), + (jsg::Lock & js, auto c) mutable { return controller->pull(js); })), + .cancel = maybeAddFunctor( JSG_VISITABLE_LAMBDA( + (controller = controller.addRef()), (controller), + (jsg::Lock & js, auto reason) mutable { return controller->cancel(js, reason); })), + .expectedLength = expectedLength, + }, + kj::mv(readableStrategy))); + + auto writableController = newWritableStreamJsController(); + auto writable = js.alloc(kj::mv(writableController)); + + auto sink = kj::heap(js, + UnderlyingSink{ + .type = kj::none, + .start = maybeAddFunctor( + JSG_VISITABLE_LAMBDA((controller = controller.addRef()), (controller), + (jsg::Lock & js, auto c) mutable { return controller->getStartPromise(js); })), + .write = maybeAddFunctor( JSG_VISITABLE_LAMBDA( + (controller = controller.addRef()), (controller), + (jsg::Lock & js, auto chunk, auto c) mutable { return controller->write(js, chunk); })), + .abort = maybeAddFunctor( + JSG_VISITABLE_LAMBDA((controller = controller.addRef()), (controller), + (jsg::Lock & js, auto reason) mutable { return controller->abort(js, reason); })), + .close = maybeAddFunctor( + JSG_VISITABLE_LAMBDA((controller = controller.addRef()), (controller), + (jsg::Lock & js) mutable { return controller->close(js); })), + }, + kj::mv(writableStrategy)); + + // If the transform supports the optional vectorized transform algorithm, then we also + // set up a vectorized writev function on the sink. + if (hasTransformv) { + sink->setWritev(maybeAddFunctor( + JSG_VISITABLE_LAMBDA((controller = controller.addRef()), (controller), + (jsg::Lock & js, kj::Array> chunks, auto c) mutable { + return controller->writev(js, kj::mv(chunks)); + }))); + } + + writable->getController().setup(js, kj::mv(sink)); + + // The controller will store c++ references to both the readable and writable + // streams underlying controllers. + controller->init(js, readable, writable); + + return js.alloc(kj::mv(readable), kj::mv(writable)); +} + jsg::Ref TransformStream::constructor(jsg::Lock& js, jsg::Optional maybeTransformer, jsg::Optional maybeWritableStrategy, jsg::Optional maybeReadableStrategy) { if (FeatureFlags::get(js).getTransformStreamJavaScriptControllers()) { - // The standard implementation. Here the TransformStream is backed by readable - // and writable streams using the JavaScript-backed controllers. Data that is - // written to the writable side passes through the transform function that is - // given in maybeTransformer. If no transform function is given, then any value - // written is passed through unchanged. - // - // Per the standard specification, any JavaScript value can be written to and - // read from the transform stream, and the readable side does *not* support BYOB - // reads. - // - // Persistent references to the TransformStreamDefaultController are held by both - // the readable and writable sides. The actual TransformStream object can be dropped - // and allowed to be garbage collected. - - auto controller = js.alloc(js); - auto transformer = kj::mv(maybeTransformer).orDefault({}); - - // By default, let's signal backpressure on the readable side by setting the highWaterMark - // to zero if a strategy is not given. This effectively means that writes/reads will be - // one to one as long as the writer is respecting backpressure signals. If buffering - // occurs, it will happen in the writable side of the transform stream. - auto readableStrategy = kj::mv(maybeReadableStrategy) - .orDefault(StreamQueuingStrategy{ - .highWaterMark = 0, - }); - - auto readable = ReadableStream::constructor(js, - UnderlyingSource{ - .type = kj::none, - .autoAllocateChunkSize = kj::none, - .start = maybeAddFunctor( - JSG_VISITABLE_LAMBDA((controller = controller.addRef()), (controller), - (jsg::Lock & js, auto c) mutable { return controller->getStartPromise(js); })), - .pull = maybeAddFunctor( - JSG_VISITABLE_LAMBDA((controller = controller.addRef()), (controller), - (jsg::Lock & js, auto c) mutable { return controller->pull(js); })), - .cancel = maybeAddFunctor( JSG_VISITABLE_LAMBDA( - (controller = controller.addRef()), (controller), - (jsg::Lock & js, auto reason) mutable { return controller->cancel(js, reason); })), - .expectedLength = transformer.expectedLength.map( - [](uint64_t expectedLength) { return expectedLength; }), - }, - kj::mv(readableStrategy)); - - auto writable = WritableStream::constructor(js, - UnderlyingSink{ - .type = kj::none, - .start = maybeAddFunctor( - JSG_VISITABLE_LAMBDA((controller = controller.addRef()), (controller), - (jsg::Lock & js, auto c) mutable { return controller->getStartPromise(js); })), - .write = maybeAddFunctor( - JSG_VISITABLE_LAMBDA((controller = controller.addRef()), (controller), - (jsg::Lock & js, auto chunk, auto c) mutable { - return controller->write(js, chunk); - })), - .abort = maybeAddFunctor( - JSG_VISITABLE_LAMBDA((controller = controller.addRef()), (controller), - (jsg::Lock & js, auto reason) mutable { return controller->abort(js, reason); })), - .close = maybeAddFunctor( - JSG_VISITABLE_LAMBDA((controller = controller.addRef()), (controller), - (jsg::Lock & js) mutable { return controller->close(js); })), - }, - kj::mv(maybeWritableStrategy)); - - // The controller will store c++ references to both the readable and writable - // streams underlying controllers. - controller->init(js, readable, writable, kj::mv(transformer)); - - return js.alloc(kj::mv(readable), kj::mv(writable)); + return constructorNoCheck( + js, kj::mv(maybeTransformer), kj::mv(maybeWritableStrategy), kj::mv(maybeReadableStrategy)); } // The old implementation just defers to IdentityTransformStream. If any of the arguments diff --git a/src/workerd/api/streams/transform.h b/src/workerd/api/streams/transform.h index b6b0ad36abe..e40e1a0f86f 100644 --- a/src/workerd/api/streams/transform.h +++ b/src/workerd/api/streams/transform.h @@ -9,6 +9,8 @@ namespace workerd::api { +WD_STRONG_BOOL(ReadableIsBytes); + // A TransformStream is a readable, writable pair in which whatever is written to the writable // side can be read from the readable side, possibly transformed into a different type of value. // @@ -31,6 +33,15 @@ class TransformStream: public jsg::Object { jsg::Optional maybeWritableStrategy, jsg::Optional maybeReadableStrategy); + // When readableIsBytes is true, the readable side uses a ReadableByteStreamController + // instead of ReadableStreamDefaultController, enabling BYOB reads. This is a non-standard + // extension for internal use by CompressionStream/DecompressionStream. + static jsg::Ref constructorNoCheck(jsg::Lock& js, + jsg::Optional maybeTransformer, + jsg::Optional maybeWritableStrategy, + jsg::Optional maybeReadableStrategy, + ReadableIsBytes readableIsBytes = ReadableIsBytes::NO); + jsg::Ref getReadable() { return readable.addRef(); } diff --git a/src/workerd/api/streams/writable-sink-adapter-test.c++ b/src/workerd/api/streams/writable-sink-adapter-test.c++ index eeaa836bacb..848d71ddd70 100644 --- a/src/workerd/api/streams/writable-sink-adapter-test.c++ +++ b/src/workerd/api/streams/writable-sink-adapter-test.c++ @@ -612,11 +612,7 @@ KJ_TEST("zero-length writes are a non-op (ArrayBuffer)") { auto adapter = kj::heap( env.js, env.context, newWritableSink(kj::mv(recordingSink))); - auto backing = jsg::BackingStore::alloc(env.js, 0); - jsg::BufferSource source(env.js, kj::mv(backing)); - jsg::JsValue handle(source.getHandle(env.js)); - - auto writePromise = adapter->write(env.js, handle); + auto writePromise = adapter->write(env.js, jsg::JsArrayBuffer::create(env.js, 0)); KJ_ASSERT(state.writeCalled == 0, "Underlying sink's write() should not have been called"); return env.context @@ -638,11 +634,7 @@ KJ_TEST("writing small ArrayBuffer") { .highWaterMark = 10, }); - auto backing = jsg::BackingStore::alloc(env.js, 10); - jsg::BufferSource source(env.js, kj::mv(backing)); - jsg::JsValue handle(source.getHandle(env.js)); - - auto writePromise = adapter->write(env.js, handle); + auto writePromise = adapter->write(env.js, jsg::JsArrayBuffer::create(env.js, 10)); KJ_ASSERT(state.writeCalled == 1, "Underlying sink's write() should not have been called"); KJ_ASSERT(KJ_ASSERT_NONNULL(adapter->getDesiredSize()) == 0, "Adapter's desired size should be 0 after writing highWaterMark bytes"); @@ -668,11 +660,7 @@ KJ_TEST("writing medium ArrayBuffer") { .highWaterMark = 5 * 1024, }); - auto backing = jsg::BackingStore::alloc(env.js, 4 * 1024); - jsg::BufferSource source(env.js, kj::mv(backing)); - jsg::JsValue handle(source.getHandle(env.js)); - - auto writePromise = adapter->write(env.js, handle); + auto writePromise = adapter->write(env.js, jsg::JsArrayBuffer::create(env.js, 4 * 1024)); KJ_ASSERT(state.writeCalled == 1, "Underlying sink's write() should not have been called"); KJ_ASSERT(KJ_ASSERT_NONNULL(adapter->getDesiredSize()) == 1024, "Adapter's desired size should be 1024 after writing 4 * 1024 bytes"); @@ -698,11 +686,7 @@ KJ_TEST("writing large ArrayBuffer") { .highWaterMark = 8 * 1024, }); - auto backing = jsg::BackingStore::alloc(env.js, 16 * 1024); - jsg::BufferSource source(env.js, kj::mv(backing)); - jsg::JsValue handle(source.getHandle(env.js)); - - auto writePromise = adapter->write(env.js, handle); + auto writePromise = adapter->write(env.js, jsg::JsArrayBuffer::create(env.js, 16 * 1024)); KJ_ASSERT(state.writeCalled == 1, "Underlying sink's write() should not have been called"); KJ_ASSERT(KJ_ASSERT_NONNULL(adapter->getDesiredSize()) == -(8 * 1024), "Adapter's desired size should be negative after writing 16 * 1024 bytes"); @@ -756,11 +740,7 @@ KJ_TEST("large number of large writes") { kj::heap(env.js, env.context, newWritableSink(kj::mv(fake))); for (int i = 0; i < 1000; i++) { - auto backing = jsg::BackingStore::alloc(env.js, 16 * 1024); - jsg::BufferSource source(env.js, kj::mv(backing)); - jsg::JsValue handle(source.getHandle(env.js)); - - adapter->write(env.js, handle); + adapter->write(env.js, jsg::JsArrayBuffer::create(env.js, 16 * 1024)); } auto endPromise = adapter->end(env.js); @@ -813,15 +793,9 @@ KJ_TEST("detachOnWrite option detaches ArrayBuffer before write") { .detachOnWrite = true, }); - auto backing = jsg::BackingStore::alloc(env.js, 10); - jsg::BufferSource source(env.js, kj::mv(backing)); - KJ_ASSERT(!source.isDetached()); - jsg::JsValue handle(source.getHandle(env.js)); - + auto handle = jsg::JsArrayBuffer::create(env.js, 10); auto writePromise = adapter->write(env.js, handle); - - jsg::BufferSource source2(env.js, handle); - KJ_ASSERT(source2.size() == 0); + KJ_ASSERT(handle.size() == 0); return env.context.awaitJs(env.js, kj::mv(writePromise)).attach(kj::mv(adapter)); }); @@ -838,15 +812,10 @@ KJ_TEST("detachOnWrite option detaches Uint8Array before write") { .detachOnWrite = true, }); - auto backing = jsg::BackingStore::alloc(env.js, 10); - jsg::BufferSource source(env.js, kj::mv(backing)); - KJ_ASSERT(!source.isDetached()); - jsg::JsValue handle(source.getHandle(env.js)); - + auto handle = jsg::JsUint8Array::create(env.js, 10); auto writePromise = adapter->write(env.js, handle); - jsg::BufferSource source2(env.js, handle); - KJ_ASSERT(source2.size() == 0); + KJ_ASSERT(handle.size() == 0); return env.context.awaitJs(env.js, kj::mv(writePromise)).attach(kj::mv(adapter)); }); @@ -911,9 +880,7 @@ jsg::Ref createSimpleWritableStream(jsg::Lock& js, WritableStrea UnderlyingSink{ .write = [&context](jsg::Lock& js, auto chunk, auto) { - jsg::BufferSource source(js, chunk); - auto data = kj::heapArray(source.asArrayPtr()); - context.chunks.add(kj::mv(data)); + context.chunks.add(jsg::JsBufferSource(chunk).copy()); return js.resolvedPromise(); }, .abort = diff --git a/src/workerd/api/streams/writable-sink-adapter.c++ b/src/workerd/api/streams/writable-sink-adapter.c++ index 4b15143776b..d70dee73409 100644 --- a/src/workerd/api/streams/writable-sink-adapter.c++ +++ b/src/workerd/api/streams/writable-sink-adapter.c++ @@ -204,12 +204,11 @@ jsg::Promise WritableStreamSinkJsAdapter::write(jsg::Lock& js, const jsg:: // types: ArrayBuffer, ArrayBufferView, and String. If it is a string, // we convert it to UTF-8 bytes. Anything else is an error. if (value.isArrayBufferView() || value.isArrayBuffer() || value.isSharedArrayBuffer()) { - // We can just wrap the value with a jsg::BufferSource and write it. - jsg::BufferSource source(js, value); - if (active.options.detachOnWrite && source.canDetach(js)) { + jsg::JsBufferSource source(value); + if (active.options.detachOnWrite && source.isDetachable()) { // Detach from the original ArrayBuffer... - // ... and re-wrap it with a new BufferSource that we own. - source = jsg::BufferSource(js, source.detach(js)); + // ... and re-wrap it with a new view that we own. + source = source.detachAndTake(js); } // Zero-length writes are a no-op. @@ -240,10 +239,11 @@ jsg::Promise WritableStreamSinkJsAdapter::write(jsg::Lock& js, const jsg:: // held by the write queue, which is itself held by Active. If active // is destroyed, the write queue is destroyed along with the lambda. auto promise = - active.enqueue(kj::coCapture([&active, source = kj::mv(source)]() -> kj::Promise { - co_await active.sink->write(source.asArrayPtr()); + active + .enqueue(kj::coCapture([&active, source = source.asArrayPtr()]() -> kj::Promise { + co_await active.sink->write(source); active.bytesInFlight -= source.size(); - })); + })).attach(source.addRef(js)); return ioContext .awaitIo(js, kj::mv(promise), [self = selfRef.addRef()](jsg::Lock& js) { // Why do we need a weak ref here? Well, because this is a JavaScript @@ -608,17 +608,16 @@ kj::Promise WritableStreamSinkKjAdapter::write( // WritableStream API has no concept of a vector write, so each write // would incur the overhead of a separate promise and microtask checkpoint. // By collapsing into a single write we reduce that overhead. - auto backing = jsg::BackingStore::alloc(js, totalAmount); - auto ptr = backing.asArrayPtr(); + auto source = jsg::JsArrayBuffer::create(js, totalAmount); + auto ptr = source.asArrayPtr(); for (auto piece: pieces) { ptr.first(piece.size()).copyFrom(piece); ptr = ptr.slice(piece.size()); } - jsg::BufferSource source(js, kj::mv(backing)); auto ready = KJ_ASSERT_NONNULL(writer->isReady(js)); - auto promise = - ready.then(js, [writer = writer.addRef(), source = kj::mv(source)](jsg::Lock& js) mutable { + auto promise = ready.then( + js, [writer = writer.addRef(), source = source.addRef(js)](jsg::Lock& js) mutable { return writer->write(js, source.getHandle(js)); }); return IoContext::current().awaitJs(js, kj::mv(promise)); diff --git a/src/workerd/api/streams/writable.c++ b/src/workerd/api/streams/writable.c++ index d0d8eaa4d7b..04c371212da 100644 --- a/src/workerd/api/streams/writable.c++ +++ b/src/workerd/api/streams/writable.c++ @@ -34,7 +34,7 @@ jsg::Promise WritableStreamDefaultWriter::abort( assertAttachedOrTerminal(); if (state.is()) { return js.rejectedPromise( - js.v8TypeError("This WritableStream writer has been released."_kj)); + js.typeError("This WritableStream writer has been released."_kj)); } if (state.is()) { return js.resolvedPromise(); @@ -62,10 +62,10 @@ jsg::Promise WritableStreamDefaultWriter::close(jsg::Lock& js) { assertAttachedOrTerminal(); if (state.is()) { return js.rejectedPromise( - js.v8TypeError("This WritableStream writer has been released."_kj)); + js.typeError("This WritableStream writer has been released."_kj)); } if (state.is()) { - return js.rejectedPromise(js.v8TypeError("This WritableStream has been closed."_kj)); + return js.rejectedPromise(js.typeError("This WritableStream has been closed."_kj)); } auto& attached = state.requireActiveUnsafe(); // In some edge cases, this writer is the last thing holding a strong @@ -114,7 +114,6 @@ void WritableStreamDefaultWriter::lockToStream(jsg::Lock& js, WritableStream& st } void WritableStreamDefaultWriter::releaseLock(jsg::Lock& js) { - // TODO(soon): Releasing the lock should cancel any pending writes. assertAttachedOrTerminal(); // Closed and Released states are no-ops. KJ_IF_SOME(attached, state.tryGetActiveUnsafe()) { @@ -139,10 +138,10 @@ jsg::Promise WritableStreamDefaultWriter::write( assertAttachedOrTerminal(); if (state.is()) { return js.rejectedPromise( - js.v8TypeError("This WritableStream writer has been released."_kj)); + js.typeError("This WritableStream writer has been released."_kj)); } if (state.is()) { - return js.rejectedPromise(js.v8TypeError("This WritableStream has been closed."_kj)); + return js.rejectedPromise(js.typeError("This WritableStream has been closed."_kj)); } auto& attached = state.requireActiveUnsafe(); return attached.stream->getController().write(js, chunk); @@ -219,7 +218,7 @@ jsg::Promise WritableStream::abort( jsg::Lock& js, jsg::Optional> reason) { if (isLocked()) { return js.rejectedPromise( - js.v8TypeError("This WritableStream is currently locked to a writer."_kj)); + js.typeError("This WritableStream is currently locked to a writer."_kj)); } return getController().abort(js, reason); } @@ -227,7 +226,7 @@ jsg::Promise WritableStream::abort( jsg::Promise WritableStream::close(jsg::Lock& js) { if (isLocked()) { return js.rejectedPromise( - js.v8TypeError("This WritableStream is currently locked to a writer."_kj)); + js.typeError("This WritableStream is currently locked to a writer."_kj)); } return getController().close(js); } @@ -235,7 +234,7 @@ jsg::Promise WritableStream::close(jsg::Lock& js) { jsg::Promise WritableStream::flush(jsg::Lock& js) { if (isLocked()) { return js.rejectedPromise( - js.v8TypeError("This WritableStream is currently locked to a writer."_kj)); + js.typeError("This WritableStream is currently locked to a writer."_kj)); } return getController().flush(js); } @@ -256,7 +255,11 @@ jsg::Ref WritableStream::constructor(jsg::Lock& js, // lifetimes are identical and memory accounting itself has a memory overhead. auto stream = js.allocAccounted( sizeof(WritableStream) + controller->jsgGetMemorySelfSize(), kj::mv(controller)); - stream->getController().setup(js, kj::mv(underlyingSink), kj::mv(queuingStrategy)); + + auto sink = kj::heap( + js, kj::mv(underlyingSink).orDefault({}), kj::mv(queuingStrategy).orDefault({})); + + stream->getController().setup(js, kj::mv(sink)); return kj::mv(stream); } @@ -409,9 +412,8 @@ class WritableStreamJsRpcAdapter final: public capnp::ExplicitEndOutputStream { if (buffer == nullptr) return kj::READY_NOW; return canceler.wrap(context.run([this, buffer](Worker::Lock& lock) mutable { auto& writer = getInner(); - auto source = KJ_ASSERT_NONNULL(jsg::BufferSource::tryAlloc(lock, buffer.size())); - source.asArrayPtr().copyFrom(buffer); - return context.awaitJs(lock, writer.write(lock, source.getHandle(lock))); + auto source = jsg::JsArrayBuffer::create(lock, buffer); + return context.awaitJs(lock, writer.write(lock, source)); })); } @@ -430,7 +432,7 @@ class WritableStreamJsRpcAdapter final: public capnp::ExplicitEndOutputStream { // guaranteed to live until the returned promise is resolved, but the application code // may hold onto the ArrayBuffer for longer. We need to make sure that the backing store // for the ArrayBuffer remains valid. - auto source = KJ_ASSERT_NONNULL(jsg::BufferSource::tryAlloc(lock, amount)); + auto source = jsg::JsArrayBuffer::create(lock, amount); auto ptr = source.asArrayPtr(); for (auto& piece: pieces) { KJ_DASSERT(ptr.size() > 0); @@ -440,7 +442,7 @@ class WritableStreamJsRpcAdapter final: public capnp::ExplicitEndOutputStream { ptr = ptr.slice(piece.size()); } - return context.awaitJs(lock, writer.write(lock, source.getHandle(lock))); + return context.awaitJs(lock, writer.write(lock, source)); })); } @@ -516,9 +518,6 @@ void WritableStream::serialize(jsg::Lock& js, jsg::Serializer& serializer) { IoContext& ioctx = IoContext::current(); - // TODO(soon): Support JS-backed WritableStreams. Currently this only supports native streams - // and IdentityTransformStream, since only they are backed by WritableStreamSink. - KJ_IF_SOME(sink, getController().removeSink(js)) { // NOTE: We're counting on `removeSink()`, to check that the stream is not locked and other // common checks. It's important we don't modify the WritableStream before this call. diff --git a/src/workerd/api/system-streams.c++ b/src/workerd/api/system-streams.c++ index 29803822c66..1b85fb69987 100644 --- a/src/workerd/api/system-streams.c++ +++ b/src/workerd/api/system-streams.c++ @@ -166,7 +166,7 @@ class EncodedAsyncOutputStream final: public WritableStreamSink { kj::Promise write(kj::ArrayPtr> pieces) override; kj::Maybe>> tryPumpFrom( - ReadableStreamSource& input, bool end) override; + ReadableStreamSource& input, End end) override; kj::Promise end() override; @@ -224,7 +224,7 @@ kj::Promise EncodedAsyncOutputStream::write( } kj::Maybe>> EncodedAsyncOutputStream::tryPumpFrom( - ReadableStreamSource& input, bool end) { + ReadableStreamSource& input, End end) { // If this output stream has already been ended, then there's nothing more to // pump into it, just return an immediately resolved promise. Alternatively diff --git a/src/workerd/api/tests/pipe-streams-test.js b/src/workerd/api/tests/pipe-streams-test.js index 28a60d586ec..45bbba5e4f3 100644 --- a/src/workerd/api/tests/pipe-streams-test.js +++ b/src/workerd/api/tests/pipe-streams-test.js @@ -10,7 +10,7 @@ export const pipeThroughJsToInternal = { async test() { const enc = new TextEncoder(); const dec = new TextDecoder(); - const chunks = [enc.encode('hello'), enc.encode('there'), 'hello']; + const chunks = [enc.encode('hello'), enc.encode('there'), '!', 1]; const rs = new ReadableStream({ pull(c) { c.enqueue(chunks.shift()); @@ -26,12 +26,13 @@ export const pipeThroughJsToInternal = { output.push(dec.decode(chunk)); } } - // The 'hello' string at the end of chunks will cause an error to be thrown. - await rejects(consumeStream, { + // The 1 number at the end of chunks will cause an error to be thrown. + await rejects(consumeStream(), { message: 'This WritableStream only supports writing byte types.', }); - deepStrictEqual(output, ['hello', 'there']); + // But we should have received the valid chunks before the error. + deepStrictEqual(output, ['hello', 'there', '!']); }, }; diff --git a/src/workerd/api/tests/streams-byob-edge-cases-test.js b/src/workerd/api/tests/streams-byob-edge-cases-test.js index deae4f9d3cb..b58a7204824 100644 --- a/src/workerd/api/tests/streams-byob-edge-cases-test.js +++ b/src/workerd/api/tests/streams-byob-edge-cases-test.js @@ -109,6 +109,7 @@ export const byobFloat32Array = { ok(!done); ok(value instanceof Float32Array); + strictEqual(value.length, 2); ok(Math.abs(value[0] - 3.14) < 0.001); ok(Math.abs(value[1] - 2.71) < 0.001); diff --git a/src/workerd/api/tests/streams-js-test.js b/src/workerd/api/tests/streams-js-test.js index 397e4762ff8..9475a81c784 100644 --- a/src/workerd/api/tests/streams-js-test.js +++ b/src/workerd/api/tests/streams-js-test.js @@ -2352,7 +2352,8 @@ export const queuingStrategies = { ok(startRan); strictEqual(highWaterMark, 10); - strictEqual(size('nothing'), undefined); + // Non-standard, but strings are interpreted as UTF-8 length... + strictEqual(size('nothing'), 7); strictEqual(size(123), undefined); strictEqual(size(undefined), undefined); strictEqual(size(null), undefined); diff --git a/src/workerd/api/tests/streams-respond-test.js b/src/workerd/api/tests/streams-respond-test.js index 42cedd8929e..99c3c4635b2 100644 --- a/src/workerd/api/tests/streams-respond-test.js +++ b/src/workerd/api/tests/streams-respond-test.js @@ -621,7 +621,7 @@ export const jsNotBytesInPull = { async test() { const rs = new ReadableStream({ pull(c) { - c.enqueue('hello'); + c.enqueue(12); c.close(); }, }); @@ -635,7 +635,7 @@ export const jsNotBytesInStart = { async test() { const rs = new ReadableStream({ start(c) { - c.enqueue('hello'); + c.enqueue(1); c.close(); }, }); diff --git a/src/workerd/api/web-socket.c++ b/src/workerd/api/web-socket.c++ index ea58697e5b0..87adcf04c13 100644 --- a/src/workerd/api/web-socket.c++ +++ b/src/workerd/api/web-socket.c++ @@ -1076,8 +1076,8 @@ kj::Promise> WebSocket::readLoop( auto blob = js.alloc(js, jsg::JsBufferSource(ab), kj::str()); dispatchEventImpl(js, js.alloc(js, kj::str("message"), kj::mv(blob))); } else { - auto ab = js.arrayBuffer(kj::mv(data)).getHandle(js); - dispatchEventImpl(js, js.alloc(js, jsg::JsValue(ab))); + auto ab = js.arrayBuffer(data); + dispatchEventImpl(js, js.alloc(js, ab)); } } KJ_CASE_ONEOF(close, kj::WebSocket::Close) { diff --git a/src/workerd/io/bundle-fs-test.c++ b/src/workerd/io/bundle-fs-test.c++ index e99ce4f104c..2b1b4946921 100644 --- a/src/workerd/io/bundle-fs-test.c++ +++ b/src/workerd/io/bundle-fs-test.c++ @@ -81,7 +81,7 @@ KJ_TEST("The BundleDirectoryDelegate works") { auto readText = file->readAllText(env.js).get(); KJ_EXPECT(readText == env.js.str("this is a commonjs module"_kj)); - auto readBytes = file->readAllBytes(env.js).get(); + auto readBytes = file->readAllBytes(env.js).get(); KJ_EXPECT(readBytes.asArrayPtr() == "this is a commonjs module"_kjb); // Reading five bytes from offset 20 should return "odule". diff --git a/src/workerd/io/worker-fs.c++ b/src/workerd/io/worker-fs.c++ index ae67afc757a..88583df0fe7 100644 --- a/src/workerd/io/worker-fs.c++ +++ b/src/workerd/io/worker-fs.c++ @@ -1153,14 +1153,14 @@ kj::OneOf File::readAllText(jsg::Lock& js) { return js.str(data); } -kj::OneOf File::readAllBytes(jsg::Lock& js) { +kj::OneOf File::readAllBytes(jsg::Lock& js) { auto info = stat(js); KJ_DASSERT(info.type == FsType::FILE); - auto backing = jsg::BackingStore::alloc(js, info.size); + auto u8 = jsg::JsUint8Array::create(js, info.size); if (info.size > 0) { - KJ_ASSERT(read(js, 0, backing) == info.size); + KJ_ASSERT(read(js, 0, u8.asArrayPtr()) == info.size); } - return jsg::BufferSource(js, kj::mv(backing)); + return u8; } void Directory::Builder::add( diff --git a/src/workerd/io/worker-fs.h b/src/workerd/io/worker-fs.h index 3bc929e8749..cfa0be43cd2 100644 --- a/src/workerd/io/worker-fs.h +++ b/src/workerd/io/worker-fs.h @@ -220,7 +220,7 @@ class File: public kj::Refcounted { kj::OneOf readAllText(jsg::Lock& js) KJ_WARN_UNUSED_RESULT; // Reads all the contents of the file as a Uint8Array. - kj::OneOf readAllBytes(jsg::Lock& js) KJ_WARN_UNUSED_RESULT; + kj::OneOf readAllBytes(jsg::Lock& js) KJ_WARN_UNUSED_RESULT; // Reads data from the file at the given offset into the given buffer. virtual uint32_t read(jsg::Lock& js, uint32_t offset, kj::ArrayPtr buffer) const = 0; diff --git a/src/workerd/jsg/buffersource.h b/src/workerd/jsg/buffersource.h index 540502a8a21..df1d0b0dbd8 100644 --- a/src/workerd/jsg/buffersource.h +++ b/src/workerd/jsg/buffersource.h @@ -492,8 +492,4 @@ class BufferSourceWrapper { } }; -inline BufferSource Lock::arrayBuffer(kj::Array data) { - return BufferSource(*this, BackingStore::from(*this, kj::mv(data))); -} - } // namespace workerd::jsg diff --git a/src/workerd/jsg/iterator.h b/src/workerd/jsg/iterator.h index ee8f314668f..05407e4d63a 100644 --- a/src/workerd/jsg/iterator.h +++ b/src/workerd/jsg/iterator.h @@ -301,6 +301,12 @@ class AsyncGenerator final { return js.rejectedPromise>(kj::mv(exception)); } + void visitForGc(GcVisitor& visitor) { + KJ_IF_SOME(active, maybeActive) { + active.visitForGc(visitor); + } + } + private: using Next = GeneratorNext; using NextSignature = Function()>; diff --git a/src/workerd/jsg/jsg.h b/src/workerd/jsg/jsg.h index c78cb4c42c9..46aa6887b07 100644 --- a/src/workerd/jsg/jsg.h +++ b/src/workerd/jsg/jsg.h @@ -2197,7 +2197,8 @@ class JsMessage; V(Function) \ V(Uint8Array) \ V(ArrayBuffer) \ - V(ArrayBufferView) + V(ArrayBufferView) \ + V(SharedArrayBuffer) #define V(Name) class Js##Name; JS_TYPE_CLASSES(V) @@ -2770,13 +2771,8 @@ class Lock { template JsObject opaque(T&& inner) KJ_WARN_UNUSED_RESULT; - // Returns a jsg::BufferSource whose underlying JavaScript handle is a Uint8Array. - BufferSource bytes(kj::Array data) KJ_WARN_UNUSED_RESULT; - - // Returns a jsg::BufferSource whose underlying JavaScript handle is an ArrayBuffer - // as opposed to the default Uint8Array. May copy and move the bytes if they are - // not in the right sandbox. - BufferSource arrayBuffer(kj::Array data) KJ_WARN_UNUSED_RESULT; + JsUint8Array bytes(kj::ArrayPtr data) KJ_WARN_UNUSED_RESULT; + JsArrayBuffer arrayBuffer(kj::ArrayPtr data) KJ_WARN_UNUSED_RESULT; enum class AllocOption { ZERO_INITIALIZED, UNINITIALIZED }; diff --git a/src/workerd/jsg/jsvalue.c++ b/src/workerd/jsg/jsvalue.c++ index 08cd1bc57db..f6e8acd9898 100644 --- a/src/workerd/jsg/jsvalue.c++ +++ b/src/workerd/jsg/jsvalue.c++ @@ -26,7 +26,7 @@ bool JsValue::strictEquals(const JsValue& other) const { } JsMap::operator JsObject() { - return JsObject(inner); + return jsg::JsObject(inner); } void JsMap::set(Lock& js, const JsValue& name, const JsValue& value) { @@ -154,7 +154,7 @@ JsValue JsObject::getPrototype(Lock& js) { return JsObject(target.As()).getPrototype(js); } JSG_REQUIRE(trap.isFunction(), TypeError, "Proxy getPrototypeOf trap is not a function"); - v8::Local fn = ((v8::Local)trap).As(); + v8::Local fn = (v8::Local(trap)).As(); v8::Local args[] = {target}; auto ret = JsValue(check(fn->Call(js.v8Context(), jsHandler.inner, 1, args))); JSG_REQUIRE(ret.isObject() || ret.isNull(), TypeError, @@ -211,7 +211,7 @@ size_t JsSet::size() const { } JsSet::operator JsArray() const { - return JsArray(inner->AsArray()); + return jsg::JsArray(inner->AsArray()); } kj::Maybe JsInt32::value(Lock& js) const { @@ -343,7 +343,7 @@ void JsArray::add(Lock& js, const JsValue& value) { } JsArray::operator JsObject() const { - return JsObject(inner.As()); + return jsg::JsObject(inner.As()); } kj::String JsString::toString(jsg::Lock& js) const { @@ -659,13 +659,26 @@ uint JsFunction::hashCode() const { return kj::hashCode(obj->GetIdentityHash()); } -BufferSource Lock::bytes(kj::Array data) { - return BufferSource(*this, BackingStore::from(*this, kj::mv(data))); +JsUint8Array Lock::bytes(kj::ArrayPtr data) { + return JsUint8Array::create(*this, data); +} + +JsArrayBuffer Lock::arrayBuffer(kj::ArrayPtr data) { + return JsArrayBuffer::create(*this, data); } // ====================================================================================== // JsArrayBuffer +kj::Maybe JsArrayBuffer::tryCreate(Lock& js, size_t length) { + JSG_REQUIRE(length < v8::ArrayBuffer::kMaxByteLength, RangeError, "The length is too large"); + auto backing = v8::ArrayBuffer::NewBackingStore(js.v8Isolate, length, + v8::BackingStoreInitializationMode::kZeroInitialized, + v8::BackingStoreOnFailureMode::kReturnNull); + if (backing == nullptr) return kj::none; + return create(js, kj::mv(backing)); +} + JsArrayBuffer JsArrayBuffer::create(Lock& js, size_t length) { JSG_REQUIRE(length < v8::ArrayBuffer::kMaxByteLength, RangeError, "The length is too large"); auto backing = v8::ArrayBuffer::NewBackingStore(js.v8Isolate, length, @@ -685,6 +698,10 @@ JsArrayBuffer JsArrayBuffer::create(Lock& js, std::unique_ptr return JsArrayBuffer(v8::ArrayBuffer::New(js.v8Isolate, kj::mv(backingStore))); } +JsArrayBuffer JsArrayBuffer::create(Lock& js, std::shared_ptr backingStore) { + return JsArrayBuffer(v8::ArrayBuffer::New(js.v8Isolate, kj::mv(backingStore))); +} + kj::ArrayPtr JsArrayBuffer::asArrayPtr() { v8::Local inner = *this; if (inner->WasDetached()) [[unlikely]] { @@ -707,15 +724,9 @@ kj::ArrayPtr JsArrayBuffer::asArrayPtr() const { JsArrayBuffer JsArrayBuffer::slice(Lock& js, size_t newLength) const { JSG_REQUIRE(newLength <= size(), RangeError, "New length exceeds buffer length"); - auto backing = v8::ArrayBuffer::NewBackingStore(js.v8Isolate, newLength, - v8::BackingStoreInitializationMode::kUninitialized, - v8::BackingStoreOnFailureMode::kReturnNull); - JSG_REQUIRE(backing != nullptr, RangeError, "Failed to allocate memory for ArrayBuffer"); - auto dest = kj::ArrayPtr(static_cast(backing->Data()), newLength); - v8::Local inner = *this; - dest.copyFrom( - kj::ArrayPtr(static_cast(inner->GetBackingStore()->Data()), newLength)); - return JsArrayBuffer(v8::ArrayBuffer::New(js.v8Isolate, kj::mv(backing))); + auto dest = create(js, newLength); + dest.asArrayPtr().copyFrom(asArrayPtr().slice(0, newLength)); + return dest; } size_t JsArrayBuffer::size() const { @@ -728,6 +739,246 @@ kj::Array JsArrayBuffer::copy() { return kj::heapArray(ptr); } +JsArrayBuffer::operator JsBufferSource() const { + v8::Local inner = *this; + return jsg::JsBufferSource(inner); +} + +bool JsArrayBuffer::isDetachable() const { + v8::Local inner = *this; + return inner->IsDetachable(); +} + +bool JsArrayBuffer::isDetached() const { + v8::Local inner = *this; + return inner->WasDetached(); +} + +void JsArrayBuffer::detachInPlace(Lock& js) { + JSG_REQUIRE(isDetachable(), TypeError, "ArrayBuffer is not detachable"); + v8::Local inner = *this; + check(inner->Detach({})); +} + +JsArrayBuffer JsArrayBuffer::detachAndTake(Lock& js) { + JSG_REQUIRE(isDetachable(), TypeError, "ArrayBuffer is not detachable"); + v8::Local inner = *this; + auto backing = inner->GetBackingStore(); + check(inner->Detach({})); + return JsArrayBuffer(v8::ArrayBuffer::New(js.v8Isolate, kj::mv(backing))); +} + +JsUint8Array JsArrayBuffer::newUint8View(size_t offset, size_t numElements) const { + v8::Local inner = *this; + return JsUint8Array(v8::Uint8Array::New(inner, offset, numElements)); +} +JsArrayBufferView JsArrayBuffer::newInt8View(size_t offset, size_t numElements) const { + v8::Local inner = *this; + return JsArrayBufferView(v8::Int8Array::New(inner, offset, numElements)); +} +JsArrayBufferView JsArrayBuffer::newUint8ClampedView(size_t offset, size_t numElements) const { + v8::Local inner = *this; + return JsArrayBufferView(v8::Uint8ClampedArray::New(inner, offset, numElements)); +} +JsArrayBufferView JsArrayBuffer::newUint16View(size_t offset, size_t numElements) const { + JSG_REQUIRE(size() % 2 == 0, TypeError, "ArrayBuffer size is not a multiple of 2"); + v8::Local inner = *this; + return JsArrayBufferView(v8::Uint16Array::New(inner, offset, numElements)); +} +JsArrayBufferView JsArrayBuffer::newInt16View(size_t offset, size_t numElements) const { + JSG_REQUIRE(size() % 2 == 0, TypeError, "ArrayBuffer size is not a multiple of 2"); + v8::Local inner = *this; + return JsArrayBufferView(v8::Int16Array::New(inner, offset, numElements)); +} +JsArrayBufferView JsArrayBuffer::newUint32View(size_t offset, size_t numElements) const { + JSG_REQUIRE(size() % 4 == 0, TypeError, "ArrayBuffer size is not a multiple of 4"); + v8::Local inner = *this; + return JsArrayBufferView(v8::Uint32Array::New(inner, offset, numElements)); +} +JsArrayBufferView JsArrayBuffer::newInt32View(size_t offset, size_t numElements) const { + JSG_REQUIRE(size() % 4 == 0, TypeError, "ArrayBuffer size is not a multiple of 4"); + v8::Local inner = *this; + return JsArrayBufferView(v8::Int32Array::New(inner, offset, numElements)); +} +JsArrayBufferView JsArrayBuffer::newFloat16View(size_t offset, size_t numElements) const { + JSG_REQUIRE(size() % 2 == 0, TypeError, "ArrayBuffer size is not a multiple of 2"); + v8::Local inner = *this; + return JsArrayBufferView(v8::Float16Array::New(inner, offset, numElements)); +} +JsArrayBufferView JsArrayBuffer::newFloat32View(size_t offset, size_t numElements) const { + JSG_REQUIRE(size() % 4 == 0, TypeError, "ArrayBuffer size is not a multiple of 4"); + v8::Local inner = *this; + return JsArrayBufferView(v8::Float32Array::New(inner, offset, numElements)); +} +JsArrayBufferView JsArrayBuffer::newFloat64View(size_t offset, size_t numElements) const { + JSG_REQUIRE(size() % 8 == 0, TypeError, "ArrayBuffer size is not a multiple of 8"); + v8::Local inner = *this; + return JsArrayBufferView(v8::Float64Array::New(inner, offset, numElements)); +} +JsArrayBufferView JsArrayBuffer::newBigInt64View(size_t offset, size_t numElements) const { + JSG_REQUIRE(size() % 8 == 0, TypeError, "ArrayBuffer size is not a multiple of 8"); + v8::Local inner = *this; + return JsArrayBufferView(v8::BigInt64Array::New(inner, offset, numElements)); +} +JsArrayBufferView JsArrayBuffer::newBigUint64View(size_t offset, size_t numElements) const { + JSG_REQUIRE(size() % 8 == 0, TypeError, "ArrayBuffer size is not a multiple of 8"); + v8::Local inner = *this; + return JsArrayBufferView(v8::BigUint64Array::New(inner, offset, numElements)); +} +JsArrayBufferView JsArrayBuffer::newDataView(size_t offset, size_t numElements) const { + v8::Local inner = *this; + return JsArrayBufferView(v8::DataView::New(inner, offset, numElements)); +} + +bool JsArrayBuffer::isResizable() const { + v8::Local inner = *this; + return inner->IsResizableByUserJavaScript(); +} + +JsArrayBuffer::operator JsUint8Array() const { + return newUint8View(0, size()); +} + +// ====================================================================================== +// JsSharedArrayBuffer + +kj::Maybe JsSharedArrayBuffer::tryCreate(Lock& js, size_t length) { + JSG_REQUIRE(length < v8::ArrayBuffer::kMaxByteLength, RangeError, "The length is too large"); + auto backing = v8::SharedArrayBuffer::NewBackingStore(js.v8Isolate, length, + v8::BackingStoreInitializationMode::kZeroInitialized, + v8::BackingStoreOnFailureMode::kReturnNull); + if (backing == nullptr) return kj::none; + return create(js, kj::mv(backing)); +} + +JsSharedArrayBuffer JsSharedArrayBuffer::create(Lock& js, size_t length) { + JSG_REQUIRE(length < v8::ArrayBuffer::kMaxByteLength, RangeError, "The length is too large"); + auto backing = v8::SharedArrayBuffer::NewBackingStore(js.v8Isolate, length, + v8::BackingStoreInitializationMode::kZeroInitialized, + v8::BackingStoreOnFailureMode::kReturnNull); + JSG_REQUIRE(backing != nullptr, RangeError, "Failed to allocate memory for ArrayBuffer"); + return create(js, kj::mv(backing)); +} + +JsSharedArrayBuffer JsSharedArrayBuffer::create(Lock& js, kj::ArrayPtr data) { + auto buf = create(js, data.size()); + buf.asArrayPtr().copyFrom(data); + return buf; +} + +JsSharedArrayBuffer JsSharedArrayBuffer::create( + Lock& js, std::unique_ptr backingStore) { + return JsSharedArrayBuffer(v8::SharedArrayBuffer::New(js.v8Isolate, kj::mv(backingStore))); +} + +JsSharedArrayBuffer JsSharedArrayBuffer::create( + Lock& js, std::shared_ptr backingStore) { + return JsSharedArrayBuffer(v8::SharedArrayBuffer::New(js.v8Isolate, kj::mv(backingStore))); +} + +kj::ArrayPtr JsSharedArrayBuffer::asArrayPtr() { + v8::Local inner = *this; + void* data = inner->GetBackingStore()->Data(); + size_t length = inner->ByteLength(); + return kj::ArrayPtr(static_cast(data), length); +} + +kj::ArrayPtr JsSharedArrayBuffer::asArrayPtr() const { + v8::Local inner = *this; + const void* data = inner->GetBackingStore()->Data(); + size_t length = inner->ByteLength(); + return kj::ArrayPtr(static_cast(data), length); +} + +JsSharedArrayBuffer JsSharedArrayBuffer::slice(Lock& js, size_t newLength) const { + JSG_REQUIRE(newLength <= size(), RangeError, "New length exceeds buffer length"); + auto dest = create(js, newLength); + dest.asArrayPtr().copyFrom(asArrayPtr().slice(0, newLength)); + return dest; +} + +size_t JsSharedArrayBuffer::size() const { + v8::Local inner = *this; + return inner->ByteLength(); +} + +kj::Array JsSharedArrayBuffer::copy() { + auto ptr = asArrayPtr(); + return kj::heapArray(ptr); +} + +JsSharedArrayBuffer::operator JsBufferSource() const { + v8::Local inner = *this; + return jsg::JsBufferSource(inner); +} + +JsUint8Array JsSharedArrayBuffer::newUint8View(size_t offset, size_t numElements) const { + v8::Local inner = *this; + return JsUint8Array(v8::Uint8Array::New(inner, offset, numElements)); +} +JsArrayBufferView JsSharedArrayBuffer::newInt8View(size_t offset, size_t numElements) const { + v8::Local inner = *this; + return JsArrayBufferView(v8::Int8Array::New(inner, offset, numElements)); +} +JsArrayBufferView JsSharedArrayBuffer::newUint8ClampedView( + size_t offset, size_t numElements) const { + v8::Local inner = *this; + return JsArrayBufferView(v8::Uint8ClampedArray::New(inner, offset, numElements)); +} +JsArrayBufferView JsSharedArrayBuffer::newUint16View(size_t offset, size_t numElements) const { + JSG_REQUIRE(size() % 2 == 0, TypeError, "ArrayBuffer size is not a multiple of 2"); + v8::Local inner = *this; + return JsArrayBufferView(v8::Uint16Array::New(inner, offset, numElements)); +} +JsArrayBufferView JsSharedArrayBuffer::newInt16View(size_t offset, size_t numElements) const { + JSG_REQUIRE(size() % 2 == 0, TypeError, "ArrayBuffer size is not a multiple of 2"); + v8::Local inner = *this; + return JsArrayBufferView(v8::Int16Array::New(inner, offset, numElements)); +} +JsArrayBufferView JsSharedArrayBuffer::newUint32View(size_t offset, size_t numElements) const { + JSG_REQUIRE(size() % 4 == 0, TypeError, "ArrayBuffer size is not a multiple of 4"); + v8::Local inner = *this; + return JsArrayBufferView(v8::Uint32Array::New(inner, offset, numElements)); +} +JsArrayBufferView JsSharedArrayBuffer::newInt32View(size_t offset, size_t numElements) const { + JSG_REQUIRE(size() % 4 == 0, TypeError, "ArrayBuffer size is not a multiple of 4"); + v8::Local inner = *this; + return JsArrayBufferView(v8::Int32Array::New(inner, offset, numElements)); +} +JsArrayBufferView JsSharedArrayBuffer::newFloat16View(size_t offset, size_t numElements) const { + JSG_REQUIRE(size() % 2 == 0, TypeError, "ArrayBuffer size is not a multiple of 2"); + v8::Local inner = *this; + return JsArrayBufferView(v8::Float16Array::New(inner, offset, numElements)); +} +JsArrayBufferView JsSharedArrayBuffer::newFloat32View(size_t offset, size_t numElements) const { + JSG_REQUIRE(size() % 4 == 0, TypeError, "ArrayBuffer size is not a multiple of 4"); + v8::Local inner = *this; + return JsArrayBufferView(v8::Float32Array::New(inner, offset, numElements)); +} +JsArrayBufferView JsSharedArrayBuffer::newFloat64View(size_t offset, size_t numElements) const { + JSG_REQUIRE(size() % 8 == 0, TypeError, "ArrayBuffer size is not a multiple of 8"); + v8::Local inner = *this; + return JsArrayBufferView(v8::Float64Array::New(inner, offset, numElements)); +} +JsArrayBufferView JsSharedArrayBuffer::newBigInt64View(size_t offset, size_t numElements) const { + JSG_REQUIRE(size() % 8 == 0, TypeError, "ArrayBuffer size is not a multiple of 8"); + v8::Local inner = *this; + return JsArrayBufferView(v8::BigInt64Array::New(inner, offset, numElements)); +} +JsArrayBufferView JsSharedArrayBuffer::newBigUint64View(size_t offset, size_t numElements) const { + JSG_REQUIRE(size() % 8 == 0, TypeError, "ArrayBuffer size is not a multiple of 8"); + v8::Local inner = *this; + return JsArrayBufferView(v8::BigUint64Array::New(inner, offset, numElements)); +} +JsArrayBufferView JsSharedArrayBuffer::newDataView(size_t offset, size_t numElements) const { + v8::Local inner = *this; + return JsArrayBufferView(v8::DataView::New(inner, offset, numElements)); +} + +JsSharedArrayBuffer::operator JsUint8Array() const { + return newUint8View(0, size()); +} + // ====================================================================================== // JsArrayBufferView @@ -736,6 +987,11 @@ size_t JsArrayBufferView::size() const { return inner->ByteLength(); } +size_t JsArrayBufferView::getOffset() const { + v8::Local inner = *this; + return inner->ByteOffset(); +} + bool JsArrayBufferView::isIntegerType() const { v8::Local inner = *this; return inner->IsUint8Array() || inner->IsUint8ClampedArray() || inner->IsInt8Array() || @@ -743,6 +999,247 @@ bool JsArrayBufferView::isIntegerType() const { inner->IsInt32Array() || inner->IsBigInt64Array() || inner->IsBigUint64Array(); } +bool JsArrayBufferView::isUint8Array() const { + v8::Local inner = *this; + return inner->IsUint8Array(); +} + +bool JsArrayBufferView::isInt8Array() const { + v8::Local inner = *this; + return inner->IsInt8Array(); +} + +bool JsArrayBufferView::isUint8ClampedArray() const { + v8::Local inner = *this; + return inner->IsUint8ClampedArray(); +} + +bool JsArrayBufferView::isUint16Array() const { + v8::Local inner = *this; + return inner->IsUint16Array(); +} + +bool JsArrayBufferView::isInt16Array() const { + v8::Local inner = *this; + return inner->IsInt16Array(); +} + +bool JsArrayBufferView::isUint32Array() const { + v8::Local inner = *this; + return inner->IsUint32Array(); +} + +bool JsArrayBufferView::isInt32Array() const { + v8::Local inner = *this; + return inner->IsInt32Array(); +} + +bool JsArrayBufferView::isFloat16Array() const { + v8::Local inner = *this; + return inner->IsFloat16Array(); +} + +bool JsArrayBufferView::isFloat32Array() const { + v8::Local inner = *this; + return inner->IsFloat32Array(); +} + +bool JsArrayBufferView::isFloat64Array() const { + v8::Local inner = *this; + return inner->IsFloat64Array(); +} + +bool JsArrayBufferView::isBigInt64Array() const { + v8::Local inner = *this; + return inner->IsBigInt64Array(); +} + +bool JsArrayBufferView::isBigUint64Array() const { + v8::Local inner = *this; + return inner->IsBigUint64Array(); +} + +bool JsArrayBufferView::isDataView() const { + v8::Local inner = *this; + return inner->IsDataView(); +} + +size_t JsArrayBufferView::getElementSize() const { + v8::Local inner = *this; + if (inner->IsUint8Array() || inner->IsInt8Array() || inner->IsUint8ClampedArray()) { + return 1; + } else if (inner->IsUint16Array() || inner->IsInt16Array() || inner->IsFloat16Array()) { + return 2; + } else if (inner->IsUint32Array() || inner->IsInt32Array() || inner->IsFloat32Array()) { + return 4; + } else if (inner->IsFloat64Array() || inner->IsBigInt64Array() || inner->IsBigUint64Array()) { + return 8; + } else if (inner->IsDataView()) { + return 1; // DataView is byte-addressable + } + KJ_UNREACHABLE; // Not a valid ArrayBufferView type +} + +JsArrayBuffer JsArrayBufferView::getBuffer() const { + v8::Local inner = *this; + return JsArrayBuffer(inner->Buffer()); +} + +bool JsArrayBufferView::isDetachable() const { + v8::Local inner = *this; + return inner->Buffer()->IsDetachable(); +} + +bool JsArrayBufferView::isDetached() const { + v8::Local inner = *this; + return inner->Buffer()->WasDetached(); +} + +void JsArrayBufferView::detachInPlace(Lock& js) { + v8::Local inner = *this; + check(inner->Buffer()->Detach({})); +} + +JsArrayBufferView JsArrayBufferView::detachAndTake(Lock& js) { + v8::Local inner = *this; + auto length = inner->ByteLength(); + auto offset = inner->ByteOffset(); + auto ab = getBuffer().detachAndTake(js); + + // We have to return the same type of vie + if (inner->IsUint8Array()) { + return ab.newUint8View(offset, length); + } else if (inner->IsInt8Array()) { + return ab.newInt8View(offset, length); + } else if (inner->IsUint8ClampedArray()) { + return ab.newUint8ClampedView(offset, length); + } else if (inner->IsUint16Array()) { + return ab.newUint16View(offset, length / getElementSize()); + } else if (inner->IsInt16Array()) { + return ab.newInt16View(offset, length / getElementSize()); + } else if (inner->IsUint32Array()) { + return ab.newUint32View(offset, length / getElementSize()); + } else if (inner->IsInt32Array()) { + return ab.newInt32View(offset, length / getElementSize()); + } else if (inner->IsFloat16Array()) { + return ab.newFloat16View(offset, length / getElementSize()); + } else if (inner->IsFloat32Array()) { + return ab.newFloat32View(offset, length / getElementSize()); + } else if (inner->IsFloat64Array()) { + return ab.newFloat64View(offset, length / getElementSize()); + } else if (inner->IsBigInt64Array()) { + return ab.newBigInt64View(offset, length / getElementSize()); + } else if (inner->IsBigUint64Array()) { + return ab.newBigUint64View(offset, length / getElementSize()); + } else if (inner->IsDataView()) { + return ab.newDataView(offset, length); + } + + KJ_UNREACHABLE; +} + +JsArrayBufferView JsArrayBufferView::slice(Lock& js, size_t offset, size_t length) const { + v8::Local inner = *this; + offset = inner->ByteOffset() + offset; + + if (inner->IsUint8Array()) { + return JsArrayBufferView(v8::Uint8Array::New(inner->Buffer(), offset, length)); + } else if (inner->IsInt8Array()) { + return JsArrayBufferView(v8::Int8Array::New(inner->Buffer(), offset, length)); + } else if (inner->IsUint8ClampedArray()) { + return JsArrayBufferView(v8::Uint8ClampedArray::New(inner->Buffer(), offset, length)); + } else if (inner->IsUint16Array()) { + return JsArrayBufferView( + v8::Uint16Array::New(inner->Buffer(), offset, length / getElementSize())); + } else if (inner->IsInt16Array()) { + return JsArrayBufferView( + v8::Int16Array::New(inner->Buffer(), offset, length / getElementSize())); + } else if (inner->IsUint32Array()) { + return JsArrayBufferView( + v8::Uint32Array::New(inner->Buffer(), offset, length / getElementSize())); + } else if (inner->IsInt32Array()) { + return JsArrayBufferView( + v8::Int32Array::New(inner->Buffer(), offset, length / getElementSize())); + } else if (inner->IsFloat16Array()) { + return JsArrayBufferView( + v8::Float16Array::New(inner->Buffer(), offset, length / getElementSize())); + } else if (inner->IsFloat32Array()) { + return JsArrayBufferView( + v8::Float32Array::New(inner->Buffer(), offset, length / getElementSize())); + } else if (inner->IsFloat64Array()) { + return JsArrayBufferView( + v8::Float64Array::New(inner->Buffer(), offset, length / getElementSize())); + } else if (inner->IsBigInt64Array()) { + return JsArrayBufferView( + v8::BigInt64Array::New(inner->Buffer(), offset, length / getElementSize())); + } else if (inner->IsBigUint64Array()) { + return JsArrayBufferView( + v8::BigUint64Array::New(inner->Buffer(), offset, length / getElementSize())); + } else if (inner->IsDataView()) { + return JsArrayBufferView(v8::DataView::New(inner->Buffer(), offset, length)); + } + + KJ_UNREACHABLE; +} + +bool JsArrayBufferView::isResizable() const { + v8::Local inner = *this; + return inner->Buffer()->IsResizableByUserJavaScript(); +} + +JsArrayBufferView::operator JsBufferSource() const { + v8::Local inner = *this; + return jsg::JsBufferSource(inner); +} + +JsArrayBufferView::operator JsUint8Array() const { + v8::Local inner = *this; + if (inner->IsUint8Array()) { + return jsg::JsUint8Array(inner.As()); + } + + auto buf = inner->Buffer(); + return jsg::JsUint8Array(v8::Uint8Array::New(buf, inner->ByteOffset(), inner->ByteLength())); +} + +JsArrayBufferView JsArrayBufferView::clone(jsg::Lock& js) { + v8::Local inner = *this; + auto backing = inner->Buffer()->GetBackingStore(); + auto ab = jsg::JsArrayBuffer::create(js, kj::mv(backing)); + + auto offset = getOffset(); + auto length = size(); + + if (inner->IsUint8Array()) { + return ab.newUint8View(offset, length); + } else if (inner->IsInt8Array()) { + return ab.newInt8View(offset, length / getElementSize()); + } else if (inner->IsUint8ClampedArray()) { + return ab.newUint8ClampedView(offset, length / getElementSize()); + } else if (inner->IsUint16Array()) { + return ab.newUint16View(offset, length / getElementSize()); + } else if (inner->IsInt16Array()) { + return ab.newInt16View(offset, length / getElementSize()); + } else if (inner->IsUint32Array()) { + return ab.newUint32View(offset, length / getElementSize()); + } else if (inner->IsInt32Array()) { + return ab.newInt32View(offset, length / getElementSize()); + } else if (inner->IsFloat16Array()) { + return ab.newFloat16View(offset, length / getElementSize()); + } else if (inner->IsFloat32Array()) { + return ab.newFloat32View(offset, length / getElementSize()); + } else if (inner->IsFloat64Array()) { + return ab.newFloat64View(offset, length / getElementSize()); + } else if (inner->IsBigInt64Array()) { + return ab.newBigInt64View(offset, length / getElementSize()); + } else if (inner->IsBigUint64Array()) { + return ab.newBigUint64View(offset, length / getElementSize()); + } else if (inner->IsDataView()) { + return ab.newDataView(offset, length); + } + KJ_UNREACHABLE; +} + // ====================================================================================== // JsBufferSource @@ -828,9 +1325,121 @@ bool JsBufferSource::isResizable() const { return false; } +bool JsBufferSource::isDetachable() const { + v8::Local inner = *this; + if (inner->IsArrayBuffer()) { + return inner.As()->IsDetachable(); + } else if (inner->IsSharedArrayBuffer()) { + return false; // SharedArrayBuffers are never detachable + } else { + KJ_DASSERT(inner->IsArrayBufferView()); + return inner.As()->Buffer()->IsDetachable(); + } +} + +bool JsBufferSource::isDetached() const { + v8::Local inner = *this; + if (inner->IsArrayBuffer()) { + return inner.As()->WasDetached(); + } else if (inner->IsSharedArrayBuffer()) { + return false; // SharedArrayBuffers are never detachable + } else { + KJ_DASSERT(inner->IsArrayBufferView()); + return inner.As()->Buffer()->WasDetached(); + } +} + +void JsBufferSource::detachInPlace(Lock& js) { + JSG_REQUIRE(isDetachable(), TypeError, "BufferSource is not detachable"); + v8::Local inner = *this; + if (inner->IsArrayBuffer()) { + auto buf = inner.As(); + check(buf->Detach({})); + } else if (inner->IsSharedArrayBuffer()) { + KJ_UNREACHABLE; // SharedArrayBuffers are never detachable + } else { + KJ_DASSERT(inner->IsArrayBufferView()); + auto view = inner.As(); + check(view->Buffer()->Detach({})); + } +} + +JsBufferSource JsBufferSource::detachAndTake(Lock& js) { + JSG_REQUIRE(isDetachable(), TypeError, "BufferSource is not detachable"); + v8::Local inner = *this; + if (inner->IsArrayBuffer()) { + JsArrayBuffer ab(inner.As()); + return ab.detachAndTake(js); + } else if (inner->IsSharedArrayBuffer()) { + KJ_UNREACHABLE; // SharedArrayBuffers are never detachable + } + + KJ_DASSERT(inner->IsArrayBufferView()); + JsArrayBufferView view(inner.As()); + return view.detachAndTake(js); +} + +JsBufferSource::operator JsUint8Array() const { + v8::Local inner = *this; + if (inner->IsArrayBuffer()) { + JsArrayBuffer ab(inner.As()); + return ab; + } + if (inner->IsSharedArrayBuffer()) { + JsSharedArrayBuffer ab(inner.As()); + return ab; + } + if (inner->IsUint8Array()) { + return jsg::JsUint8Array(inner.As()); + } + JsArrayBufferView view(inner.As()); + return view; +} + +size_t JsBufferSource::getOffset() const { + v8::Local inner = *this; + if (inner->IsArrayBuffer() || inner->IsSharedArrayBuffer()) { + return 0; + } + KJ_DASSERT(inner->IsArrayBufferView()); + auto view = inner.As(); + return view->ByteOffset(); +} + +size_t JsBufferSource::underlyingArrayBufferSize(Lock& js) const { + v8::Local inner = *this; + if (inner->IsArrayBuffer()) { + auto buf = inner.As(); + if (buf->WasDetached()) [[unlikely]] { + return 0; + } + return buf->ByteLength(); + } else if (inner->IsSharedArrayBuffer()) { + auto buf = inner.As(); + return buf->ByteLength(); + } else { + KJ_DASSERT(inner->IsArrayBufferView()); + auto view = inner.As(); + auto buf = view->Buffer(); + if (buf->WasDetached()) [[unlikely]] { + return 0; + } + return buf->ByteLength(); + } +} + // ====================================================================================== // JsUint8Array +kj::Maybe JsUint8Array::tryCreate(Lock& js, size_t length) { + JSG_REQUIRE(length < v8::ArrayBuffer::kMaxByteLength, RangeError, "The length is too large"); + auto backing = v8::ArrayBuffer::NewBackingStore(js.v8Isolate, length, + v8::BackingStoreInitializationMode::kZeroInitialized, + v8::BackingStoreOnFailureMode::kReturnNull); + if (backing == nullptr) return kj::none; + return create(js, kj::mv(backing), 0, length); +} + JsUint8Array JsUint8Array::create(Lock& js, size_t length) { JSG_REQUIRE(length < v8::ArrayBuffer::kMaxByteLength, RangeError, "The length is too large"); auto backing = v8::ArrayBuffer::NewBackingStore(js.v8Isolate, length, @@ -851,6 +1460,11 @@ JsUint8Array JsUint8Array::create(Lock& js, JsArrayBuffer& buffer) { return JsUint8Array(v8::Uint8Array::New(ab, 0, ab->ByteLength())); } +JsUint8Array JsUint8Array::create(Lock& js, JsSharedArrayBuffer& buffer) { + v8::Local ab = buffer; + return JsUint8Array(v8::Uint8Array::New(ab, 0, ab->ByteLength())); +} + JsUint8Array JsUint8Array::create( Lock& js, std::unique_ptr backingStore, size_t byteOffset, size_t length) { return JsUint8Array(v8::Uint8Array::New( @@ -859,8 +1473,7 @@ JsUint8Array JsUint8Array::create( JsUint8Array JsUint8Array::slice(Lock& js, size_t newLength) const { JSG_REQUIRE(newLength <= size(), RangeError, "New length exceeds array length"); - auto u8 = v8::Uint8Array::New(inner->Buffer(), inner->ByteOffset(), newLength); - return JsUint8Array(u8); + return slice(js, 0, newLength); } kj::ArrayPtr JsUint8Array::asArrayPtr() const { @@ -882,4 +1495,59 @@ kj::Array JsUint8Array::copy() { return kj::heapArray(ptr); } +JsArrayBuffer JsUint8Array::getBuffer() const { + auto buf = inner->Buffer(); + return JsArrayBuffer(buf); +} + +bool JsUint8Array::isDetachable() const { + auto buf = inner->Buffer(); + return buf->IsDetachable(); +} + +bool JsUint8Array::isDetached() const { + auto buf = inner->Buffer(); + return buf->WasDetached(); +} + +void JsUint8Array::detachInPlace(Lock& js) { + auto buf = inner->Buffer(); + check(buf->Detach({})); +} + +JsUint8Array JsUint8Array::detachAndTake(Lock& js) { + v8::Local inner = *this; + auto length = inner->ByteLength(); + auto offset = inner->ByteOffset(); + auto ab = getBuffer().detachAndTake(js); + return JsUint8Array(v8::Uint8Array::New(ab, offset, length)); +} + +JsUint8Array JsUint8Array::slice(Lock& js, size_t offset, size_t length) const { + auto buf = inner->Buffer(); + return JsUint8Array(v8::Uint8Array::New(buf, inner->ByteOffset() + offset, length)); +} + +bool JsUint8Array::isResizable() const { + auto buf = inner->Buffer(); + return buf->IsResizableByUserJavaScript(); +} + +JsUint8Array::operator JsArrayBufferView() const { + v8::Local inner = *this; + return jsg::JsArrayBufferView(inner); +} + +JsUint8Array::operator JsBufferSource() const { + v8::Local inner = *this; + return jsg::JsBufferSource(inner); +} + +JsUint8Array JsUint8Array::clone(jsg::Lock& js) { + auto buf = inner->Buffer(); + auto backing = buf->GetBackingStore(); + auto ab = jsg::JsArrayBuffer::create(js, kj::mv(backing)); + return JsUint8Array(v8::Uint8Array::New(ab, inner->ByteOffset(), inner->ByteLength())); +} + } // namespace workerd::jsg diff --git a/src/workerd/jsg/jsvalue.h b/src/workerd/jsg/jsvalue.h index bee149d27e5..25fc6efdcca 100644 --- a/src/workerd/jsg/jsvalue.h +++ b/src/workerd/jsg/jsvalue.h @@ -58,7 +58,6 @@ inline void requireOnStack(void* self) { V(BigInt64Array) \ V(BigUint64Array) \ V(DataView) \ - V(SharedArrayBuffer) \ V(WasmMemoryObject) \ V(WasmModuleObject) \ JS_TYPE_CLASSES(V) @@ -234,12 +233,16 @@ class JsArray final: public JsBase { class JsArrayBuffer final: public JsBase { public: + static kj::Maybe tryCreate(Lock& js, size_t length); + static JsArrayBuffer create(Lock& js, size_t length); // Allocate and copy data from the given ArrayPtr in a single step. static JsArrayBuffer create(Lock& js, kj::ArrayPtr data); + // Take ownership of the given backing store. static JsArrayBuffer create(Lock& js, std::unique_ptr backingStore); + static JsArrayBuffer create(Lock& js, std::shared_ptr backingStore); JsArrayBuffer slice(Lock& js, size_t newLength) const; @@ -251,9 +254,85 @@ class JsArrayBuffer final: public JsBase { // Return a copy of this buffer's data as a kj::Array. kj::Array copy(); + // A JsArrayBuffer can be used as a JsBufferSource, which is a more general type that + // also includes JsArrayBufferView. + operator JsBufferSource() const; + + // A JsArrayBuffer might be detachable. + bool isDetachable() const; + bool isDetached() const; + void detachInPlace(Lock& js); + JsArrayBuffer detachAndTake(Lock& js) KJ_WARN_UNUSED_RESULT; + + // Return a view over this buffer + JsUint8Array newUint8View(size_t offset, size_t numElements) const; + JsArrayBufferView newInt8View(size_t offset, size_t numElements) const; + JsArrayBufferView newUint8ClampedView(size_t offset, size_t numElements) const; + JsArrayBufferView newUint16View(size_t offset, size_t numElements) const; + JsArrayBufferView newInt16View(size_t offset, size_t numElements) const; + JsArrayBufferView newUint32View(size_t offset, size_t numElements) const; + JsArrayBufferView newInt32View(size_t offset, size_t numElements) const; + JsArrayBufferView newFloat16View(size_t offset, size_t numElements) const; + JsArrayBufferView newFloat32View(size_t offset, size_t numElements) const; + JsArrayBufferView newFloat64View(size_t offset, size_t numElements) const; + JsArrayBufferView newBigInt64View(size_t offset, size_t numElements) const; + JsArrayBufferView newBigUint64View(size_t offset, size_t numElements) const; + JsArrayBufferView newDataView(size_t offset, size_t numElements) const; + + bool isResizable() const; + + operator JsUint8Array() const; + using JsBase::JsBase; }; +class JsSharedArrayBuffer final: public JsBase { + public: + static kj::Maybe tryCreate(Lock& js, size_t length); + + static JsSharedArrayBuffer create(Lock& js, size_t length); + + // Allocate and copy data from the given ArrayPtr in a single step. + static JsSharedArrayBuffer create(Lock& js, kj::ArrayPtr data); + + // Take ownership of the given backing store. + static JsSharedArrayBuffer create(Lock& js, std::unique_ptr backingStore); + static JsSharedArrayBuffer create(Lock& js, std::shared_ptr backingStore); + + JsSharedArrayBuffer slice(Lock& js, size_t newLength) const; + + kj::ArrayPtr asArrayPtr(); + kj::ArrayPtr asArrayPtr() const; + + size_t size() const; + + // Return a copy of this buffer's data as a kj::Array. + kj::Array copy(); + + // A JsArrayBuffer can be used as a JsBufferSource, which is a more general type that + // also includes JsArrayBufferView. + operator JsBufferSource() const; + + // Return a view over this buffer + JsUint8Array newUint8View(size_t offset, size_t numElements) const; + JsArrayBufferView newInt8View(size_t offset, size_t numElements) const; + JsArrayBufferView newUint8ClampedView(size_t offset, size_t numElements) const; + JsArrayBufferView newUint16View(size_t offset, size_t numElements) const; + JsArrayBufferView newInt16View(size_t offset, size_t numElements) const; + JsArrayBufferView newUint32View(size_t offset, size_t numElements) const; + JsArrayBufferView newInt32View(size_t offset, size_t numElements) const; + JsArrayBufferView newFloat16View(size_t offset, size_t numElements) const; + JsArrayBufferView newFloat32View(size_t offset, size_t numElements) const; + JsArrayBufferView newFloat64View(size_t offset, size_t numElements) const; + JsArrayBufferView newBigInt64View(size_t offset, size_t numElements) const; + JsArrayBufferView newBigUint64View(size_t offset, size_t numElements) const; + JsArrayBufferView newDataView(size_t offset, size_t numElements) const; + + operator JsUint8Array() const; + + using JsBase::JsBase; +}; + class JsArrayBufferView final: public JsBase { public: template @@ -269,17 +348,56 @@ class JsArrayBufferView final: public JsBase::JsBase; }; class JsUint8Array final: public JsBase { public: + static kj::Maybe tryCreate(Lock& js, size_t length); static JsUint8Array create(Lock& js, size_t length); // Allocate and copy data from the given ArrayPtr in a single step. @@ -288,6 +406,8 @@ class JsUint8Array final: public JsBase { // Create a Uint8Array view over the given ArrayBuffer. static JsUint8Array create(Lock& js, JsArrayBuffer& buffer); + static JsUint8Array create(Lock& js, JsSharedArrayBuffer& buffer); + static JsUint8Array create( Lock& js, std::unique_ptr backingStore, size_t byteOffset, size_t length); @@ -312,6 +432,25 @@ class JsUint8Array final: public JsBase { // Return a copy of this buffer's data as a kj::Array. kj::Array copy(); + JsArrayBuffer getBuffer() const; + + bool isDetachable() const; + bool isDetached() const; + void detachInPlace(Lock& js); + JsUint8Array detachAndTake(Lock& js) KJ_WARN_UNUSED_RESULT; + + // Get a new view of the same type over the same buffer with the given offset and length. + // The offset is relative to the start of this view, not the start of the underlying buffer. + // The length is the number of elements (not bytes). + JsUint8Array slice(Lock& js, size_t offset, size_t length) const; + + bool isResizable() const; + + operator JsArrayBufferView() const; + operator JsBufferSource() const; + + JsUint8Array clone(jsg::Lock& js); + using JsBase::JsBase; }; @@ -333,6 +472,8 @@ class JsBufferSource final: public JsBase { kj::ArrayPtr asArrayPtr(); size_t size() const; + size_t getOffset() const; + size_t underlyingArrayBufferSize(Lock& js) const; // Returns true if the underlying value is an integer-typed TypedArray. bool isIntegerType() const; @@ -342,9 +483,17 @@ class JsBufferSource final: public JsBase { bool isArrayBufferView() const; bool isResizable() const; + bool isDetachable() const; + bool isDetached() const; + void detachInPlace(Lock& js); + JsBufferSource detachAndTake(Lock& js) KJ_WARN_UNUSED_RESULT; + // Return a copy of this buffer's data as a kj::Array. kj::Array copy(); + // Regardless of what kind of typed array view this is, we can always get it as a Uint8Array + operator JsUint8Array() const; + using JsBase::JsBase; }; diff --git a/src/workerd/jsg/modules-new.c++ b/src/workerd/jsg/modules-new.c++ index 73f3d5dd5c0..ac18ad81d99 100644 --- a/src/workerd/jsg/modules-new.c++ +++ b/src/workerd/jsg/modules-new.c++ @@ -1984,10 +1984,7 @@ Module::EvaluateCallback Module::newDataModuleHandler(kj::ArrayPtr bool { JSG_TRY(js) { - auto backing = jsg::BackingStore::alloc(js, data.size()); - backing.asArrayPtr().copyFrom(data); - auto buffer = jsg::BufferSource(js, kj::mv(backing)); - return ns.setDefault(js, JsValue(buffer.getHandle(js))); + return ns.setDefault(js, jsg::JsArrayBuffer::create(js, data)); } JSG_CATCH(exception) { js.v8Isolate->ThrowException(exception.getHandle(js)); diff --git a/src/workerd/jsg/promise-test.c++ b/src/workerd/jsg/promise-test.c++ index d43c5c3a873..81cb85092ae 100644 --- a/src/workerd/jsg/promise-test.c++ +++ b/src/workerd/jsg/promise-test.c++ @@ -98,6 +98,172 @@ struct PromiseContext: public jsg::Object, public jsg::ContextGlobal { return result; } + // ---- PersistentContinuation tests ---- + + void testPersistentContinuation(jsg::Lock& js) { + // Test PersistentContinuation (then+catch) with reuse across multiple promises. + struct Callbacks { + int successCount = 0; + int failureCount = 0; + + void thenFunc(jsg::Lock& js) { + successCount++; + } + void catchFunc(jsg::Lock& js, jsg::Value reason) { + failureCount++; + } + }; + + using FuncPair = jsg::ThenCatchPair, + kj::Function>; + + Callbacks callbacks; + auto continuation = jsg::PersistentContinuation::create(js, + FuncPair{ + .thenFunc = [&callbacks](jsg::Lock& js) { callbacks.thenFunc(js); }, + .catchFunc = [&callbacks](jsg::Lock& js, + jsg::Value reason) { callbacks.catchFunc(js, kj::mv(reason)); }, + }); + + // Use the continuation for a resolved promise. + { + auto [promise, resolver] = js.newPromiseAndResolver(); + promise.thenRef(js, continuation); + resolver.resolve(js); + js.runMicrotasks(); + KJ_ASSERT(callbacks.successCount == 1); + KJ_ASSERT(callbacks.failureCount == 0); + } + + // Reuse the same continuation for another resolved promise. + { + auto [promise, resolver] = js.newPromiseAndResolver(); + promise.thenRef(js, continuation); + resolver.resolve(js); + js.runMicrotasks(); + KJ_ASSERT(callbacks.successCount == 2); + KJ_ASSERT(callbacks.failureCount == 0); + } + + // Reuse for a rejected promise — the catch side should fire. + { + auto promise = js.rejectedPromise(v8StrIntern(js.v8Isolate, "test error")); + promise.markAsHandled(js); + promise.thenRef(js, continuation); + js.runMicrotasks(); + KJ_ASSERT(callbacks.successCount == 2); + KJ_ASSERT(callbacks.failureCount == 1); + } + } + + void testPersistentThenContinuation(jsg::Lock& js) { + // Test PersistentThenContinuation (then-only, identity on rejection) with reuse. + int result = 0; + + using Func = kj::Function; + auto continuation = jsg::PersistentThenContinuation::create( + js, [&result](jsg::Lock& js, int value) -> int { + result = value * 2; + return result; + }); + + // First use. + { + auto [promise, resolver] = js.newPromiseAndResolver(); + promise.thenRef(js, continuation); + resolver.resolve(js, 21); + js.runMicrotasks(); + KJ_ASSERT(result == 42); + } + + // Reuse with a different value. + { + auto [promise, resolver] = js.newPromiseAndResolver(); + promise.thenRef(js, continuation); + resolver.resolve(js, 5); + js.runMicrotasks(); + KJ_ASSERT(result == 10); + } + + // Rejection should propagate through (identity behavior). + { + bool rejected = false; + auto promise = js.rejectedPromise(v8StrIntern(js.v8Isolate, "oops")); + promise.markAsHandled(js); + auto chained = promise.thenRef(js, continuation); + chained.catch_(js, [&rejected](jsg::Lock& js, jsg::Value reason) -> int { + rejected = true; + return 0; + }); + js.runMicrotasks(); + KJ_ASSERT(rejected); + KJ_ASSERT(result == 10); // Unchanged — then callback not called. + } + } + + void testPersistentCatchContinuation(jsg::Lock& js) { + // Test PersistentCatchContinuation (catch-only, identity on fulfillment) with reuse. + int catchCount = 0; + + using ErrorFunc = kj::Function; + auto continuation = jsg::PersistentCatchContinuation::create( + js, [&catchCount](jsg::Lock& js, jsg::Value reason) -> int { + catchCount++; + return -1; + }); + + // Rejected promise — catch handler fires. + { + auto promise = js.rejectedPromise(v8StrIntern(js.v8Isolate, "err")); + promise.markAsHandled(js); + int result = 0; + promise.catchRef(js, continuation).then(js, [&result](jsg::Lock& js, int v) { result = v; }); + js.runMicrotasks(); + KJ_ASSERT(catchCount == 1); + KJ_ASSERT(result == -1); + } + + // Reuse with another rejection. + { + auto promise = js.rejectedPromise(v8StrIntern(js.v8Isolate, "err2")); + promise.markAsHandled(js); + int result = 0; + promise.catchRef(js, continuation).then(js, [&result](jsg::Lock& js, int v) { result = v; }); + js.runMicrotasks(); + KJ_ASSERT(catchCount == 2); + KJ_ASSERT(result == -1); + } + + // Fulfilled promise — identity propagation, catch handler not called. + { + auto [promise, resolver] = js.newPromiseAndResolver(); + int result = 0; + promise.catchRef(js, continuation).then(js, [&result](jsg::Lock& js, int v) { result = v; }); + resolver.resolve(js, 99); + js.runMicrotasks(); + KJ_ASSERT(catchCount == 2); // Unchanged. + KJ_ASSERT(result == 99); + } + } + + void testPersistentContinuationChaining(jsg::Lock& js) { + // Test that the promise returned by thenRef can be further chained with .then(). + int finalResult = 0; + + using Func = kj::Function; + auto continuation = jsg::PersistentThenContinuation::create( + js, [](jsg::Lock& js, int value) -> int { return value + 10; }); + + auto [promise, resolver] = js.newPromiseAndResolver(); + promise.thenRef(js, continuation).then(js, [&finalResult](jsg::Lock& js, int v) -> kj::String { + finalResult = v; + return kj::str(v); + }); + resolver.resolve(js, 32); + js.runMicrotasks(); + KJ_ASSERT(finalResult == 42); + } + JSG_RESOURCE_TYPE(PromiseContext) { JSG_READONLY_PROTOTYPE_PROPERTY(promise, makePromise); JSG_METHOD(resolvePromise); @@ -111,6 +277,11 @@ struct PromiseContext: public jsg::Object, public jsg::ContextGlobal { JSG_METHOD(whenResolved); JSG_METHOD(thenable); + + JSG_METHOD(testPersistentContinuation); + JSG_METHOD(testPersistentThenContinuation); + JSG_METHOD(testPersistentCatchContinuation); + JSG_METHOD(testPersistentContinuationChaining); } kj::Maybe::Resolver> resolver; @@ -192,5 +363,25 @@ KJ_TEST("thenable") { e.expectEval("thenable({ then(res) { res(123) } })", "number", "123"); } +KJ_TEST("PersistentContinuation") { + Evaluator e(v8System); + e.expectEval("testPersistentContinuation()", "undefined", "undefined"); +} + +KJ_TEST("PersistentThenContinuation") { + Evaluator e(v8System); + e.expectEval("testPersistentThenContinuation()", "undefined", "undefined"); +} + +KJ_TEST("PersistentCatchContinuation") { + Evaluator e(v8System); + e.expectEval("testPersistentCatchContinuation()", "undefined", "undefined"); +} + +KJ_TEST("PersistentContinuation chaining") { + Evaluator e(v8System); + e.expectEval("testPersistentContinuationChaining()", "undefined", "undefined"); +} + } // namespace } // namespace workerd::jsg::test diff --git a/src/workerd/jsg/promise.h b/src/workerd/jsg/promise.h index b6e044cca68..5c058cb6848 100644 --- a/src/workerd/jsg/promise.h +++ b/src/workerd/jsg/promise.h @@ -211,6 +211,174 @@ void identityPromiseContinuation(const v8::FunctionCallbackInfo& args } } +// ======================================================================================= +// Persistent (reusable) promise continuations +// +// The standard promiseContinuation/identityPromiseContinuation functions consume their +// OpaqueWrappable data on first invocation (via unwrapOpaque/dropOpaque). The "Ref" +// variants below borrow by reference instead, allowing the same OpaqueWrappable to be +// reused across multiple .then() calls. The caller must ensure the OpaqueWrappable +// outlives all promises that reference it (typically by storing the PersistentContinuation +// on the same object whose methods the callbacks invoke, and tracing it via visitForGc). + +// Like promiseContinuation, but borrows the FuncPair by reference instead of consuming it. +template +void promiseContinuationRef(const v8::FunctionCallbackInfo& args) { + liftKj(args, [&]() { + auto isolate = args.GetIsolate(); + auto& funcPair = unwrapOpaqueRef(isolate, args.Data()); + + auto callFunc = [&]() -> Output { + auto& js = Lock::from(isolate); + if constexpr (isCatch) { + return funcPair.catchFunc(js, Value(isolate, args[0])); + } else if constexpr (isVoid()) { + return funcPair.thenFunc(js); + } else if constexpr (isV8Ref()) { + return funcPair.thenFunc(js, Input(isolate, args[0])); + } else { + return funcPair.thenFunc(js, unwrapOpaque(isolate, args[0])); + } + }; + if constexpr (isVoid()) { + callFunc(); + } else if constexpr (isPromise()) { + return v8::Local(callFunc().consumeHandle(Lock::from(isolate))); + } else if constexpr (isV8Ref()) { + return callFunc().getHandle(isolate); + } else { + return wrapOpaque(isolate->GetCurrentContext(), callFunc()); + } + }); +} + +// Like identityPromiseContinuation, but does NOT destroy the OpaqueWrappable data. +template +void identityPromiseContinuationRef(const v8::FunctionCallbackInfo& args) { + if constexpr (isCatch) { + args.GetIsolate()->ThrowException(args[0]); + } else { + args.GetReturnValue().Set(args[0]); + } +} + +// A pre-built pair of v8::Functions backed by a persistent OpaqueWrappable. +// Created once, reused across multiple promise.then() calls. The FuncPair's thenFunc +// and catchFunc must be safe to invoke repeatedly (they must not consume captured state). +// +// The holder is responsible for calling visitForGc() to trace the stored V8 references. +template +struct PersistentContinuation { + V8Ref data; + V8Ref onFulfilled; + V8Ref onRejected; + + static PersistentContinuation create(Lock& js, FuncPair&& funcPair) { + auto isolate = js.v8Isolate; + auto context = js.v8Context(); + auto dataHandle = wrapOpaque(context, kj::mv(funcPair)); + + auto fulfilled = + check(v8::Function::New(context, &promiseContinuationRef, + dataHandle, 1, v8::ConstructorBehavior::kThrow)); + auto rejected = + check(v8::Function::New(context, &promiseContinuationRef, + dataHandle, 1, v8::ConstructorBehavior::kThrow)); + + return { + .data = V8Ref(isolate, dataHandle), + .onFulfilled = V8Ref(isolate, fulfilled), + .onRejected = V8Ref(isolate, rejected), + }; + } + + void visitForGc(GcVisitor& visitor) { + visitor.visit(data, onFulfilled, onRejected); + } + + JSG_MEMORY_INFO(PersistentContinuation) { + tracker.trackField("data", data); + tracker.trackField("onFulfilled", onFulfilled); + tracker.trackField("onRejected", onRejected); + } +}; + +// Pre-built continuation for promise.then(onFulfilled) — identity propagation on rejection. +template +struct PersistentThenContinuation { + V8Ref data; + V8Ref onFulfilled; + V8Ref onRejected; + + static PersistentThenContinuation create(Lock& js, Func&& func) { + using FuncPair = ThenCatchPair; + auto isolate = js.v8Isolate; + auto context = js.v8Context(); + auto dataHandle = wrapOpaque(context, FuncPair{kj::mv(func), false}); + + auto fulfilled = + check(v8::Function::New(context, &promiseContinuationRef, + dataHandle, 1, v8::ConstructorBehavior::kThrow)); + auto rejected = check(v8::Function::New(context, &identityPromiseContinuationRef, + dataHandle, 1, v8::ConstructorBehavior::kThrow)); + + return { + .data = V8Ref(isolate, dataHandle), + .onFulfilled = V8Ref(isolate, fulfilled), + .onRejected = V8Ref(isolate, rejected), + }; + } + + void visitForGc(GcVisitor& visitor) { + visitor.visit(data, onFulfilled, onRejected); + } + + JSG_MEMORY_INFO(PersistentThenContinuation) { + tracker.trackField("data", data); + tracker.trackField("onFulfilled", onFulfilled); + tracker.trackField("onRejected", onRejected); + } +}; + +// Pre-built continuation for promise.catch_(onRejected) — identity propagation on fulfillment. +template +struct PersistentCatchContinuation { + V8Ref data; + V8Ref onFulfilled; + V8Ref onRejected; + + static PersistentCatchContinuation create(Lock& js, ErrorFunc&& errorFunc) { + using FuncPair = ThenCatchPair; + auto isolate = js.v8Isolate; + auto context = js.v8Context(); + auto dataHandle = wrapOpaque(context, FuncPair{false, kj::mv(errorFunc)}); + + auto fulfilled = check(v8::Function::New(context, &identityPromiseContinuationRef, + dataHandle, 1, v8::ConstructorBehavior::kThrow)); + auto rejected = + check(v8::Function::New(context, &promiseContinuationRef, + dataHandle, 1, v8::ConstructorBehavior::kThrow)); + + return { + .data = V8Ref(isolate, dataHandle), + .onFulfilled = V8Ref(isolate, fulfilled), + .onRejected = V8Ref(isolate, rejected), + }; + } + + void visitForGc(GcVisitor& visitor) { + visitor.visit(data, onFulfilled, onRejected); + } + + JSG_MEMORY_INFO(PersistentCatchContinuation) { + tracker.trackField("data", data); + tracker.trackField("onFulfilled", onFulfilled); + tracker.trackField("onRejected", onRejected); + } +}; + +// ======================================================================================= + template class PromiseWrapper; @@ -274,6 +442,44 @@ class Promise { &promiseContinuation); } + // Attach pre-built then+catch continuation functions from a PersistentContinuation. + // Unlike then(), this does not allocate any new V8 objects per call — it reuses the + // pre-built functions from the continuation. The PersistentContinuation must outlive + // the returned promise's resolution. + template + MaintainPromise thenRef( + Lock& js, PersistentContinuation& continuation) { + return js.withinHandleScope([&] { + auto context = js.v8Context(); + return MaintainPromise(js.v8Isolate, + check(consumeHandle(js)->Then(context, continuation.onFulfilled.getHandle(js), + continuation.onRejected.getHandle(js)))); + }); + } + + // Attach pre-built then-only continuation (identity propagation on rejection). + template + MaintainPromise thenRef( + Lock& js, PersistentThenContinuation& continuation) { + return js.withinHandleScope([&] { + auto context = js.v8Context(); + return MaintainPromise(js.v8Isolate, + check(consumeHandle(js)->Then(context, continuation.onFulfilled.getHandle(js), + continuation.onRejected.getHandle(js)))); + }); + } + + // Attach pre-built catch-only continuation (identity propagation on fulfillment). + template + Promise catchRef(Lock& js, PersistentCatchContinuation& continuation) { + return js.withinHandleScope([&] { + auto context = js.v8Context(); + return Promise(js.v8Isolate, + check(consumeHandle(js)->Then(context, continuation.onFulfilled.getHandle(js), + continuation.onRejected.getHandle(js)))); + }); + } + // whenResolved returns a new Promise that resolves when this promise resolves, // stopping the propagation of the resolved value. Unlike then(), calling whenResolved() // does not consume the promise, and whenResolved() can be called multiple times, diff --git a/src/workerd/tests/bench-pumpto.c++ b/src/workerd/tests/bench-pumpto.c++ index b15669be930..9e01469082d 100644 --- a/src/workerd/tests/bench-pumpto.c++ +++ b/src/workerd/tests/bench-pumpto.c++ @@ -100,10 +100,9 @@ jsg::Ref createValueStream( KJ_ASSERT_NONNULL(controller.template tryGet>()); if ((*counter)++ < numChunks) { - auto backing = jsg::BackingStore::alloc(js, chunkSize); - jsg::BufferSource buffer(js, kj::mv(backing)); - buffer.asArrayPtr().fill(0xAB); - c->enqueue(js, buffer.getHandle(js)); + auto ab = jsg::JsArrayBuffer::create(js, chunkSize); + ab.asArrayPtr().fill(0xAB); + c->enqueue(js, ab); } if (*counter == numChunks) { c->close(js); @@ -129,10 +128,9 @@ jsg::Ref createByteStream( KJ_ASSERT_NONNULL(controller.template tryGet>()); if ((*counter)++ < numChunks) { - auto backing = jsg::BackingStore::alloc(js, chunkSize); - jsg::BufferSource buffer(js, kj::mv(backing)); - buffer.asArrayPtr().fill(0xAB); - c->enqueue(js, kj::mv(buffer)); + auto ab = jsg::JsArrayBuffer::create(js, chunkSize); + ab.asArrayPtr().fill(0xAB); + c->enqueue(js, ab); } if (*counter == numChunks) { c->close(js); @@ -171,10 +169,9 @@ jsg::Ref createIoLatencyValueStream( JSG_VISITABLE_LAMBDA( (cRef = kj::mv(cRef), chunkSize, numChunks, counter), (cRef), (jsg::Lock & js) mutable { if ((*counter)++ < numChunks) { - auto backing = jsg::BackingStore::alloc(js, chunkSize); - jsg::BufferSource buffer(js, kj::mv(backing)); - buffer.asArrayPtr().fill(0xAB); - cRef->enqueue(js, buffer.getHandle(js)); + auto ab = jsg::JsArrayBuffer::create(js, chunkSize); + ab.asArrayPtr().fill(0xAB); + cRef->enqueue(js, ab); } if (*counter == numChunks) { cRef->close(js); diff --git a/src/workerd/tests/bench-stream-piping.c++ b/src/workerd/tests/bench-stream-piping.c++ index 59207c82e58..84ab83c255d 100644 --- a/src/workerd/tests/bench-stream-piping.c++ +++ b/src/workerd/tests/bench-stream-piping.c++ @@ -131,10 +131,9 @@ jsg::Ref createValueStream( KJ_ASSERT_NONNULL(controller.template tryGet>()); if ((*counter)++ < numChunks) { - auto backing = jsg::BackingStore::alloc(js, chunkSize); - jsg::BufferSource buffer(js, kj::mv(backing)); - buffer.asArrayPtr().fill(0xAB); - c->enqueue(js, buffer.getHandle(js)); + auto ab = jsg::JsArrayBuffer::create(js, chunkSize); + ab.asArrayPtr().fill(0xAB); + c->enqueue(js, ab); } if (*counter == numChunks) { c->close(js); @@ -164,10 +163,9 @@ jsg::Ref createByteStream(jsg::Lock& js, KJ_ASSERT_NONNULL(controller.template tryGet>()); if ((*counter)++ < numChunks) { - auto backing = jsg::BackingStore::alloc(js, chunkSize); - jsg::BufferSource buffer(js, kj::mv(backing)); - buffer.asArrayPtr().fill(0xAB); - c->enqueue(js, kj::mv(buffer)); + auto ab = jsg::JsArrayBuffer::create(js, chunkSize); + ab.asArrayPtr().fill(0xAB); + c->enqueue(js, ab); } if (*counter == numChunks) { c->close(js); @@ -213,10 +211,9 @@ jsg::Ref createSlowValueStream( JSG_VISITABLE_LAMBDA( (cRef = kj::mv(cRef), chunkSize, numChunks, counter), (cRef), (jsg::Lock & js) mutable { if ((*counter)++ < numChunks) { - auto backing = jsg::BackingStore::alloc(js, chunkSize); - jsg::BufferSource buffer(js, kj::mv(backing)); - buffer.asArrayPtr().fill(0xAB); - cRef->enqueue(js, buffer.getHandle(js)); + auto ab = jsg::JsArrayBuffer::create(js, chunkSize); + ab.asArrayPtr().fill(0xAB); + cRef->enqueue(js, ab); } if (*counter == numChunks) { cRef->close(js); @@ -261,10 +258,9 @@ jsg::Ref createIoLatencyValueStream( JSG_VISITABLE_LAMBDA( (cRef = kj::mv(cRef), chunkSize, numChunks, counter), (cRef), (jsg::Lock & js) mutable { if ((*counter)++ < numChunks) { - auto backing = jsg::BackingStore::alloc(js, chunkSize); - jsg::BufferSource buffer(js, kj::mv(backing)); - buffer.asArrayPtr().fill(0xAB); - cRef->enqueue(js, buffer.getHandle(js)); + auto ab = jsg::JsArrayBuffer::create(js, chunkSize); + ab.asArrayPtr().fill(0xAB); + cRef->enqueue(js, ab); } if (*counter == numChunks) { cRef->close(js); @@ -301,10 +297,9 @@ jsg::Ref createIoLatencyByteStream( JSG_VISITABLE_LAMBDA( (cRef = kj::mv(cRef), chunkSize, numChunks, counter), (cRef), (jsg::Lock & js) mutable { if ((*counter)++ < numChunks) { - auto backing = jsg::BackingStore::alloc(js, chunkSize); - jsg::BufferSource buffer(js, kj::mv(backing)); - buffer.asArrayPtr().fill(0xAB); - cRef->enqueue(js, kj::mv(buffer)); + auto ab = jsg::JsArrayBuffer::create(js, chunkSize); + ab.asArrayPtr().fill(0xAB); + cRef->enqueue(js, ab); } if (*counter == numChunks) { cRef->close(js); @@ -351,10 +346,9 @@ jsg::Ref createTimedValueStream(jsg::Lock& js, JSG_VISITABLE_LAMBDA( (cRef = kj::mv(cRef), chunkSize, numChunks, counter), (cRef), (jsg::Lock & js) mutable { if ((*counter)++ < numChunks) { - auto backing = jsg::BackingStore::alloc(js, chunkSize); - jsg::BufferSource buffer(js, kj::mv(backing)); - buffer.asArrayPtr().fill(0xAB); - cRef->enqueue(js, buffer.getHandle(js)); + auto ab = jsg::JsArrayBuffer::create(js, chunkSize); + ab.asArrayPtr().fill(0xAB); + cRef->enqueue(js, ab); } if (*counter == numChunks) { cRef->close(js); diff --git a/src/workerd/util/autogate.c++ b/src/workerd/util/autogate.c++ index 33900382587..d85896c4332 100644 --- a/src/workerd/util/autogate.c++ +++ b/src/workerd/util/autogate.c++ @@ -35,8 +35,6 @@ kj::StringPtr KJ_STRINGIFY(AutogateKey key) { return "wasm-shutdown-signal-shim"_kj; case AutogateKey::ENABLE_FAST_TEXTENCODER: return "enable-fast-textencoder"_kj; - case AutogateKey::ENABLE_DRAINING_READ_ON_STANDARD_STREAMS: - return "enable-draining-read-on-standard-streams"_kj; case AutogateKey::SQL_RESTRICT_RESERVED_NAMES: return "sql-restrict-reserved-names"_kj; case AutogateKey::INCREASE_SQLITE_HARD_HEAP_LIMIT: diff --git a/src/workerd/util/autogate.h b/src/workerd/util/autogate.h index 7dfcadcb69b..df88e1fc440 100644 --- a/src/workerd/util/autogate.h +++ b/src/workerd/util/autogate.h @@ -40,8 +40,6 @@ enum class AutogateKey { WASM_SHUTDOWN_SIGNAL_SHIM, // Enable fast TextEncoder implementation using simdutf ENABLE_FAST_TEXTENCODER, - // Enable draining read on standard streams - ENABLE_DRAINING_READ_ON_STANDARD_STREAMS, // Make SqlStorage::isAllowedName case-insensitive and enforce it on virtual tables (FTS5). SQL_RESTRICT_RESERVED_NAMES, // Increase the SQLite hard heap limit from 512 MiB to 8 GiB. diff --git a/src/wpt/fetch/api-test.ts b/src/wpt/fetch/api-test.ts index 65d8efe86e9..7e86e1f81d3 100644 --- a/src/wpt/fetch/api-test.ts +++ b/src/wpt/fetch/api-test.ts @@ -836,7 +836,16 @@ export default { 'Check response returned by static method redirect(), status = 308', ], }, - 'response/response-stream-bad-chunk.any.js': {}, + 'response/response-stream-bad-chunk.any.js': { + comment: 'Our impl is slightly more permissive in accepting strings', + expectedFailures: [ + 'ReadableStream with non-Uint8Array chunk passed to Response.arrayBuffer() causes TypeError', + 'ReadableStream with non-Uint8Array chunk passed to Response.blob() causes TypeError', + 'ReadableStream with non-Uint8Array chunk passed to Response.bytes() causes TypeError', + 'ReadableStream with non-Uint8Array chunk passed to Response.json() causes TypeError', + 'ReadableStream with non-Uint8Array chunk passed to Response.text() causes TypeError', + ], + }, 'response/response-stream-disturbed-1.any.js': {}, 'response/response-stream-disturbed-2.any.js': {}, 'response/response-stream-disturbed-3.any.js': {}, diff --git a/src/wpt/streams-test.ts b/src/wpt/streams-test.ts index c0db76fef0c..60a05341c62 100644 --- a/src/wpt/streams-test.ts +++ b/src/wpt/streams-test.ts @@ -207,7 +207,6 @@ export default { 'ReadableStream with byte source: getReader(), read(view), then cancel()', 'ReadableStream with byte source: read(view) with Uint32Array, then fill it by multiple enqueue() calls', 'ReadableStream with byte source: enqueue(), read(view) partially, then read()', - 'ReadableStream with byte source: read(view), then respond() and close() in pull()', // TODO(conform): The spec expects the read to fail here. Instead, we end up cancelling // it with a zero-length result, with the subsequent read marked as done. 'ReadableStream with byte source: read(view) with Uint16Array on close()-d stream with 1 byte enqueue()-d must fail', @@ -287,7 +286,6 @@ export default { 'ReadableStream teeing with byte source: canceling both branches in reverse order should aggregate the cancel reasons into an array', 'ReadableStream teeing with byte source: pull with BYOB reader, then pull with default reader', 'ReadableStream teeing with byte source: failing to cancel the original stream should cause cancel() to reject on branches', - 'ReadableStream teeing with byte source: should be able to read one branch to the end without affecting the other', 'ReadableStream teeing with byte source: canceling branch1 should not impact branch2', 'ReadableStream teeing with byte source: canceling branch2 should not impact branch1', 'ReadableStream teeing with byte source: canceling both branches in sequence with delay', @@ -579,7 +577,6 @@ export default { comment: 'To be investigated', expectedFailures: [ 'readable.cancel() and a parallel writable.close() should reject if a transformer.cancel() calls controller.error()', - 'writable.abort() and readable.cancel() should reject if a transformer.cancel() calls controller.error()', 'writable.abort() should not call cancel() again when already called from readable.cancel()', ], }, @@ -595,21 +592,13 @@ export default { 'when strategy.size calls controller.error() then throws, the constructor should throw the first error', 'controller.error() should do nothing after a transformer method has thrown an exception', 'controller.error() should close writable immediately after readable.cancel()', - 'erroring during write with backpressure should result in the write failing', - ], - }, - 'transform-streams/flush.any.js': { - comment: 'To be investigated', - expectedFailures: [ - 'error() during flush should cause writer.close() to reject', ], }, + 'transform-streams/flush.any.js': {}, 'transform-streams/general.any.js': { comment: 'To be investigated', expectedFailures: [ 'it should be possible to call transform() synchronously', - 'specifying a defined readableType should throw', - 'specifying a defined writableType should throw', 'terminate() should abort writable immediately after readable.cancel()', ], },