1use std::cell::RefCell;
4use std::rc::Rc;
5use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender, channel};
6use std::sync::{Arc, Mutex};
7use std::task::{Context as TaskContext, Waker};
8
9use blitz_dom::{
10 BaseDocument, DEFAULT_CSS, DocGuard, DocGuardMut, Document, DocumentConfig, EventDriver, NodeId,
11};
12use blitz_html::stream::StreamingParser;
13use blitz_html::{DocumentHtmlParser, HtmlProvider};
14use blitz_traits::events::{BlitzPointerEvent, DomEvent, UiEvent};
15use url::Url;
16use web_time::{Duration, Instant};
17
18use crate::event_handler::ScriptEventHandler;
19use crate::fetch::{DefaultScriptFetcher, ScriptFetcher};
20use crate::module::SharedFetcher;
21use crate::runtime::ScriptRuntime;
22
23type PollHook =
24 Box<dyn for<'a> FnMut(&mut ScriptDocument, Option<&TaskContext<'a>>) -> bool + 'static>;
25
26#[derive(Clone, Copy, PartialEq, Eq)]
28enum ScriptKind {
29 Classic,
30 Module,
33 ImportMap,
36}
37
38struct PendingScript {
40 node_id: NodeId,
41 src: Option<String>,
42 inline_text: String,
43 kind: ScriptKind,
44 deferred: bool,
50}
51
52pub struct ScriptDocument {
61 inner: Rc<RefCell<BaseDocument>>,
62 runtime: ScriptRuntime,
63 base_url: Option<Url>,
64 fetcher: SharedFetcher,
71 scripts_executed: bool,
72 startup_events_pending: bool,
73 load_event_pending: bool,
74 executed_scripts: std::collections::HashSet<NodeId>,
81 poll_hook: Option<PollHook>,
82
83 waker: Arc<Mutex<Option<Waker>>>,
86 timer_thread: Option<Sender<Instant>>,
87}
88
89impl ScriptDocument {
90 pub fn from_html(html: &str, mut config: DocumentConfig) -> Self {
99 if let Some(ss) = &mut config.ua_stylesheets {
100 if !ss.iter().any(|s| s == DEFAULT_CSS) {
101 ss.push(String::from(DEFAULT_CSS));
102 }
103 } else {
104 config.ua_stylesheets = Some(vec![String::from(DEFAULT_CSS)]);
105 }
106 if config.html_parser_provider.is_none() {
107 config.html_parser_provider = Some(Arc::new(HtmlProvider));
108 }
109
110 let base_url = config
111 .base_url
112 .as_deref()
113 .and_then(|url| Url::parse(url).ok());
114
115 let is_xml = DocumentHtmlParser::is_xhtml_document(html);
116 let mut doc = BaseDocument::new(config);
117 if is_xml {
118 let mut mutr = doc.mutate();
119 DocumentHtmlParser::parse_into_mutator(&mut mutr, html);
120 drop(mutr);
121 }
122
123 let inner = Rc::new(RefCell::new(doc));
124 let parser = if is_xml {
125 None
126 } else {
127 let mut parser = StreamingParser::new(Rc::clone(&inner), html);
128 if parser.next_script().is_some() {
129 Some(parser)
130 } else {
131 parser.finish();
132 None
133 }
134 };
135 let fetcher: SharedFetcher = Rc::new(RefCell::new(Rc::new(DefaultScriptFetcher)));
136 let runtime = ScriptRuntime::new(Rc::clone(&inner), base_url.as_ref(), Rc::clone(&fetcher));
137 crate::docwrite::prepare(&runtime.context, html, Rc::clone(&fetcher), parser);
138
139 Self {
140 inner,
141 runtime,
142 base_url,
143 fetcher,
144 scripts_executed: false,
145 startup_events_pending: false,
146 load_event_pending: false,
147 executed_scripts: std::collections::HashSet::new(),
148 poll_hook: None,
149 waker: Arc::new(Mutex::new(None)),
150 timer_thread: None,
151 }
152 }
153
154 pub fn set_job_budget(&mut self, duration: Duration) {
159 self.runtime.set_job_budget(duration);
160 }
161
162 pub fn job_budget(&self) -> Duration {
164 self.runtime.job_budget()
165 }
166
167 pub fn has_pending_jobs(&self) -> bool {
171 self.runtime.jobs_remain()
172 }
173
174 pub fn pending_task_count(&self) -> u64 {
177 self.runtime.pending_task_count()
178 }
179
180 pub fn run_until_idle(&mut self) -> Result<(), &'static str> {
205 const MODULE_TIMEOUT: Duration = Duration::from_secs(60);
206 const LOAD_POLL_INTERVAL: Duration = Duration::from_millis(5);
207 let module_deadline = Instant::now() + MODULE_TIMEOUT;
208
209 loop {
210 let progressed = self.poll(None);
211 let now = Instant::now();
212 let next_timer = self.runtime.next_timer_deadline();
213 let loads_pending = self.runtime.has_pending_module_loads();
214 let evaluations_pending = self.runtime.has_pending_module_evaluations();
215
216 if now >= module_deadline {
219 if loads_pending {
220 return Err("module loading did not complete within sixty seconds");
221 }
222 if evaluations_pending {
223 return Err("module evaluation did not complete within sixty seconds");
224 }
225 }
226
227 if self.runtime.jobs_remain()
228 || next_timer.is_some_and(|deadline| deadline <= now)
229 || progressed
230 {
231 continue;
232 }
233
234 if loads_pending || evaluations_pending {
235 let wait = if loads_pending {
239 LOAD_POLL_INTERVAL
240 } else {
241 next_timer.map_or(LOAD_POLL_INTERVAL, |deadline| {
242 deadline.saturating_duration_since(now)
243 })
244 };
245 let wait = next_timer.map_or(wait, |deadline| {
246 wait.min(deadline.saturating_duration_since(now))
247 });
248 std::thread::sleep(wait.min(module_deadline.saturating_duration_since(now)));
249 continue;
250 }
251
252 if crate::docwrite::active(&self.runtime.context)
253 || self.startup_events_pending
254 || self.collect_scripts().iter().any(|script| {
255 !self.executed_scripts.contains(&script.node_id)
256 && !crate::docwrite::was_executed(&self.runtime.context, script.node_id)
257 })
258 {
259 return Err("script startup cannot make progress");
260 }
261
262 return Ok(());
263 }
264 }
265
266 pub fn next_timer_deadline(&self) -> Option<std::time::Instant> {
270 self.runtime.next_timer_deadline()
271 }
272
273 pub fn with_fetcher(self, fetcher: impl ScriptFetcher) -> Self {
276 *self.fetcher.borrow_mut() = Rc::new(fetcher);
277 self
278 }
279
280 pub fn set_ipc_handler(&mut self, handler: impl Fn(String) + 'static) {
285 self.runtime.ctx.state.borrow_mut().ipc_handler = Some(Rc::new(handler));
286 }
287
288 pub fn set_poll_hook(
292 &mut self,
293 hook: impl for<'a> FnMut(&mut ScriptDocument, Option<&TaskContext<'a>>) -> bool + 'static,
294 ) {
295 self.poll_hook = Some(Box::new(hook));
296 }
297
298 pub fn add_poll_hook(
301 &mut self,
302 mut hook: impl for<'a> FnMut(&mut ScriptDocument, Option<&TaskContext<'a>>) -> bool + 'static,
303 ) {
304 let Some(mut existing) = self.poll_hook.take() else {
305 self.poll_hook = Some(Box::new(hook));
306 return;
307 };
308 self.poll_hook = Some(Box::new(move |document, task_context| {
309 let ran_existing = existing(document, task_context);
310 let ran_added = hook(document, task_context);
311 ran_existing | ran_added
312 }));
313 }
314
315 pub fn execute_scripts(&mut self) {
320 let _profiling = self.runtime.ctx.enter_profiling_boundary();
321 let _slice = self.runtime.begin_job_slice();
322 if self.scripts_executed {
323 return;
324 }
325 self.scripts_executed = true;
326 self.startup_events_pending = true;
327
328 crate::docwrite::begin(&mut self.runtime.context);
329 loop {
330 let progressed = self.run_pending_scripts();
331 if !progressed
332 || self.runtime.jobs_remain()
333 || !crate::docwrite::active(&self.runtime.context)
334 {
335 break;
336 }
337 }
338 self.finish_startup_events();
339
340 self.request_redraw();
341 self.arm_timer_thread();
342 }
343
344 pub fn external_script_urls(&self) -> Vec<Url> {
352 if let Some(original) = crate::docwrite::original(&self.runtime.context) {
353 return blitz_html::scan_script_tags(&original)
356 .into_iter()
357 .filter(|script| script.is_javascript())
358 .filter_map(|script| script.src)
359 .filter_map(|src| self.resolve_script_url(src.as_ref()))
360 .collect();
361 }
362 self.collect_scripts()
363 .iter()
364 .filter(|script| script.kind != ScriptKind::ImportMap)
367 .filter_map(|script| script.src.as_deref())
368 .filter_map(|src| self.resolve_script_url(src))
369 .collect()
370 }
371
372 fn resolve_script_url(&self, src: &str) -> Option<Url> {
374 match &self.base_url {
375 Some(base) => base.join(src).ok(),
376 None => Url::parse(src).ok(),
377 }
378 }
379
380 pub fn with_js_context<R>(
386 &mut self,
387 callback: impl FnOnce(&mut boa_engine::Context) -> R,
388 ) -> R {
389 callback(&mut self.runtime.context)
390 }
391
392 pub fn eval(&mut self, code: &str) {
394 let _profiling = self.runtime.ctx.enter_profiling_boundary();
395 self.runtime.eval(code, "<eval>");
396 self.request_redraw();
397 self.arm_timer_thread();
398 }
399
400 pub fn eval_named(&mut self, code: &str, name: &str) {
403 let _profiling = self.runtime.ctx.enter_profiling_boundary();
404 self.runtime.eval_internal(code, name);
405 self.runtime.run_jobs(name);
406 self.request_redraw();
407 self.arm_timer_thread();
408 }
409
410 pub fn eval_json(&mut self, code: &str) -> Result<serde_json::Value, String> {
416 let _profiling = self.runtime.ctx.enter_profiling_boundary();
417 let result = self.runtime.eval_json(code, "<eval with result>");
418 self.request_redraw();
419 self.arm_timer_thread();
420 result
421 }
422
423 #[cfg(feature = "debug-control")]
424 pub(crate) fn console_entries_after(
425 &self,
426 sequence: u64,
427 ) -> Vec<crate::runtime::DiagnosticEntry> {
428 self.runtime.console_entries_after(sequence)
429 }
430
431 #[cfg(feature = "debug-control")]
432 pub(crate) fn runtime_errors_after(
433 &self,
434 sequence: u64,
435 ) -> Vec<crate::runtime::DiagnosticEntry> {
436 self.runtime.runtime_errors_after(sequence)
437 }
438
439 pub fn current_url(&self) -> Option<&Url> {
441 self.base_url.as_ref()
442 }
443
444 pub fn page_source(&self) -> String {
446 self.inner.borrow().root_element().outer_html()
447 }
448
449 pub fn dispatch_dom_event(&mut self, event: DomEvent) {
454 let profiling_boundary = self.runtime.ctx.enter_profiling_boundary();
455 let profiling = profiling_boundary.enabled();
456 let handler = ScriptEventHandler {
457 runtime: &mut self.runtime,
458 profiling,
459 };
460 let mut driver = EventDriver::new(&mut self.inner, handler);
461 driver.handle_dom_event(event);
462
463 self.request_redraw();
464 self.arm_timer_thread();
465 }
466
467 fn run_pending_scripts(&mut self) -> bool {
480 let _slice = self.runtime.begin_job_slice();
481 if self.runtime.jobs_remain() {
482 return false;
483 }
484 if crate::docwrite::take_restart(&self.runtime.context) {
485 self.executed_scripts.clear();
486 self.startup_events_pending = true;
487 self.load_event_pending = false;
488 }
489 let parsed = crate::docwrite::advance(&mut self.runtime.context);
490 let generation = crate::docwrite::generation(&self.runtime.context);
491 if parsed {
492 self.runtime.ctx.mark_layout_dirty();
493 }
494 let pending: Vec<PendingScript> = self
497 .collect_scripts()
498 .into_iter()
499 .filter(|script| !self.executed_scripts.contains(&script.node_id))
500 .filter(|script| {
501 !crate::docwrite::was_executed(&self.runtime.context, script.node_id)
502 && (!script.deferred || !crate::docwrite::active(&self.runtime.context))
503 })
504 .collect();
505 let ran = parsed || !pending.is_empty();
506
507 for script in &pending {
512 if script.kind == ScriptKind::ImportMap && !script.inline_text.trim().is_empty() {
513 self.runtime
514 .set_import_map(&script.inline_text, self.base_url.as_ref());
515 }
516 }
517
518 let (deferred, immediate): (Vec<_>, Vec<_>) =
529 pending.into_iter().partition(|script| script.deferred);
530
531 for script in immediate.into_iter().chain(deferred) {
532 if generation != crate::docwrite::generation(&self.runtime.context) {
533 break;
534 }
535 if self.startup_events_pending && script.deferred {
536 self.runtime.set_document_ready_state("interactive");
537 if self.runtime.jobs_remain() {
538 break;
539 }
540 }
541 self.executed_scripts.insert(script.node_id);
545 if script.kind == ScriptKind::ImportMap {
546 continue;
547 }
548 match script.src {
549 Some(src) => {
550 let Some(url) = self.resolve_script_url(&src) else {
551 eprintln!("blitz-script: could not resolve script URL {src:?}");
552 self.runtime.dispatch_node_event(script.node_id, "error");
553 if self.runtime.jobs_remain() {
554 break;
555 }
556 continue;
557 };
558 let fetcher = Rc::clone(&self.fetcher.borrow());
559 match fetcher.fetch(&url) {
560 Ok(code) => {
561 if script.kind == ScriptKind::Module {
562 self.runtime.eval_module(&code, Some(&url), url.as_str());
563 } else {
564 crate::domc::set_current_script(
565 &self.runtime.context,
566 Some(script.node_id),
567 );
568 self.runtime.eval_at(&code, Some(&url), url.as_str());
569 }
570 self.runtime.dispatch_node_event(script.node_id, "load");
571 }
572 Err(error) => {
573 eprintln!("blitz-script: failed to fetch script {url}: {error}");
574 self.runtime.dispatch_node_event(script.node_id, "error");
575 }
576 }
577 }
578 None => {
579 if !script.inline_text.trim().is_empty() {
580 let base_url = self.base_url.clone();
584 if script.kind == ScriptKind::Module {
585 self.runtime.eval_module(
586 &script.inline_text,
587 base_url.as_ref(),
588 "<inline module>",
589 );
590 } else {
591 crate::domc::set_current_script(
592 &self.runtime.context,
593 Some(script.node_id),
594 );
595 self.runtime.eval_at(
596 &script.inline_text,
597 base_url.as_ref(),
598 "<inline script>",
599 );
600 }
601 }
602 }
603 }
604 if self.runtime.jobs_remain() {
607 break;
608 }
609 }
610 ran
611 }
612
613 fn finish_startup_events(&mut self) -> bool {
614 if !(self.startup_events_pending || self.load_event_pending)
615 || self.runtime.jobs_remain()
616 || crate::docwrite::active(&self.runtime.context)
617 {
618 return false;
619 }
620 if self.startup_events_pending
621 && (self.runtime.has_pending_module_evaluations()
622 || self.collect_scripts().iter().any(|script| {
623 !self.executed_scripts.contains(&script.node_id)
624 && !crate::docwrite::was_executed(&self.runtime.context, script.node_id)
625 }))
626 {
627 return false;
628 }
629
630 let mut ran = false;
631 if self.startup_events_pending {
632 ran |= self.runtime.set_document_ready_state("interactive");
633 if self.runtime.jobs_remain() {
634 return ran;
635 }
636 self.startup_events_pending = false;
637 self.load_event_pending = true;
638 self.runtime.dispatch_document_event("DOMContentLoaded");
639 ran = true;
640 }
641 let resources_pending = {
642 let doc = self.inner.borrow();
643 doc.has_pending_critical_resources() || doc.pending_image_count() != 0
644 };
645 if self.load_event_pending
646 && !self.runtime.jobs_remain()
647 && !resources_pending
648 && !self.runtime.has_pending_module_evaluations()
649 {
650 ran |= self.runtime.set_document_ready_state("complete");
651 if !self.runtime.jobs_remain() {
652 self.load_event_pending = false;
653 self.runtime.dispatch_window_event("load");
654 ran = true;
655 }
656 }
657 ran
658 }
659
660 fn collect_scripts(&self) -> Vec<PendingScript> {
662 let doc = self.inner.borrow();
663 let mut scripts = Vec::new();
664 let mut stack = vec![doc.root_node().id];
665
666 while let Some(node_id) = stack.pop() {
667 let Some(node) = doc.get_node(node_id) else {
668 continue;
669 };
670
671 if let Some(element) = node.element_data() {
672 if element.name.local == blitz_dom::local_name!("script") {
673 if node
674 .flags
675 .contains(blitz_dom::node::NodeFlags::IS_PARSER_INERT_SCRIPT)
676 {
677 continue;
678 }
679 let script_type = element
681 .attr(blitz_dom::local_name!("type"))
682 .unwrap_or("")
683 .trim()
684 .to_ascii_lowercase();
685 let kind = match script_type.as_str() {
686 "module" => Some(ScriptKind::Module),
687 "importmap" => Some(ScriptKind::ImportMap),
688 "" | "text/javascript" | "application/javascript" => {
689 if element.attr(blitz_dom::local_name!("nomodule")).is_some() {
694 None
695 } else {
696 Some(ScriptKind::Classic)
697 }
698 }
699 _ => None,
700 };
701
702 if let Some(kind) = kind {
703 let src = element
704 .attr(blitz_dom::local_name!("src"))
705 .map(str::to_string);
706 let is_async = element.attr(blitz_dom::local_name!("async")).is_some();
707
708 let deferred = !is_async
712 && match kind {
713 ScriptKind::Module => true,
714 ScriptKind::Classic => {
715 src.is_some()
716 && element.attr(blitz_dom::local_name!("defer")).is_some()
717 }
718 ScriptKind::ImportMap => false,
719 };
720
721 scripts.push(PendingScript {
722 node_id,
723 src,
724 inline_text: node.text_content(),
725 kind,
726 deferred,
727 });
728 }
729 continue;
730 }
731 }
732
733 stack.extend(node.children.iter().rev().copied());
734 }
735
736 scripts
737 }
738
739 fn request_redraw(&self) {
740 self.inner.borrow().shell_provider.request_redraw();
741 }
742
743 fn arm_timer_thread(&mut self) {
746 let Some(deadline) = self.runtime.next_timer_deadline() else {
747 return;
748 };
749
750 let sender = self.timer_thread.get_or_insert_with(|| {
751 let (tx, rx) = channel::<Instant>();
752 let waker = Arc::clone(&self.waker);
753 std::thread::Builder::new()
754 .name("blitz-script-timers".to_string())
755 .spawn(move || timer_thread_main(rx, waker))
756 .expect("failed to spawn timer thread");
757 tx
758 });
759
760 if sender.send(deadline).is_err() {
763 self.timer_thread = None;
764 }
765 }
766}
767
768fn timer_thread_main(rx: Receiver<Instant>, waker: Arc<Mutex<Option<Waker>>>) {
770 let mut deadline: Option<Instant> = None;
771
772 loop {
773 match deadline {
774 None => match rx.recv() {
775 Ok(new_deadline) => deadline = Some(new_deadline),
776 Err(_) => return,
777 },
778 Some(current) => {
779 let now = Instant::now();
780 if current <= now {
781 if let Some(waker) = waker.lock().unwrap().as_ref() {
782 waker.wake_by_ref();
783 }
784 deadline = None;
785 continue;
786 }
787 match rx.recv_timeout(current - now) {
788 Ok(new_deadline) => deadline = Some(new_deadline.min(current)),
789 Err(RecvTimeoutError::Timeout) => {
790 if let Some(waker) = waker.lock().unwrap().as_ref() {
791 waker.wake_by_ref();
792 }
793 deadline = None;
794 }
795 Err(RecvTimeoutError::Disconnected) => return,
796 }
797 }
798 }
799 }
800}
801
802impl Document for ScriptDocument {
803 fn inner(&self) -> DocGuard<'_> {
804 DocGuard::RefCell(self.inner.borrow())
805 }
806
807 fn inner_mut(&mut self) -> DocGuardMut<'_> {
808 DocGuardMut::RefCell(self.inner.borrow_mut())
809 }
810
811 fn handle_ui_event(&mut self, event: UiEvent) {
812 let profiling_boundary = self.runtime.ctx.enter_profiling_boundary();
813 let profiling = profiling_boundary.enabled();
814 let handler = ScriptEventHandler {
815 runtime: &mut self.runtime,
816 profiling,
817 };
818 let mut driver = EventDriver::new(&mut self.inner, handler);
819 driver.handle_ui_event(event);
820
821 self.request_redraw();
823 self.arm_timer_thread();
824 }
825
826 fn poll(&mut self, task_context: Option<TaskContext>) -> bool {
827 self.poll_with_timer_policy(task_context, None)
828 }
829
830 fn poll_for_settle(&mut self, task_context: Option<TaskContext>, run_timers: bool) -> bool {
831 self.poll_with_timer_policy(task_context, Some(run_timers))
832 }
833}
834
835impl ScriptDocument {
836 fn poll_with_timer_policy(
838 &mut self,
839 task_context: Option<TaskContext>,
840 settle_timers: Option<bool>,
841 ) -> bool {
842 let profiling_boundary = self.runtime.ctx.enter_profiling_boundary();
843 let profiling = profiling_boundary.enabled();
844 let poll_started = profiling.then(std::time::Instant::now);
845 let _slice = self.runtime.begin_job_slice();
846 let ran = self.poll_inner(task_context, profiling, settle_timers);
847 if let Some(started) = poll_started {
848 crate::script_stats::record_poll(started.elapsed(), ran);
849 }
850 ran
851 }
852
853 pub fn handle_pointer_move_to_node(&mut self, event: BlitzPointerEvent, node_id: NodeId) {
860 let profiling_boundary = self.runtime.ctx.enter_profiling_boundary();
861 let profiling = profiling_boundary.enabled();
862 let handler = ScriptEventHandler {
863 runtime: &mut self.runtime,
864 profiling,
865 };
866 let mut driver = EventDriver::new(&mut self.inner, handler);
867 driver.handle_pointer_move_to_node(&event, node_id);
868
869 self.request_redraw();
870 self.arm_timer_thread();
871 }
872
873 pub fn handle_ui_event_to_node(&mut self, event: UiEvent, node_id: NodeId) {
877 let profiling_boundary = self.runtime.ctx.enter_profiling_boundary();
878 let profiling = profiling_boundary.enabled();
879 let handler = ScriptEventHandler {
880 runtime: &mut self.runtime,
881 profiling,
882 };
883 let mut driver = EventDriver::new(&mut self.inner, handler);
884 driver.handle_ui_event_to_node(event, node_id);
885
886 self.request_redraw();
887 self.arm_timer_thread();
888 }
889
890 fn poll_inner(
893 &mut self,
894 task_context: Option<TaskContext>,
895 profiling: bool,
896 settle_timers: Option<bool>,
897 ) -> bool {
898 if let Some(cx) = &task_context {
900 let mut waker = self.waker.lock().unwrap();
901 let stale = waker
902 .as_ref()
903 .map(|old| !old.will_wake(cx.waker()))
904 .unwrap_or(true);
905 if stale {
906 *waker = Some(cx.waker().clone());
907 }
908 }
909
910 let subdocument_changes = {
915 let mut inner = self.inner.borrow_mut();
916 let waker = task_context.as_ref().map(TaskContext::waker);
917 match settle_timers {
918 Some(run_timers) => inner.poll_subdocuments_for_settle(waker, run_timers),
919 None => inner.poll_subdocuments(waker),
920 }
921 };
922
923 let mut ran = subdocument_changes;
924 ran |= self.runtime.poll_domc();
925 ran |= self.runtime.run_jobs_with_progress("poll microtasks");
928
929 if !self.scripts_executed {
931 let started = profiling.then(std::time::Instant::now);
935 self.execute_scripts();
936 if let Some(started) = started {
937 crate::script_stats::record_work("startup:execute_scripts", started.elapsed());
938 }
939 ran = true;
940 } else {
941 ran |= self.run_pending_scripts();
945 }
946 ran |= self.finish_startup_events();
947
948 if settle_timers.unwrap_or(true) {
949 ran |= self.runtime.run_due_timers(profiling);
950 }
951
952 self.runtime.poll_module_evaluations();
956
957 if let Some(mut hook) = self.poll_hook.take() {
958 let started = profiling.then(std::time::Instant::now);
962 ran |= hook(self, task_context.as_ref());
963 if let Some(started) = started {
964 crate::script_stats::record_work("poll_hook", started.elapsed());
965 }
966 self.poll_hook = Some(hook);
967 }
968
969 self.runtime.sweep_detached_nodes();
974
975 if self.runtime.jobs_remain() {
976 ran = true;
977 self.request_redraw();
978 if let Some(cx) = &task_context {
979 cx.waker().wake_by_ref();
980 }
981 }
982 self.arm_timer_thread();
983 ran
984 }
985}