Background
PR #754 added a structural stub for node:async_hooks (crates/perry-jsruntime/src/modules.rs) so @nestjs/core's context-bind path could compile. The stub provides AsyncResource, AsyncLocalStorage, executionAsyncId, and createHook shapes — enough that NestJS's bootstrap doesn't crash — but with no real async-context tracking. From the fixture's WALLS.md:
The bind/runInAsyncScope shape is enough for the NestJS code path; no real async-context tracking yet.
This is the same silent-failure shape that PR #754's review item 2a was solving for reflect-metadata: a polyfill the user import-ed turns out not to be the polyfill they expected. Anything that relies on AsyncLocalStorage for context propagation will compile, run, and silently lose context:
- Sentry's request-scoped error context
- OpenTelemetry's distributed-trace propagation
- NestJS request-scoped providers (
@Injectable({ scope: Scope.REQUEST })) once the rest of the stack lights up
pino-style child loggers bound to a request
Suggested fix
Mirror the 2a solution — emit a one-shot [perry] note: to stderr at compile time when async_hooks (or node:async_hooks) is imported, listing the implemented surface and explicitly stating that AsyncLocalStorage.run() does not propagate context across await/setImmediate/process.nextTick boundaries. That way users get the warning at compile time rather than discovering missing trace context in production.
Same AtomicBool::swap one-shot pattern as emit_reflect_metadata_shim_note() in crates/perry-hir/src/lower.rs.
Longer-term
Real async-context tracking via tokio task_local! (or equivalent through Perry's promise/microtask scheduler) is the proper fix, but it's a substantial piece of work. The compile-time note is the minimum-viable mitigation that keeps the silent-failure surface from growing.
Related
Background
PR #754 added a structural stub for
node:async_hooks(crates/perry-jsruntime/src/modules.rs) so@nestjs/core's context-bind path could compile. The stub providesAsyncResource,AsyncLocalStorage,executionAsyncId, andcreateHookshapes — enough that NestJS's bootstrap doesn't crash — but with no real async-context tracking. From the fixture'sWALLS.md:This is the same silent-failure shape that PR #754's review item 2a was solving for
reflect-metadata: a polyfill the userimport-ed turns out not to be the polyfill they expected. Anything that relies onAsyncLocalStoragefor context propagation will compile, run, and silently lose context:@Injectable({ scope: Scope.REQUEST })) once the rest of the stack lights uppino-style child loggers bound to a requestSuggested fix
Mirror the 2a solution — emit a one-shot
[perry] note:to stderr at compile time whenasync_hooks(ornode:async_hooks) is imported, listing the implemented surface and explicitly stating thatAsyncLocalStorage.run()does not propagate context acrossawait/setImmediate/process.nextTickboundaries. That way users get the warning at compile time rather than discovering missing trace context in production.Same
AtomicBool::swapone-shot pattern asemit_reflect_metadata_shim_note()incrates/perry-hir/src/lower.rs.Longer-term
Real async-context tracking via tokio
task_local!(or equivalent through Perry's promise/microtask scheduler) is the proper fix, but it's a substantial piece of work. The compile-time note is the minimum-viable mitigation that keeps the silent-failure surface from growing.Related
crates/perry-jsruntime/src/modules.rs— currentasync_hooksstubcrates/perry-hir/src/lower.rs— pattern to mirror (emit_reflect_metadata_shim_note)