Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
164 commits
Select commit Hold shift + click to select a range
f1fa832
fetch: yield the async-iterable pump per write when feeding an ArrayB…
robobun Jul 27, 2026
61faf95
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 27, 2026
4c9c022
fetch: hand a DirectPending body's pull() straight to the ResumableSink
robobun Jul 27, 2026
e63fb41
test: inherit stderr instead of piping unused
robobun Jul 27, 2026
67a25c5
ResumableSink: match JSDirectStreamController's end/close/error contract
robobun Jul 27, 2026
ae14d0e
ResumableSink: controller.error() with no argument aborts
robobun Jul 27, 2026
b201c14
ci: retrigger
robobun Jul 27, 2026
b95068b
Merge branch 'main' into farm/40eaa140/fetch-node-readable-body-spin
Jarred-Sumner Jul 29, 2026
5563870
fetch: drive async-iterable request bodies into the ResumableSink nat…
robobun Jul 29, 2026
6157425
async-iterable pump: recover the op from the bound pull instead of m_…
robobun Jul 29, 2026
653d7c2
Merge remote-tracking branch 'origin/main' into farm/40eaa140/fetch-n…
robobun Jul 30, 2026
a0fa8a1
revert src/ to main for ResumableSink→JSSink consolidation
robobun Jul 30, 2026
0622880
Register FetchRequestBodySink JSSink in codegen + Sink.h + StartTag
robobun Jul 30, 2026
2f5078d
NetworkSink: report Writable::Backpressure + signal.ready on drain
robobun Jul 30, 2026
3bf80d4
fetch: route sink cancel/detach through cancel_request_body_sink
robobun Jul 30, 2026
a64ede4
Delete C++ ResumableSink pump (JSResumableSinkPumpOperation et al)
robobun Jul 30, 2026
9d6031b
multipart: replace ResumableSinkBackpressure with local UploadBackpre…
robobun Jul 30, 2026
c88d7b8
webcore/fetch: add FetchRequestBodySink JSSink type
robobun Jul 30, 2026
2cc910c
webcore: unregister resumable_sink module + re-exports
robobun Jul 30, 2026
ce04e51
FetchTasklet: swap sink to FetchRequestBodySink; drain/write off JSSink
robobun Jul 30, 2026
85d2f16
Register fetch/S3 request-stream .then() handlers in thenable dispatch
robobun Jul 30, 2026
485e4e0
s3: upload_stream uses NetworkSink + assign_to_stream
robobun Jul 30, 2026
8addbf6
FetchRequestBodySink: delegate write to write_request_data
robobun Jul 30, 2026
f8fa2be
Declare FetchRequestBodySink ABI in headers.h; drop ResumableSink stubs
robobun Jul 30, 2026
05e05e7
fetch request-body sink: single write_end_request via pump-promise path
robobun Jul 30, 2026
11d2c74
fetch: drop signal.clear in ignore_remaining; resolve flush on cancel
robobun Jul 30, 2026
ee22dd5
s3 upload_stream: use AnyPromise safe accessors + set_handled
robobun Jul 30, 2026
35f6c15
FetchRequestBodySink: gate backpressure on JS-side pending_bytes
robobun Jul 30, 2026
452b00f
FetchRequestBodySink: backpressure on every write, not at HWM
robobun Jul 30, 2026
b8acab7
FetchRequestBodySink: convert latin1/utf16 directly into the stream b…
robobun Jul 30, 2026
f2c8477
s3 upload_stream: capture end_promise before assign_to_stream; settle…
robobun Jul 30, 2026
e6d4f33
drop dead code: cancel_from_native, FetchRequestBodySink::{new,global…
robobun Jul 30, 2026
728441c
address review: utf16 length with replacement; s3 continue_stream bef…
robobun Jul 30, 2026
57fade4
s3: drop carried-over TODO comments in moved lines
robobun Jul 30, 2026
1b51f1a
s3: keep js_err alive across the Failure branch's JSC calls
robobun Jul 30, 2026
f7de8ae
s3: drop task.ended guard in handle_reject_stream
robobun Jul 30, 2026
e64e795
s3: trim handle_reject_stream comment
robobun Jul 30, 2026
fec25c2
s3: ref the MultiPartUpload across the single-file PUT
robobun Jul 30, 2026
3c9c618
address review nits: drop test timeout; gate NetworkSink end_promise …
robobun Jul 30, 2026
863eb83
FileSink: signal→source JsCell<SourceHandle>; to_result backpressure …
robobun Jul 30, 2026
d768829
streams: add SourceHandle<T: JsSinkAbi> tagged source enum
robobun Jul 30, 2026
0e14582
readStreamIntoSink: backpressure via m_onPull instead of flush(true)
robobun Jul 30, 2026
360d455
NetworkSink: signal→source SourceHandle, drop flush_promise; s3 callers
robobun Jul 30, 2026
90b13a3
Add SinkHandle enum; swap ByteStream pipe→sink with backpressure
robobun Jul 30, 2026
1ffd3b9
HTTPServerWritable: swap signal:Signal -> source:SourceHandle<Self>
robobun Jul 30, 2026
335efb7
JSSink::assign_to_stream: wrap bits into SourceHandle; drop jsvalue_ptr
robobun Jul 30, 2026
de7eb66
test: add native pipe backpressure/duplex/s3 tests; un-todo spawn cancel
robobun Jul 30, 2026
4bbf301
RequestContext: write_chunk/end_chunk + SinkHandle::http_response hookup
robobun Jul 30, 2026
6b46a40
FetchRequestBodySink: signal→source SourceHandle, drop flush_promise
robobun Jul 30, 2026
fb320ea
Add is_backed_up() to Posix/WindowsStreamingWriter
robobun Jul 30, 2026
b938b1b
ArrayBufferSink: swap signal field to SourceHandle<Self>
robobun Jul 30, 2026
c36aa30
Subprocess/ShellWritable use SourceHandle; drop SignalHandler impls
robobun Jul 30, 2026
b22ebd3
ValueBufferer: write_chunk->Writable + SinkHandle hookup; drop PipeHa…
robobun Jul 30, 2026
85ac139
fetch/s3: native ByteStream→sink bypass of assign_to_stream
robobun Jul 30, 2026
ab8ebff
Delete Signal/SignalVTable/SignalHandler/SinkSignal/Pipe/PipeHandler/…
robobun Jul 30, 2026
80b8e2c
NetworkSink: restore flush_promise for .writer() path
robobun Jul 30, 2026
16cd2e3
ByteStream: forward upstream Err to sink.end when back-pressured
robobun Jul 30, 2026
55e0c08
rsis: settle m_result when sink closes during backpressure wait
robobun Jul 30, 2026
76e6bce
FileSink: gate Backpressure to pump sources; resume on POSIX drain
robobun Jul 30, 2026
1e3ba61
is_backed_up(): track EAGAIN, not buffered bytes (match #33399)
robobun Jul 30, 2026
f559d76
Body.rs: fix ValueBufferer SinkHandle SAFETY comment attribution
robobun Jul 30, 2026
6336d00
subprocess: migrate FileSink signal→SourceHandle::Subprocess checks
robobun Jul 30, 2026
7294221
RequestContext: raw-ptr dispatch in on_writable_byte_stream; drop dea…
robobun Jul 30, 2026
3c5ad02
JSSink::assign_to_stream: pre-seed JSController to catch sync detach
robobun Jul 30, 2026
e22db5f
test: fix s3 stream arg, fetch RSS bound, serve backpressure observable
robobun Jul 30, 2026
7b6042d
JSSink::detach: unpipe ByteStream; FetchTasklet: bytes.sink.is_some()
robobun Jul 30, 2026
925745b
FetchRequestBodySink: restore flush_promise for async-iterable pump
robobun Jul 30, 2026
c5cb0f2
SourceHandle: treat JSController(0) placeholder as no-op in close/ready
robobun Jul 30, 2026
7cd300b
fetch: drive write_end_request on native ByteStream sink completion
robobun Jul 30, 2026
f375b8d
test: spawn stdin + Bun.write fed by native ByteStream (req/resp.body)
robobun Jul 30, 2026
99372d0
NetworkSink: keep flush_promise for writer().flush(); settle on S3 done
robobun Jul 30, 2026
ecde998
FetchRequestBodySink: delete dead flush_promise/pending_bytes fields
robobun Jul 30, 2026
5efc802
FileSink::assign_to_stream: add native ByteStream fast-path
robobun Jul 30, 2026
8479003
FetchRequestBodySink: restore flush_promise for async-iterable pump
robobun Jul 30, 2026
50be62c
test: correct Bun.write(Bun.file,Response) comment re: FileSink path
robobun Jul 30, 2026
4383813
FileSink: assert ByteStream sink unset before fast-path hookup
robobun Jul 30, 2026
9bc59a7
Blob: pre-check Source::Bytes before FileSink assign_to_stream
robobun Jul 30, 2026
534baaf
NetworkSink: drop source!=None flush short-circuit; keep flush_promise
robobun Jul 30, 2026
ca6df91
Blob: revert Source::Bytes pre-check; native path resolved 0 early
robobun Jul 30, 2026
d5a4b5a
trim comment-cop flagged comments
robobun Jul 30, 2026
213dd64
trim comment-cop flagged comments (all 40)
robobun Jul 30, 2026
41eac3e
SinkHandle::end(ValueBufferer): route Err through write thunk; drop d…
robobun Jul 30, 2026
940ee1f
SinkHandle::end: propagate full StreamError to sinks
robobun Jul 30, 2026
3a21562
FileReader: native SinkHandle fast-path for fetch request body
robobun Jul 30, 2026
efb4406
test: spawn+fetch native SourceHandle/SinkHandle end-to-end coverage
robobun Jul 30, 2026
07a9582
HTTPServerWritable: fire source.ready() on drain so readStreamIntoSin…
robobun Jul 30, 2026
e915bca
readDirectStream: leave m_onPull empty so onReady cannot re-enter pull
robobun Jul 30, 2026
21badb2
Detach native source on end_from_stream; keep S3 wrapper ref across f…
robobun Jul 30, 2026
b60016a
FileSink: keep JSController on Pending path; resume ByteStream from o…
robobun Jul 30, 2026
f8d1aa2
FileSink: snapshot SourceHandle before ready()/close() to avoid JsCel…
robobun Jul 30, 2026
90302f8
S3UploadStreamWrapper: release pump ref when S3 fails on native ByteS…
robobun Jul 30, 2026
83015f4
FileSink::on_write: call writer.end() on done+Drained outside pending…
robobun Jul 30, 2026
4d62746
trim comment-cop flagged on_write comment
robobun Jul 30, 2026
ff21b71
multipart: re-check queue after on_writable; ByteStream: surface pre-…
robobun Jul 30, 2026
235563e
trim comment-cop flagged comments
robobun Jul 30, 2026
785e879
test: skip spawn stderr backpressure test on Windows
robobun Jul 30, 2026
38481f7
Address review nits: NetworkSink write contract, ByteStream sink guar…
robobun Jul 30, 2026
2b63270
Trim review-bot-flagged comments
robobun Jul 31, 2026
aa59ec1
test: bound parked poll loop with 30s deadline
robobun Jul 31, 2026
fd8b824
FileSink: fall through to assign_to_stream when ByteStream already ha…
robobun Jul 31, 2026
f78b9b2
streams: pause upstream socket when native sink backpressures; write(…
robobun Jul 31, 2026
93cd4d0
streams: write() returns Promise on backpressure for HTTPServerWritab…
robobun Jul 31, 2026
8230dbf
Remove stray test registry artifacts
robobun Jul 31, 2026
3494ea7
FetchRequestBodySink: park JS pump on every scheduled write
robobun Jul 31, 2026
5dacd83
NewSource: replace 5 fn-ptr fields with producer:SourceHandle
robobun Jul 31, 2026
9ea865d
SinkHandle: collapse Http*/H3Response into ServerResponse(AnyRequestC…
robobun Jul 31, 2026
62dff10
FetchTasklet: re-sig stream callbacks; use SourceHandle producer
robobun Jul 31, 2026
149e5e9
RequestContext: re-sig write_chunk/end_chunk/on_request_body_stream_d…
robobun Jul 31, 2026
7fabfdd
streams: degenericize SourceHandle, add SinkKind + 3 producer variants
robobun Jul 31, 2026
1df1d05
AnyRequestContext: add write_chunk/end_chunk/on_request_body_stream_d…
robobun Jul 31, 2026
389fc3d
Add SINK_KIND to JsSinkAbi; degenericize assign_to_stream/detach
robobun Jul 31, 2026
3eb13d8
s3: hoist S3DownloadStreamWrapper, wire to SourceHandle::S3DownloadBody
robobun Jul 31, 2026
8cab178
streams: add UpstreamSource trait + impls; collapse SourceHandle arms
robobun Jul 31, 2026
f003ff4
ByteStream: route drain/cancel/attach signals through producer
robobun Jul 31, 2026
ec520b4
PendingValue: replace 3 stream fn-ptrs with producer:SourceHandle
robobun Jul 31, 2026
c292f99
webcore: add DownstreamSink trait; SinkHandle dispatches via it
robobun Jul 31, 2026
a4353f5
De-parameterize SourceHandle<X> fields + source() impls
robobun Jul 31, 2026
544e087
server: replace on_stream_drained fn-ptr with producer:ServerRequestBody
robobun Jul 31, 2026
503c27d
SourceHandle: type Subprocess/ShellWritable ptrs at call-sites
robobun Jul 31, 2026
af4ae58
Dead-code sweep: drop detach_abi dup, unused JsSinkAbi externs, inlin…
robobun Jul 31, 2026
55602a3
FetchRequestBodySink: degenericize source; allow dead_code in sink ex…
robobun Jul 31, 2026
69dd9f7
RequestContext: rename clear_*_drain_handler -> detach_request_body_p…
robobun Jul 31, 2026
2969578
streams: restore SAFETY comments + allow ptr-deref on UpstreamSource …
robobun Jul 31, 2026
bf5fde6
RequestContext: allow not_unsafe_ptr_arg_deref on write_chunk/end_chunk
robobun Jul 31, 2026
860eaf9
Sink: split decl_js_sink_externs per-subset, drop dead_code allow
robobun Jul 31, 2026
00203c2
s3: clear sink.source before clean-EOF end; settle sink.pending on re…
robobun Jul 31, 2026
776ed0f
Merge remote-tracking branch 'origin/main' into farm/40eaa140/fetch-n…
robobun Jul 31, 2026
6a10dca
s3: drop pre-existing TODO comment surfaced by de-indent
robobun Jul 31, 2026
3fd09b0
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 31, 2026
1d7b29c
UpstreamSource/DownstreamSink: take &mut self, centralize unsafe dere…
robobun Jul 31, 2026
c28f981
SourceHandle/SinkHandle: BackRef<T> variants; Box::leak in fast-paths…
robobun Jul 31, 2026
3dc2bec
Drop SinkKind: unify controller onReady/onClose via JSReadableSinkCon…
robobun Jul 31, 2026
45d1b3f
RequestContext: restore raw-ptr dispatch for re-entrant write_chunk/e…
robobun Jul 31, 2026
687c7a8
webcore: drop dead DownstreamSink trait
robobun Jul 31, 2026
bffe087
FileSink: return Backpressure for FileReader source on pending write
robobun Jul 31, 2026
b8d9780
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 31, 2026
a8ca0cc
test(spawn): await cancel() via Promise.withResolvers instead of polling
robobun Jul 31, 2026
f781147
test: drop native-path tests that also pass via JS pump on main
robobun Jul 31, 2026
0f106b7
test(spawn): drop fetch-body-as-stdin bounded-memory test
robobun Jul 31, 2026
1ea7dc5
headers.h: drop dead per-sink __detachPtr/onClose/onReady CPP_DECLs
robobun Jul 31, 2026
ee20d5f
headers.h: drop remaining H3ResponseSink dead decls
robobun Jul 31, 2026
8210cae
test(spawn): fetch response.body → child stdin bounded-memory regress…
Jarred-Sumner Jul 31, 2026
f1c0f54
test(spawn): run FileSink leak iterations concurrently (debug+ASAN ti…
robobun Jul 31, 2026
c0a5be4
Merge remote-tracking branch 'origin/main' into farm/40eaa140/fetch-n…
Jarred-Sumner Jul 31, 2026
024bc9f
test: cover JS-pull spawn stdin, locked-after-attach, serve proxy bac…
Jarred-Sumner Jul 31, 2026
11418cd
test(spawn): await child.exited + close source in bounds-memory fixtu…
robobun Jul 31, 2026
b9025ef
Backpressure regression tests; normalize resourceUsage().maxRSS units
Jarred-Sumner Jul 31, 2026
37f6cd6
Merge branch 'farm/40eaa140/fetch-node-readable-body-spin' of https:/…
Jarred-Sumner Jul 31, 2026
aec4a56
boringssl: remove duplicate is_safe_alt_name from merge
robobun Jul 31, 2026
d6d45d1
test(serve): measure stall-phase RSS, not whole-process maxRSS, in pr…
robobun Jul 31, 2026
c50455e
test(s3): drop vacuous RSS bound from ByteStream -> NetworkSink uploa…
robobun Jul 31, 2026
d9d0046
test(spawn): parallelize stdin FileSink leak iterations and drop expl…
robobun Jul 31, 2026
440f8c3
Remove orphaned setHandlers builtin name
robobun Jul 31, 2026
688a574
test: move runFixtureMaxRSS/emptyProcessMaxRSS helpers into harness.ts
robobun Jul 31, 2026
b07816c
test(fetch): wait for body bytes at the target before asserting the s…
Jarred-Sumner Jul 31, 2026
3cd26a7
Merge branch 'farm/40eaa140/fetch-node-readable-body-spin' of https:/…
Jarred-Sumner Jul 31, 2026
b58eb63
Revert "test(serve): measure stall-phase RSS, not whole-process maxRS…
Jarred-Sumner Jul 31, 2026
45d0d3b
Merge branch 'farm/40eaa140/fetch-node-readable-body-spin' of https:/…
Jarred-Sumner Jul 31, 2026
2ebb0fc
test: halve the bounded-memory fixtures' payload to 128 MB
Jarred-Sumner Jul 31, 2026
f3d5e30
Body: do not report a Locked body's content-length as allocated memory
robobun Jul 31, 2026
2ebc01e
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 31, 2026
7cc45af
Trim long comments flagged by comment-cop
robobun Jul 31, 2026
d6f092d
bench: add fetch streaming/backpressure harness
Jarred-Sumner Jul 31, 2026
6b34885
Merge branch 'farm/40eaa140/fetch-node-readable-body-spin' of https:/…
Jarred-Sumner Jul 31, 2026
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
5 changes: 3 additions & 2 deletions src/jsc/bindings/webcore/streams/BunAsyncIterableSource.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -270,8 +270,9 @@ static NextStep asyncIterHandleNextResult(JSGlobalObject* globalObject, JSAsyncI
JSValue wrote = invokeOptionalMethod(globalObject, controller, WebCore::builtinNames(vm).writePublicName(), writeArgs);
if (scope.exception()) [[unlikely]]
goto abrupt;
if (wrote && wrote.isNumber() && wrote.asNumber() < 0) {
// The HTTP sink reports backpressure with a negative return: wait for the drain.
if ((wrote && wrote.isNumber() && wrote.asNumber() < 0) || wrote.isFalse()) {
// Backpressure: a JSSink returns a negative number, a ResumableSink returns false.
// Either way `flush(true)` returns the drain promise.
Comment thread
robobun marked this conversation as resolved.
Outdated
MarkedArgumentBuffer flushArgs;
flushArgs.append(jsBoolean(true));
JSValue flushed = invokeOptionalMethod(globalObject, controller, builtinNames(vm).flushPublicName(), flushArgs);
Expand Down
114 changes: 113 additions & 1 deletion src/jsc/bindings/webcore/streams/BunStreamSource.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1381,7 +1381,7 @@
if (hasError)
args.append(error);
ASSERT(!args.hasOverflowed());
invokeMethod(vm, globalObject, sink, builtinNames(vm).endPublicName(), args);
invokeMethod(vm, globalObject, sink, hasError ? vm.propertyNames->error : builtinNames(vm).endPublicName(), args);
if (catchScope.exception()) [[unlikely]] {
if (takeAbruptCompletion(globalObject, catchScope).isEmpty())
return;
Expand Down Expand Up @@ -1527,12 +1527,87 @@
RELEASE_AND_RETURN(scope, resumableDrain(vm, globalObject, op));
}

// DirectPending -> ResumableSink without an ArrayBufferSink intermediary: the underlying
// source's pull() writes straight into sink.write (bool backpressure, flush(true) drain
// promise) exactly as readDirectStream hands a JSSink to the same pull.
Comment thread
robobun marked this conversation as resolved.
Outdated
static void resumableSetupDirect(JSC::VM& vm, JSGlobalObject* globalObject, JSReadableStream* stream, JSObject* sink, JSObject* underlyingSource)
{
auto scope = DECLARE_THROW_SCOPE(vm);
auto* runtime = WebCore::JSStreamsRuntime::from(globalObject);

stream->m_directUnderlyingSource.clear();
stream->m_bunMode = BunStreamMode::Default;

JSValue pull = underlyingSource->get(globalObject, builtinNames(vm).pullPublicName());
RETURN_IF_EXCEPTION(scope, );

double rawHighWaterMark = stream->m_bunHighWaterMark;
auto* startOptions = constructEmptyObject(globalObject);
startOptions->putDirect(vm, builtinNames(vm).highWaterMarkPublicName(), jsNumber(std::isnan(rawHighWaterMark) ? 0 : rawHighWaterMark));
MarkedArgumentBuffer startArgs;
startArgs.append(startOptions);
ASSERT(!startArgs.hasOverflowed());
invokeMethod(vm, globalObject, sink, builtinNames(vm).startPublicName(), startArgs);
RETURN_IF_EXCEPTION(scope, );

auto* cancelBound = createBoundHandler(globalObject, runtime->boundResumableSinkDirectCancel(), underlyingSource);
RETURN_IF_EXCEPTION(scope, );
MarkedArgumentBuffer handlerArgs;
handlerArgs.append(jsUndefined());
handlerArgs.append(cancelBound);
ASSERT(!handlerArgs.hasOverflowed());
invokeMethod(vm, globalObject, sink, builtinNames(vm).setHandlersPublicName(), handlerArgs);
RETURN_IF_EXCEPTION(scope, );

Check warning on line 1560 in src/jsc/bindings/webcore/streams/BunStreamSource.cpp

View check run for this annotation

Claude / Claude Code Review

resumableSetupDirect drops the underlyingSource.close() lifecycle hook

`resumableSetupDirect` reads only `pull` and wires only `cancel` from the underlying source, so a hand-written `{type:'direct', pull, close}` fetch/S3 body no longer sees its `underlyingSource.close(reason)` hook fire on `controller.close()/end()/error()` — pre-PR the materialized `JSDirectStreamController` invoked it via `callUnderlyingSourceClose` on both `onClose` and `handleError`. The async-iterator wrapper is coincidentally masked (its `asyncIterFinishSuccess` independently returns the ite
Comment thread
robobun marked this conversation as resolved.
Outdated

stream->m_lockedWithoutReader = true;

if (!pull.isCallable()) {
MarkedArgumentBuffer noArgs;
scope.release();
invokeMethod(vm, globalObject, sink, builtinNames(vm).endPublicName(), noArgs);
return;
}

MarkedArgumentBuffer pullArgs;
pullArgs.append(sink);
ASSERT(!pullArgs.hasOverflowed());
JSValue maybePromise = JSC::call(globalObject, pull, getCallData(pull), underlyingSource, pullArgs);
RETURN_IF_EXCEPTION(scope, );

Check warning on line 1575 in src/jsc/bindings/webcore/streams/BunStreamSource.cpp

View check run for this annotation

Claude / Claude Code Review

Destructured controller methods throw on the direct-ResumableSink path

The pre-PR path handed `pull()` a `JSDirectStreamController`, which installs `write/end/close/flush/error` as **own `JSBoundFunction` properties** (see `installDirectControllerMethods`), so `const {write, close} = c; write('hi'); close()` worked. Here `pull()` now receives the ResumableSink, whose methods are prototype entries with the default `invalidThisBehavior: Throw` — a destructured call reaches the codegen thunk with `thisValue()===undefined`, throws `createInvalidThisError`, the pull rej
Comment thread
robobun marked this conversation as resolved.
Outdated

if (auto* pullPromise = dynamicDowncast<JSPromise>(maybePromise)) {
markPromiseAsHandled(vm, pullPromise);
pullPromise->performPromiseThenWithContext(vm, globalObject, runtime->onReturnUndefined(), runtime->onResumableSinkDirectPullRejected(), jsUndefined(), sink);
}
}
Comment thread
robobun marked this conversation as resolved.
Outdated

JSValue assignStreamIntoResumableSink(JSGlobalObject* globalObject, JSReadableStream* stream, JSObject* resumableSink)
{
auto& vm = getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
auto* domGlobalObject = defaultGlobalObject(globalObject);
auto* runtime = WebCore::JSStreamsRuntime::from(globalObject);

if (JSObject* underlyingSource = stream->m_directUnderlyingSource.get(); stream->m_bunMode == BunStreamMode::DirectPending && underlyingSource) {
JSValue thrown;
{
auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm);
resumableSetupDirect(vm, globalObject, stream, resumableSink, underlyingSource);
if (catchScope.exception()) [[unlikely]] {
thrown = takeAbruptCompletion(globalObject, catchScope);
if (thrown.isEmpty())
return {};
}
}
if (!thrown.isEmpty()) {
MarkedArgumentBuffer args;
args.append(thrown);
ASSERT(!args.hasOverflowed());
invokeMethod(vm, globalObject, resumableSink, vm.propertyNames->error, args);
}
RETURN_IF_EXCEPTION(scope, {});
return jsUndefined();
}

auto* op = JSResumableSinkPumpOperation::create(vm, runtime->resumableSinkPumpOperationStructure(domGlobalObject));
op->m_stream.set(vm, op, stream);
op->m_sink.set(vm, op, resumableSink);
Expand Down Expand Up @@ -1803,4 +1878,41 @@
return JSValue::encode(jsUndefined());
}

// Native cancel on the direct path: (underlyingSource, unused, reason) -> source.cancel(reason).
JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_boundResumableSinkDirectCancel, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
auto& vm = getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
JSObject* underlyingSource = callFrame->argument(0).getObject();
if (!underlyingSource)
return JSValue::encode(jsUndefined());
MarkedArgumentBuffer args;
args.append(callFrame->argument(2));
ASSERT(!args.hasOverflowed());
JSValue result = invokeOptionalMethod(globalObject, underlyingSource, builtinNames(vm).cancelPublicName(), args);
RETURN_IF_EXCEPTION(scope, {});
if (result && result.isCell()) {
if (auto* promise = dynamicDowncast<JSPromise>(result))
markPromiseAsHandled(vm, promise);
}
return JSValue::encode(jsUndefined());
}

// pull(sink) rejected on the direct path: end the sink with the error so the native side
// aborts the upload; the pump already ran iterator.throw().
Comment thread
robobun marked this conversation as resolved.
Outdated
JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onResumableSinkDirectPullRejected, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
auto& vm = getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
JSObject* sink = callFrame->argument(1).getObject();
if (!sink)
return JSValue::encode(jsUndefined());
MarkedArgumentBuffer args;
args.append(callFrame->argument(0));
ASSERT(!args.hasOverflowed());
invokeOptionalMethod(globalObject, sink, vm.propertyNames->error, args);
RETURN_IF_EXCEPTION(scope, {});
return JSValue::encode(jsUndefined());
}

} // namespace WebCore
8 changes: 6 additions & 2 deletions src/jsc/bindings/webcore/streams/JSStreamsRuntime.h
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,8 @@ class JSDirectStreamController;
V(onResumableSinkChunk) \
V(onResumableSinkClose) \
V(onResumableSinkReadRejected) \
V(onResumableSinkEndMicrotask)
V(onResumableSinkEndMicrotask) \
V(onResumableSinkDirectPullRejected)

// owner: JSDirectStreamController.cpp. context = the JSDirectStreamController.
#define FOR_EACH_WEB_STREAMS_REACTION_HANDLER_DIRECT_CONTROLLER(V) \
Expand Down Expand Up @@ -248,13 +249,16 @@ class JSDirectStreamController;
// boundReadStreamIntoSinkOnClose(op, stream, reason): readStreamIntoSink's JSSink onClose.
// boundResumableSinkDrain(op) / boundResumableSinkCancel(op, unused, reason): stored on
// the native ResumableSink via setHandlers.
// boundResumableSinkDirectCancel(underlyingSource, unused, reason): the direct-path
// oncancel for a DirectPending stream handed to a ResumableSink.
Comment thread
robobun marked this conversation as resolved.
Outdated
#define FOR_EACH_WEB_STREAMS_BOUND_HANDLER_TARGET_BUN_SOURCE(V) \
V(boundOnNativeSourceClose) \
V(boundOnNativeSourceDrain) \
V(boundReadDirectStreamOnClose) \
V(boundReadStreamIntoSinkOnClose) \
V(boundResumableSinkDrain) \
V(boundResumableSinkCancel)
V(boundResumableSinkCancel) \
V(boundResumableSinkDirectCancel)

// owner: JSDirectStreamController.cpp — the FIVE detachable own methods of the direct
// controller: `end` and `close` are two bound cells over the ONE boundDirectClose target.
Expand Down
6 changes: 3 additions & 3 deletions src/jsc/generated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1133,9 +1133,9 @@ js_class_module!(JSImmediate = "Immediate" { callback, arguments });
js_class_module!(JSBlob = "Blob" as crate::webcore_types::Blob { name, stream });
js_class_module!(JSResponse = "Response" { body, headers, url, statusText, stream });
js_class_module!(JSRequest = "Request" { body, headers, url, signal, stream });
// `values: ["ondrain", "oncancel", "stream"]` in src/runtime/api/ResumableSink.classes.ts.
js_class_module!(JSResumableFetchSink = "ResumableFetchSink" { ondrain, oncancel, stream });
js_class_module!(JSResumableS3UploadSink = "ResumableS3UploadSink" { ondrain, oncancel, stream });
// `values: ["ondrain", "oncancel", "stream", "flushPromise"]` in src/runtime/api/ResumableSink.classes.ts.
js_class_module!(JSResumableFetchSink = "ResumableFetchSink" { ondrain, oncancel, stream, flushPromise });
js_class_module!(JSResumableS3UploadSink = "ResumableS3UploadSink" { ondrain, oncancel, stream, flushPromise });
// `values: ["resolve", "reject"]` in src/runtime/api/Shell.classes.ts.
js_class_module!(JSShellInterpreter = "ShellInterpreter" { resolve, reject });
// `src/runtime/crypto/crypto.classes.ts` — one entry per `StaticCryptoHasher`
Expand Down
18 changes: 17 additions & 1 deletion src/runtime/api/ResumableSink.classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,33 @@ function generate(name) {
fn: "jsWrite",
length: 1,
},
flush: {
fn: "jsFlush",
length: 1,
passThis: true,
},
end: {
fn: "jsClose",
length: 0,
passThis: true,
},
close: {
fn: "jsClose",
length: 0,
passThis: true,
},
error: {
fn: "jsEnd",
length: 1,
passThis: true,
},
setHandlers: {
fn: "jsSetHandlers",
length: 2,
passThis: true,
},
},
values: ["ondrain", "oncancel", "stream"],
values: ["ondrain", "oncancel", "stream", "flushPromise"],
});
}
export default [generate("ResumableFetchSink"), generate("ResumableS3UploadSink")];
97 changes: 91 additions & 6 deletions src/runtime/webcore/ResumableSink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
fn ondrain_get_cached(this_value: JSValue) -> Option<JSValue>;
fn stream_set_cached(this_value: JSValue, global: &JSGlobalObject, value: JSValue);
fn stream_get_cached(this_value: JSValue) -> Option<JSValue>;
fn flushpromise_set_cached(this_value: JSValue, global: &JSGlobalObject, value: JSValue);
fn flushpromise_get_cached(this_value: JSValue) -> Option<JSValue>;
}

/// Trait capturing the per-`Context` callbacks the sink invokes.
Expand Down Expand Up @@ -118,6 +120,19 @@
fn set_stream(this_value: JSValue, global: &JSGlobalObject, value: JSValue) {
Js::stream_set_cached(this_value, global, value);
}
#[inline]
fn set_flush_promise(this_value: JSValue, global: &JSGlobalObject, value: JSValue) {
Js::flushpromise_set_cached(this_value, global, value);
}
fn take_flush_promise(
this_value: JSValue,
global: &JSGlobalObject,
) -> Option<&'static mut bun_jsc::JSPromise> {
let value = Js::flushpromise_get_cached(this_value)?;
let promise = value.as_promise()?;
Js::flushpromise_set_cached(this_value, global, JSValue::ZERO);
Some(bun_jsc::JSPromise::opaque_mut(promise))
}

#[inline]
fn on_write(ctx: *mut Context, bytes: &[u8]) -> ResumableSinkBackpressure {
Expand Down Expand Up @@ -333,26 +348,81 @@
Ok(JSValue::from(this.status != Status::Paused))
}

/// `flush(true)` while paused returns a promise that `drain()` fulfills (or `cancel()`
/// rejects); the direct-stream pump awaits it when `write()` returns `false`.
Comment thread
robobun marked this conversation as resolved.
Outdated
#[bun_jsc::host_fn(method)]
pub fn js_flush(
this: &mut Self,
global_this: &JSGlobalObject,
callframe: &CallFrame,
this_value: JSValue,
) -> JsResult<JSValue> {
bun_jsc::mark_binding!();
if this.is_detached() {
return Ok(JSValue::UNDEFINED);
}
let args = callframe.arguments();
let wait = args.first().is_some_and(|v| v.to_boolean());
if !wait || this.status != Status::Paused {
return Ok(JSValue::UNDEFINED);
}
if let Some(pending) = Js::flushpromise_get_cached(this_value) {
if pending.as_promise().is_some() {
return Ok(pending);
}
}
let promise = bun_jsc::JSPromise::create(global_this);
let promise_value = promise.to_js();
Self::set_flush_promise(this_value, global_this, promise_value);
Ok(promise_value)
}

/// `error(e)` on the controller surface: abort the upload with `e`.
#[bun_jsc::host_fn(method)]
pub fn js_end(
this: &mut Self,
_global_this: &JSGlobalObject,
global_this: &JSGlobalObject,
callframe: &CallFrame,
this_value: JSValue,
) -> JsResult<JSValue> {
bun_jsc::mark_binding!();
let args = callframe.arguments();
// ignore any call if detached
if this.is_detached() {
return Ok(JSValue::UNDEFINED);
}
let err = if args.len() > 0 { Some(args[0]) } else { None };
if let Some(promise) = Self::take_flush_promise(this_value, global_this) {
let _ = promise.reject_as_handled(global_this, err.unwrap_or(JSValue::UNDEFINED));
}
this.detach_js();
scoped_log!(ResumableSink, "jsEnd {}", args.len());
this.status = Status::Done;

Self::on_end(
this.context,
if args.len() > 0 { Some(args[0]) } else { None },
);
Self::on_end(this.context, err);

Check failure on line 402 in src/runtime/webcore/ResumableSink.rs

View check run for this annotation

Claude / Claude Code Review

controller.error() with no argument clean-closes instead of aborting

After 67a25c5 remapped `error` → `jsEnd` (and `end`/`close` → `jsClose`), `js_end` is reachable **only** via `controller.error(...)` — but it still branches on `args.len() > 0`, a leftover from when it also served zero-arg `end()`. So `controller.error()` with no argument yields `err = None` → `on_end(context, None)` → `FetchTasklet::write_end_request(None)` → sends the terminating chunk and cleanly finishes the upload, whereas pre-PR `boundDirectError` read `callFrame->argument(1)` (= `jsUndefi
Comment thread
robobun marked this conversation as resolved.
Outdated
Ok(JSValue::UNDEFINED)
}

/// `end()` / `close()` on the controller surface: a clean end regardless of the reason
/// argument (the direct controller's `end`/`close` are the same no-error target).
Comment thread
robobun marked this conversation as resolved.
Outdated
#[bun_jsc::host_fn(method)]
pub fn js_close(
this: &mut Self,
global_this: &JSGlobalObject,
_callframe: &CallFrame,
this_value: JSValue,
) -> JsResult<JSValue> {
bun_jsc::mark_binding!();
if this.is_detached() {
return Ok(JSValue::UNDEFINED);
}
if let Some(promise) = Self::take_flush_promise(this_value, global_this) {
let _ = promise.resolve(global_this, JSValue::UNDEFINED);
}
this.detach_js();
scoped_log!(ResumableSink, "jsClose");
this.status = Status::Done;
Self::on_end(this.context, None);
Ok(JSValue::UNDEFINED)
}

Expand All @@ -365,8 +435,11 @@
let global_object = self.global_this;
let global_object = global_object.get();

self.status = Status::Started;
if let Some(promise) = Self::take_flush_promise(js_this, global_object) {
let _ = promise.resolve(global_object, JSValue::UNDEFINED);
}
if let Some(ondrain) = Self::get_drain(js_this) {
self.status = Status::Started;
// SAFETY: `bun_vm()` returns a live `*mut VirtualMachine` owned by
// the global; `event_loop()` returns its self-referential
// `*mut EventLoop`. Both outlive this call.
Expand Down Expand Up @@ -402,6 +475,9 @@
let global_object = self.global_this;
let global_object = global_object.get();

if let Some(promise) = Self::take_flush_promise(js_this, global_object) {
let _ = promise.reject_as_handled(global_object, reason);
}
// detach first so if cancel calls end will be a no-op
self.detach_js();

Expand Down Expand Up @@ -443,6 +519,7 @@
Self::set_drain(js_this, global, JSValue::ZERO);
Self::set_cancel(js_this, global, JSValue::ZERO);
Self::set_stream(js_this, global, JSValue::ZERO);
Self::set_flush_promise(js_this, global, JSValue::ZERO);
self.js_this.downgrade();
Comment thread
robobun marked this conversation as resolved.
Outdated
}
}
Expand Down Expand Up @@ -617,6 +694,14 @@
fn stream_get_cached(this: JSValue) -> Option<JSValue> {
bun_jsc::generated::$name::stream_get_cached(this)
}
#[inline]
fn flushpromise_set_cached(this: JSValue, global: &JSGlobalObject, v: JSValue) {
bun_jsc::generated::$name::flush_promise_set_cached(this, global, v)
}
#[inline]
fn flushpromise_get_cached(this: JSValue) -> Option<JSValue> {
bun_jsc::generated::$name::flush_promise_get_cached(this)
}
}
)*};
}
Expand Down
Loading
Loading