diff --git a/crates/perry-ext-http/src/client_request_surface.rs b/crates/perry-ext-http/src/client_request_surface.rs index 0a3117e19d..d74fe7a04b 100644 --- a/crates/perry-ext-http/src/client_request_surface.rs +++ b/crates/perry-ext-http/src/client_request_surface.rs @@ -341,6 +341,41 @@ fn dispatch_property(handle: Handle, property: &str) -> Option { }); } Some(match property { + "method" => { + with_handle_mut::(handle, |req| string_value(&req.method)) + .unwrap_or_else(undefined_value) + } + "protocol" => with_handle_mut::(handle, |req| { + reqwest::Url::parse(&req.url) + .map(|u| string_value(&format!("{}:", u.scheme()))) + .unwrap_or_else(|_| string_value("")) + }) + .unwrap_or_else(undefined_value), + "host" => with_handle_mut::(handle, |req| { + let host = reqwest::Url::parse(&req.url) + .ok() + .and_then(|u| u.host_str().map(|s| s.to_string())) + .unwrap_or_default(); + string_value(&host) + }) + .unwrap_or_else(undefined_value), + "path" => with_handle_mut::(handle, |req| { + let path = reqwest::Url::parse(&req.url) + .map(|u| { + let mut path = u.path().to_string(); + if path.is_empty() { + path.push('/'); + } + if let Some(q) = u.query() { + path.push('?'); + path.push_str(q); + } + path + }) + .unwrap_or_default(); + string_value(&path) + }) + .unwrap_or_else(undefined_value), "aborted" => js_http_client_request_aborted(handle), "destroyed" => js_http_client_request_destroyed(handle), "finished" => js_http_client_request_finished(handle), @@ -358,6 +393,24 @@ fn dispatch_method(handle: Handle, method: &str, args: &[f64]) -> Option { return None; } Some(match method { + "end" => { + unsafe { + client_request_end_impl( + handle, + args.first().copied().unwrap_or_else(undefined_value), + ); + } + handle_value(handle) + } + "write" => { + unsafe { + client_request_write_impl( + handle, + args.first().copied().unwrap_or_else(undefined_value), + ); + } + handle_value(handle) + } "setHeader" => { let name = string_arg(args, 0).unwrap_or_default(); let value = string_arg(args, 1).unwrap_or_default(); @@ -382,6 +435,26 @@ fn dispatch_method(handle: Handle, method: &str, args: &[f64]) -> Option { "getHeaderNames" => headers_array(handle, false), "getHeaders" => headers_object(handle), "getRawHeaderNames" => headers_array(handle, true), + "setTimeout" => { + unsafe { + client_request_set_timeout_impl(handle, args.first().copied().unwrap_or(0.0)); + } + handle_value(handle) + } + "listenerCount" => { + let event = string_arg(args, 0).unwrap_or_default(); + get_handle_mut::(handle) + .map(|req| { + let explicit = req.listeners.get(&event).map(|v| v.len()).unwrap_or(0); + let implicit_response = if event == "response" && req.response_callback != 0 { + 1 + } else { + 0 + }; + (explicit + implicit_response) as f64 + }) + .unwrap_or(0.0) + } "abort" => js_http_client_request_abort(handle), "destroy" => handle_value(js_http_client_request_destroy(handle, undefined_value())), "flushHeaders" | "cork" | "uncork" | "setNoDelay" | "setSocketKeepAlive" => { diff --git a/crates/perry-ext-http/src/lib.rs b/crates/perry-ext-http/src/lib.rs index 5899d41dcc..22baf8e6d2 100644 --- a/crates/perry-ext-http/src/lib.rs +++ b/crates/perry-ext-http/src/lib.rs @@ -612,10 +612,10 @@ fn dispatch_request( let try_h = tokio::runtime::Handle::try_current(); std::hint::black_box(&try_h); if try_h.is_err() { - eprintln!( - "[perry-ext-http] BUG: dispatch_request Handle::try_current returned Err — \ - LTO has likely dead-stripped tokio's CONTEXT statics." - ); + push_event(PendingHttpEvent::Error { + request_handle, + error_message: "http client runtime unavailable".to_string(), + }); return; } let handle = tokio::runtime::Handle::current(); @@ -786,10 +786,10 @@ fn dispatch_request_over_socket( let try_h = tokio::runtime::Handle::try_current(); std::hint::black_box(&try_h); if try_h.is_err() { - eprintln!( - "[perry-ext-http] BUG: dispatch_request_over_socket Handle::try_current returned \ - Err — LTO has likely dead-stripped tokio's CONTEXT statics." - ); + push_event(PendingHttpEvent::Error { + request_handle, + error_message: "http client runtime unavailable".to_string(), + }); return; } let handle = tokio::runtime::Handle::current(); @@ -1137,6 +1137,10 @@ pub unsafe extern "C" fn js_https_get_overload(args_array: i64) -> Handle { /// `req.write(chunk)` — append data to the request body. #[no_mangle] pub unsafe extern "C" fn js_http_client_request_write(handle: Handle, body_f64: f64) -> Handle { + client_request_write_impl(handle, body_f64) +} + +unsafe fn client_request_write_impl(handle: Handle, body_f64: f64) -> Handle { if let Some(body) = extract_string_value(body_f64) { with_handle_mut::(handle, |req| { req.body.extend_from_slice(body.as_bytes()); @@ -1150,6 +1154,10 @@ pub unsafe extern "C" fn js_http_client_request_write(handle: Handle, body_f64: /// second call after `ended=true` is a no-op. #[no_mangle] pub unsafe extern "C" fn js_http_client_request_end(handle: Handle, body_f64: f64) -> Handle { + client_request_end_impl(handle, body_f64) +} + +unsafe fn client_request_end_impl(handle: Handle, body_f64: f64) -> Handle { if let Some(body) = extract_string_value(body_f64) { with_handle_mut::(handle, |req| { req.body.extend_from_slice(body.as_bytes()); @@ -1242,6 +1250,10 @@ pub unsafe extern "C" fn js_http_on( event_ptr: *const StringHeader, callback: i64, ) -> Handle { + http_on_impl(handle, event_ptr, callback) +} + +unsafe fn http_on_impl(handle: Handle, event_ptr: *const StringHeader, callback: i64) -> Handle { ensure_gc_scanner_registered(); let event = match read_str(event_ptr) { Some(e) => e, @@ -1290,6 +1302,10 @@ pub unsafe extern "C" fn js_http_set_header( /// `req.setTimeout(ms)`. #[no_mangle] pub unsafe extern "C" fn js_http_set_timeout(handle: Handle, ms: f64) -> Handle { + client_request_set_timeout_impl(handle, ms) +} + +unsafe fn client_request_set_timeout_impl(handle: Handle, ms: f64) -> Handle { with_handle_mut::(handle, |req| { req.timeout_ms = Some(ms.max(0.0) as u64); }); @@ -1534,6 +1550,15 @@ fn body_chunk_value(body: &[u8], encoding: Option<&str>) -> f64 { /// Number of pending events the main loop should drain. #[no_mangle] pub extern "C" fn js_http_has_pending() -> i32 { + let has_events = HTTP_PENDING_EVENTS + .lock() + .map(|q| !q.is_empty()) + .unwrap_or(false); + if has_events { + unsafe { + js_http_process_pending(); + } + } HTTP_PENDING_EVENTS .lock() .map(|q| if q.is_empty() { 0 } else { 1 }) diff --git a/crates/perry-hir/src/destructuring/var_decl.rs b/crates/perry-hir/src/destructuring/var_decl.rs index d9a3eb4e41..56f768c09a 100644 --- a/crates/perry-hir/src/destructuring/var_decl.rs +++ b/crates/perry-hir/src/destructuring/var_decl.rs @@ -626,9 +626,14 @@ pub(crate) fn lower_var_decl_with_destructuring( _ => None, }; if let Some(class_name) = class_name { + let class_module = if class_name == "ClientRequest" { + "http" + } else { + module_name + }; ctx.register_native_instance( name.clone(), - module_name.to_string(), + class_module.to_string(), class_name.to_string(), ); } diff --git a/crates/perry-hir/src/js_transform/local_natives.rs b/crates/perry-hir/src/js_transform/local_natives.rs index b5ade6c99f..b994ab1620 100644 --- a/crates/perry-hir/src/js_transform/local_natives.rs +++ b/crates/perry-hir/src/js_transform/local_natives.rs @@ -1323,6 +1323,7 @@ pub fn detect_native_instance_creation_with_context( // factory call lived in (mirrors lower.rs:5517). let owning_module = match (module.as_str(), method.as_str()) { ("tls", "connect") => "net".to_string(), + ("https", "request" | "get") => "http".to_string(), _ => module.clone(), }; Some((owning_module, class_name.to_string())) diff --git a/crates/perry-hir/src/lower/module_decl.rs b/crates/perry-hir/src/lower/module_decl.rs index b433fadfb2..b9e30cfd23 100644 --- a/crates/perry-hir/src/lower/module_decl.rs +++ b/crates/perry-hir/src/lower/module_decl.rs @@ -739,9 +739,15 @@ pub(crate) fn lower_module_decl( _ => None, }; if let Some(class_name) = class_name { + let class_module = + if class_name == "ClientRequest" { + "http".to_string() + } else { + module_name_owned.clone() + }; ctx.register_native_instance( name.clone(), - module_name_owned.clone(), + class_module.clone(), class_name.to_string(), ); // Also register as module-level native instance so it survives scope exits. @@ -750,7 +756,7 @@ pub(crate) fn lower_module_decl( // causing pool.query() inside functions to miss the Pool dispatch. ctx.module_native_instances.push(( name.clone(), - module_name_owned, + class_module, class_name.to_string(), )); } @@ -884,9 +890,15 @@ pub(crate) fn lower_module_decl( _ => None, }; if let Some(class_name) = class_name { + let class_module = + if class_name == "ClientRequest" { + "http" + } else { + module_name + }; ctx.register_native_instance( name.clone(), - module_name.to_string(), + class_module.to_string(), class_name.to_string(), ); } diff --git a/crates/perry-stdlib/src/common/dispatch.rs b/crates/perry-stdlib/src/common/dispatch.rs index 3c84bf0ef5..14a7ecdec4 100644 --- a/crates/perry-stdlib/src/common/dispatch.rs +++ b/crates/perry-stdlib/src/common/dispatch.rs @@ -228,6 +228,11 @@ pub unsafe extern "C" fn js_handle_method_dispatch( return value; } + #[cfg(feature = "http-client")] + if let Some(value) = unsafe { crate::http::dispatch_agent_method(handle, method_name, &args) } { + return value; + } + #[cfg(feature = "http-client")] if let Some(value) = crate::http::dispatch_client_request_method(handle, method_name, &args) { return value; @@ -1599,6 +1604,11 @@ pub unsafe extern "C" fn js_handle_property_dispatch( return value; } + #[cfg(feature = "http-client")] + if let Some(value) = crate::http::dispatch_agent_property(handle, property_name) { + return value; + } + #[cfg(feature = "http-client")] if let Some(value) = crate::http::dispatch_client_request_property(handle, property_name) { return value; diff --git a/crates/perry-stdlib/src/common/dispatch_http.rs b/crates/perry-stdlib/src/common/dispatch_http.rs index 80d9317238..4bc83f43e2 100644 --- a/crates/perry-stdlib/src/common/dispatch_http.rs +++ b/crates/perry-stdlib/src/common/dispatch_http.rs @@ -9,7 +9,11 @@ pub(super) unsafe fn dispatch_client_request_method( ) -> Option { if !matches!( method_name, - "setHeader" + "end" + | "write" + | "setHeader" + | "setTimeout" + | "listenerCount" | "getHeader" | "hasHeader" | "removeHeader" @@ -65,6 +69,10 @@ pub(super) unsafe fn dispatch_client_request_property( | "setHeader" | "setTimeout" | "listenerCount" + | "method" + | "protocol" + | "host" + | "path" | "getHeader" | "hasHeader" | "removeHeader" diff --git a/crates/perry-stdlib/src/http.rs b/crates/perry-stdlib/src/http.rs index 5a8961fcec..583759c22b 100644 --- a/crates/perry-stdlib/src/http.rs +++ b/crates/perry-stdlib/src/http.rs @@ -21,6 +21,10 @@ mod client_request_surface; pub(crate) use client_request_surface::{ dispatch_client_request_method, dispatch_client_request_property, }; +mod agent_dispatch; +pub(crate) use agent_dispatch::{dispatch_agent_method, dispatch_agent_property}; +#[cfg(feature = "external-http-client-pump")] +mod external_client_request; extern "C" { fn js_value_is_closure(value_bits: i64) -> i32; @@ -839,6 +843,12 @@ pub unsafe extern "C" fn js_http_client_request_write(handle: Handle, body_f64: if let Some(body_str) = extract_string_value(body_f64) { req.body.extend_from_slice(body_str.as_bytes()); } + return handle; + } + + #[cfg(feature = "external-http-client-pump")] + { + let _ = unsafe { external_client_request::dispatch_method(handle, "write", &[body_f64]) }; } handle } @@ -859,7 +869,15 @@ pub unsafe extern "C" fn js_http_client_request_end(handle: Handle, body_f64: f6 let (method, url, headers, body, timeout_ms, agent_pool) = { let req = match get_handle_mut::(handle) { Some(r) => r, - None => return handle, + None => { + #[cfg(feature = "external-http-client-pump")] + { + let _ = unsafe { + external_client_request::dispatch_method(handle, "end", &[body_f64]) + }; + } + return handle; + } }; if req.ended { return handle; // Already sent @@ -1048,7 +1066,23 @@ pub unsafe extern "C" fn js_http_set_header( None => return handle, }; - client_request_surface::set_header(handle, &name, value); + if client_request_surface::is_client_request_handle(handle) { + client_request_surface::set_header(handle, &name, value); + return handle; + } + + #[cfg(feature = "external-http-client-pump")] + { + let name_value = f64::from_bits(0x7FFF_0000_0000_0000u64 | (name_ptr as u64 & PTR_MASK)); + let value_value = f64::from_bits(0x7FFF_0000_0000_0000u64 | (value_ptr as u64 & PTR_MASK)); + let _ = unsafe { + external_client_request::dispatch_method( + handle, + "setHeader", + &[name_value, value_value], + ) + }; + } handle } @@ -1058,6 +1092,12 @@ pub unsafe extern "C" fn js_http_set_header( pub unsafe extern "C" fn js_http_set_timeout(handle: Handle, ms: f64) -> Handle { if let Some(req) = get_handle_mut::(handle) { req.timeout_ms = Some(ms as u64); + return handle; + } + + #[cfg(feature = "external-http-client-pump")] + { + let _ = unsafe { external_client_request::dispatch_method(handle, "setTimeout", &[ms]) }; } handle } @@ -1109,56 +1149,81 @@ pub unsafe extern "C" fn js_http_incoming_message_set_encoding( #[no_mangle] pub extern "C" fn js_http_client_request_method(handle: Handle) -> *mut StringHeader { - let method = get_handle_mut::(handle) - .map(|req| req.method.clone()) - .unwrap_or_default(); + let method = match get_handle_mut::(handle) { + Some(req) => req.method.clone(), + None => { + #[cfg(feature = "external-http-client-pump")] + if let Some(ptr) = unsafe { external_client_request::string_property(handle, "method") } + { + return ptr; + } + String::new() + } + }; unsafe { js_string_from_bytes(method.as_ptr(), method.len() as u32) } } #[no_mangle] pub extern "C" fn js_http_client_request_protocol(handle: Handle) -> *mut StringHeader { - let protocol = get_handle_mut::(handle) - .map(|req| { - reqwest::Url::parse(&req.url) - .map(|u| format!("{}:", u.scheme())) - .unwrap_or_default() - }) - .unwrap_or_default(); + let protocol = match get_handle_mut::(handle) { + Some(req) => reqwest::Url::parse(&req.url) + .map(|u| format!("{}:", u.scheme())) + .unwrap_or_default(), + None => { + #[cfg(feature = "external-http-client-pump")] + if let Some(ptr) = + unsafe { external_client_request::string_property(handle, "protocol") } + { + return ptr; + } + String::new() + } + }; unsafe { js_string_from_bytes(protocol.as_ptr(), protocol.len() as u32) } } #[no_mangle] pub extern "C" fn js_http_client_request_host(handle: Handle) -> *mut StringHeader { - let host = get_handle_mut::(handle) - .map(|req| { - reqwest::Url::parse(&req.url) - .ok() - .and_then(|u| u.host_str().map(|s| s.to_string())) - .unwrap_or_default() - }) - .unwrap_or_default(); + let host = match get_handle_mut::(handle) { + Some(req) => reqwest::Url::parse(&req.url) + .ok() + .and_then(|u| u.host_str().map(|s| s.to_string())) + .unwrap_or_default(), + None => { + #[cfg(feature = "external-http-client-pump")] + if let Some(ptr) = unsafe { external_client_request::string_property(handle, "host") } { + return ptr; + } + String::new() + } + }; unsafe { js_string_from_bytes(host.as_ptr(), host.len() as u32) } } #[no_mangle] pub extern "C" fn js_http_client_request_path(handle: Handle) -> *mut StringHeader { - let path = get_handle_mut::(handle) - .map(|req| { - reqwest::Url::parse(&req.url) - .map(|u| { - let mut path = u.path().to_string(); - if path.is_empty() { - path.push('/'); - } - if let Some(q) = u.query() { - path.push('?'); - path.push_str(q); - } - path - }) - .unwrap_or_default() - }) - .unwrap_or_default(); + let path = match get_handle_mut::(handle) { + Some(req) => reqwest::Url::parse(&req.url) + .map(|u| { + let mut path = u.path().to_string(); + if path.is_empty() { + path.push('/'); + } + if let Some(q) = u.query() { + path.push('?'); + path.push_str(q); + } + path + }) + .unwrap_or_default(), + None => { + #[cfg(feature = "external-http-client-pump")] + if let Some(ptr) = unsafe { external_client_request::string_property(handle, "path") } { + return ptr; + } + String::new() + } + }; unsafe { js_string_from_bytes(path.as_ptr(), path.len() as u32) } } @@ -1171,8 +1236,8 @@ pub unsafe extern "C" fn js_http_client_request_listener_count( Some(e) => e, None => return 0.0, }; - get_handle_mut::(handle) - .map(|req| { + match get_handle_mut::(handle) { + Some(req) => { let explicit = req.listeners.get(&event).map(|v| v.len()).unwrap_or(0); let implicit_response = if event == "response" && req.response_callback != 0 { 1 @@ -1180,8 +1245,25 @@ pub unsafe extern "C" fn js_http_client_request_listener_count( 0 }; (explicit + implicit_response) as f64 - }) - .unwrap_or(0.0) + } + None => { + #[cfg(feature = "external-http-client-pump")] + { + let event_value = + f64::from_bits(0x7FFF_0000_0000_0000u64 | (event_ptr as u64 & PTR_MASK)); + if let Some(value) = unsafe { + external_client_request::dispatch_method( + handle, + "listenerCount", + &[event_value], + ) + } { + return value; + } + } + 0.0 + } + } } /// IncomingMessage.statusCode — get response status code @@ -1833,39 +1915,6 @@ pub extern "C" fn js_http_agent_destroy(handle: Handle) -> Handle { handle } -/// Allocate a fresh empty JS object — Node returns `{}` from -/// `agent.sockets` / `.freeSockets` / `.requests` until the agent has -/// dispatched a request. Returns NaN-boxed pointer bits as `f64` -/// (same ABI as `__get_protocol` etc. for the codegen-direct dispatch -/// rows). -fn empty_object_bits_f64() -> f64 { - // `js_object_alloc(num_keys, capacity)` returns an empty object - // pointer; the `0,0` shape is reused across allocations. - let obj = unsafe { perry_runtime::js_object_alloc(0, 0) }; - if obj.is_null() { - return f64::from_bits(JSValue::undefined().bits()); - } - f64::from_bits(JSValue::object_ptr(obj as *mut u8).bits()) -} - -#[no_mangle] -pub extern "C" fn js_http_agent_sockets(handle: Handle) -> f64 { - let _ = handle; - empty_object_bits_f64() -} - -#[no_mangle] -pub extern "C" fn js_http_agent_free_sockets(handle: Handle) -> f64 { - let _ = handle; - empty_object_bits_f64() -} - -#[no_mangle] -pub extern "C" fn js_http_agent_requests(handle: Handle) -> f64 { - let _ = handle; - empty_object_bits_f64() -} - #[no_mangle] pub extern "C" fn js_http_agent_set_create_connection(handle: Handle, closure_ptr: i64) { if let Some(agent) = get_handle_mut::(handle) { @@ -1880,36 +1929,6 @@ pub extern "C" fn js_http_agent_set_create_socket(handle: Handle, closure_ptr: i } } -fn bind_agent_method(handle: Handle, name: &'static [u8]) -> i64 { - let instance = f64::from_bits(POINTER_TAG | (handle as u64 & PTR_MASK)); - let bound = unsafe { js_class_method_bind(instance, name.as_ptr(), name.len()) }; - (bound.to_bits() & PTR_MASK) as i64 -} - -#[no_mangle] -pub extern "C" fn js_http_agent_create_connection(handle: Handle) -> i64 { - let stored = get_handle_mut::(handle) - .map(|a| a.create_connection) - .unwrap_or(0); - if stored != 0 { - stored - } else { - bind_agent_method(handle, b"createConnection") - } -} - -#[no_mangle] -pub extern "C" fn js_http_agent_create_socket(handle: Handle) -> i64 { - let stored = get_handle_mut::(handle) - .map(|a| a.create_socket) - .unwrap_or(0); - if stored != 0 { - stored - } else { - bind_agent_method(handle, b"createSocket") - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/perry-stdlib/src/http/agent_dispatch.rs b/crates/perry-stdlib/src/http/agent_dispatch.rs new file mode 100644 index 0000000000..492c7d6f4a --- /dev/null +++ b/crates/perry-stdlib/src/http/agent_dispatch.rs @@ -0,0 +1,114 @@ +use perry_runtime::JSValue; + +use crate::common::{get_handle_mut, Handle}; + +use super::{ + js_class_method_bind, js_http_agent_destroy, js_http_agent_get_name, js_http_agent_noop_self, + AgentHandle, POINTER_TAG, PTR_MASK, +}; + +fn bind_agent_method(handle: Handle, name: &'static [u8]) -> i64 { + (bind_agent_method_value(handle, name).to_bits() & PTR_MASK) as i64 +} + +fn bind_agent_method_value(handle: Handle, name: &'static [u8]) -> f64 { + let instance = f64::from_bits(POINTER_TAG | (handle as u64 & PTR_MASK)); + unsafe { js_class_method_bind(instance, name.as_ptr(), name.len()) } +} + +fn pointer_value(ptr: i64) -> f64 { + if ptr == 0 { + f64::from_bits(JSValue::undefined().bits()) + } else { + f64::from_bits(POINTER_TAG | (ptr as u64 & PTR_MASK)) + } +} + +pub(crate) fn dispatch_agent_property(handle: Handle, property: &str) -> Option { + get_handle_mut::(handle)?; + Some(match property { + "createConnection" => pointer_value(js_http_agent_create_connection(handle)), + "createSocket" => pointer_value(js_http_agent_create_socket(handle)), + "getName" => bind_agent_method_value(handle, b"getName"), + "destroy" => bind_agent_method_value(handle, b"destroy"), + "close" => bind_agent_method_value(handle, b"close"), + "keepSocketAlive" => bind_agent_method_value(handle, b"keepSocketAlive"), + "reuseSocket" => bind_agent_method_value(handle, b"reuseSocket"), + _ => return None, + }) +} + +pub(crate) unsafe fn dispatch_agent_method( + handle: Handle, + method: &str, + args: &[f64], +) -> Option { + get_handle_mut::(handle)?; + Some(match method { + "getName" => { + let options = args + .first() + .copied() + .unwrap_or_else(|| f64::from_bits(JSValue::undefined().bits())); + let ptr = js_http_agent_get_name(handle, options); + f64::from_bits(JSValue::string_ptr(ptr).bits()) + } + "destroy" => pointer_value(js_http_agent_destroy(handle)), + "close" | "keepSocketAlive" | "reuseSocket" => { + pointer_value(js_http_agent_noop_self(handle)) + } + _ => return None, + }) +} + +/// Allocate the empty object Node exposes for `agent.sockets`, +/// `agent.freeSockets`, and `agent.requests` before any requests are pooled. +fn empty_object_bits_f64() -> f64 { + let obj = perry_runtime::js_object_alloc(0, 0); + if obj.is_null() { + return f64::from_bits(JSValue::undefined().bits()); + } + f64::from_bits(JSValue::object_ptr(obj as *mut u8).bits()) +} + +#[no_mangle] +pub extern "C" fn js_http_agent_sockets(handle: Handle) -> f64 { + let _ = handle; + empty_object_bits_f64() +} + +#[no_mangle] +pub extern "C" fn js_http_agent_free_sockets(handle: Handle) -> f64 { + let _ = handle; + empty_object_bits_f64() +} + +#[no_mangle] +pub extern "C" fn js_http_agent_requests(handle: Handle) -> f64 { + let _ = handle; + empty_object_bits_f64() +} + +#[no_mangle] +pub extern "C" fn js_http_agent_create_connection(handle: Handle) -> i64 { + let stored = get_handle_mut::(handle) + .map(|a| a.create_connection) + .unwrap_or(0); + if stored != 0 { + stored + } else { + bind_agent_method(handle, b"createConnection") + } +} + +#[no_mangle] +pub extern "C" fn js_http_agent_create_socket(handle: Handle) -> i64 { + let stored = get_handle_mut::(handle) + .map(|a| a.create_socket) + .unwrap_or(0); + if stored != 0 { + stored + } else { + bind_agent_method(handle, b"createSocket") + } +} diff --git a/crates/perry-stdlib/src/http/client_request_surface.rs b/crates/perry-stdlib/src/http/client_request_surface.rs index 9a2aea7447..30ec0932e5 100644 --- a/crates/perry-stdlib/src/http/client_request_surface.rs +++ b/crates/perry-stdlib/src/http/client_request_surface.rs @@ -319,6 +319,14 @@ pub(crate) fn dispatch_client_request_property(handle: Handle, property: &str) - }); } Some(match property { + "method" => { + f64::from_bits(JSValue::string_ptr(js_http_client_request_method(handle)).bits()) + } + "protocol" => { + f64::from_bits(JSValue::string_ptr(js_http_client_request_protocol(handle)).bits()) + } + "host" => f64::from_bits(JSValue::string_ptr(js_http_client_request_host(handle)).bits()), + "path" => f64::from_bits(JSValue::string_ptr(js_http_client_request_path(handle)).bits()), "aborted" => js_http_client_request_aborted(handle), "destroyed" => js_http_client_request_destroyed(handle), "finished" => js_http_client_request_finished(handle), @@ -364,6 +372,20 @@ pub(crate) fn dispatch_client_request_method( "getHeaderNames" => headers_array(handle, false), "getHeaders" => headers_object(handle), "getRawHeaderNames" => headers_array(handle, true), + "listenerCount" => { + let event = string_arg(args, 0).unwrap_or_default(); + get_handle_mut::(handle) + .map(|req| { + let explicit = req.listeners.get(&event).map(|v| v.len()).unwrap_or(0); + let implicit_response = if event == "response" && req.response_callback != 0 { + 1 + } else { + 0 + }; + (explicit + implicit_response) as f64 + }) + .unwrap_or(0.0) + } "abort" => js_http_client_request_abort(handle), "destroy" => handle_value(js_http_client_request_destroy(handle, undefined_value())), "flushHeaders" | "cork" | "uncork" | "setNoDelay" | "setSocketKeepAlive" => { diff --git a/crates/perry-stdlib/src/http/external_client_request.rs b/crates/perry-stdlib/src/http/external_client_request.rs new file mode 100644 index 0000000000..a39bb7cd43 --- /dev/null +++ b/crates/perry-stdlib/src/http/external_client_request.rs @@ -0,0 +1,62 @@ +use perry_runtime::StringHeader; + +use crate::common::Handle; + +use super::{POINTER_TAG, PTR_MASK}; + +pub(super) unsafe fn dispatch_method(handle: Handle, method: &str, args: &[f64]) -> Option { + extern "C" { + fn js_ext_http_client_request_is_handle(handle: i64) -> i32; + fn js_ext_http_client_request_dispatch_method( + handle: i64, + method_ptr: *const u8, + method_len: usize, + args_ptr: *const f64, + args_len: usize, + ) -> f64; + } + if unsafe { js_ext_http_client_request_is_handle(handle) } == 0 { + return None; + } + Some(unsafe { + js_ext_http_client_request_dispatch_method( + handle, + method.as_ptr(), + method.len(), + args.as_ptr(), + args.len(), + ) + }) +} + +unsafe fn dispatch_property(handle: Handle, property: &str) -> Option { + extern "C" { + fn js_ext_http_client_request_is_handle(handle: i64) -> i32; + fn js_ext_http_client_request_dispatch_property( + handle: i64, + property_ptr: *const u8, + property_len: usize, + ) -> f64; + } + if unsafe { js_ext_http_client_request_is_handle(handle) } == 0 { + return None; + } + Some(unsafe { + js_ext_http_client_request_dispatch_property(handle, property.as_ptr(), property.len()) + }) +} + +pub(super) unsafe fn string_property(handle: Handle, property: &str) -> Option<*mut StringHeader> { + let value = unsafe { dispatch_property(handle, property) }?; + let bits = value.to_bits(); + let tag = bits & !PTR_MASK; + if tag != 0x7FFF_0000_0000_0000 && tag != POINTER_TAG { + return None; + } + let ptr = (bits & PTR_MASK) as *mut StringHeader; + if ptr.is_null() { + None + } else { + Some(ptr) + } +} diff --git a/run_parity_tests.sh b/run_parity_tests.sh index 99d534be76..83245e7a57 100755 --- a/run_parity_tests.sh +++ b/run_parity_tests.sh @@ -319,8 +319,20 @@ echo "" TARGET_DIR="${CARGO_TARGET_DIR:-$SCRIPT_DIR/target}" PERRY_BIN="$TARGET_DIR/release/perry" echo "Building compiler (release)..." -if ! cargo build --release --quiet -p perry -p perry-runtime -p perry-stdlib 2>/dev/null; then - echo -e "${RED}Failed to build compiler${NC}" +BUILD_PACKAGES=(-p perry -p perry-runtime -p perry-stdlib) +BUILD_FEATURES=() +if [[ -n "${PERRY_NO_AUTO_OPTIMIZE:-}" && "$TEST_SUITE" == "node-suite" ]]; then + case "$MODULE_FILTER" in + ""|http|http/*|https|https/*|http2|http2/*) + # No-auto optimized links still consume prebuilt well-known ext archives + # and need the matching stdlib pump hooks compiled into libperry_stdlib.a. + BUILD_PACKAGES+=(-p perry-ext-http) + BUILD_FEATURES+=(--features perry-stdlib/external-http-server-pump,perry-stdlib/external-http-client-pump) + ;; + esac +fi +if ! cargo build --release --quiet "${BUILD_PACKAGES[@]}" "${BUILD_FEATURES[@]}" 2>/dev/null; then + echo -e "${RED}Failed to build compiler/runtime archives${NC}" exit 1 fi if [[ ! -x "$PERRY_BIN" ]]; then @@ -328,7 +340,7 @@ if [[ ! -x "$PERRY_BIN" ]]; then exit 1 fi -echo -e "${GREEN}Compiler and runtime built successfully${NC}" +echo -e "${GREEN}Compiler and runtime archives built successfully${NC}" echo "" echo "Running parity tests (backend: $BACKEND_LABEL, suite: $TEST_SUITE${MODULE_FILTER:+, module: $MODULE_FILTER})..." echo ""