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 run_until_idle(&mut self) -> Result<(), &'static str> {
199 const MODULE_TIMEOUT: Duration = Duration::from_secs(60);
200 const LOAD_POLL_INTERVAL: Duration = Duration::from_millis(5);
201 let module_deadline = Instant::now() + MODULE_TIMEOUT;
202
203 loop {
204 let progressed = self.poll(None);
205 let now = Instant::now();
206 let next_timer = self.runtime.next_timer_deadline();
207 let loads_pending = self.runtime.has_pending_module_loads();
208 let evaluations_pending = self.runtime.has_pending_module_evaluations();
209
210 if now >= module_deadline {
213 if loads_pending {
214 return Err("module loading did not complete within sixty seconds");
215 }
216 if evaluations_pending {
217 return Err("module evaluation did not complete within sixty seconds");
218 }
219 }
220
221 if self.runtime.jobs_remain()
222 || next_timer.is_some_and(|deadline| deadline <= now)
223 || progressed
224 {
225 continue;
226 }
227
228 if loads_pending || evaluations_pending {
229 let wait = if loads_pending {
233 LOAD_POLL_INTERVAL
234 } else {
235 next_timer.map_or(LOAD_POLL_INTERVAL, |deadline| {
236 deadline.saturating_duration_since(now)
237 })
238 };
239 let wait = next_timer.map_or(wait, |deadline| {
240 wait.min(deadline.saturating_duration_since(now))
241 });
242 std::thread::sleep(wait.min(module_deadline.saturating_duration_since(now)));
243 continue;
244 }
245
246 if crate::docwrite::active(&self.runtime.context)
247 || self.startup_events_pending
248 || self.collect_scripts().iter().any(|script| {
249 !self.executed_scripts.contains(&script.node_id)
250 && !crate::docwrite::was_executed(&self.runtime.context, script.node_id)
251 })
252 {
253 return Err("script startup cannot make progress");
254 }
255
256 return Ok(());
257 }
258 }
259
260 pub fn next_timer_deadline(&self) -> Option<std::time::Instant> {
264 self.runtime.next_timer_deadline()
265 }
266
267 pub fn with_fetcher(self, fetcher: impl ScriptFetcher) -> Self {
270 *self.fetcher.borrow_mut() = Rc::new(fetcher);
271 self
272 }
273
274 pub fn set_ipc_handler(&mut self, handler: impl Fn(String) + 'static) {
279 self.runtime.ctx.state.borrow_mut().ipc_handler = Some(Rc::new(handler));
280 }
281
282 pub fn set_poll_hook(
286 &mut self,
287 hook: impl for<'a> FnMut(&mut ScriptDocument, Option<&TaskContext<'a>>) -> bool + 'static,
288 ) {
289 self.poll_hook = Some(Box::new(hook));
290 }
291
292 pub fn add_poll_hook(
295 &mut self,
296 mut hook: impl for<'a> FnMut(&mut ScriptDocument, Option<&TaskContext<'a>>) -> bool + 'static,
297 ) {
298 let Some(mut existing) = self.poll_hook.take() else {
299 self.poll_hook = Some(Box::new(hook));
300 return;
301 };
302 self.poll_hook = Some(Box::new(move |document, task_context| {
303 let ran_existing = existing(document, task_context);
304 let ran_added = hook(document, task_context);
305 ran_existing | ran_added
306 }));
307 }
308
309 pub fn execute_scripts(&mut self) {
314 let _profiling = self.runtime.ctx.enter_profiling_boundary();
315 let _slice = self.runtime.begin_job_slice();
316 if self.scripts_executed {
317 return;
318 }
319 self.scripts_executed = true;
320 self.startup_events_pending = true;
321
322 crate::docwrite::begin(&mut self.runtime.context);
323 loop {
324 let progressed = self.run_pending_scripts();
325 if !progressed
326 || self.runtime.jobs_remain()
327 || !crate::docwrite::active(&self.runtime.context)
328 {
329 break;
330 }
331 }
332 self.finish_startup_events();
333
334 self.request_redraw();
335 self.arm_timer_thread();
336 }
337
338 pub fn external_script_urls(&self) -> Vec<Url> {
346 if let Some(original) = crate::docwrite::original(&self.runtime.context) {
347 return blitz_html::scan_script_tags(&original)
350 .into_iter()
351 .filter(|script| script.is_javascript())
352 .filter_map(|script| script.src)
353 .filter_map(|src| self.resolve_script_url(src.as_ref()))
354 .collect();
355 }
356 self.collect_scripts()
357 .iter()
358 .filter(|script| script.kind != ScriptKind::ImportMap)
361 .filter_map(|script| script.src.as_deref())
362 .filter_map(|src| self.resolve_script_url(src))
363 .collect()
364 }
365
366 fn resolve_script_url(&self, src: &str) -> Option<Url> {
368 match &self.base_url {
369 Some(base) => base.join(src).ok(),
370 None => Url::parse(src).ok(),
371 }
372 }
373
374 pub fn with_js_context<R>(
380 &mut self,
381 callback: impl FnOnce(&mut boa_engine::Context) -> R,
382 ) -> R {
383 callback(&mut self.runtime.context)
384 }
385
386 pub fn eval(&mut self, code: &str) {
388 let _profiling = self.runtime.ctx.enter_profiling_boundary();
389 self.runtime.eval(code, "<eval>");
390 self.request_redraw();
391 self.arm_timer_thread();
392 }
393
394 pub fn eval_named(&mut self, code: &str, name: &str) {
397 let _profiling = self.runtime.ctx.enter_profiling_boundary();
398 self.runtime.eval_internal(code, name);
399 self.runtime.run_jobs(name);
400 self.request_redraw();
401 self.arm_timer_thread();
402 }
403
404 pub fn eval_json(&mut self, code: &str) -> Result<serde_json::Value, String> {
410 let _profiling = self.runtime.ctx.enter_profiling_boundary();
411 let result = self.runtime.eval_json(code, "<eval with result>");
412 self.request_redraw();
413 self.arm_timer_thread();
414 result
415 }
416
417 #[cfg(feature = "debug-control")]
418 pub(crate) fn console_entries_after(
419 &self,
420 sequence: u64,
421 ) -> Vec<crate::runtime::DiagnosticEntry> {
422 self.runtime.console_entries_after(sequence)
423 }
424
425 #[cfg(feature = "debug-control")]
426 pub(crate) fn runtime_errors_after(
427 &self,
428 sequence: u64,
429 ) -> Vec<crate::runtime::DiagnosticEntry> {
430 self.runtime.runtime_errors_after(sequence)
431 }
432
433 pub fn current_url(&self) -> Option<&Url> {
435 self.base_url.as_ref()
436 }
437
438 pub fn page_source(&self) -> String {
440 self.inner.borrow().root_element().outer_html()
441 }
442
443 pub fn dispatch_dom_event(&mut self, event: DomEvent) {
448 let profiling_boundary = self.runtime.ctx.enter_profiling_boundary();
449 let profiling = profiling_boundary.enabled();
450 let handler = ScriptEventHandler {
451 runtime: &mut self.runtime,
452 profiling,
453 };
454 let mut driver = EventDriver::new(&mut self.inner, handler);
455 driver.handle_dom_event(event);
456
457 self.request_redraw();
458 self.arm_timer_thread();
459 }
460
461 fn run_pending_scripts(&mut self) -> bool {
474 let _slice = self.runtime.begin_job_slice();
475 if self.runtime.jobs_remain() {
476 return false;
477 }
478 if crate::docwrite::take_restart(&self.runtime.context) {
479 self.executed_scripts.clear();
480 self.startup_events_pending = true;
481 self.load_event_pending = false;
482 }
483 let parsed = crate::docwrite::advance(&mut self.runtime.context);
484 let generation = crate::docwrite::generation(&self.runtime.context);
485 if parsed {
486 self.runtime.ctx.mark_layout_dirty();
487 }
488 let pending: Vec<PendingScript> = self
491 .collect_scripts()
492 .into_iter()
493 .filter(|script| !self.executed_scripts.contains(&script.node_id))
494 .filter(|script| {
495 !crate::docwrite::was_executed(&self.runtime.context, script.node_id)
496 && (!script.deferred || !crate::docwrite::active(&self.runtime.context))
497 })
498 .collect();
499 let ran = parsed || !pending.is_empty();
500
501 for script in &pending {
506 if script.kind == ScriptKind::ImportMap && !script.inline_text.trim().is_empty() {
507 self.runtime
508 .set_import_map(&script.inline_text, self.base_url.as_ref());
509 }
510 }
511
512 let (deferred, immediate): (Vec<_>, Vec<_>) =
523 pending.into_iter().partition(|script| script.deferred);
524
525 for script in immediate.into_iter().chain(deferred) {
526 if generation != crate::docwrite::generation(&self.runtime.context) {
527 break;
528 }
529 if self.startup_events_pending && script.deferred {
530 self.runtime.set_document_ready_state("interactive");
531 if self.runtime.jobs_remain() {
532 break;
533 }
534 }
535 self.executed_scripts.insert(script.node_id);
539 if script.kind == ScriptKind::ImportMap {
540 continue;
541 }
542 match script.src {
543 Some(src) => {
544 let Some(url) = self.resolve_script_url(&src) else {
545 eprintln!("blitz-script: could not resolve script URL {src:?}");
546 self.runtime.dispatch_node_event(script.node_id, "error");
547 if self.runtime.jobs_remain() {
548 break;
549 }
550 continue;
551 };
552 let fetcher = Rc::clone(&self.fetcher.borrow());
553 match fetcher.fetch(&url) {
554 Ok(code) => {
555 if script.kind == ScriptKind::Module {
556 self.runtime.eval_module(&code, Some(&url), url.as_str());
557 } else {
558 crate::domc::set_current_script(
559 &self.runtime.context,
560 Some(script.node_id),
561 );
562 self.runtime.eval_at(&code, Some(&url), url.as_str());
563 }
564 self.runtime.dispatch_node_event(script.node_id, "load");
565 }
566 Err(error) => {
567 eprintln!("blitz-script: failed to fetch script {url}: {error}");
568 self.runtime.dispatch_node_event(script.node_id, "error");
569 }
570 }
571 }
572 None => {
573 if !script.inline_text.trim().is_empty() {
574 let base_url = self.base_url.clone();
578 if script.kind == ScriptKind::Module {
579 self.runtime.eval_module(
580 &script.inline_text,
581 base_url.as_ref(),
582 "<inline module>",
583 );
584 } else {
585 crate::domc::set_current_script(
586 &self.runtime.context,
587 Some(script.node_id),
588 );
589 self.runtime.eval_at(
590 &script.inline_text,
591 base_url.as_ref(),
592 "<inline script>",
593 );
594 }
595 }
596 }
597 }
598 if self.runtime.jobs_remain() {
601 break;
602 }
603 }
604 ran
605 }
606
607 fn finish_startup_events(&mut self) -> bool {
608 if !(self.startup_events_pending || self.load_event_pending)
609 || self.runtime.jobs_remain()
610 || crate::docwrite::active(&self.runtime.context)
611 {
612 return false;
613 }
614 if self.startup_events_pending
615 && (self.runtime.has_pending_module_evaluations()
616 || self.collect_scripts().iter().any(|script| {
617 !self.executed_scripts.contains(&script.node_id)
618 && !crate::docwrite::was_executed(&self.runtime.context, script.node_id)
619 }))
620 {
621 return false;
622 }
623
624 let mut ran = false;
625 if self.startup_events_pending {
626 ran |= self.runtime.set_document_ready_state("interactive");
627 if self.runtime.jobs_remain() {
628 return ran;
629 }
630 self.startup_events_pending = false;
631 self.load_event_pending = true;
632 self.runtime.dispatch_document_event("DOMContentLoaded");
633 ran = true;
634 }
635 let resources_pending = {
636 let doc = self.inner.borrow();
637 doc.has_pending_critical_resources() || doc.pending_image_count() != 0
638 };
639 if self.load_event_pending
640 && !self.runtime.jobs_remain()
641 && !resources_pending
642 && !self.runtime.has_pending_module_evaluations()
643 {
644 ran |= self.runtime.set_document_ready_state("complete");
645 if !self.runtime.jobs_remain() {
646 self.load_event_pending = false;
647 self.runtime.dispatch_window_event("load");
648 ran = true;
649 }
650 }
651 ran
652 }
653
654 fn collect_scripts(&self) -> Vec<PendingScript> {
656 let doc = self.inner.borrow();
657 let mut scripts = Vec::new();
658 let mut stack = vec![doc.root_node().id];
659
660 while let Some(node_id) = stack.pop() {
661 let Some(node) = doc.get_node(node_id) else {
662 continue;
663 };
664
665 if let Some(element) = node.element_data() {
666 if element.name.local == blitz_dom::local_name!("script") {
667 if node
668 .flags
669 .contains(blitz_dom::node::NodeFlags::IS_PARSER_INERT_SCRIPT)
670 {
671 continue;
672 }
673 let script_type = element
675 .attr(blitz_dom::local_name!("type"))
676 .unwrap_or("")
677 .trim()
678 .to_ascii_lowercase();
679 let kind = match script_type.as_str() {
680 "module" => Some(ScriptKind::Module),
681 "importmap" => Some(ScriptKind::ImportMap),
682 "" | "text/javascript" | "application/javascript" => {
683 if element.attr(blitz_dom::local_name!("nomodule")).is_some() {
688 None
689 } else {
690 Some(ScriptKind::Classic)
691 }
692 }
693 _ => None,
694 };
695
696 if let Some(kind) = kind {
697 let src = element
698 .attr(blitz_dom::local_name!("src"))
699 .map(str::to_string);
700 let is_async = element.attr(blitz_dom::local_name!("async")).is_some();
701
702 let deferred = !is_async
706 && match kind {
707 ScriptKind::Module => true,
708 ScriptKind::Classic => {
709 src.is_some()
710 && element.attr(blitz_dom::local_name!("defer")).is_some()
711 }
712 ScriptKind::ImportMap => false,
713 };
714
715 scripts.push(PendingScript {
716 node_id,
717 src,
718 inline_text: node.text_content(),
719 kind,
720 deferred,
721 });
722 }
723 continue;
724 }
725 }
726
727 stack.extend(node.children.iter().rev().copied());
728 }
729
730 scripts
731 }
732
733 fn request_redraw(&self) {
734 self.inner.borrow().shell_provider.request_redraw();
735 }
736
737 fn arm_timer_thread(&mut self) {
740 let Some(deadline) = self.runtime.next_timer_deadline() else {
741 return;
742 };
743
744 let sender = self.timer_thread.get_or_insert_with(|| {
745 let (tx, rx) = channel::<Instant>();
746 let waker = Arc::clone(&self.waker);
747 std::thread::Builder::new()
748 .name("blitz-script-timers".to_string())
749 .spawn(move || timer_thread_main(rx, waker))
750 .expect("failed to spawn timer thread");
751 tx
752 });
753
754 if sender.send(deadline).is_err() {
757 self.timer_thread = None;
758 }
759 }
760}
761
762fn timer_thread_main(rx: Receiver<Instant>, waker: Arc<Mutex<Option<Waker>>>) {
764 let mut deadline: Option<Instant> = None;
765
766 loop {
767 match deadline {
768 None => match rx.recv() {
769 Ok(new_deadline) => deadline = Some(new_deadline),
770 Err(_) => return,
771 },
772 Some(current) => {
773 let now = Instant::now();
774 if current <= now {
775 if let Some(waker) = waker.lock().unwrap().as_ref() {
776 waker.wake_by_ref();
777 }
778 deadline = None;
779 continue;
780 }
781 match rx.recv_timeout(current - now) {
782 Ok(new_deadline) => deadline = Some(new_deadline.min(current)),
783 Err(RecvTimeoutError::Timeout) => {
784 if let Some(waker) = waker.lock().unwrap().as_ref() {
785 waker.wake_by_ref();
786 }
787 deadline = None;
788 }
789 Err(RecvTimeoutError::Disconnected) => return,
790 }
791 }
792 }
793 }
794}
795
796impl Document for ScriptDocument {
797 fn inner(&self) -> DocGuard<'_> {
798 DocGuard::RefCell(self.inner.borrow())
799 }
800
801 fn inner_mut(&mut self) -> DocGuardMut<'_> {
802 DocGuardMut::RefCell(self.inner.borrow_mut())
803 }
804
805 fn handle_ui_event(&mut self, event: UiEvent) {
806 let profiling_boundary = self.runtime.ctx.enter_profiling_boundary();
807 let profiling = profiling_boundary.enabled();
808 let handler = ScriptEventHandler {
809 runtime: &mut self.runtime,
810 profiling,
811 };
812 let mut driver = EventDriver::new(&mut self.inner, handler);
813 driver.handle_ui_event(event);
814
815 self.request_redraw();
817 self.arm_timer_thread();
818 }
819
820 fn poll(&mut self, task_context: Option<TaskContext>) -> bool {
821 self.poll_with_timer_policy(task_context, None)
822 }
823
824 fn poll_for_settle(&mut self, task_context: Option<TaskContext>, run_timers: bool) -> bool {
825 self.poll_with_timer_policy(task_context, Some(run_timers))
826 }
827}
828
829impl ScriptDocument {
830 fn poll_with_timer_policy(
832 &mut self,
833 task_context: Option<TaskContext>,
834 settle_timers: Option<bool>,
835 ) -> bool {
836 let profiling_boundary = self.runtime.ctx.enter_profiling_boundary();
837 let profiling = profiling_boundary.enabled();
838 let poll_started = profiling.then(std::time::Instant::now);
839 let _slice = self.runtime.begin_job_slice();
840 let ran = self.poll_inner(task_context, profiling, settle_timers);
841 if let Some(started) = poll_started {
842 crate::script_stats::record_poll(started.elapsed(), ran);
843 }
844 ran
845 }
846
847 pub fn handle_pointer_move_to_node(&mut self, event: BlitzPointerEvent, node_id: NodeId) {
854 let profiling_boundary = self.runtime.ctx.enter_profiling_boundary();
855 let profiling = profiling_boundary.enabled();
856 let handler = ScriptEventHandler {
857 runtime: &mut self.runtime,
858 profiling,
859 };
860 let mut driver = EventDriver::new(&mut self.inner, handler);
861 driver.handle_pointer_move_to_node(&event, node_id);
862
863 self.request_redraw();
864 self.arm_timer_thread();
865 }
866
867 pub fn handle_ui_event_to_node(&mut self, event: UiEvent, node_id: NodeId) {
871 let profiling_boundary = self.runtime.ctx.enter_profiling_boundary();
872 let profiling = profiling_boundary.enabled();
873 let handler = ScriptEventHandler {
874 runtime: &mut self.runtime,
875 profiling,
876 };
877 let mut driver = EventDriver::new(&mut self.inner, handler);
878 driver.handle_ui_event_to_node(event, node_id);
879
880 self.request_redraw();
881 self.arm_timer_thread();
882 }
883
884 fn poll_inner(
887 &mut self,
888 task_context: Option<TaskContext>,
889 profiling: bool,
890 settle_timers: Option<bool>,
891 ) -> bool {
892 if let Some(cx) = &task_context {
894 let mut waker = self.waker.lock().unwrap();
895 let stale = waker
896 .as_ref()
897 .map(|old| !old.will_wake(cx.waker()))
898 .unwrap_or(true);
899 if stale {
900 *waker = Some(cx.waker().clone());
901 }
902 }
903
904 let subdocument_changes = {
909 let mut inner = self.inner.borrow_mut();
910 let waker = task_context.as_ref().map(TaskContext::waker);
911 match settle_timers {
912 Some(run_timers) => inner.poll_subdocuments_for_settle(waker, run_timers),
913 None => inner.poll_subdocuments(waker),
914 }
915 };
916
917 let mut ran = subdocument_changes;
918 ran |= self.runtime.poll_domc();
919 ran |= self.runtime.run_jobs_with_progress("poll microtasks");
922
923 if !self.scripts_executed {
925 let started = profiling.then(std::time::Instant::now);
929 self.execute_scripts();
930 if let Some(started) = started {
931 crate::script_stats::record_work("startup:execute_scripts", started.elapsed());
932 }
933 ran = true;
934 } else {
935 ran |= self.run_pending_scripts();
939 }
940 ran |= self.finish_startup_events();
941
942 if settle_timers.unwrap_or(true) {
943 ran |= self.runtime.run_due_timers(profiling);
944 }
945
946 self.runtime.poll_module_evaluations();
950
951 if let Some(mut hook) = self.poll_hook.take() {
952 let started = profiling.then(std::time::Instant::now);
956 ran |= hook(self, task_context.as_ref());
957 if let Some(started) = started {
958 crate::script_stats::record_work("poll_hook", started.elapsed());
959 }
960 self.poll_hook = Some(hook);
961 }
962
963 self.runtime.sweep_detached_nodes();
968
969 if self.runtime.jobs_remain() {
970 ran = true;
971 self.request_redraw();
972 if let Some(cx) = &task_context {
973 cx.waker().wake_by_ref();
974 }
975 }
976 self.arm_timer_thread();
977 ran
978 }
979}