Skip to content
Merged
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
5 changes: 5 additions & 0 deletions crates/perry-codegen-js/src/emit/exprs_more.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ impl JsEmitter {
body,
headers,
headers_dynamic,
signal,
} => {
self.output.push_str("fetch(");
self.emit_expr(url);
Expand All @@ -63,6 +64,10 @@ impl JsEmitter {
}
self.output.push('}');
}
if let Some(sig) = signal {
self.output.push_str(", signal: ");
self.emit_expr(sig);
}
self.output.push_str("})");
}
Expr::FetchGetWithAuth { url, auth_header } => {
Expand Down
3 changes: 3 additions & 0 deletions crates/perry-codegen-wasm/src/emit/expr/net_fetch_crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ impl<'a> FuncEmitCtx<'a> {
body,
headers,
headers_dynamic,
// The WASM fetch backend has no AbortSignal cancellation (it is a
// separate target from the native binary), so the signal is unused.
signal: _,
} => {
self.emit_expr(func, url);
self.emit_expr(func, method);
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen-wasm/src/emit/js_fallback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ impl WasmModuleEmitter {
body,
headers,
headers_dynamic,
signal: _,
} => {
let url_js = self.emit_js_expr(url, locals);
let method_js = self.emit_js_expr(method, locals);
Expand Down
4 changes: 4 additions & 0 deletions crates/perry-codegen-wasm/src/emit/string_collection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,7 @@ impl WasmModuleEmitter {
body,
headers,
headers_dynamic,
signal,
} => {
self.collect_strings_in_expr(url);
self.collect_strings_in_expr(method);
Expand All @@ -983,6 +984,9 @@ impl WasmModuleEmitter {
if let Some(hd) = headers_dynamic {
self.collect_strings_in_expr(hd);
}
if let Some(s) = signal {
self.collect_strings_in_expr(s);
}
}
Expr::FetchGetWithAuth { url, auth_header } => {
self.collect_strings_in_expr(url);
Expand Down
4 changes: 4 additions & 0 deletions crates/perry-codegen/src/collectors/escape_check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,7 @@ pub fn check_escapes_in_expr(
body,
headers,
headers_dynamic,
signal,
} => {
check_escapes_in_expr(url, candidates, classes, escaped);
check_escapes_in_expr(method, candidates, classes, escaped);
Expand All @@ -679,6 +680,9 @@ pub fn check_escapes_in_expr(
if let Some(hd) = headers_dynamic {
check_escapes_in_expr(hd, candidates, classes, escaped);
}
if let Some(s) = signal {
check_escapes_in_expr(s, candidates, classes, escaped);
}
}
Expr::SuperCall(args)
| Expr::StaticMethodCall { args, .. }
Expand Down
4 changes: 4 additions & 0 deletions crates/perry-codegen/src/collectors/escape_news.rs
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,7 @@ fn collect_used_new_fields_in_expr(
body,
headers,
headers_dynamic,
signal,
} => {
collect_used_new_fields_in_expr(url, non_escaping_news, used);
collect_used_new_fields_in_expr(method, non_escaping_news, used);
Expand All @@ -764,6 +765,9 @@ fn collect_used_new_fields_in_expr(
if let Some(hd) = headers_dynamic {
collect_used_new_fields_in_expr(hd, non_escaping_news, used);
}
if let Some(s) = signal {
collect_used_new_fields_in_expr(s, non_escaping_news, used);
}
}
Expr::FetchGetWithAuth { url, auth_header } => {
collect_used_new_fields_in_expr(url, non_escaping_news, used);
Expand Down
12 changes: 12 additions & 0 deletions crates/perry-codegen/src/expr/logical_collections.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,17 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
body,
headers,
headers_dynamic,
signal,
} => {
let url_box = lower_expr(ctx, url)?;
let method_box = lower_expr(ctx, method)?;
let body_box = lower_expr(ctx, body)?;
// Lower `init.signal` (if any) up front so it can be stashed for
// `js_fetch_with_options` right before the call below.
let signal_box = match signal {
Some(s) => Some(lower_expr(ctx, s)?),
None => None,
};

// Obtain the headers as a NaN-boxed object value, then JSON-stringify
// it below. Two cases:
Expand Down Expand Up @@ -141,6 +148,11 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
let url_handle = unbox_to_i64(blk, &url_box);
let method_handle = unbox_to_i64(blk, &method_box);
let body_handle = unbox_to_i64(blk, &body_box);
// Stash the AbortSignal so `js_fetch_with_options` can cancel the
// request when it aborts (`controller.abort()` / `AbortSignal.timeout`).
if let Some(sig) = &signal_box {
blk.call_void("js_fetch_set_pending_signal", &[(DOUBLE, sig)]);
}
let promise = blk.call(
I64,
"js_fetch_with_options",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ pub(crate) fn declare_core(module: &mut LlModule) {
module.declare_function("js_fetch_stream_status", DOUBLE, &[DOUBLE]);
module.declare_function("js_fetch_text", I64, &[I64]);
module.declare_function("js_fetch_with_options", I64, &[I64, I64, I64, I64]);
// Stashes the `fetch(url, { signal })` AbortSignal for the next
// `js_fetch_with_options` so the request can be aborted.
module.declare_function("js_fetch_set_pending_signal", VOID, &[DOUBLE]);
// Headers-aware JSON stringify for the `fetch(url, { headers })` request
// path: takes the headers value (f64) and returns a `*const StringHeader`
// (i64) holding `{name:value}` JSON, treating a `Headers` handle safely.
Expand Down
1 change: 1 addition & 0 deletions crates/perry-hir/src/capability.rs
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,7 @@ mod tests {
body: Box::new(Expr::Undefined),
headers: vec![],
headers_dynamic: None,
signal: None,
}));
let v = audit_module_capabilities(
&m,
Expand Down
4 changes: 4 additions & 0 deletions crates/perry-hir/src/egress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,7 @@ mod tests {
body: Box::new(Expr::Undefined),
headers: vec![],
headers_dynamic: None,
signal: None,
}));
let v = audit_module_egress(&m, "/repo/main.ts", &pats(&["api.example.com"]), false);
assert_eq!(v.len(), 1);
Expand All @@ -506,6 +507,7 @@ mod tests {
body: Box::new(Expr::Undefined),
headers: vec![],
headers_dynamic: None,
signal: None,
}));
let v = audit_module_egress(&m, "/repo/main.ts", &pats(&["api.example.com"]), false);
assert!(v.is_empty());
Expand All @@ -520,6 +522,7 @@ mod tests {
body: Box::new(Expr::Undefined),
headers: vec![],
headers_dynamic: None,
signal: None,
}));
let v = audit_module_egress(&m, "/repo/main.ts", &pats(&["api.example.com"]), false);
assert_eq!(v.len(), 1);
Expand All @@ -539,6 +542,7 @@ mod tests {
body: Box::new(Expr::Undefined),
headers: vec![],
headers_dynamic: None,
signal: None,
}));
let v = audit_module_egress(
&m,
Expand Down
4 changes: 4 additions & 0 deletions crates/perry-hir/src/ir/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1475,6 +1475,10 @@ pub enum Expr {
// serialized at runtime. When `Some`, it takes precedence over the
// static `headers` pairs above. See #4932.
headers_dynamic: Option<Box<Expr>>,
// The `init.signal` AbortSignal, when present, so the request can be
// aborted (`controller.abort()` / `AbortSignal.timeout`). Lowered to a
// `js_fetch_set_pending_signal` call emitted just before the fetch.
signal: Option<Box<Expr>>,
},
FetchGetWithAuth {
// fetchWithAuth(url, authHeader) -> Promise<Response>
Expand Down
8 changes: 8 additions & 0 deletions crates/perry-hir/src/lower/expr_call/globals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ pub(super) fn try_global_builtins(
let mut body = Expr::Undefined;
let mut headers_obj: Vec<(String, Expr)> = Vec::new();
let mut headers_dynamic: Option<Box<Expr>> = None;
let mut signal: Option<Box<Expr>> = None;

for prop in &obj.props {
if let ast::PropOrSpread::Prop(prop) = prop {
Expand Down Expand Up @@ -434,6 +435,10 @@ pub(super) fn try_global_builtins(
));
}
}
"signal" => {
signal =
Some(Box::new(lower_expr(ctx, &kv.value)?));
}
_ => {}
}
}
Expand All @@ -449,6 +454,7 @@ pub(super) fn try_global_builtins(
match key.as_str() {
"method" => method = value,
"body" => body = value,
"signal" => signal = Some(Box::new(value)),
_ => {}
}
}
Expand All @@ -465,6 +471,7 @@ pub(super) fn try_global_builtins(
body: Box::new(body),
headers: headers_obj,
headers_dynamic,
signal,
}));
}
}
Expand All @@ -478,6 +485,7 @@ pub(super) fn try_global_builtins(
body: Box::new(Expr::Undefined),
headers: Vec::new(),
headers_dynamic: None,
signal: None,
}));
}
_ => {} // Fall through to generic handling
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-hir/src/stable_hash/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ impl SH for Expr {
Expr::ChildProcessSpawnBackground { command, args, log_file, env_json, } => { tag(h, 240); command.as_ref().hash(h); args.hash(h); log_file.as_ref().hash(h); env_json.hash(h); }
Expr::ChildProcessGetProcessStatus(e) => { tag(h, 241); e.as_ref().hash(h); }
Expr::ChildProcessKillProcess(e) => { tag(h, 242); e.as_ref().hash(h); }
Expr::FetchWithOptions { url, method, body, headers, headers_dynamic, } => { tag(h, 243); url.as_ref().hash(h); method.as_ref().hash(h); body.as_ref().hash(h); headers.hash(h); if let Some(hd) = headers_dynamic { tag(h, 1); hd.as_ref().hash(h); } else { tag(h, 0); } }
Expr::FetchWithOptions { url, method, body, headers, headers_dynamic, signal, } => { tag(h, 243); url.as_ref().hash(h); method.as_ref().hash(h); body.as_ref().hash(h); headers.hash(h); if let Some(hd) = headers_dynamic { tag(h, 1); hd.as_ref().hash(h); } else { tag(h, 0); } if let Some(s) = signal { tag(h, 1); s.as_ref().hash(h); } else { tag(h, 0); } }
Expr::FetchGetWithAuth { url, auth_header } => { tag(h, 244); url.as_ref().hash(h); auth_header.as_ref().hash(h); }
Expr::FetchPostWithAuth { url, auth_header, body, } => { tag(h, 245); url.as_ref().hash(h); auth_header.as_ref().hash(h); body.as_ref().hash(h); }
Expr::NetCreateServer { options, connection_listener, } => { tag(h, 246); options.hash(h); connection_listener.hash(h); }
Expand Down
4 changes: 4 additions & 0 deletions crates/perry-hir/src/walker/expr_mut.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1464,6 +1464,7 @@ where
body,
headers,
headers_dynamic,
signal,
} => {
f(url);
f(method);
Expand All @@ -1474,6 +1475,9 @@ where
if let Some(hd) = headers_dynamic {
f(hd);
}
if let Some(s) = signal {
f(s);
}
}
Expr::FetchGetWithAuth { url, auth_header } => {
f(url);
Expand Down
4 changes: 4 additions & 0 deletions crates/perry-hir/src/walker/expr_ref.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1441,6 +1441,7 @@ where
body,
headers,
headers_dynamic,
signal,
} => {
f(url);
f(method);
Expand All @@ -1451,6 +1452,9 @@ where
if let Some(hd) = headers_dynamic {
f(hd);
}
if let Some(s) = signal {
f(s);
}
}
Expr::FetchGetWithAuth { url, auth_header } => {
f(url);
Expand Down
36 changes: 36 additions & 0 deletions crates/perry-runtime/src/object/global_fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,41 @@
//! repository's 2,000-line lint gate.

use super::*;
use std::cell::Cell;
use std::ptr::null_mut;
use std::sync::atomic::{AtomicPtr, Ordering};

thread_local! {
/// The `signal` from the in-progress `fetch(url, { signal })` call, stashed
/// so the stdlib `js_fetch_with_options` (whose 4-arg ABI predates
/// AbortSignal support) can pick it up at entry without an ABI change.
static PENDING_FETCH_SIGNAL: Cell<f64> =
const { Cell::new(f64::from_bits(crate::value::TAG_UNDEFINED)) };
}

/// Stash the `signal` for the fetch call about to be dispatched. Set on the main
/// thread immediately before the fetch call and consumed at the start of
/// `js_fetch_with_options` — JS is single-threaded between those two points, so
/// there is no interleaving with another fetch. The runtime
/// `global_this_fetch_thunk` calls this for the dynamic/aliased fetch path; the
/// codegen `fetch(url, {static init})` fast path can emit it too.
#[no_mangle]
pub extern "C" fn js_fetch_set_pending_signal(signal: f64) {
PENDING_FETCH_SIGNAL.with(|c| c.set(signal));
}

/// Consume and clear the pending fetch signal, returning `undefined` when none
/// was set for this call (so a signal never leaks into a later signal-less
/// fetch on the same thread).
#[no_mangle]
pub extern "C" fn js_fetch_take_pending_signal() -> f64 {
PENDING_FETCH_SIGNAL.with(|c| {
let v = c.get();
c.set(f64::from_bits(crate::value::TAG_UNDEFINED));
v
})
}

#[cfg(not(feature = "external-fetch-symbols"))]
const FETCH_REASON: &str =
"fetch symbol from perry-stdlib not linked into this binary (runtime-only build)";
Expand Down Expand Up @@ -618,6 +650,10 @@ pub(super) extern "C" fn global_this_fetch_thunk(
let body_ptr = fetch_option_string_ptr(init, b"body");
let headers_json_ptr = fetch_headers_json_ptr(init);

// Hand the `init.signal` (if any) to `js_fetch_with_options` so an
// `AbortController` / `AbortSignal.timeout` can cancel this request.
js_fetch_set_pending_signal(fetch_option(init, b"signal"));

let promise =
unsafe { call_fetch_with_options(url_ptr, method_ptr, body_ptr, headers_json_ptr) };
if promise.is_null() {
Expand Down
Loading
Loading