From 173b02803a28a736601465de30fe374a1b5a0b4a Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Fri, 15 May 2026 11:06:50 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20#775=20=E2=80=94=20emit=20shim=20note=20?= =?UTF-8?q?when=20async=5Fhooks=20is=20imported?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `node:async_hooks` stub added in PR #754 is structural: it gives NestJS bootstrap enough shape (`AsyncResource`, `AsyncLocalStorage`, `executionAsyncId`, `createHook`) to compile, but does NOT track async context across `await` / `setImmediate` / `process.nextTick`. Anything that relies on `AsyncLocalStorage` for context propagation — Sentry request scopes, OpenTelemetry trace propagation, NestJS request-scoped providers, pino child loggers — compiles, runs, and silently loses context. Mirror the reflect-metadata 2a mitigation: 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 the silent-failure boundary. Same `AtomicBool::swap` pattern as `emit_reflect_metadata_shim_note()`. Unlike reflect-metadata, the import still falls through so `AsyncLocalStorage` / `AsyncResource` keep binding against the structural stub. --- crates/perry-hir/src/lower.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/crates/perry-hir/src/lower.rs b/crates/perry-hir/src/lower.rs index b2b56a8d..c39330b1 100644 --- a/crates/perry-hir/src/lower.rs +++ b/crates/perry-hir/src/lower.rs @@ -4004,6 +4004,33 @@ See docs/src/language/decorators.md." ); } +/// Emit a one-shot note when the user imports `node:async_hooks`. Perry +/// ships a structural stub (see crates/perry-jsruntime/src/modules.rs) +/// that satisfies the NestJS bootstrap path, but does NOT yet implement +/// real async-context tracking. Anything relying on `AsyncLocalStorage` +/// for context propagation across `await` / `setImmediate` / +/// `process.nextTick` boundaries (Sentry request scopes, OpenTelemetry +/// trace propagation, NestJS request-scoped providers, pino child +/// loggers) will compile and run but silently lose context. Warning at +/// compile time avoids the production surprise. +fn emit_async_hooks_shim_note() { + use std::sync::atomic::{AtomicBool, Ordering}; + static EMITTED: AtomicBool = AtomicBool::new(false); + if EMITTED.swap(true, Ordering::Relaxed) { + return; + } + eprintln!( + "[perry] note: `import \"node:async_hooks\"` is satisfied by Perry's structural \ +stub. Implemented surface: AsyncResource, AsyncLocalStorage, executionAsyncId, \ +and createHook shapes — enough that NestJS bootstrap compiles. \ +AsyncLocalStorage.run() does NOT propagate context across \ +`await`/`setImmediate`/`process.nextTick` boundaries, so anything that \ +relies on async-context tracking (Sentry request scopes, OpenTelemetry \ +trace propagation, NestJS request-scoped providers, pino child loggers) \ +will silently lose context. See https://github.com/PerryTS/perry/issues/775." + ); +} + fn lower_module_decl( ctx: &mut LoweringContext, module: &mut Module, @@ -4024,6 +4051,13 @@ fn lower_module_decl( return Ok(()); } + if source == "async_hooks" { + emit_async_hooks_shim_note(); + // Fall through — the import still needs to bind + // AsyncLocalStorage / AsyncResource so calling code + // compiles against the structural stub. + } + // Check if this is a native module import let is_native = is_native_module(&source);