Background
#775 / #787 landed a compile-time shim note for node:async_hooks, mirroring the reflect-metadata 2a mitigation. That stops the silent-failure surface from growing, but the structural stub still does not track async context — AsyncLocalStorage.run() does not propagate across await / setImmediate / process.nextTick / Promise microtasks. Anything relying on context propagation (Sentry request scopes, OpenTelemetry trace propagation, NestJS request-scoped providers, pino child loggers) still silently loses context.
This issue tracks the real fix.
Design surface to nail down before writing code
- Context storage. Per-thread stack in
perry-runtime (one entry per active .run() frame), snapshotted by value when we cross an async boundary. tokio::task_local! can layer on top later if needed.
await propagation. Hook into the async-function state-machine lowering (perry-transform / perry-codegen) to capture context on suspend and restore on resume.
- Microtask / timer / immediate scheduling.
.then / .catch / .finally (in perry-runtime/src/promise.rs), js_promise_run_microtasks(), setTimeout / setImmediate / process.nextTick: capture context at schedule time, restore at execute time.
AsyncResource.runInAsyncScope / .bind. Thin wrappers around the per-thread stack.
- Threads (
perry/thread). Node does not propagate ALS into workers — default to not serializing context across spawn / parallelMap / parallelFilter.
createHook lifecycle. Out of scope for v1 (full init/before/after/destroy is a separate effort — tracked in its own follow-up issue).
Acceptance tests
Out of scope
createHook / executionAsyncId returning real (non-stub) IDs — tracked separately.
- AsyncResource subclassing edge cases beyond
runInAsyncScope / bind — depends on real createHook, tracked with it.
Related
Background
#775 / #787 landed a compile-time shim note for
node:async_hooks, mirroring the reflect-metadata 2a mitigation. That stops the silent-failure surface from growing, but the structural stub still does not track async context —AsyncLocalStorage.run()does not propagate acrossawait/setImmediate/process.nextTick/ Promise microtasks. Anything relying on context propagation (Sentry request scopes, OpenTelemetry trace propagation, NestJS request-scoped providers, pino child loggers) still silently loses context.This issue tracks the real fix.
Design surface to nail down before writing code
perry-runtime(one entry per active.run()frame), snapshotted by value when we cross an async boundary.tokio::task_local!can layer on top later if needed.awaitpropagation. Hook into the async-function state-machine lowering (perry-transform/perry-codegen) to capture context on suspend and restore on resume..then/.catch/.finally(inperry-runtime/src/promise.rs),js_promise_run_microtasks(),setTimeout/setImmediate/process.nextTick: capture context at schedule time, restore at execute time.AsyncResource.runInAsyncScope/.bind. Thin wrappers around the per-thread stack.perry/thread). Node does not propagate ALS into workers — default to not serializing context acrossspawn/parallelMap/parallelFilter.createHooklifecycle. Out of scope for v1 (fullinit/before/after/destroyis a separate effort — tracked in its own follow-up issue).Acceptance tests
als.run(store, async () => { await x(); als.getStore(); })als.run(store, () => setTimeout(() => als.getStore(), 0))als.run(store, () => setImmediate(() => als.getStore()))als.run(store, () => process.nextTick(() => als.getStore()))als.run(store, () => Promise.resolve().then(() => als.getStore()))als.run(a, () => als.run(b, () => ...))(inner seesb, outer keepsa)perry/threadworkers (Node-compatible behavior)[perry] note:from fix: #775 — emit shim note when async_hooks is imported #787 once the above passOut of scope
createHook/executionAsyncIdreturning real (non-stub) IDs — tracked separately.runInAsyncScope/bind— depends on realcreateHook, tracked with it.Related