| use std::time::Duration; |
|
|
| use tokio_util::task::AbortOnDropHandle; |
|
|
| use super::RuntimeCommand; |
| use super::RuntimeState; |
| use super::value::value_to_error_text; |
|
|
| pub(super) struct ScheduledTimeout { |
| callback: v8::Global<v8::Function>, |
| |
| _task: AbortOnDropHandle<()>, |
| } |
|
|
| pub(super) fn schedule_timeout( |
| scope: &mut v8::PinScope<'_, '_>, |
| args: v8::FunctionCallbackArguments, |
| ) -> Result<u64, String> { |
| let callback = args.get(0); |
| if !callback.is_function() { |
| return Err("setTimeout expects a function callback".to_string()); |
| } |
| let callback = v8::Local::<v8::Function>::try_from(callback) |
| .map_err(|_| "setTimeout expects a function callback".to_string())?; |
|
|
| let delay_ms = args |
| .get(1) |
| .number_value(scope) |
| .map(normalize_delay_ms) |
| .unwrap_or(0); |
|
|
| let callback = v8::Global::new(scope, callback); |
| let state = scope |
| .get_slot_mut::<RuntimeState>() |
| .ok_or_else(|| "runtime state unavailable".to_string())?; |
| let timeout_id = state.next_timeout_id; |
| state.next_timeout_id = state.next_timeout_id.saturating_add(1); |
| let runtime_command_tx = state.runtime_command_tx.clone(); |
| let sleep = tokio::time::sleep(Duration::from_millis(delay_ms)); |
| let task = tokio::spawn(async move { |
| sleep.await; |
| let _ = runtime_command_tx.send(RuntimeCommand::TimeoutFired { id: timeout_id }); |
| }); |
| state.pending_timeouts.insert( |
| timeout_id, |
| ScheduledTimeout { |
| callback, |
| _task: AbortOnDropHandle::new(task), |
| }, |
| ); |
|
|
| Ok(timeout_id) |
| } |
|
|
| pub(super) fn clear_timeout( |
| scope: &mut v8::PinScope<'_, '_>, |
| args: v8::FunctionCallbackArguments, |
| ) -> Result<(), String> { |
| let Some(timeout_id) = timeout_id_from_args(scope, args)? else { |
| return Ok(()); |
| }; |
|
|
| let Some(state) = scope.get_slot_mut::<RuntimeState>() else { |
| return Err("runtime state unavailable".to_string()); |
| }; |
| state.pending_timeouts.remove(&timeout_id); |
| Ok(()) |
| } |
|
|
| pub(super) fn invoke_timeout_callback( |
| scope: &mut v8::PinScope<'_, '_>, |
| timeout_id: u64, |
| ) -> Result<(), String> { |
| let callback = { |
| let state = scope |
| .get_slot_mut::<RuntimeState>() |
| .ok_or_else(|| "runtime state unavailable".to_string())?; |
| state.pending_timeouts.remove(&timeout_id) |
| }; |
| let Some(callback) = callback else { |
| return Ok(()); |
| }; |
|
|
| let tc = std::pin::pin!(v8::TryCatch::new(scope)); |
| let mut tc = tc.init(); |
| let callback = v8::Local::new(&tc, &callback.callback); |
| let receiver = v8::undefined(&tc).into(); |
| let _ = callback.call(&tc, receiver, &[]); |
| if tc.has_caught() { |
| return Err(tc |
| .exception() |
| .map(|exception| value_to_error_text(&mut tc, exception)) |
| .unwrap_or_else(|| "unknown code mode exception".to_string())); |
| } |
|
|
| Ok(()) |
| } |
| fn timeout_id_from_args( |
| scope: &mut v8::PinScope<'_, '_>, |
| args: v8::FunctionCallbackArguments, |
| ) -> Result<Option<u64>, String> { |
| if args.length() == 0 || args.get(0).is_null_or_undefined() { |
| return Ok(None); |
| } |
|
|
| let Some(timeout_id) = args.get(0).number_value(scope) else { |
| return Err("clearTimeout expects a numeric timeout id".to_string()); |
| }; |
| if !timeout_id.is_finite() || timeout_id <= 0.0 { |
| return Ok(None); |
| } |
|
|
| Ok(Some(timeout_id.trunc().min(u64::MAX as f64) as u64)) |
| } |
|
|
| fn normalize_delay_ms(delay_ms: f64) -> u64 { |
| if !delay_ms.is_finite() || delay_ms <= 0.0 { |
| 0 |
| } else { |
| delay_ms.trunc().min(u64::MAX as f64) as u64 |
| } |
| } |
|
|