Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions changelog.d/7772-stdlib-no-default-features.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
### Fixed

- **`perry-stdlib` builds with `--no-default-features` again (#7764).** That is the configuration the auto-optimize relink uses, so while it was broken every `perry` compile that triggered auto-optimize silently fell back to the prebuilt archives, and ad-hoc builds needed `PERRY_NO_AUTO_OPTIMIZE=1` as a workaround.

Twelve errors, from two causes, both violations of the contract `common/mod.rs` states in prose: *"Always-on code that references it must also be `#[cfg(feature = "async-runtime")]`-gated."*

**One** was #7745's omission, exactly as the issue diagnosed: the `js_set_native_events_dispatch` registration referenced `crate::events` without the `#[cfg(feature = "bundled-events")]` that gates the module. The neighbouring registrations in the same function are gated (`database-sqlite` on the next line), which is what makes it an omission rather than a decision.

**Eleven** were `worker_threads` — always-on, and referencing `common::async_bridge` across five files. Neither obvious repair works: two sites are value-producing (`js_promise_new_for_native_resolution`), so `#[cfg]` on the statement leaves nothing to return; and gating the whole `worker_threads` module is worse, because it has no feature of its own, so its FFI symbols would vanish from the stripped archive and a program importing `node:worker_threads` would fail to LINK — trading a build error for the #7629 family of failure.

So `worker_threads/async_shim.rs` provides the four entry points in both configurations: forwarding to `async_bridge` when it is compiled in, and settling **inline** when it is not. That is not invented semantics — the queue exists to hand work to the pump, and with no pump there is nothing to hand it to, so doing the same work synchronously reaches the same observable end state. The pinning `js_promise_new_for_native_resolution` performs is likewise a consequence of deferral, and an inline settle spans no collection point, so a plain `js_promise_new` is its correct counterpart.

Verified in BOTH directions — `cargo build -p perry-stdlib` and `--no-default-features` each build clean — because the first cut of the shim accidentally imported itself, which only the default-features build could see.
9 changes: 9 additions & 0 deletions crates/perry-stdlib/src/common/dispatch/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,15 @@ pub unsafe extern "C" fn js_stdlib_init_dispatch() {
);
// Module-level `events.*` helpers reached indirectly (captured value,
// type-erased receiver, spread call) — see `js_events_native_dispatch`.
//
// #7764: gated to match `pub mod events`, which is `bundled-events`. #7745
// added this line ungated, so `--no-default-features` — the configuration
// the auto-optimize relink builds with — stopped compiling, and every
// `perry` compile that triggers auto-optimize silently fell back to the
// prebuilt archives. The neighbouring registrations are gated the same way
// (`database-sqlite` on the next line), which is what makes this an
// omission rather than a decision.
#[cfg(feature = "bundled-events")]
perry_runtime::js_set_native_events_dispatch(crate::events::js_events_native_dispatch);
#[cfg(feature = "database-sqlite")]
perry_runtime::js_set_native_sqlite_dispatch(crate::sqlite::js_node_sqlite_native_dispatch);
Expand Down
9 changes: 6 additions & 3 deletions crates/perry-stdlib/src/worker_threads.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ use perry_runtime::thread::{
};
use perry_runtime::value::JSValue;

// #7764: async-bridge entry points that exist in both feature configurations.
mod async_shim;
mod broadcast_channel;
mod channel_pump;
mod direct_message;
Expand Down Expand Up @@ -968,7 +970,8 @@ pub extern "C" fn js_worker_threads_worker_start_heap_profile(receiver: i64) ->
}

fn worker_terminate_by_id(worker_id: u64) -> f64 {
let promise = unsafe { crate::common::async_bridge::js_promise_new_for_native_resolution() };
let promise =
unsafe { crate::worker_threads::async_shim::js_promise_new_for_native_resolution() };
Comment on lines +973 to +974

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files of interest:"
fd -a 'worker_threads\.rs|async_bridge\.rs|async_shim\.rs' . | sed 's#^\./##'

echo
echo "Inspect worker_threads around lines 950-1015:"
if [ -f crates/perry-stdlib/src/worker_threads.rs ]; then
  sed -n '940,1025p' crates/perry-stdlib/src/worker_threads.rs | cat -n
fi

echo
echo "Search for async_shim symbols:"
rg -n "queue_promise_resolution|js_promise_resolve|js_promise_new_for_native_resolution|promise_unpin|native_resolution|pin|resolve" crates/perry-stdlib/src crates -g '*.rs' | head -n 200

Repository: PerryTS/perry

Length of output: 26635


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "async_shim outline:"
ast-grep outline crates/perry-stdlib/src/worker_threads/async_shim.rs --view expanded | sed -n '1,220p' || true
echo
echo "async_shim relevant content:"
sed -n '1,260p' crates/perry-stdlib/src/worker_threads/async_shim.rs | cat -n

echo
echo "async_bridge outline:"
ast-grep outline crates/perry-stdlib/src/common/async_bridge.rs --view expanded | sed -n '1,240p' || true
echo
echo "async_bridge relevant content:"
sed -n '1,260p' crates/perry-stdlib/src/common/async_bridge.rs | cat -n

echo
echo "worker_threads module/header relevant content:"
sed -n '1,80p' crates/perry-stdlib/src/worker_threads.rs | cat -n
sed -n '680,790p' crates/perry-stdlib/src/worker_threads.rs | cat -n

Repository: PerryTS/perry

Length of output: 28996


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Inspect async_bridge resolution/settle paths:"
sed -n '350,430p' crates/perry-stdlib/src/common/async_bridge.rs | cat -n
echo
echo "Inspect process_pending and pending drain:"
sed -n '440,530p' crates/perry-stdlib/src/common/async_bridge.rs | cat -n
echo
echo "Search unpin_promise_after_native_resolution:"
rg -n "unpin_promise_after_native_resolution|js_stdlib_process_pending" crates/perry-stdlib/src crates -g '*.rs'

Repository: PerryTS/perry

Length of output: 17175


Balance the native-resolution promise on immediate termination.

When async-runtime is enabled, js_promise_new_for_native_resolution() pins the Promise. The resolved_now branch calls perry_runtime::js_promise_resolve() directly, so unpin_promise_after_native_resolution() is never run through js_stdlib_process_pending(). Repeated termination of exited workers leaves pinned Promises that cannot be collected. Route native Promise creation and settlement through async_shim in both branches, or add a branch that directly calls unpin_promise_after_native_resolution() before resolve only for the async-runtime path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-stdlib/src/worker_threads.rs` around lines 973 - 974, The
immediate-termination path around js_promise_new_for_native_resolution must
balance the Promise pin when resolved_now is true. Route creation and settlement
through the async_shim in both branches, or invoke
unpin_promise_after_native_resolution() before the direct
perry_runtime::js_promise_resolve() only under async-runtime, while preserving
normal pending-worker processing.

Source: MCP tools

let promise_ptr = promise as usize;
let resolved_now = {
let mut workers = WORKERS.lock().unwrap();
Expand Down Expand Up @@ -1100,7 +1103,7 @@ fn object_u64_field(value: f64, field_name: &str) -> Option<u64> {
#[no_mangle]
pub extern "C" fn js_worker_threads_message_channel_new() -> f64 {
ensure_environment_data_gc_scanner();
crate::common::async_bridge::ensure_pump_registered();
crate::worker_threads::async_shim::ensure_pump_registered();
let (id1, id2) = NEXT_PORT_ID.with(|n| {
let mut n = n.borrow_mut();
let a = *n;
Expand Down Expand Up @@ -1136,7 +1139,7 @@ pub extern "C" fn js_worker_threads_message_channel_new() -> f64 {
#[no_mangle]
pub extern "C" fn js_worker_threads_worker_new(entry_ptr: i64, options: f64) -> f64 {
ensure_worker_gc_scanner();
crate::common::async_bridge::ensure_pump_registered();
crate::worker_threads::async_shim::ensure_pump_registered();

let worker_id = NEXT_WORKER_ID.fetch_add(1, Ordering::Relaxed);
let options_state = WorkerOptions::from_value(options);
Expand Down
80 changes: 80 additions & 0 deletions crates/perry-stdlib/src/worker_threads/async_shim.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//! #7764: the four `common::async_bridge` entry points `worker_threads` needs,
//! available in BOTH feature configurations.
//!
//! `common/mod.rs` states the contract this exists to satisfy:
//!
//! > Tokio-backed promise/runtime bridge — only needed when an async feature …
//! > pulls in `async-runtime`. **Always-on code that references it must also be
//! > `#[cfg(feature = "async-runtime")]`-gated.**
//!
//! `worker_threads` is always-on and referenced it in eleven places across five
//! files, so `cargo build -p perry-stdlib --no-default-features` did not
//! compile. That is the configuration the auto-optimize relink uses, so every
//! `perry` compile that triggered auto-optimize fell back to the prebuilt
//! archives with a warning, and ad-hoc builds needed `PERRY_NO_AUTO_OPTIMIZE=1`.
//!
//! Gating each call site individually was not an option: two of them are
//! value-producing (`js_promise_new_for_native_resolution`) and the rest settle
//! a promise, so `#[cfg]` on the statement leaves nothing to return. Gating the
//! whole `worker_threads` module was worse — it has no feature of its own, so
//! its FFI symbols would vanish from the stripped archive and a program that
//! imports `node:worker_threads` would fail to LINK, which is the #7629 family
//! of failure rather than a fix.
//!
//! So: forward when the bridge is compiled in, and settle INLINE when it is not.
//! That is not an invented semantic. The queue exists to hand work to the pump;
//! with no pump there is nothing to hand it to, and doing the same work
//! synchronously reaches the same observable end state (the promise settles).
//! The pinning `js_promise_new_for_native_resolution` performs is likewise a
//! consequence of deferral — it keeps the promise alive across the window
//! between creation and the pump's resolution — and an inline settle spans no
//! collection point, so a plain `js_promise_new` is the correct counterpart.

#[cfg(feature = "async-runtime")]
pub(crate) use crate::common::async_bridge::{
ensure_pump_registered, js_promise_new_for_native_resolution, queue_deferred_resolution,
queue_promise_resolution,
};

#[cfg(not(feature = "async-runtime"))]
mod inline {
/// No bridge means no pump to register.
pub(crate) fn ensure_pump_registered() {}

Comment on lines +39 to +43

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 8 \
  'ensure_pump_registered|js_register_stdlib_pump|js_register_stdlib_has_active|js_worker_threads_(process_pending|has_pending|channels_process_pending|channels_has_pending)' \
  crates/perry-stdlib crates/perry-runtime

Repository: PerryTS/perry

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- worker async_shim feature gates ---\n'
sed -n '1,140p' crates/perry-stdlib/src/worker_threads/async_shim.rs

printf '\n--- worker pump outline and relevant sections ---\n'
ast-grep outline crates/perry-stdlib/src/worker_threads/worker_pump.rs --view expanded || true
rg -n -C 8 'js_worker_threads_(process_pending|has_pending)|start_worker|start_stdin_reader|worker_threads_process_pending|worker_threads_has_pending|ensure_pump_registered|queue_promise'| crates/perry-stdlib/src/worker_threads src/worker_threads crates/perry-runtime/src/common crates/perry-runtime/src/event_pump 2>/dev/null || true

printf '\n--- async_bridge registrations and active/pending methods ---\n'
sed -n '430,475p' crates/perry-stdlib/src/common/async_bridge.rs
rg -n -C 5 'js_register_wait_driver|ensure_gc_scanner_registered|js_stdlib_init_dispatch|ensure_pump_registered|js_run_stdlib_pump|js_stdlib_process_pending|js_stdlib_has_active_handles' crates/perry-runtime crates -g '*.rs' | head -240

printf '\n--- Cargo features for perry-stdlib/runtime ---\n'
rg -n 'async-runtime|perry-stdlib|perry-runtime' Cargo.toml crates -g 'Cargo.toml' | head -160

Repository: PerryTS/perry

Length of output: 242


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- worker async_shim feature gates ---'
sed -n '1,140p' crates/perry-stdlib/src/worker_threads/async_shim.rs

printf '%s\n' ''
printf '%s\n' '--- worker pump outline and relevant sections ---'
ast-grep outline crates/perry-stdlib/src/worker_threads/worker_pump.rs --view expanded || true
rg -n -C 8 'js_worker_threads_(process_pending|has_pending)|start_worker|start_stdin_reader|worker_threads_process_pending|worker_threads_has_pending|ensure_pump_registered|queue_promise' \
  crates/perry-stdlib/src/worker_threads crates/perry-runtime/src/common crates/perry-runtime/src/event_pump 2>/dev/null || true

printf '%s\n' ''
printf '%s/n' '--- async_bridge registrations and active/pending methods ---'
sed -n '430,475p' crates/perry-stdlib/src/common/async_bridge.rs
rg -n -C 5 'js_register_wait_driver|ensure_gc_scanner_registered|js_stdlib_init_dispatch|ensure_pump_registered|js_run_stdlib_pump|js_stdlib_process_pending|js_stdlib_has_active_handles' \
  crates/perry-runtime crates -g '*.rs' | head -240

printf '%s\n' ''
printf '%$s\n' '--- Cargo features for perry-stdlib/runtime ---'
rg -n 'async-runtime|perry-stdlib|perry-runtime' Cargo.toml crates -g 'Cargo.toml' | head -160

Repository: PerryTS/perry

Length of output: 46426


Provide a no-default worker pump registration path.

js_worker_threads_worker_new() and js_worker_threads_message_channel_new() call worker_threads::async_shim::ensure_pump_registered() unconditionally. Without async-runtime, the inline shim is a no-op, so worker events/parentPort messages and termination promises can remain queued, and the event loop can exit before they drain. Keep the Tokio runtime setup behind async-runtime, but still register the stdlib pump and active-handle hooks in the no-default path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-stdlib/src/worker_threads/async_shim.rs` around lines 39 - 43,
Update the no-async-runtime inline ensure_pump_registered() path so it registers
the stdlib worker pump and active-handle hooks instead of remaining a no-op.
Keep Tokio-specific runtime setup gated by async-runtime, and preserve the
unconditional callers from js_worker_threads_worker_new() and
js_worker_threads_message_channel_new() so queued worker events, parentPort
messages, and termination promises continue draining.

Source: MCP tools

/// # Safety
/// Mirrors `async_bridge::js_promise_new_for_native_resolution`.
///
/// No pinning: pinning guards the deferral window, and there is none here.
pub(crate) unsafe fn js_promise_new_for_native_resolution() -> *mut perry_runtime::Promise {
perry_runtime::js_promise_new()
}
Comment on lines +44 to +50

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
git ls-files | rg 'crates/perry-stdlib/src/(worker_threads(_test)?|common/async_bridge\.rs)' || true

echo
echo "async_shim outline:"
ast-grep outline crates/perry-stdlib/src/worker_threads/async_shim.rs --view expanded || true

echo
echo "worker_threads relevant sections:"
wc -l crates/perry-stdlib/src/worker_threads.rs crates/perry-stdlib/src/worker_threads/async_shim.rs
sed -n '1,90p' crates/perry-stdlib/src/worker_threads/async_shim.rs
sed -n '930,990p' crates/perry-stdlib/src/worker_threads.rs

echo
echo "Search promise pins/unpins and js_promise_new:"
rg -n "js_promise_new|js_promise_(pin|unpin)|Promise|native_resolution|resolve" crates/perry-stdlib/src crates/perry-rt crates/perry-runtime crates/perry-rt-bindings 2>/dev/null | head -n 200

Repository: PerryTS/perry

Length of output: 28993


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate async_bridge docs and perry_runtime Promise APIs:"
rg -n "js_promise_new|js_promise_new_for_native_resolution|pinning|deferral|queued settlement|unpin" crates/perry-stdlib/src crates 2>/dev/null | head -n 300
fd -a 'async_bridge\.rs' . 
fd -a -i 'promise|worker_threads' . | head -n 200

Repository: PerryTS/perry

Length of output: 40429


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "async_bridge full:"
cat -n crates/perry-stdlib/src/common/async_bridge.rs | sed -n '1,110p'
cat -n crates/perry-stdlib/src/common/async_bridge.rs | sed -n '470,555p'

echo
echo "worker_threads full context around promised termination:"
cat -n crates/perry-stdlib/src/worker_threads.rs | sed -n '960,1025p'

echo
echo "direct_message call sites:"
cat -n crates/perry-stdlib/src/worker_threads/direct_message.rs | sed -n '1,140p'

echo
echo "promises tests related to 859 native pin:"
cat -n test-files/test_issue_859_native_promise_pin.ts | sed -n '1,220p'

Repository: PerryTS/perry

Length of output: 22648


Keep the bridge Promise contract balanced for async-runtime too.

With async-runtime, js_promise_new_for_native_resolution() now pins the Promise, but the inline immediate-settlement path still resolves it without unpinning. Keep deferral/non-deferral behavior in sync: preserve a GC root until all queued worker resolutions complete, or unpin before the immediate js_promise_resolve() path in the async-runtime fork as well.

📍 Affects 2 files
  • crates/perry-stdlib/src/worker_threads/async_shim.rs#L44-L50 (this comment)
  • crates/perry-stdlib/src/worker_threads.rs#L973-L974
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-stdlib/src/worker_threads/async_shim.rs` around lines 44 - 50,
The async-runtime Promise lifecycle is unbalanced between deferred and immediate
settlement paths. Update js_promise_new_for_native_resolution in
crates/perry-stdlib/src/worker_threads/async_shim.rs:44-50 and the immediate
js_promise_resolve path in crates/perry-stdlib/src/worker_threads.rs:973-974 so
the Promise remains rooted until queued worker resolutions finish, or is
explicitly unpinned before immediate settlement, matching the bridge contract.

Source: MCP tools


/// Settle now rather than queueing for a pump that does not exist.
pub(crate) fn queue_promise_resolution(promise_ptr: usize, is_success: bool, result_bits: u64) {
if promise_ptr == 0 {
return;
}
let promise = promise_ptr as *mut perry_runtime::Promise;
let value = f64::from_bits(result_bits);
if is_success {
perry_runtime::js_promise_resolve(promise, value);
} else {
perry_runtime::js_promise_reject(promise, value);
}
}

/// As above, running the converter inline. The `Send + 'static` bound is
/// kept so the two configurations accept the same call sites.
pub(crate) fn queue_deferred_resolution<F>(promise_ptr: usize, is_success: bool, converter: F)
where
F: FnOnce() -> u64 + Send + 'static,
{
queue_promise_resolution(promise_ptr, is_success, converter());
}
Comment on lines +52 to +73

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 10 \
  'queue_(deferred_)?resolution|std::thread::spawn|worker_messaging_error_value|js_promise_(resolve|reject)|js_string_from_bytes|js_error_new' \
  crates/perry-stdlib/src/worker_threads

rg -n -C 8 \
  'thread-local arenas|main thread|converter' \
  crates/perry-stdlib/src/common/async_bridge.rs

Repository: PerryTS/perry

Length of output: 36754


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- Rust files under perry-runtime/thread and promise ---\n'
git ls-files -- crates/perry-runtime | rg 'thread|promise|value|gc' | sed -n '1,200p'

printf '\n--- Runtime imports in async_bridge.rs ---\n'
rg -n 'js_(resolve|reject|promise_)|thread-local|arena|RuntimeRootVisitor|RuntimeHandleScope|main_thread' crates/perry-stdlib/src/common/async_bridge.rs crates/perry-runtime | sed -n '1,240p'

Repository: PerryTS/perry

Length of output: 242


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Runtime files likely to contain thread arena/runtime context ---'
git ls-files -- crates/perry-runtime | rg 'thread|promise|value|gc|main' | sed -n '1,200p' || true

printf '%s\n' ''
printf '%s\n' '--- Async bridge import/context ---'
sed -n '1,80p' crates/perry-stdlib/src/common/async_bridge.rs

printf '%s\n' ''
printf '%s\n' '--- Direct message rejection path with spawned worker worker ack ---'
sed -n '130,180p' crates/perry-stdlib/src/worker_threads/direct_message.rs

Repository: PerryTS/perry

Length of output: 13680


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- thread.rs arena definitions/usages ---'
sed -n '1,240p' crates/perry-runtime/src/thread.rs

printf '%s\n' ''
printf '%s\n' '--- promise API/resolve/reject definitions ---'
rg -n -C 8 'fn js_promise_(new|resolve|reject|resol|rejec)|js_promise_new_for_native_resolution|RuntimeHandleScope|RuntimeRootVisitor|ThreadLocal|thread_local!' crates/perry-runtime/src/promise crates/perry-runtime/src/gc/roots crates/perry-runtime/src/gc/mod.rs | sed -n '1,260p'

printf '%s\n' ''
printf '%s\n' '--- inline shim resolution functions ---'
sed -n '4,85p' crates/perry-stdlib/src/worker_threads/async_shim.rs

Repository: PerryTS/perry

Length of output: 39964


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- locate RuntimeHandleScope definitions ---'
rg -n -C 8 'struct RuntimeHandleScope|impl RuntimeHandleScope|enum RuntimeHandleScope|RuntimeHandleScope' crates/perry-runtime/src/gc/roots crates/perry-runtime/src/gc/mod.rs crates/perry-runtime/src/gc | sed -n '1,220p'

printf '%s\n' ''
printf '%s\n' '--- locate thread-local arena initializers and allocation symbols ---'
rg -n -C 5 'thread_local|THREAD_LOCAL|PERRY_THREAD_LOCAL|gc_init|GcInit|Arena|alloc|malloc|thread_spawn|worker_thread|spawn' crates/perry-runtime/src/gc/mod.rs crates/perry-runtime/src/gc/roots.rs crates/perry-runtime/src/gc/roots crates/perry-runtime/src/thread.rs crates/perry-runtime/src/promise/mod.rs | sed -n '1,260p'

printf '%s\n' ''
printf '%s\n' '--- direct_message imports and start call paths ---'
sed -n '1,110p' crates/perry-stdlib/src/worker_threads/direct_message.rs
rg -n -C 6 'js_worker_threads_post_message_to_thread|wait_for_direct_message_ack|DirectMessaging|worker_messaging_error_value|queue_worker_messaging_rejection' crates/perry-stdlib/src/worker_threads

Repository: PerryTS/perry

Length of output: 50371


Do not run deferred promise converters on background threads.

js_worker_threads_post_message_to_thread() spawns wait_for_direct_message_ack(), and rejection converts worker_messaging_error_value(...) through queue_deferred_resolution(...). In the fallback shim, that converted js_error_new() runs on the spawned thread; in the async bridge, deferred converters are queued because perry-runtime uses thread-local arenas and the async bridge converters create JS values. Keep main-thread conversion in both shims/bridges so rejected promises do not allocate through the background thread’s runtime context.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-stdlib/src/worker_threads/async_shim.rs` around lines 52 - 73,
Update queue_deferred_resolution and the corresponding async bridge so converter
is never executed on the spawned wait_for_direct_message_ack thread. Dispatch
the conversion to the main-thread/runtime context, then call
queue_promise_resolution with the converted value there; preserve the existing
success/rejection behavior and call-site compatibility.

Source: MCP tools

}

#[cfg(not(feature = "async-runtime"))]
pub(crate) use inline::{
ensure_pump_registered, js_promise_new_for_native_resolution, queue_deferred_resolution,
queue_promise_resolution,
};
4 changes: 2 additions & 2 deletions crates/perry-stdlib/src/worker_threads/broadcast_channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ extern "C" fn broadcast_add_event_listener(
let Some(cb_bits) = callback_bits_from_value(callback) else {
return js_undefined();
};
crate::common::async_bridge::ensure_pump_registered();
super::async_shim::ensure_pump_registered();
BROADCAST_CHANNELS.with(|channels| {
if let Some(state) = channels.borrow_mut().get_mut(&channel_id) {
if event_name == "message" && !state.message_event_cbs.contains(&cb_bits) {
Expand Down Expand Up @@ -99,7 +99,7 @@ extern "C" fn broadcast_remove_event_listener(
#[no_mangle]
pub extern "C" fn js_worker_threads_broadcast_channel_new(name: f64) -> f64 {
ensure_environment_data_gc_scanner();
crate::common::async_bridge::ensure_pump_registered();
super::async_shim::ensure_pump_registered();
let id = NEXT_BROADCAST_ID.with(|n| {
let mut n = n.borrow_mut();
let id = *n;
Expand Down
10 changes: 3 additions & 7 deletions crates/perry-stdlib/src/worker_threads/direct_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ pub extern "C" fn js_worker_threads_post_message_to_thread(
return rejected_worker_messaging_promise(WorkerMessagingError::Failed);
};

let promise = unsafe { crate::common::async_bridge::js_promise_new_for_native_resolution() };
let promise = unsafe { super::async_shim::js_promise_new_for_native_resolution() };
let promise_ptr = promise as usize;
let timeout = timeout_duration(timeout);
if sender
Expand Down Expand Up @@ -157,11 +157,7 @@ fn wait_for_direct_message_ack(

match result {
Ok(DirectMessageResult::Delivered) => {
crate::common::async_bridge::queue_promise_resolution(
promise_ptr,
true,
js_undefined_bits(),
);
super::async_shim::queue_promise_resolution(promise_ptr, true, js_undefined_bits());
}
Ok(DirectMessageResult::Failed) => {
queue_worker_messaging_rejection(promise_ptr, WorkerMessagingError::Failed);
Expand All @@ -171,7 +167,7 @@ fn wait_for_direct_message_ack(
}

fn queue_worker_messaging_rejection(promise_ptr: usize, error: WorkerMessagingError) {
crate::common::async_bridge::queue_deferred_resolution(promise_ptr, false, move || {
super::async_shim::queue_deferred_resolution(promise_ptr, false, move || {
worker_messaging_error_value(error).to_bits()
});
}
4 changes: 2 additions & 2 deletions crates/perry-stdlib/src/worker_threads/message_port.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ extern "C" fn port_on(closure: *const ClosureHeader, event: f64, callback: f64)
// the runtime pump would otherwise never be registered and `main` would
// return before any queued `message` is delivered. Register it here (mirrors
// readline #347), so the event loop ticks and drains the inboxes.
crate::common::async_bridge::ensure_pump_registered();
super::async_shim::ensure_pump_registered();
MESSAGE_PORTS.with(|ports| {
if let Some(state) = ports.borrow_mut().get_mut(&port_id) {
match event_name.as_str() {
Expand Down Expand Up @@ -186,7 +186,7 @@ extern "C" fn port_add_event_listener(
let Some(cb_bits) = callback_bits_from_value(callback) else {
return js_undefined();
};
crate::common::async_bridge::ensure_pump_registered();
super::async_shim::ensure_pump_registered();
MESSAGE_PORTS.with(|ports| {
if let Some(state) = ports.borrow_mut().get_mut(&port_id) {
match event_name.as_str() {
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-stdlib/src/worker_threads/worker_pump.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ pub extern "C" fn js_worker_threads_process_pending() -> i32 {
};
dispatch_worker_event(worker_id, "exit", Some(code as f64));
if let Some(promise) = terminate_promise {
crate::common::async_bridge::queue_promise_resolution(
super::async_shim::queue_promise_resolution(
promise,
true,
(code as f64).to_bits(),
Expand Down