@@ -91,10 +91,10 @@ function assertConnectionIdAllowed(
9191 * the wrong `$.Actions`/user identity, with no error at all. See the RFC's
9292 * Decisions and Trade-Offs for the full reasoning.
9393 *
94- * Implementation: a simple promise-chain mutex. `queueTail` always resolves
95- * (errors are swallowed via `.catch(() => {})` before being chained) so a
96- * rejected execution never wedges the queue for whatever runs after it; the
97- * real rejection is still preserved and returned to that call 's own caller.
94+ * `queueTail` is a promise-chain mutex whose chained handlers always resolve
95+ * (rejections are swallowed via `.catch(() => {})`), so a rejected execution
96+ * never wedges the queue — the real rejection still propagates to that
97+ * execution 's own caller via `result` .
9898 */
9999let queueTail : Promise < unknown > = Promise . resolve ( ) ;
100100
@@ -192,20 +192,17 @@ function isModuleNotFoundError(error: unknown): boolean {
192192/**
193193 * Returns a synchronous re-register function bound to the resolved
194194 * `setExecuteActionImplementation` setter, so a later poison/replace doesn't
195- * need to re-run `loadModule` (an async call) — see `runScriptLocally`'s
196- * `poisonActionCatalogRegistration` for why that matters: poisoning has to be
195+ * need to re-run `loadModule` (an async call) — poisoning has to be
197196 * synchronous relative to the timeout that triggers it, or a newer
198197 * execution's own registration could land in between and get clobbered.
199- * Returns `undefined` if action-catalog isn't installed, same as before .
198+ * Returns `undefined` if action-catalog isn't installed.
200199 *
201200 * `isCurrent` is checked inside `register` itself, not just before this
202- * function's own initial call — `loadModule` above is async and can be slow
203- * enough for a newer execution to start and validly register its own
204- * implementation while this call is still in flight. Guarding only the
205- * initial call would still leave a later call to the returned `register`
206- * (e.g. `runScriptLocally`'s own poison call) free to clobber that newer
207- * registration; checking fresh, live epoch state inside `register` itself
208- * protects every call site the same way, at the moment it actually runs.
201+ * function's own initial call, because `loadModule` above is async: a newer
202+ * execution can start and validly register its own implementation while
203+ * this call is still in flight. Checking only the initial call would leave
204+ * a later call to the returned `register` (e.g. a poison call) free to
205+ * clobber that newer registration.
209206 */
210207async function registerActionCatalogIfInstalled (
211208 loadModule : LoadModule ,
@@ -398,37 +395,32 @@ async function runScriptLocally(
398395
399396 // A timed-out execution is abandoned, not cancelled — enqueue() lets the
400397 // next queued execution start as soon as this one loses the Promise.race
401- // below, while this one may still be running in the background. If it
402- // later calls executeAction, it must not be allowed to silently run under
403- // whatever execution is now current. There are two distinct call paths,
404- // each needing its own guard:
398+ // below, while this one may still be running in the background. A late
399+ // executeAction call from it must not run under whatever execution is
400+ // now current, via two distinct paths that each need their own guard:
405401 //
406- // 1. A raw `$.Actions` call made through a reference the customer's code
402+ // 1. A raw `$.Actions` call through a reference the customer's code
407403 // captured early (e.g. `const { Actions } = $` before any `await`).
408- // That reference closes over `guardedExecuteAction` specifically , so
409- // checking `abandoned` inside it protects this path correctly no
410- // matter what a later execution does.
404+ // That reference closes over `guardedExecuteAction`, so checking
405+ // `abandoned` inside it protects this path regardless of what a
406+ // later execution does.
411407 // 2. An @datadog/action-catalog typed-wrapper call. This does NOT go
412- // through the closure above — action-catalog stores exactly one
413- // `executeAction` implementation in shared, module-level state
414- // (`setExecuteActionImplementation`), and a typed-wrapper call always
415- // invokes whichever implementation is CURRENTLY registered. Once a
416- // newer execution registers its own (valid) implementation over this
417- // one, this execution's `abandoned` flag becomes unreachable — nobody
418- // calls this closure anymore, so checking it protects nothing. The
419- // real fix is to proactively replace (poison) the registration itself
420- // as soon as this execution concludes, via `poisonActionCatalogRegistration`
421- // below, so a late call finds a rejecting stub already in place
422- // instead of finding out mid-call that it's stale.
408+ // through that closure — action-catalog stores exactly one
409+ // `executeAction` implementation in shared, module-level state, and a
410+ // typed-wrapper call always invokes whichever implementation is
411+ // CURRENTLY registered. Once a newer execution registers its own
412+ // implementation over this one, this execution's `abandoned` flag
413+ // becomes unreachable — nobody calls this closure anymore. The fix is
414+ // to proactively replace (poison) the registration itself as soon as
415+ // this execution concludes, via `poisonActionCatalogRegistration`
416+ // below, so a late call finds a rejecting stub already in place.
423417 //
424418 // Poisoning is only safe if nothing newer has started yet — a newer
425- // execution may already be running (having registered its own valid
426- // implementation) by the time this one's `fn()` finally settles or its
427- // timeout fires, and clobbering ITS registration would be the same bug
428- // in a new shape. `scope` gates that everywhere (see `concludeExecution`
429- // below) — including the `finally` block, since settling late for an
430- // abandoned execution is exactly the case this guards against, not just
431- // the timeout path.
419+ // execution may already be running by the time this one's `fn()`
420+ // finally settles or its timeout fires, and clobbering its registration
421+ // would be the same bug in a new shape. `scope` gates every poison call
422+ // against that, including in the `finally` block below, where settling
423+ // late for an abandoned execution is the case that guard exists for.
432424 const scope = executionEpoch . start ( ) ;
433425 let abandoned = false ;
434426 let reRegisterActionCatalog : ( ( nextExecuteAction : ExecuteAction ) => void ) | undefined ;
@@ -446,10 +438,9 @@ async function runScriptLocally(
446438 return executeAction ( fqn , inputs , connectionId ) ;
447439 } ;
448440
449- // An external package's setter throwing on re-registration must not be
450- // allowed to propagate — that would otherwise mask a successful result
451- // via `finally`, or abort the `setTimeout` callback below before it can
452- // reject.
441+ // An external package's setter throwing on re-registration must not
442+ // propagate, or it would mask a successful result via `finally`, or
443+ // abort the `setTimeout` callback below before it can reject.
453444 const poisonActionCatalogRegistration = ( ) => {
454445 try {
455446 reRegisterActionCatalog ?.( ( ) =>
@@ -475,21 +466,17 @@ async function runScriptLocally(
475466 }
476467 } ;
477468
478- // Shared by the finally block below and the timeout handler: only safe
479- // to poison if nothing newer has started yet. For the timeout handler
480- // this is (currently) always true, since enqueue's mutex can't start a
481- // newer execution before this same synchronous callback runs — but the
482- // finally block runs whenever this execution's own `fn()` settles,
483- // which for an abandoned execution can be well after a newer one has
484- // already started and registered, so the check is load-bearing there.
485- // Kept in one place so both call sites stay consistent.
469+ // Shared by the finally block below and the timeout handler, so both stay
470+ // consistent: only safe to poison if nothing newer has started yet. The
471+ // finally block is where this matters — it runs whenever this
472+ // execution's own `fn()` settles, which for an abandoned execution can be
473+ // well after a newer one has already started and registered.
486474 //
487- // Poisons BEFORE concluding, not after: `register()`'s own re-registration
488- // guard (see `registerActionCatalogIfInstalled`) checks `scope.isCurrent()`
489- // too, and the poison calls below route back through that same `register`
490- // closure — concluding first would make the scope look already-inactive
491- // to that inner check, silently skipping the poison it was meant to
492- // perform.
475+ // Poisons BEFORE concluding: `register()`'s own re-registration guard
476+ // also checks `scope.isCurrent()`, and the poison calls below route
477+ // through that same `register` closure — concluding first would make
478+ // the scope look already-inactive to that inner check and silently skip
479+ // the poison.
493480 const concludeExecution = ( ) => {
494481 if ( scope . isCurrent ( ) ) {
495482 abandoned = true ;
@@ -509,11 +496,9 @@ async function runScriptLocally(
509496 ( globalThis as Record < string , unknown > ) . $ = $ ;
510497 // Publish each handle the moment its own promise resolves, not via a
511498 // destructuring assignment after Promise.all: if one registration
512- // call rejects (or just outlasts the timeout) while the other has
513- // already invoked its shared setter, a post-Promise.all assignment
514- // would never run at all, losing the completed registration's
515- // poison handle and leaving a timed-out execution's live setter
516- // reachable by a newer execution.
499+ // rejects or outlasts the timeout while the other has already fired
500+ // its shared setter, a post-Promise.all assignment would never run,
501+ // losing the completed registration's poison handle.
517502 await Promise . all ( [
518503 registerActionCatalogIfInstalled (
519504 loadModule ,
@@ -530,11 +515,10 @@ async function runScriptLocally(
530515 ) ,
531516 ] ) ;
532517 if ( abandoned ) {
533- // This execution was already abandoned while the registration
534- // calls above were still in flight — registering is a side
535- // effect of the calls themselves, not something we can undo, so
536- // immediately poison what they just registered rather than
537- // leaving it live for a newer execution to inherit.
518+ // Abandoned while the registration calls above were still in
519+ // flight — registering is a side effect of those calls, not
520+ // something we can undo, so poison what they just registered
521+ // instead of leaving it live for a newer execution to inherit.
538522 poisonActionCatalogRegistration ( ) ;
539523 poisonBackendRuntimeRegistration ( ) ;
540524 }
@@ -551,22 +535,20 @@ async function runScriptLocally(
551535 }
552536 if ( abandoned ) {
553537 // Already known-abandoned before the customer function was
554- // even reached (the registration Promise.all above was slow
555- // enough to outlast the timeout, or this loadModule call
556- // was) — no point invoking it now; concludeExecution()
557- // below is a no-op re-poison at this point, which is fine.
538+ // even reached (the registration Promise.all above, or this
539+ // loadModule call, outlasted the timeout) — no point
540+ // invoking it now.
558541 throw new Error (
559542 `Execution of "${ func . name } " was abandoned after timing out before it could start.` ,
560543 ) ;
561544 }
562545 const result = await fn ( ...args ) ;
563546 return { data : assertJsonSerializable ( result , func ) } ;
564547 } finally {
565- // Covers the loadModule/export-validation steps above too, not
566- // just the fn() call — if either of those throws (including the
567- // native-module load failure case below), this execution has
568- // still concluded and must poison the registrations the same
569- // way a successful or thrown fn() call would.
548+ // Also covers the loadModule/export-validation steps above, not
549+ // just the fn() call — however this execution ends, it has
550+ // concluded and must poison the registrations the same way a
551+ // successful or thrown fn() call would.
570552 concludeExecution ( ) ;
571553 }
572554 } ;
0 commit comments