1use std::cell::RefCell;
4use std::future::Future;
5use std::pin::Pin;
6use std::rc::Rc;
7use std::sync::Arc;
8use std::task::{Context as TaskContext, Poll, Waker};
9
10use blitz_dom::{
11 BaseDocument, DEFAULT_CSS, DocGuard, DocGuardMut, Document, DocumentConfig, EventDriver, NodeId,
12};
13use blitz_html::stream::StreamingParser;
14use blitz_html::{DocumentHtmlParser, HtmlProvider};
15use blitz_traits::events::{BlitzPointerEvent, DomEvent, UiEvent};
16use url::Url;
17use web_time::{Duration, Instant};
18
19use crate::event_handler::ScriptEventHandler;
20use crate::fetch::{DefaultScriptFetcher, ScriptFetcher};
21use crate::module::SharedFetcher;
22use crate::runtime::{BoaScriptEngine, ScriptRuntime};
23
24type PollHook =
25 Box<dyn for<'a> FnMut(&mut ScriptDocument, Option<&TaskContext<'a>>) -> bool + 'static>;
26
27#[derive(Clone, Copy, PartialEq, Eq)]
29enum ScriptKind {
30 Classic,
31 Module,
34 ImportMap,
37}
38
39struct PendingScript {
41 node_id: NodeId,
42 src: Option<String>,
43 inline_text: String,
44 kind: ScriptKind,
45 deferred: bool,
51}
52
53type BeforeScriptHook = Box<dyn FnMut(&mut BaseDocument)>;
54
55pub struct ScriptDocument {
64 inner: Rc<RefCell<BaseDocument>>,
65 runtime: ScriptRuntime,
66 base_url: Option<Url>,
67 fetcher: SharedFetcher,
74 scripts_executed: bool,
75 startup_events_pending: bool,
76 load_event_pending: bool,
77 executed_scripts: std::collections::HashSet<NodeId>,
84 poll_hook: Option<PollHook>,
85 teardown_hooks: Vec<Box<dyn FnOnce()>>,
86 before_script_hook: Option<BeforeScriptHook>,
87 _runtime_lease: blitz_dom::release::ReleaseLease,
88
89 waker: Option<Waker>,
91 timer_wake: Option<(Instant, nagoya::time::Sleep)>,
92}
93
94impl ScriptDocument {
95 pub fn from_html(html: &str, mut config: DocumentConfig) -> Self {
104 if let Some(ss) = &mut config.ua_stylesheets {
105 if !ss.iter().any(|s| s == DEFAULT_CSS) {
106 ss.push(String::from(DEFAULT_CSS));
107 }
108 } else {
109 config.ua_stylesheets = Some(vec![String::from(DEFAULT_CSS)]);
110 }
111 if config.html_parser_provider.is_none() {
112 config.html_parser_provider = Some(Arc::new(HtmlProvider));
113 }
114
115 let base_url = config
116 .base_url
117 .as_deref()
118 .and_then(|url| Url::parse(url).ok());
119
120 let is_xml = DocumentHtmlParser::is_xhtml_document(html);
121 let mut doc = BaseDocument::new(config);
122 if is_xml {
123 let mut mutr = doc.mutate();
124 DocumentHtmlParser::parse_into_mutator(&mut mutr, html);
125 drop(mutr);
126 }
127
128 let inner = Rc::new(RefCell::new(doc));
129 let parser = if is_xml {
130 None
131 } else {
132 let mut parser = StreamingParser::new(Rc::clone(&inner), html);
133 if parser.next_script().is_some() {
134 Some(parser)
135 } else {
136 parser.finish();
137 None
138 }
139 };
140 let fetcher: SharedFetcher = Rc::new(RefCell::new(Rc::new(DefaultScriptFetcher)));
141 let runtime = ScriptRuntime::new(BoaScriptEngine::new(
142 Rc::clone(&inner),
143 base_url.as_ref(),
144 Rc::clone(&fetcher),
145 ));
146 crate::docwrite::prepare(&runtime.context, html, Rc::clone(&fetcher), parser);
147
148 let runtime_lease = inner.borrow().teardown.lease();
149 Self {
150 inner,
151 runtime,
152 teardown_hooks: Vec::new(),
153 before_script_hook: None,
154 _runtime_lease: runtime_lease,
155 base_url,
156 fetcher,
157 scripts_executed: false,
158 startup_events_pending: false,
159 load_event_pending: false,
160 executed_scripts: std::collections::HashSet::new(),
161 poll_hook: None,
162 waker: None,
163 timer_wake: None,
164 }
165 }
166
167 pub fn set_before_script_hook(&mut self, hook: impl FnMut(&mut BaseDocument) + 'static) {
168 assert!(!self.scripts_executed, "restore precedes bootstrap");
169 self.before_script_hook = Some(Box::new(hook));
170 }
171
172 fn prepare_native_restore(&mut self) {
173 if let Some(mut hook) = self.before_script_hook.take() {
174 hook(&mut self.inner.borrow_mut());
175 self.before_script_hook = Some(hook);
176 }
177 }
178
179 pub fn add_teardown_hook(&mut self, hook: impl FnOnce() + 'static) {
181 self.teardown_hooks.push(Box::new(hook));
182 }
183
184 pub fn teardown_receipt(&self) -> Arc<blitz_dom::release::Teardown> {
185 Arc::clone(&self.inner.borrow().teardown)
186 }
187
188 pub fn set_was_discarded(&mut self, discarded: bool) {
189 assert!(
190 !self.scripts_executed,
191 "wasDiscarded must precede page bootstrap"
192 );
193 self.inner.borrow_mut().was_discarded = discarded;
194 }
195
196 fn cancel_host_work(&mut self) {
197 self.timer_wake.take();
198 self.before_script_hook.take();
199 self.waker.take();
200 for hook in std::mem::take(&mut self.teardown_hooks) {
201 hook();
202 }
203 self.poll_hook.take();
204 self.runtime.retire();
205 }
206
207 pub fn set_job_budget(&mut self, duration: Duration) {
213 self.runtime.set_job_budget(duration);
214 }
215
216 pub fn job_budget(&self) -> Duration {
218 self.runtime.job_budget()
219 }
220
221 pub fn has_pending_jobs(&self) -> bool {
225 self.runtime.jobs_remain()
226 }
227
228 pub fn pending_task_count(&self) -> u64 {
231 self.runtime.pending_task_count()
232 }
233
234 #[cfg(feature = "debug-control")]
236 pub(crate) fn wait_with<T>(
237 &mut self,
238 mut wait: impl FnMut(&mut Self, &mut TaskContext<'_>) -> Poll<T>,
239 ) -> T {
240 let host_waker = self.waker.clone();
241 let result = nagoya::block_on(std::future::poll_fn(|cx| {
242 self.poll(Some(TaskContext::from_waker(cx.waker())));
243 wait(self, cx)
244 }));
245 self.waker = host_waker;
246 self.arm_timer_wake();
247 result
248 }
249
250 pub fn run_until_idle(&mut self) -> Result<(), &'static str> {
275 const MODULE_TIMEOUT: Duration = Duration::from_secs(60);
276 let module_deadline = Instant::now() + MODULE_TIMEOUT;
277 let mut module_timeout = nagoya::time::sleep(MODULE_TIMEOUT);
278 let host_waker = self.waker.clone();
279
280 let result = nagoya::block_on(std::future::poll_fn(|cx| {
281 let progressed = self.poll(Some(TaskContext::from_waker(cx.waker())));
282 let now = Instant::now();
283 let next_timer = self.runtime.next_timer_deadline();
284 let loads_pending = self.runtime.has_pending_module_loads();
285 let evaluations_pending = self.runtime.has_pending_module_evaluations();
286
287 if now >= module_deadline {
289 if loads_pending {
290 return Poll::Ready(Err(
291 "module loading did not complete within sixty seconds",
292 ));
293 }
294 if evaluations_pending {
295 return Poll::Ready(Err(
296 "module evaluation did not complete within sixty seconds",
297 ));
298 }
299 }
300
301 if self.runtime.jobs_remain()
302 || next_timer.is_some_and(|deadline| deadline <= now)
303 || progressed
304 {
305 cx.waker().wake_by_ref();
306 return Poll::Pending;
307 }
308
309 if loads_pending || evaluations_pending {
310 let _ = Pin::new(&mut module_timeout).poll(cx);
313 return Poll::Pending;
314 }
315
316 if crate::docwrite::active(&self.runtime.context)
317 || self.startup_events_pending
318 || self.collect_scripts().iter().any(|script| {
319 !self.executed_scripts.contains(&script.node_id)
320 && !crate::docwrite::was_executed(&self.runtime.context, script.node_id)
321 })
322 {
323 return Poll::Ready(Err("script startup cannot make progress"));
324 }
325
326 Poll::Ready(Ok(()))
327 }));
328 self.waker = host_waker;
329 self.arm_timer_wake();
330 result
331 }
332
333 pub fn next_timer_deadline(&self) -> Option<std::time::Instant> {
337 self.runtime.next_timer_deadline()
338 }
339
340 pub fn enter_execution_time_limit(&self) -> boa_engine::vm::ExecutionScope {
345 self.runtime.context.enter_execution_time_limit()
346 }
347 pub fn with_fetcher(self, fetcher: impl ScriptFetcher) -> Self {
350 *self.fetcher.borrow_mut() = Rc::new(fetcher);
351 self
352 }
353
354 pub fn set_ipc_handler(&mut self, handler: impl Fn(String) + 'static) {
359 self.runtime.ctx.state.borrow_mut().ipc_handler = Some(Rc::new(handler));
360 }
361
362 pub fn set_poll_hook(
366 &mut self,
367 hook: impl for<'a> FnMut(&mut ScriptDocument, Option<&TaskContext<'a>>) -> bool + 'static,
368 ) {
369 self.poll_hook = Some(Box::new(hook));
370 }
371
372 pub fn add_poll_hook(
375 &mut self,
376 mut hook: impl for<'a> FnMut(&mut ScriptDocument, Option<&TaskContext<'a>>) -> bool + 'static,
377 ) {
378 let Some(mut existing) = self.poll_hook.take() else {
379 self.poll_hook = Some(Box::new(hook));
380 return;
381 };
382 self.poll_hook = Some(Box::new(move |document, task_context| {
383 let ran_existing = existing(document, task_context);
384 let ran_added = hook(document, task_context);
385 ran_existing | ran_added
386 }));
387 }
388
389 pub fn execute_scripts(&mut self) {
394 let _execution = self.enter_execution_time_limit();
395 let _profiling = self.runtime.ctx.enter_profiling_boundary();
396 let _slice = self.runtime.begin_job_slice();
397 if self.scripts_executed {
398 return;
399 }
400 self.scripts_executed = true;
401 self.startup_events_pending = true;
402
403 crate::docwrite::begin(&mut self.runtime.context);
404 self.resume_parser_and_scripts();
405 self.finish_startup_events();
406
407 self.request_redraw();
408 self.arm_timer_wake();
409 }
410
411 pub fn external_script_urls(&self) -> Vec<Url> {
419 if let Some(original) = crate::docwrite::original(&self.runtime.context) {
420 return blitz_html::scan_script_tags(&original)
423 .into_iter()
424 .filter(|script| script.is_javascript())
425 .filter_map(|script| script.src)
426 .filter_map(|src| self.resolve_script_url(src.as_ref()))
427 .collect();
428 }
429 self.collect_scripts()
430 .iter()
431 .filter(|script| script.kind != ScriptKind::ImportMap)
434 .filter_map(|script| script.src.as_deref())
435 .filter_map(|src| self.resolve_script_url(src))
436 .collect()
437 }
438
439 fn resolve_script_url(&self, src: &str) -> Option<Url> {
441 match &self.base_url {
442 Some(base) => base.join(src).ok(),
443 None => Url::parse(src).ok(),
444 }
445 }
446
447 pub fn with_js_context<R>(
453 &mut self,
454 callback: impl FnOnce(&mut boa_engine::Context) -> R,
455 ) -> R {
456 let _execution = self.enter_execution_time_limit();
457 callback(&mut self.runtime.context)
458 }
459
460 pub fn eval(&mut self, code: &str) {
462 let _execution = self.enter_execution_time_limit();
463 let _profiling = self.runtime.ctx.enter_profiling_boundary();
464 self.runtime.eval(code, "<eval>");
465 self.request_redraw();
466 self.arm_timer_wake();
467 }
468
469 pub fn eval_named(&mut self, code: &str, name: &str) {
472 let _execution = self.enter_execution_time_limit();
473 let _profiling = self.runtime.ctx.enter_profiling_boundary();
474 self.runtime.eval_internal(code, name);
475 self.runtime.run_jobs(name);
476 self.request_redraw();
477 self.arm_timer_wake();
478 }
479
480 pub fn eval_json(&mut self, code: &str) -> Result<serde_json::Value, String> {
486 let _execution = self.enter_execution_time_limit();
487 let _profiling = self.runtime.ctx.enter_profiling_boundary();
488 let result = self.runtime.eval_json(code, "<eval with result>");
489 self.request_redraw();
490 self.arm_timer_wake();
491 result
492 }
493
494 #[cfg(feature = "debug-control")]
495 pub fn diagnostic_batch(
496 &self,
497 console_after: u64,
498 errors_after: u64,
499 limit: usize,
500 ) -> crate::DiagnosticBatch {
501 self.runtime
502 .diagnostic_batch(console_after, errors_after, limit)
503 }
504
505 #[cfg(feature = "debug-control")]
506 pub(crate) fn console_entries_after(
507 &self,
508 sequence: u64,
509 ) -> Vec<crate::runtime::DiagnosticEntry> {
510 self.runtime.console_entries_after(sequence)
511 }
512
513 #[cfg(feature = "debug-control")]
514 pub(crate) fn runtime_errors_after(
515 &self,
516 sequence: u64,
517 ) -> Vec<crate::runtime::DiagnosticEntry> {
518 self.runtime.runtime_errors_after(sequence)
519 }
520
521 pub fn current_url(&self) -> Option<&Url> {
523 self.base_url.as_ref()
524 }
525
526 pub fn page_source(&self) -> String {
528 self.inner.borrow().root_element().outer_html()
529 }
530
531 pub fn dispatch_dom_event(&mut self, event: DomEvent) {
536 if self.inner.borrow().page_frozen
537 || self.inner.borrow().page_retiring
538 || self.inner.borrow().page_input_blocked
539 {
540 return;
541 }
542 let _execution = self.enter_execution_time_limit();
543 let profiling_boundary = self.runtime.ctx.enter_profiling_boundary();
544 let profiling = profiling_boundary.enabled();
545 let handler = ScriptEventHandler {
546 runtime: &mut self.runtime,
547 profiling,
548 };
549 let mut driver = EventDriver::new(&mut self.inner, handler);
550 driver.handle_dom_event(event);
551
552 self.request_redraw();
553 self.arm_timer_wake();
554 }
555
556 fn resume_parser_and_scripts(&mut self) -> bool {
562 let _slice = self.runtime.begin_job_slice();
563 let mut ran = false;
564 loop {
565 let progressed = self.run_pending_scripts();
566 ran |= progressed;
567 if !progressed
568 || self.runtime.jobs_remain()
569 || !crate::docwrite::active(&self.runtime.context)
570 {
571 break;
572 }
573 }
574 ran
575 }
576
577 fn run_pending_scripts(&mut self) -> bool {
590 let _slice = self.runtime.begin_job_slice();
591 if self.runtime.jobs_remain() || self.runtime.job_slice_exhausted() {
592 return false;
593 }
594 if crate::docwrite::take_restart(&self.runtime.context) {
595 self.executed_scripts.clear();
596 self.startup_events_pending = true;
597 self.load_event_pending = false;
598 }
599 let parsed = crate::docwrite::advance(&mut self.runtime.context);
600 let generation = crate::docwrite::generation(&self.runtime.context);
601 self.prepare_native_restore();
602 if parsed {
603 self.runtime.ctx.mark_layout_dirty();
604 }
605 let pending: Vec<PendingScript> = self
608 .collect_scripts()
609 .into_iter()
610 .filter(|script| !self.executed_scripts.contains(&script.node_id))
611 .filter(|script| {
612 !crate::docwrite::was_executed(&self.runtime.context, script.node_id)
613 && (!script.deferred || !crate::docwrite::active(&self.runtime.context))
614 })
615 .collect();
616 let ran = parsed || !pending.is_empty();
617
618 for script in &pending {
623 if script.kind == ScriptKind::ImportMap && !script.inline_text.trim().is_empty() {
624 self.runtime
625 .set_import_map(&script.inline_text, self.base_url.as_ref());
626 }
627 }
628
629 let (deferred, immediate): (Vec<_>, Vec<_>) =
640 pending.into_iter().partition(|script| script.deferred);
641
642 for script in immediate.into_iter().chain(deferred) {
643 if self.runtime.job_slice_exhausted() {
644 break;
645 }
646 if generation != crate::docwrite::generation(&self.runtime.context) {
647 break;
648 }
649 if self.startup_events_pending && script.deferred {
650 self.runtime.set_document_ready_state("interactive");
651 if self.runtime.jobs_remain() {
652 break;
653 }
654 }
655 self.prepare_native_restore();
659 self.executed_scripts.insert(script.node_id);
660 if script.kind == ScriptKind::ImportMap {
661 continue;
662 }
663 match script.src {
664 Some(src) => {
665 let Some(url) = self.resolve_script_url(&src) else {
666 eprintln!("blitz-script: could not resolve script URL {src:?}");
667 self.runtime.dispatch_node_event(script.node_id, "error");
668 if self.runtime.jobs_remain() {
669 break;
670 }
671 continue;
672 };
673 let fetcher = Rc::clone(&self.fetcher.borrow());
674 match fetcher.fetch(&url) {
675 Ok(code) => {
676 if script.kind == ScriptKind::Module {
677 self.runtime.eval_module(&code, Some(&url), url.as_str());
678 } else {
679 crate::domc::set_current_script(
680 &self.runtime.context,
681 Some(script.node_id),
682 );
683 self.runtime.eval_at(&code, Some(&url), url.as_str());
684 }
685 self.runtime.dispatch_node_event(script.node_id, "load");
686 }
687 Err(error) => {
688 eprintln!("blitz-script: failed to fetch script {url}: {error}");
689 self.runtime.dispatch_node_event(script.node_id, "error");
690 }
691 }
692 }
693 None => {
694 if !script.inline_text.trim().is_empty() {
695 let base_url = self.base_url.clone();
699 if script.kind == ScriptKind::Module {
700 self.runtime.eval_module(
701 &script.inline_text,
702 base_url.as_ref(),
703 "<inline module>",
704 );
705 } else {
706 crate::domc::set_current_script(
707 &self.runtime.context,
708 Some(script.node_id),
709 );
710 self.runtime.eval_at(
711 &script.inline_text,
712 base_url.as_ref(),
713 "<inline script>",
714 );
715 }
716 }
717 }
718 }
719 if self.runtime.jobs_remain() {
722 break;
723 }
724 }
725 ran
726 }
727
728 fn finish_startup_events(&mut self) -> bool {
729 if !(self.startup_events_pending || self.load_event_pending)
730 || self.runtime.jobs_remain()
731 || crate::docwrite::active(&self.runtime.context)
732 || self.runtime.job_slice_exhausted()
733 {
734 return false;
735 }
736 if self.startup_events_pending
737 && (self.runtime.has_pending_module_evaluations()
738 || self.collect_scripts().iter().any(|script| {
739 !self.executed_scripts.contains(&script.node_id)
740 && !crate::docwrite::was_executed(&self.runtime.context, script.node_id)
741 }))
742 {
743 return false;
744 }
745
746 let mut ran = false;
747 if self.startup_events_pending {
748 ran |= self.runtime.set_document_ready_state("interactive");
749 if self.runtime.jobs_remain() {
750 return ran;
751 }
752 self.startup_events_pending = false;
753 self.load_event_pending = true;
754 self.runtime.dispatch_document_event("DOMContentLoaded");
755 ran = true;
756 }
757 let resources_pending = {
758 let doc = self.inner.borrow();
759 doc.has_pending_critical_resources() || doc.pending_image_count() != 0
760 };
761 if self.load_event_pending
762 && !self.runtime.job_slice_exhausted()
763 && !self.runtime.jobs_remain()
764 && !resources_pending
765 && !self.runtime.has_pending_module_evaluations()
766 {
767 ran |= self.runtime.set_document_ready_state("complete");
768 if !self.runtime.jobs_remain() {
769 self.load_event_pending = false;
770 self.runtime.dispatch_window_event("load");
771 ran = true;
772 }
773 }
774 ran
775 }
776
777 fn collect_scripts(&self) -> Vec<PendingScript> {
779 let doc = self.inner.borrow();
780 let mut scripts = Vec::new();
781 let mut stack = vec![doc.root_node().id];
782
783 while let Some(node_id) = stack.pop() {
784 let Some(node) = doc.get_node(node_id) else {
785 continue;
786 };
787
788 if let Some(element) = node.element_data() {
789 if element.name.local == blitz_dom::local_name!("script") {
790 if node
791 .flags
792 .contains(blitz_dom::node::NodeFlags::IS_PARSER_INERT_SCRIPT)
793 {
794 continue;
795 }
796 let script_type = element
798 .attr(blitz_dom::local_name!("type"))
799 .unwrap_or("")
800 .trim()
801 .to_ascii_lowercase();
802 let kind = match script_type.as_str() {
803 "module" => Some(ScriptKind::Module),
804 "importmap" => Some(ScriptKind::ImportMap),
805 "" | "text/javascript" | "application/javascript" => {
806 if element.attr(blitz_dom::local_name!("nomodule")).is_some() {
811 None
812 } else {
813 Some(ScriptKind::Classic)
814 }
815 }
816 _ => None,
817 };
818
819 if let Some(kind) = kind {
820 let src = element
821 .attr(blitz_dom::local_name!("src"))
822 .map(str::to_string);
823 let is_async = element.attr(blitz_dom::local_name!("async")).is_some();
824
825 let deferred = !is_async
829 && match kind {
830 ScriptKind::Module => true,
831 ScriptKind::Classic => {
832 src.is_some()
833 && element.attr(blitz_dom::local_name!("defer")).is_some()
834 }
835 ScriptKind::ImportMap => false,
836 };
837
838 scripts.push(PendingScript {
839 node_id,
840 src,
841 inline_text: node.text_content(),
842 kind,
843 deferred,
844 });
845 }
846 continue;
847 }
848 }
849
850 stack.extend(node.children.iter().rev().copied());
851 }
852
853 scripts
854 }
855
856 fn request_redraw(&self) {
857 self.inner.borrow().shell_provider.request_redraw();
858 }
859
860 fn arm_timer_wake(&mut self) {
862 let Some(deadline) = self.runtime.next_timer_deadline() else {
863 self.timer_wake = None;
864 return;
865 };
866 let Some(waker) = &self.waker else {
867 self.timer_wake = None;
868 return;
869 };
870 if self
871 .timer_wake
872 .as_ref()
873 .is_none_or(|(armed, _)| *armed != deadline)
874 {
875 self.timer_wake = Some((
876 deadline,
877 nagoya::time::sleep(deadline.saturating_duration_since(Instant::now())),
878 ));
879 }
880 let (_, timer) = self.timer_wake.as_mut().unwrap();
881 let mut cx = TaskContext::from_waker(waker);
882 if Pin::new(timer).poll(&mut cx).is_ready() {
883 waker.wake_by_ref();
884 }
885 }
886}
887
888impl Drop for ScriptDocument {
889 fn drop(&mut self) {
890 self.cancel_host_work();
891 self.inner.borrow_mut().prepare_teardown();
892 }
893}
894
895impl Document for ScriptDocument {
896 fn prepare_teardown(&mut self) {
897 self.cancel_host_work();
898 self.inner.borrow_mut().prepare_teardown();
899 }
900
901 fn dispose(mut self: Box<Self>) -> Arc<blitz_dom::release::Teardown> {
902 self.prepare_teardown();
903 let receipt = self.teardown_receipt();
904 drop(self);
905 boa_gc::force_collect();
907 receipt
908 }
909
910 fn set_page_visible(&mut self, visible: bool) {
911 let changed = self.inner.borrow().page_visible != visible;
912 self.inner.borrow_mut().page_visible = visible;
913 if changed {
914 self.runtime.dispatch_document_event("visibilitychange");
915 }
916 }
917
918 fn freeze(&mut self) {
919 if self.inner.borrow().page_frozen {
920 return;
921 }
922 self.runtime
923 .context
924 .set_execution_time_limit(Some(std::time::Duration::from_millis(50)));
925 let _execution = self.enter_execution_time_limit();
926 self.runtime.dispatch_lifecycle_event("freeze");
927 self.runtime.run_jobs_with_progress("freeze microtasks");
928 self.inner.borrow_mut().page_frozen = true;
929 self.timer_wake.take();
930 }
931
932 fn finish_restore(&mut self) {
933 self.before_script_hook.take();
934 self.inner.borrow_mut().page_input_blocked = false;
935 }
936
937 fn resume(&mut self) {
938 if !self.inner.borrow().page_frozen || self.inner.borrow().page_retiring {
939 return;
940 }
941 self.inner.borrow_mut().page_frozen = false;
942 self.runtime.dispatch_lifecycle_event("resume");
943 self.arm_timer_wake();
944 self.request_redraw();
945 }
946
947 fn inner(&self) -> DocGuard<'_> {
948 DocGuard::RefCell(self.inner.borrow())
949 }
950
951 fn inner_mut(&mut self) -> DocGuardMut<'_> {
952 DocGuardMut::RefCell(self.inner.borrow_mut())
953 }
954
955 fn handle_ui_event(&mut self, event: UiEvent) {
956 if self.inner.borrow().page_frozen
957 || self.inner.borrow().page_retiring
958 || self.inner.borrow().page_input_blocked
959 {
960 return;
961 }
962 let _execution = self.enter_execution_time_limit();
963 let profiling_boundary = self.runtime.ctx.enter_profiling_boundary();
964 let profiling = profiling_boundary.enabled();
965 let handler = ScriptEventHandler {
966 runtime: &mut self.runtime,
967 profiling,
968 };
969 let mut driver = EventDriver::new(&mut self.inner, handler);
970 driver.handle_ui_event(event);
971
972 self.request_redraw();
974 self.arm_timer_wake();
975 }
976
977 fn poll(&mut self, task_context: Option<TaskContext>) -> bool {
978 self.poll_with_timer_policy(task_context, None)
979 }
980
981 fn poll_for_settle(&mut self, task_context: Option<TaskContext>, run_timers: bool) -> bool {
982 self.poll_with_timer_policy(task_context, Some(run_timers))
983 }
984}
985
986impl ScriptDocument {
987 fn poll_with_timer_policy(
989 &mut self,
990 task_context: Option<TaskContext>,
991 settle_timers: Option<bool>,
992 ) -> bool {
993 if self.inner.borrow().page_frozen || self.inner.borrow().page_retiring {
994 return false;
995 }
996 let _execution = self.enter_execution_time_limit();
997 let profiling_boundary = self.runtime.ctx.enter_profiling_boundary();
998 let profiling = profiling_boundary.enabled();
999 let poll_started = profiling.then(std::time::Instant::now);
1000 let _slice = self.runtime.begin_job_slice();
1001 let ran = self.poll_inner(task_context, profiling, settle_timers);
1002 if let Some(started) = poll_started {
1003 crate::script_stats::record_poll(started.elapsed(), ran);
1004 }
1005 ran
1006 }
1007
1008 pub fn handle_pointer_move_to_node(&mut self, event: BlitzPointerEvent, node_id: NodeId) {
1015 if self.inner.borrow().page_frozen
1016 || self.inner.borrow().page_retiring
1017 || self.inner.borrow().page_input_blocked
1018 {
1019 return;
1020 }
1021 let _execution = self.enter_execution_time_limit();
1022 let profiling_boundary = self.runtime.ctx.enter_profiling_boundary();
1023 let profiling = profiling_boundary.enabled();
1024 let handler = ScriptEventHandler {
1025 runtime: &mut self.runtime,
1026 profiling,
1027 };
1028 let mut driver = EventDriver::new(&mut self.inner, handler);
1029 driver.handle_pointer_move_to_node(&event, node_id);
1030
1031 self.request_redraw();
1032 self.arm_timer_wake();
1033 }
1034
1035 pub fn handle_ui_event_to_node(&mut self, event: UiEvent, node_id: NodeId) {
1039 if self.inner.borrow().page_frozen
1040 || self.inner.borrow().page_retiring
1041 || self.inner.borrow().page_input_blocked
1042 {
1043 return;
1044 }
1045 let _execution = self.enter_execution_time_limit();
1046 let profiling_boundary = self.runtime.ctx.enter_profiling_boundary();
1047 let profiling = profiling_boundary.enabled();
1048 let handler = ScriptEventHandler {
1049 runtime: &mut self.runtime,
1050 profiling,
1051 };
1052 let mut driver = EventDriver::new(&mut self.inner, handler);
1053 driver.handle_ui_event_to_node(event, node_id);
1054
1055 self.request_redraw();
1056 self.arm_timer_wake();
1057 }
1058
1059 fn poll_inner(
1062 &mut self,
1063 task_context: Option<TaskContext>,
1064 profiling: bool,
1065 settle_timers: Option<bool>,
1066 ) -> bool {
1067 if let Some(cx) = &task_context {
1069 if self
1070 .waker
1071 .as_ref()
1072 .is_none_or(|old| !old.will_wake(cx.waker()))
1073 {
1074 self.waker = Some(cx.waker().clone());
1075 }
1076 }
1077
1078 let subdocument_changes = {
1083 let mut inner = self.inner.borrow_mut();
1084 let waker = task_context.as_ref().map(TaskContext::waker);
1085 match settle_timers {
1086 Some(run_timers) => inner.poll_subdocuments_for_settle(waker, run_timers),
1087 None => inner.poll_subdocuments(waker),
1088 }
1089 };
1090
1091 let mut ran = subdocument_changes;
1092 ran |= self.runtime.poll_domc();
1093 ran |= self.runtime.run_jobs_with_progress("poll microtasks");
1096
1097 if !self.scripts_executed {
1099 let started = profiling.then(std::time::Instant::now);
1103 self.execute_scripts();
1104 if let Some(started) = started {
1105 crate::script_stats::record_work("startup:execute_scripts", started.elapsed());
1106 }
1107 ran = true;
1108 } else {
1109 ran |= self.resume_parser_and_scripts();
1114 }
1115 ran |= self.finish_startup_events();
1116
1117 if settle_timers.unwrap_or(true) {
1118 ran |= self.runtime.run_due_timers(profiling);
1119 }
1120
1121 self.runtime.poll_module_evaluations();
1125
1126 if let Some(mut hook) = self.poll_hook.take() {
1127 let started = profiling.then(std::time::Instant::now);
1131 ran |= hook(self, task_context.as_ref());
1132 if let Some(started) = started {
1133 crate::script_stats::record_work("poll_hook", started.elapsed());
1134 }
1135 self.poll_hook = Some(hook);
1136 }
1137
1138 self.runtime.sweep_detached_nodes();
1143
1144 if self.runtime.jobs_remain() {
1145 ran = true;
1146 self.request_redraw();
1147 if let Some(cx) = &task_context {
1148 cx.waker().wake_by_ref();
1149 }
1150 }
1151 self.arm_timer_wake();
1152 ran
1153 }
1154}