Skip to main content

blitz_script/
document.rs

1//! [`ScriptDocument`]: a [`Document`] implementation with JavaScript support
2
3use 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::{DocumentHtmlParser, HtmlProvider};
13use blitz_traits::events::{BlitzPointerEvent, DomEvent, UiEvent};
14use url::Url;
15use web_time::Instant;
16
17use crate::event_handler::ScriptEventHandler;
18use crate::fetch::{DefaultScriptFetcher, ScriptFetcher};
19use crate::module::SharedFetcher;
20use crate::runtime::ScriptRuntime;
21
22type PollHook =
23    Box<dyn for<'a> FnMut(&mut ScriptDocument, Option<&TaskContext<'a>>) -> bool + 'static>;
24
25/// What a `<script>` element's `type` says it holds.
26#[derive(Clone, Copy, PartialEq, Eq)]
27enum ScriptKind {
28    Classic,
29    /// `type="module"`. Parsed in module goal and given a loader; a module run
30    /// as a classic script fails on its own first `import` statement.
31    Module,
32    /// `type="importmap"`. Not code: the JSON that gives bare specifiers their
33    /// meaning, and it has to be installed before any module resolves one.
34    ImportMap,
35}
36
37/// A `<script>` element found in the document
38struct PendingScript {
39    node_id: NodeId,
40    src: Option<String>,
41    inline_text: String,
42    kind: ScriptKind,
43    /// Runs after every non-deferred script in the same batch.
44    ///
45    /// True for module scripts, which are deferred by definition, and for
46    /// `<script defer src>`. `async` takes it back off both: an async script
47    /// runs whenever its fetch lands, which here is immediately.
48    deferred: bool,
49}
50
51/// A [`Document`] which executes the JavaScript contained in the document's
52/// `<script>` tags, exposing DOM APIs backed by `blitz-dom` to the scripts.
53///
54/// Construct with [`ScriptDocument::from_html`], then call
55/// [`execute_scripts`](ScriptDocument::execute_scripts) (this also happens
56/// automatically on the first [`poll`](Document::poll)). UI events pushed via
57/// [`handle_ui_event`](Document::handle_ui_event) are dispatched to JavaScript
58/// event listeners before Blitz's default actions run.
59pub struct ScriptDocument {
60    inner: Rc<RefCell<BaseDocument>>,
61    runtime: ScriptRuntime,
62    base_url: Option<Url>,
63    /// Shared with the module loader inside the script context.
64    ///
65    /// The loader is fixed at context construction, which happens in
66    /// `from_html`, while an embedder installs its fetcher afterwards through
67    /// [`with_fetcher`](Self::with_fetcher). The cell is what lets the later
68    /// call reach the earlier object.
69    fetcher: SharedFetcher,
70    scripts_executed: bool,
71    /// Which `<script>` nodes have already run.
72    ///
73    /// By node rather than a single "done" flag, because scripts keep arriving
74    /// after the first pass: a loader that appends its bundle, an analytics
75    /// snippet, a lazily inserted chunk. Keyed this way, a rescan can tell a
76    /// script it has run from one it has not without running anything twice.
77    executed_scripts: std::collections::HashSet<NodeId>,
78    poll_hook: Option<PollHook>,
79
80    // Timer wakeups: a background thread which wakes the event loop (via the
81    // `Waker` passed to `poll`) when the next JS timer is due.
82    waker: Arc<Mutex<Option<Waker>>>,
83    timer_thread: Option<Sender<Instant>>,
84}
85
86impl ScriptDocument {
87    /// Parse HTML into a [`ScriptDocument`].
88    ///
89    /// Note: this does *not* execute any scripts yet. Call
90    /// [`execute_scripts`](Self::execute_scripts) to do so (or rely on the
91    /// first `poll` doing it automatically).
92    pub fn from_html(html: &str, mut config: DocumentConfig) -> Self {
93        if let Some(ss) = &mut config.ua_stylesheets {
94            if !ss.iter().any(|s| s == DEFAULT_CSS) {
95                ss.push(String::from(DEFAULT_CSS));
96            }
97        } else {
98            config.ua_stylesheets = Some(vec![String::from(DEFAULT_CSS)]);
99        }
100        if config.html_parser_provider.is_none() {
101            config.html_parser_provider = Some(Arc::new(HtmlProvider));
102        }
103
104        let base_url = config
105            .base_url
106            .as_deref()
107            .and_then(|url| Url::parse(url).ok());
108
109        let mut doc = BaseDocument::new(config);
110        let mut mutr = doc.mutate();
111        DocumentHtmlParser::parse_into_mutator(&mut mutr, html);
112        drop(mutr);
113
114        let inner = Rc::new(RefCell::new(doc));
115        let fetcher: SharedFetcher = Rc::new(RefCell::new(Rc::new(DefaultScriptFetcher)));
116        let runtime = ScriptRuntime::new(Rc::clone(&inner), base_url.as_ref(), Rc::clone(&fetcher));
117
118        Self {
119            inner,
120            runtime,
121            base_url,
122            fetcher,
123            scripts_executed: false,
124            executed_scripts: std::collections::HashSet::new(),
125            poll_hook: None,
126            waker: Arc::new(Mutex::new(None)),
127            timer_thread: None,
128        }
129    }
130
131    /// Override the [`ScriptFetcher`] used to load external (`src="..."`) scripts.
132    /// The default fetcher supports `file:` and `data:` URLs.
133    pub fn with_fetcher(self, fetcher: impl ScriptFetcher) -> Self {
134        *self.fetcher.borrow_mut() = Rc::new(fetcher);
135        self
136    }
137
138    /// Install the host callback invoked by `window.ipc.postMessage(body)`.
139    ///
140    /// The callback may forward work to another thread, but JavaScript and DOM state remain on
141    /// the document's owning thread. Replacing the callback is supported before script startup.
142    pub fn set_ipc_handler(&mut self, handler: impl Fn(String) + 'static) {
143        self.runtime.ctx.state.borrow_mut().ipc_handler = Some(Rc::new(handler));
144    }
145
146    /// Install work that an embedder needs to run on the document thread during polling.
147    ///
148    /// The hook runs after document scripts and due timers. Returning `true` requests a redraw.
149    pub fn set_poll_hook(
150        &mut self,
151        hook: impl for<'a> FnMut(&mut ScriptDocument, Option<&TaskContext<'a>>) -> bool + 'static,
152    ) {
153        self.poll_hook = Some(Box::new(hook));
154    }
155
156    /// Append document-thread work without discarding an embedder's existing
157    /// poll lifecycle.
158    pub fn add_poll_hook(
159        &mut self,
160        mut hook: impl for<'a> FnMut(&mut ScriptDocument, Option<&TaskContext<'a>>) -> bool + 'static,
161    ) {
162        let Some(mut existing) = self.poll_hook.take() else {
163            self.poll_hook = Some(Box::new(hook));
164            return;
165        };
166        self.poll_hook = Some(Box::new(move |document, task_context| {
167            let ran_existing = existing(document, task_context);
168            let ran_added = hook(document, task_context);
169            ran_existing | ran_added
170        }));
171    }
172
173    /// Execute the document's `<script>` elements in document order, then fire
174    /// the `DOMContentLoaded` and `load` events.
175    ///
176    /// Does nothing if scripts have already been executed.
177    pub fn execute_scripts(&mut self) {
178        let _profiling = self.runtime.ctx.enter_profiling_boundary();
179        if self.scripts_executed {
180            return;
181        }
182        self.scripts_executed = true;
183
184        self.run_pending_scripts();
185
186        self.runtime.dispatch_document_event("DOMContentLoaded");
187        self.runtime.dispatch_window_event("load");
188
189        self.request_redraw();
190        self.arm_timer_thread();
191    }
192
193    /// The resolved URLs of the document's external (`<script src="...">`) scripts,
194    /// in document order.
195    ///
196    /// The [`ScriptFetcher`] API is synchronous, so embedders with asynchronous
197    /// networking can use this to prefetch script sources before calling
198    /// [`execute_scripts`](Self::execute_scripts), and then serve them from memory
199    /// via a custom fetcher (see [`with_fetcher`](Self::with_fetcher)).
200    pub fn external_script_urls(&self) -> Vec<Url> {
201        self.collect_scripts()
202            .iter()
203            // An import map is inline-only per the HTML spec, so a `src` on one
204            // names nothing an embedder should prefetch.
205            .filter(|script| script.kind != ScriptKind::ImportMap)
206            .filter_map(|script| script.src.as_deref())
207            .filter_map(|src| self.resolve_script_url(src))
208            .collect()
209    }
210
211    /// Resolve a script `src` attribute against the document's base URL
212    fn resolve_script_url(&self, src: &str) -> Option<Url> {
213        match &self.base_url {
214            Some(base) => base.join(src).ok(),
215            None => Url::parse(src).ok(),
216        }
217    }
218
219    /// Evaluate arbitrary JavaScript code in the document's script context
220    pub fn eval(&mut self, code: &str) {
221        let _profiling = self.runtime.ctx.enter_profiling_boundary();
222        self.runtime.eval(code, "<eval>");
223        self.request_redraw();
224        self.arm_timer_thread();
225    }
226
227    /// Evaluate JavaScript and convert its result to JSON.
228    ///
229    /// Embedders use this for APIs that return an evaluation result, such as Tauri's
230    /// `eval_script_with_callback`. JavaScript exceptions are recorded in the runtime diagnostics
231    /// and returned as an error.
232    pub fn eval_json(&mut self, code: &str) -> Result<serde_json::Value, String> {
233        let _profiling = self.runtime.ctx.enter_profiling_boundary();
234        let result = self.runtime.eval_json(code, "<eval with result>");
235        self.request_redraw();
236        self.arm_timer_thread();
237        result
238    }
239
240    #[cfg(feature = "debug-control")]
241    pub(crate) fn console_entries_after(
242        &self,
243        sequence: u64,
244    ) -> Vec<crate::runtime::DiagnosticEntry> {
245        self.runtime.console_entries_after(sequence)
246    }
247
248    #[cfg(feature = "debug-control")]
249    pub(crate) fn runtime_errors_after(
250        &self,
251        sequence: u64,
252    ) -> Vec<crate::runtime::DiagnosticEntry> {
253        self.runtime.runtime_errors_after(sequence)
254    }
255
256    /// Current document URL for automation and diagnostics.
257    pub fn current_url(&self) -> Option<&Url> {
258        self.base_url.as_ref()
259    }
260
261    /// Serialize the current document tree as HTML.
262    pub fn page_source(&self) -> String {
263        self.inner.borrow().root_element().outer_html()
264    }
265
266    /// Dispatch a synthetic DOM event (e.g. a click created with
267    /// [`Node::synthetic_click_event`](blitz_dom::Node::synthetic_click_event))
268    /// through the document's event driver. The event is exposed to JavaScript
269    /// event listeners, and Blitz's default actions run unless prevented.
270    pub fn dispatch_dom_event(&mut self, event: DomEvent) {
271        let profiling_boundary = self.runtime.ctx.enter_profiling_boundary();
272        let profiling = profiling_boundary.enabled();
273        let handler = ScriptEventHandler {
274            runtime: &mut self.runtime,
275            profiling,
276        };
277        let mut driver = EventDriver::new(&mut self.inner, handler);
278        driver.handle_dom_event(event);
279
280        self.request_redraw();
281        self.arm_timer_thread();
282    }
283
284    /// Run every `<script>` that has appeared since the last pass.
285    ///
286    /// Called on each poll as well as at startup, because script elements are
287    /// not only a parser product: a page can build one and append it, and that
288    /// is how most real loaders bring in their bundle. Running only the markup
289    /// the parser produced meant those were fetched by nobody and the page
290    /// simply never started.
291    ///
292    /// A `src` script reports `load` when it runs and `error` when it does not.
293    /// Loaders wait on those before continuing — nofilter.io keeps the body
294    /// hidden until the bundle's `load` arrives — so a script that ran silently
295    /// would still leave the page blank.
296    fn run_pending_scripts(&mut self) {
297        // Collect first, then run: executing a script can append more, and the
298        // borrow on the document has to be released before any of them runs.
299        let pending: Vec<PendingScript> = self
300            .collect_scripts()
301            .into_iter()
302            .filter(|script| !self.executed_scripts.contains(&script.node_id))
303            .collect();
304
305        // Import maps first, across the whole batch. A page is free to write
306        // its map after the module that needs it, and a map installed too late
307        // is the same as no map at all: the module has already failed to
308        // resolve its first bare specifier.
309        for script in &pending {
310            if script.kind == ScriptKind::ImportMap && !script.inline_text.trim().is_empty() {
311                self.runtime
312                    .set_import_map(&script.inline_text, self.base_url.as_ref());
313            }
314        }
315
316        // Two passes, because a module is a deferred script and a browser runs
317        // every non-deferred classic script before any of them. Measured
318        // against Chromium on a page mixing all five forms: the parser-blocking
319        // classics run first in document order, then modules and `defer`
320        // scripts together, also in document order.
321        //
322        // Document order alone was right until modules existed. It is now
323        // wrong in a way that bites: an inline classic script writing
324        // `window.__CONFIG__` after a module tag runs *before* that module in
325        // a browser, and the module reads the config it expects.
326        let (deferred, immediate): (Vec<_>, Vec<_>) =
327            pending.into_iter().partition(|script| script.deferred);
328
329        for script in immediate.into_iter().chain(deferred) {
330            // Marked before running, not after: a script that appends another
331            // copy of itself, or that throws, must not be retried on every
332            // poll for the life of the page.
333            self.executed_scripts.insert(script.node_id);
334            if script.kind == ScriptKind::ImportMap {
335                continue;
336            }
337            match script.src {
338                Some(src) => {
339                    let Some(url) = self.resolve_script_url(&src) else {
340                        eprintln!("blitz-script: could not resolve script URL {src:?}");
341                        self.runtime.dispatch_node_event(script.node_id, "error");
342                        continue;
343                    };
344                    let fetcher = Rc::clone(&self.fetcher.borrow());
345                    match fetcher.fetch(&url) {
346                        Ok(code) => {
347                            if script.kind == ScriptKind::Module {
348                                self.runtime.eval_module(&code, Some(&url), url.as_str());
349                            } else {
350                                self.runtime.eval_at(&code, Some(&url), url.as_str());
351                            }
352                            self.runtime.dispatch_node_event(script.node_id, "load");
353                        }
354                        Err(error) => {
355                            eprintln!("blitz-script: failed to fetch script {url}: {error}");
356                            self.runtime.dispatch_node_event(script.node_id, "error");
357                        }
358                    }
359                }
360                None => {
361                    if !script.inline_text.trim().is_empty() {
362                        // An inline script has no URL of its own, so its
363                        // relative imports and `import.meta.url` resolve
364                        // against the document, exactly as in a browser.
365                        let base_url = self.base_url.clone();
366                        if script.kind == ScriptKind::Module {
367                            self.runtime.eval_module(
368                                &script.inline_text,
369                                base_url.as_ref(),
370                                "<inline module>",
371                            );
372                        } else {
373                            self.runtime.eval_at(
374                                &script.inline_text,
375                                base_url.as_ref(),
376                                "<inline script>",
377                            );
378                        }
379                    }
380                }
381            }
382        }
383    }
384
385    /// Find `<script>` elements in document order
386    fn collect_scripts(&self) -> Vec<PendingScript> {
387        let doc = self.inner.borrow();
388        let mut scripts = Vec::new();
389        let mut stack = vec![doc.root_node().id];
390
391        while let Some(node_id) = stack.pop() {
392            let Some(node) = doc.get_node(node_id) else {
393                continue;
394            };
395
396            if let Some(element) = node.element_data() {
397                if element.name.local == blitz_dom::local_name!("script") {
398                    // Skip non-JavaScript script types (e.g. JSON data blocks).
399                    let script_type = element
400                        .attr(blitz_dom::local_name!("type"))
401                        .unwrap_or("")
402                        .trim()
403                        .to_ascii_lowercase();
404                    let kind = match script_type.as_str() {
405                        "module" => Some(ScriptKind::Module),
406                        "importmap" => Some(ScriptKind::ImportMap),
407                        "" | "text/javascript" | "application/javascript" => {
408                            // `nomodule` marks the classic fallback a page
409                            // ships for engines without module support. Now
410                            // that modules run, taking the fallback as well
411                            // would mount the same application twice.
412                            if element.attr(blitz_dom::local_name!("nomodule")).is_some() {
413                                None
414                            } else {
415                                Some(ScriptKind::Classic)
416                            }
417                        }
418                        _ => None,
419                    };
420
421                    if let Some(kind) = kind {
422                        let src = element
423                            .attr(blitz_dom::local_name!("src"))
424                            .map(str::to_string);
425                        let is_async = element.attr(blitz_dom::local_name!("async")).is_some();
426
427                        // `defer` is only meaningful on an external classic
428                        // script; the spec ignores it on an inline one. A
429                        // module is deferred whether or not it says so.
430                        let deferred = !is_async
431                            && match kind {
432                                ScriptKind::Module => true,
433                                ScriptKind::Classic => {
434                                    src.is_some()
435                                        && element.attr(blitz_dom::local_name!("defer")).is_some()
436                                }
437                                ScriptKind::ImportMap => false,
438                            };
439
440                        scripts.push(PendingScript {
441                            node_id,
442                            src,
443                            inline_text: node.text_content(),
444                            kind,
445                            deferred,
446                        });
447                    }
448                    continue;
449                }
450            }
451
452            stack.extend(node.children.iter().rev().copied());
453        }
454
455        scripts
456    }
457
458    fn request_redraw(&self) {
459        self.inner.borrow().shell_provider.request_redraw();
460    }
461
462    /// Ensure the timer thread is armed to wake the event loop when the next
463    /// JS timer is due.
464    fn arm_timer_thread(&mut self) {
465        let Some(deadline) = self.runtime.next_timer_deadline() else {
466            return;
467        };
468
469        let sender = self.timer_thread.get_or_insert_with(|| {
470            let (tx, rx) = channel::<Instant>();
471            let waker = Arc::clone(&self.waker);
472            std::thread::Builder::new()
473                .name("blitz-script-timers".to_string())
474                .spawn(move || timer_thread_main(rx, waker))
475                .expect("failed to spawn timer thread");
476            tx
477        });
478
479        // If the thread has exited (channel disconnected) drop the sender so a
480        // new thread is spawned next time.
481        if sender.send(deadline).is_err() {
482            self.timer_thread = None;
483        }
484    }
485}
486
487/// Background thread which wakes the event loop when JS timers are due
488fn timer_thread_main(rx: Receiver<Instant>, waker: Arc<Mutex<Option<Waker>>>) {
489    let mut deadline: Option<Instant> = None;
490
491    loop {
492        match deadline {
493            None => match rx.recv() {
494                Ok(new_deadline) => deadline = Some(new_deadline),
495                Err(_) => return,
496            },
497            Some(current) => {
498                let now = Instant::now();
499                if current <= now {
500                    if let Some(waker) = waker.lock().unwrap().as_ref() {
501                        waker.wake_by_ref();
502                    }
503                    deadline = None;
504                    continue;
505                }
506                match rx.recv_timeout(current - now) {
507                    Ok(new_deadline) => deadline = Some(new_deadline.min(current)),
508                    Err(RecvTimeoutError::Timeout) => {
509                        if let Some(waker) = waker.lock().unwrap().as_ref() {
510                            waker.wake_by_ref();
511                        }
512                        deadline = None;
513                    }
514                    Err(RecvTimeoutError::Disconnected) => return,
515                }
516            }
517        }
518    }
519}
520
521impl Document for ScriptDocument {
522    fn inner(&self) -> DocGuard<'_> {
523        DocGuard::RefCell(self.inner.borrow())
524    }
525
526    fn inner_mut(&mut self) -> DocGuardMut<'_> {
527        DocGuardMut::RefCell(self.inner.borrow_mut())
528    }
529
530    fn handle_ui_event(&mut self, event: UiEvent) {
531        let profiling_boundary = self.runtime.ctx.enter_profiling_boundary();
532        let profiling = profiling_boundary.enabled();
533        let handler = ScriptEventHandler {
534            runtime: &mut self.runtime,
535            profiling,
536        };
537        let mut driver = EventDriver::new(&mut self.inner, handler);
538        driver.handle_ui_event(event);
539
540        // JS may have mutated the DOM or scheduled timers
541        self.request_redraw();
542        self.arm_timer_thread();
543    }
544
545    fn poll(&mut self, task_context: Option<TaskContext>) -> bool {
546        let profiling_boundary = self.runtime.ctx.enter_profiling_boundary();
547        let profiling = profiling_boundary.enabled();
548        let poll_started = profiling.then(std::time::Instant::now);
549        let ran = self.poll_inner(task_context, profiling);
550        if let Some(started) = poll_started {
551            crate::script_stats::record_poll(started.elapsed(), ran);
552        }
553        ran
554    }
555}
556
557impl ScriptDocument {
558    /// Move a semantic automation pointer to the exact resolved DOM node.
559    ///
560    /// Normal window input remains coordinate hit-tested through
561    /// [`Document::handle_ui_event`]. Debug-control callers already selected a
562    /// node by id, so routing those coordinates through hit testing again can
563    /// silently select an overlapping descendant or retained overlay.
564    pub fn handle_pointer_move_to_node(&mut self, event: BlitzPointerEvent, node_id: NodeId) {
565        let profiling_boundary = self.runtime.ctx.enter_profiling_boundary();
566        let profiling = profiling_boundary.enabled();
567        let handler = ScriptEventHandler {
568            runtime: &mut self.runtime,
569            profiling,
570        };
571        let mut driver = EventDriver::new(&mut self.inner, handler);
572        driver.handle_pointer_move_to_node(&event, node_id);
573
574        self.request_redraw();
575        self.arm_timer_thread();
576    }
577
578    /// The real poll. Split out so every exit path is timed by the wrapper
579    /// above rather than by a stopwatch threaded through each early return.
580    fn poll_inner(&mut self, task_context: Option<TaskContext>, profiling: bool) -> bool {
581        // Store the waker so the timer thread can wake the event loop
582        if let Some(cx) = &task_context {
583            let mut waker = self.waker.lock().unwrap();
584            let stale = waker
585                .as_ref()
586                .map(|old| !old.will_wake(cx.waker()))
587                .unwrap_or(true);
588            if stale {
589                *waker = Some(cx.waker().clone());
590            }
591        }
592
593        // A scripted document may itself be an embedder. Chuzz's Solid chrome
594        // is one: its `<web-view>` elements own the page documents. Poll those
595        // children at the same outer boundary so their timers, resource
596        // completions, and script work continue to make progress.
597        let subdocument_changes = self
598            .inner
599            .borrow_mut()
600            .poll_subdocuments(task_context.as_ref().map(TaskContext::waker));
601
602        // Execute scripts on first poll if they haven't been run explicitly
603        let mut ran = subdocument_changes;
604        if !self.scripts_executed {
605            // One-time: parsing and running the application bundle. Separated
606            // because it is startup cost, and folding it into the steady-state
607            // numbers made every per-poll average meaningless.
608            let started = profiling.then(std::time::Instant::now);
609            self.execute_scripts();
610            if let Some(started) = started {
611                crate::script_stats::record_work("startup:execute_scripts", started.elapsed());
612            }
613            ran = true;
614        } else {
615            // Steady state: pick up any script the page appended since the last
616            // turn. Cheap when there are none, and it is the only path by which
617            // a runtime-injected bundle ever runs.
618            self.run_pending_scripts();
619        }
620
621        ran |= self.runtime.run_due_timers(profiling);
622
623        // A module that opened with a top-level `await` may have settled since
624        // the last turn. Checking here is what turns a silent half-mounted page
625        // into a reported rejection.
626        self.runtime.poll_module_evaluations();
627
628        if let Some(mut hook) = self.poll_hook.take() {
629            // The embedder's per-poll work. For a Solid application this is
630            // where reactive updates and DOM mutation actually happen, so it is
631            // the bucket that matters once startup is excluded.
632            let started = profiling.then(std::time::Instant::now);
633            ran |= hook(self, task_context.as_ref());
634            if let Some(started) = started {
635                crate::script_stats::record_work("poll_hook", started.elapsed());
636            }
637            self.poll_hook = Some(hook);
638        }
639
640        // Reclaim what removal could not judge at the time. A node is detached
641        // while script may still hold its wrapper, and the answer only becomes
642        // knowable once the collector has run; this is the point where asking
643        // is cheap and the answer is current.
644        self.runtime.sweep_detached_nodes();
645
646        self.arm_timer_thread();
647        ran
648    }
649}