@@ -184,7 +184,8 @@ void ReactRejected(const FunctionCallbackInfo<Value>& args) {
184184}
185185
186186// Attaches fulfill/reject reactions that re-enter C++. The reaction functions
187- // carry the controller wrapper as their Data.
187+ // carry the controller wrapper as their Data. Used by cold paths (start);
188+ // the per-pull hot path uses ThenReactCached below.
188189void ThenReact (Environment* env,
189190 Local<Promise> promise,
190191 Local<Object> controller_obj,
@@ -200,12 +201,38 @@ void ThenReact(Environment* env,
200201 USE (promise->Then (context, ff, rj));
201202}
202203
204+ // Like ThenReact, but reuses the controller's cached reaction functions
205+ // (creating them on first use), so the per-pull path allocates no V8 Function.
206+ void ThenReactCached (Environment* env,
207+ Local<Promise> promise,
208+ Local<Object> controller_obj,
209+ v8::FunctionCallback on_fulfilled,
210+ v8::Global<Function>* ff_slot,
211+ v8::Global<Function>* rj_slot) {
212+ Isolate* isolate = env->isolate ();
213+ Local<Context> context = env->context ();
214+ Local<Function> ff = ff_slot->Get (isolate);
215+ if (ff.IsEmpty ()) {
216+ if (!Function::New (context, on_fulfilled, controller_obj).ToLocal (&ff))
217+ return ;
218+ ff_slot->Reset (isolate, ff);
219+ }
220+ Local<Function> rj = rj_slot->Get (isolate);
221+ if (rj.IsEmpty ()) {
222+ if (!Function::New (context, ReactRejected, controller_obj).ToLocal (&rj))
223+ return ;
224+ rj_slot->Reset (isolate, rj);
225+ }
226+ USE (promise->Then (context, ff, rj));
227+ }
228+
203229// A function that returns undefined; used both to map a promise's fulfilment
204230// value to undefined and as a no-op rejection handler.
205231void Noop (const FunctionCallbackInfo<Value>& args) {}
206232
207233// Marks a promise as handled to avoid spurious unhandled-rejection warnings on
208234// internal promises, mirroring `PromisePrototypeThen(p, undefined, () => {})`.
235+ // Cold path (close/error/release), so the per-call Function is acceptable.
209236void MarkHandled (Environment* env, Local<Promise> promise) {
210237 Local<Context> context = env->context ();
211238 Local<Function> noop;
@@ -478,13 +505,19 @@ void ReadableStreamDefaultController::CallPullIfNeeded() {
478505 return ;
479506 // The pull algorithm is wrapped as an async function, so it returns a Promise.
480507 if (!result->IsPromise ()) return ;
481- ThenReact (env, result.As <Promise>(), controller_obj, ReactPullFulfilled);
508+ ThenReactCached (env, result.As <Promise>(), controller_obj, ReactPullFulfilled,
509+ &on_pull_fulfilled_, &on_rejected_);
482510}
483511
484512void ReadableStreamDefaultController::ClearAlgorithms () {
485513 pull_algorithm_.Reset ();
486514 cancel_algorithm_.Reset ();
487515 size_algorithm_.Reset ();
516+ // Break the controller<->wrapper cycle created by the cached reaction
517+ // functions (their Data is this controller's wrapper). No further pulls occur
518+ // after the algorithms are cleared, so they will not be recreated.
519+ on_pull_fulfilled_.Reset ();
520+ on_rejected_.Reset ();
488521}
489522
490523void ReadableStreamDefaultController::CloseInternal () {
@@ -1450,12 +1483,16 @@ void ReadableByteStreamController::CallPullIfNeeded() {
14501483 if (!pull->Call (context, Undefined (isolate), 1 , argv).ToLocal (&result))
14511484 return ;
14521485 if (!result->IsPromise ()) return ;
1453- ThenReact (env, result.As <Promise>(), controller_obj, ReactPullFulfilled);
1486+ ThenReactCached (env, result.As <Promise>(), controller_obj, ReactPullFulfilled,
1487+ &on_pull_fulfilled_, &on_rejected_);
14541488}
14551489
14561490void ReadableByteStreamController::ClearAlgorithms () {
14571491 pull_algorithm_.Reset ();
14581492 cancel_algorithm_.Reset ();
1493+ // Break the controller<->wrapper cycle from the cached reaction functions.
1494+ on_pull_fulfilled_.Reset ();
1495+ on_rejected_.Reset ();
14591496}
14601497
14611498void ReadableByteStreamController::InvalidateBYOBRequest () {
0 commit comments