Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
328 changes: 245 additions & 83 deletions src/jsc/bindings/webcore/streams/BunStreamSource.cpp

Large diffs are not rendered by default.

10 changes: 8 additions & 2 deletions src/jsc/bindings/webcore/streams/BunStreamSource.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "root.h"
#include "StreamsForward.h"

#include "JSReadableByteStreamController.h"
#include "JSReadableStreamDefaultController.h"
#include <JavaScriptCore/JSCast.h>
#include <JavaScriptCore/JSInternalFieldObjectImpl.h>
Expand Down Expand Up @@ -67,13 +68,16 @@ class JSNativeStreamSourceAdapter final : public JSC::JSInternalFieldObjectImpl<
JSC::JSObject* pendingView() const { return internalField(Field::PendingView).get().getObject(); }
JSC::JSObject* closer() const { return internalField(Field::Closer).get().getObject(); }
JSC::JSValue drainValue() const { return internalField(Field::DrainValue).get(); }
JSReadableStreamDefaultController* controller() const { return dynamicDowncast<JSReadableStreamDefaultController>(internalField(Field::Controller).get()); }
// Exactly one is non-null once set: default for text-mode, byte for binary.
JSC::JSObject* controller() const { return internalField(Field::Controller).get().getObject(); }
JSReadableStreamDefaultController* defaultController() const { return dynamicDowncast<JSReadableStreamDefaultController>(internalField(Field::Controller).get()); }
JSReadableByteStreamController* byteController() const { return dynamicDowncast<JSReadableByteStreamController>(internalField(Field::Controller).get()); }

void setHandle(JSC::VM& vm, JSC::JSValue v) { internalField(Field::Handle).set(vm, this, v); }
void setPendingView(JSC::VM& vm, JSC::JSValue v) { internalField(Field::PendingView).set(vm, this, v); }
void setCloser(JSC::VM& vm, JSC::JSValue v) { internalField(Field::Closer).set(vm, this, v); }
void setDrainValue(JSC::VM& vm, JSC::JSValue v) { internalField(Field::DrainValue).set(vm, this, v); }
void setController(JSC::VM& vm, JSReadableStreamDefaultController* c) { internalField(Field::Controller).set(vm, this, c); }
void setController(JSC::VM& vm, JSC::JSObject* c) { internalField(Field::Controller).set(vm, this, c); }

void clearHandle(JSC::VM& vm) { internalField(Field::Handle).set(vm, this, JSC::jsUndefined()); }
void clearPendingView(JSC::VM& vm) { internalField(Field::PendingView).set(vm, this, JSC::jsUndefined()); }
Expand All @@ -88,6 +92,8 @@ class JSNativeStreamSourceAdapter final : public JSC::JSInternalFieldObjectImpl<
bool m_closed : 1 { false };
// Body.textStream(): each pulled byte span is UTF-8-decoded before enqueue.
bool m_textMode : 1 { false };
// the in-flight async pull's PendingView is the BYOB pull-into buffer (respond on fulfil).
bool m_pendingIsBYOB : 1 { false };
Bun::WebStreams::StreamingUTF8DecodeState m_textState;

private:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ static JSC::JSPromise* invokePromiseReturningMethod(JSC::VM& vm, JSC::JSGlobalOb
}

// The [[pullAlgorithm]] dispatch. The reachable kind set on a byte controller is exactly
// {JavaScript, Nothing, ByteTeeBranch}; the switch is total over SourceKind.
// {JavaScript, Nothing, ByteTeeBranch, Native}; the switch is total over SourceKind.
// Returns nullptr with no exception pending when the pull completed synchronously with a
// non-thenable result: the caller queues the upon-fulfillment handler without a wrapper promise.
static JSC::JSPromise* performByteControllerPullAlgorithm(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSReadableByteStreamController* controller)
Expand Down Expand Up @@ -156,11 +156,12 @@ static JSC::JSPromise* performByteControllerPullAlgorithm(JSC::VM& vm, JSC::JSGl
return nullptr;
case SourceKind::ByteTeeBranch:
RELEASE_AND_RETURN(scope, byteTeePullAlgorithm(globalObject, uncheckedDowncast<JSStreamTeeState>(controller->m_algorithms.algorithmContext.get()), controller->m_algorithms.teeBranchIndex));
case SourceKind::Native:
RELEASE_AND_RETURN(scope, nativeSourcePull(globalObject, controller));
case SourceKind::Transform:
case SourceKind::TeeBranch:
case SourceKind::FromIterable:
case SourceKind::CrossRealm:
case SourceKind::Native:
case SourceKind::TextDecode:
break;
}
Expand Down Expand Up @@ -190,11 +191,12 @@ static JSC::JSPromise* performByteControllerCancelAlgorithm(JSC::VM& vm, JSC::JS
RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined()));
case SourceKind::ByteTeeBranch:
RELEASE_AND_RETURN(scope, byteTeeCancelAlgorithm(globalObject, uncheckedDowncast<JSStreamTeeState>(controller->m_algorithms.algorithmContext.get()), controller->m_algorithms.teeBranchIndex, reason));
case SourceKind::Native:
RELEASE_AND_RETURN(scope, nativeSourceCancel(globalObject, controller, reason));
case SourceKind::Transform:
case SourceKind::TeeBranch:
case SourceKind::FromIterable:
case SourceKind::CrossRealm:
case SourceKind::Native:
case SourceKind::TextDecode:
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ class JSReadableByteStreamController final : public JSC::JSDestructibleObject {
// stream has NO size algorithm (a byte stream given a size strategy is a RangeError at
// construction). See SourceAlgorithmSlots (StreamQueue.h).
// The reachable m_algorithms.kind set on a BYTE controller is EXACTLY
// {JavaScript, Nothing, ByteTeeBranch}. CrossRealm is impossible (the cross-realm
// {JavaScript, Nothing, ByteTeeBranch, Native}. CrossRealm is impossible (the cross-realm
// readable endpoint is always a DEFAULT controller — JSCrossRealmTransformState's
// back-pointer is exact-typed to one) and Native always uses a DEFAULT controller.
// back-pointer is exact-typed to one). A text-mode Native source uses a DEFAULT controller.
Bun::WebStreams::SourceAlgorithmSlots m_algorithms;

// Internal methods
Expand Down
3 changes: 2 additions & 1 deletion src/jsc/bindings/webcore/streams/JSReadableStream.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,8 @@ JSC_DEFINE_HOST_FUNCTION(jsReadableStreamPrototypeFunction_getReader, (JSGlobalO
}

if (isBYOB) {
// A BYOB reader never materializes Bun's lazy modes.
stream->materializeForBYOBIfNeeded(lexicalGlobalObject);
RETURN_IF_EXCEPTION(scope, {});
auto* reader = acquireReadableStreamBYOBReader(lexicalGlobalObject, stream);
RETURN_IF_EXCEPTION(scope, {});
return JSValue::encode(reader);
Expand Down
8 changes: 8 additions & 0 deletions src/jsc/bindings/webcore/streams/JSReadableStream.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ class JSReadableStream final : public JSC::JSNonFinalObject {
// does. userJS: YES (direct pull setup / native handle.start()).
void materializeIfNeeded(JSC::JSGlobalObject*);

// Materialize a lazy binary native stream (a byte stream) so a BYOB reader attaches.
// Text-mode / DirectPending are left alone so the reader set-up rejects without user code.
Comment thread
robobun marked this conversation as resolved.
void materializeForBYOBIfNeeded(JSC::JSGlobalObject* globalObject)
{
if (m_bunMode == BunStreamMode::NativePending && !m_nativeTextMode)
materializeIfNeeded(globalObject);
}

// The value the old `$bunNativePtr` DOMAttribute getter returned.
JSC::JSValue nativePtrForJS() const
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,9 @@ template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSReadableStreamBYOBRead
if (!stream)
return throwVMTypeError(lexicalGlobalObject, scope, "ReadableStreamBYOBReader constructor requires a ReadableStream as its first argument"_s);

stream->materializeForBYOBIfNeeded(lexicalGlobalObject);
RETURN_IF_EXCEPTION(scope, {});

auto* structure = structureForNewTarget(vm, constructor, lexicalGlobalObject, asObject(callFrame->newTarget()));
RETURN_IF_EXCEPTION(scope, {});
auto* reader = JSReadableStreamBYOBReader::create(vm, structure);
Expand Down
33 changes: 15 additions & 18 deletions src/jsc/bindings/webcore/streams/ReadableStreamOperations.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,7 @@ void readableStreamReaderGenericRelease(JSGlobalObject* globalObject, JSReadable
}
markPromiseAsHandled(vm, reader->m_closedPromise.get());

WebCore::JSNativeStreamSourceAdapter* nativeAdapter = nullptr;
switch (stream->m_controllerKind) {
case ControllerKind::None:
case ControllerKind::NativeSink:
Expand All @@ -537,30 +538,26 @@ void readableStreamReaderGenericRelease(JSGlobalObject* globalObject, JSReadable
case ControllerKind::Default: {
auto* controller = defaultControllerOf(stream);
controller->releaseSteps();
// Bun: drop the native handle's event-loop ref when its consumer releases the lock.
if (stream->m_nativePtr && controller->m_algorithms.kind == SourceKind::Native) {
const auto* adapter = uncheckedDowncast<WebCore::JSNativeStreamSourceAdapter>(controller->m_algorithms.algorithmContext.get());
if (auto* handle = adapter->handle()) {
JSValue updateRef = handle->getIfPropertyExists(globalObject, builtinNames(vm).updateRefPublicName());
RETURN_IF_EXCEPTION(scope, void());
if (updateRef && updateRef.isCallable()) {
auto callData = JSC::getCallData(updateRef);
MarkedArgumentBuffer args;
args.append(jsBoolean(false));
ASSERT(!args.hasOverflowed());
JSC::call(globalObject, updateRef, callData, handle, args);
RETURN_IF_EXCEPTION(scope, void());
}
}
}
if (stream->m_nativePtr && controller->m_algorithms.kind == SourceKind::Native)
nativeAdapter = uncheckedDowncast<WebCore::JSNativeStreamSourceAdapter>(controller->m_algorithms.algorithmContext.get());
break;
}
case ControllerKind::Byte:
byteControllerOf(stream)->releaseSteps();
case ControllerKind::Byte: {
auto* controller = byteControllerOf(stream);
controller->releaseSteps();
if (stream->m_nativePtr && controller->m_algorithms.kind == SourceKind::Native)
nativeAdapter = uncheckedDowncast<WebCore::JSNativeStreamSourceAdapter>(controller->m_algorithms.algorithmContext.get());
break;
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
stream->m_reader.clear();
reader->m_stream.clear();
// Bun: drop the native handle's event-loop ref after the reader is unlinked so an
// exception inside updateRef cannot leave the stream locked.
Comment thread
robobun marked this conversation as resolved.
if (nativeAdapter) {
nativeSourceDropEventLoopRef(globalObject, nativeAdapter);
RETURN_IF_EXCEPTION(scope, void());
}
}

// ReadableStreamReaderGenericCancel(reader, reason)
Expand Down
2 changes: 1 addition & 1 deletion src/jsc/bindings/webcore/streams/StreamsForward.h
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ enum class SourceKind : uint8_t {
ByteTeeBranch, // a ReadableByteStreamTee branch (context = the JSStreamTeeState)
FromIterable, // ReadableStream.from(asyncIterable) (context = JSStreamFromIterableContext)
CrossRealm, // receiving end of a postMessage transfer (context = JSCrossRealmTransformState)
Native, // Bun: lazily-materialized native source on a DEFAULT controller
Native, // Bun: lazily-materialized native source (byte controller; default in text mode)
// (context = JSNativeStreamSourceAdapter)
TextDecode, // Body.textStream() reading from an existing byte stream
// (algorithmContext = source JSReadableStreamDefaultReader;
Expand Down
3 changes: 2 additions & 1 deletion src/jsc/bindings/webcore/streams/WebStreamsExports.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,8 @@ extern "C" JSC::EncodedJSValue ReadableStream__empty(Zig::GlobalObject* globalOb
{
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
auto* stream = createReadableStream(globalObject, SourceKind::Nothing, nullptr, jsUndefined());
// A closed byte stream so a BYOB reader attaches (every caller is an empty byte source).
auto* stream = createReadableByteStream(globalObject, SourceKind::Nothing, nullptr);
RETURN_IF_EXCEPTION(scope, {});
readableStreamClose(globalObject, stream);
RETURN_IF_EXCEPTION(scope, {});
Expand Down
16 changes: 9 additions & 7 deletions src/jsc/bindings/webcore/streams/WebStreamsInternals.h
Original file line number Diff line number Diff line change
Expand Up @@ -508,17 +508,19 @@ void pipeToReadRequestErrorSteps(JSC::JSGlobalObject*, JSStreamPipeToOperation*,

// BunStreamSource.cpp — the lazy native source and the native-sink pumps.

// lazyLoadStream: installs the Native default controller (or the empty fast path).
// lazyLoadStream: installs the Native controller (byte, or default in text mode) or the empty fast path.
void materializeNativeSource(JSC::JSGlobalObject*, JSReadableStream*); // userJS: yes — BunStreamSource.cpp

// The SourceKind::Native algorithm ARMS. The pull/cancel dispatch is a TOTAL
// `switch (m_algorithms.kind)` in JSReadableStreamDefaultController.cpp (a Native source is
// ALWAYS a default controller); these bodies live HERE per BunStreamSource.h's owner rule,
// so this is the declared bridge between the two files. The controller's algorithmContext is
// the JSNativeStreamSourceAdapter for all three.
JSC::JSValue nativeSourceStart(JSC::JSGlobalObject*, JSReadableStreamDefaultController*); // userJS: no (native handle.start; enqueues the drain value) — BunStreamSource.cpp
// The SourceKind::Native algorithm ARMS. Binary sources install a byte controller and text-mode
// sources a default controller; both controllers dispatch into the matching overload here.
// controller->m_algorithms.algorithmContext is the JSNativeStreamSourceAdapter.
Comment thread
robobun marked this conversation as resolved.
JSC::JSValue nativeSourceStart(JSC::JSGlobalObject*, JSNativeStreamSourceAdapter*); // userJS: no (native handle.start; enqueues the drain value) — BunStreamSource.cpp
JSC::JSPromise* nativeSourcePull(JSC::JSGlobalObject*, JSReadableStreamDefaultController*); // userJS: no (native handle.pull; its promise's reactions are onNativePull*) — BunStreamSource.cpp
JSC::JSPromise* nativeSourcePull(JSC::JSGlobalObject*, JSReadableByteStreamController*); // userJS: no — BunStreamSource.cpp
JSC::JSPromise* nativeSourceCancel(JSC::JSGlobalObject*, JSReadableStreamDefaultController*, JSC::JSValue reason); // userJS: no (native handle.cancel + teardown) — BunStreamSource.cpp
JSC::JSPromise* nativeSourceCancel(JSC::JSGlobalObject*, JSReadableByteStreamController*, JSC::JSValue reason); // userJS: no — BunStreamSource.cpp
// Bun: drop the native handle's event-loop ref when its consumer releases the lock.
void nativeSourceDropEventLoopRef(JSC::JSGlobalObject*, const JSNativeStreamSourceAdapter*); // userJS: no — BunStreamSource.cpp
// readableStreamCancel's ControllerKind::None arm for a still-NativePending stream: calls
// handle.updateRef(false) + handle.cancel(reason) on m_nativePtr directly, no materialize.
JSC::JSPromise* cancelPendingNativeSource(JSC::JSGlobalObject*, JSReadableStream*, JSC::JSValue reason); // userJS: no — BunStreamSource.cpp
Expand Down
Loading
Loading