Skip to main content

workflow_dom/
loader.rs

1use crate::error::Error;
2use crate::result::Result;
3use futures::future::{BoxFuture, FutureExt, join_all};
4use js_sys::{Array, Uint8Array};
5use std::collections::HashMap;
6use std::sync::Arc;
7use std::sync::Mutex;
8use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
9use web_sys::{Blob, Document, Url};
10use workflow_core::channel::oneshot;
11use workflow_core::lookup::*;
12use workflow_core::time::*;
13use workflow_log::*;
14use workflow_wasm::callback::*;
15
16/// Unique identifier of a [`Content`] entry within a [`Context`].
17pub type Id = u64;
18/// Map of content entries keyed by their [`Id`].
19pub type ContentMap = HashMap<Id, Arc<Content>>;
20/// Borrowed slice of `(id, content)` pairs used to declare content entries.
21pub type ContentList<'l> = &'l [(Id, Arc<Content>)];
22
23static mut DOCUMENT_ROOT: Option<web_sys::Element> = None;
24
25/// Return the current browser [`web_sys::Document`].
26pub fn document() -> Document {
27    web_sys::window().unwrap().document().unwrap()
28}
29
30/// Return (lazily caching) the root element into which content is injected,
31/// preferring the document `<head>` and falling back to `<body>`.
32pub fn root() -> web_sys::Element {
33    let document_root_ptr = &raw const DOCUMENT_ROOT;
34    unsafe {
35        match (*document_root_ptr).as_ref() {
36            Some(root) => root.clone(),
37            None => {
38                let root = {
39                    let collection = document().get_elements_by_tag_name("head");
40                    if collection.length() > 0 {
41                        collection.item(0).unwrap()
42                    } else {
43                        document().get_elements_by_tag_name("body").item(0).unwrap()
44                    }
45                };
46                DOCUMENT_ROOT = Some(root.clone());
47                root
48            }
49        }
50    }
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
54/// The kind of resource a [`Content`] entry holds.
55pub enum ContentType {
56    /// A JavaScript module (injected as `<script type="module">`).
57    Module,
58    /// A classic JavaScript script.
59    Script,
60    /// A CSS stylesheet.
61    Style,
62}
63
64impl ContentType {
65    /// Return `true` if this content type is JavaScript (a module or script).
66    pub fn is_js(&self) -> bool {
67        self == &ContentType::Script || self == &ContentType::Module
68    }
69}
70
71#[allow(dead_code)]
72/// The kind of dependency one [`Content`] entry declares on another,
73/// determining how the reference is emitted into the generated source.
74pub enum Reference {
75    /// An `import` of another JavaScript module.
76    Module,
77    /// A reference to another script.
78    Script,
79    /// A reference to a stylesheet.
80    Style,
81    /// An `export ... from` re-export of another module.
82    Export,
83}
84
85#[allow(dead_code)]
86#[derive(Debug, Clone)]
87/// Outcome of attempting to load a [`Content`] entry.
88pub enum ContentStatus {
89    /// The content was freshly loaded and injected into the DOM.
90    Loaded,
91    /// The content was already loaded; nothing was done.
92    Exists,
93    /// Loading failed.
94    Error,
95}
96
97/// A single loadable DOM resource (JavaScript module, script or CSS
98/// stylesheet) together with its identity, embedded source and the other
99/// content entries it references.
100pub struct Content {
101    /// The kind of resource this content represents.
102    pub content_type: ContentType,
103    /// Object URL of the generated blob, set once the content is loaded.
104    pub url: Mutex<Option<String>>,
105    /// Unique identifier of this content entry.
106    pub id: Id,
107    /// Human-readable identifier, used as the injected element's `id`.
108    pub ident: &'static str,
109    /// The embedded source text of the resource.
110    pub content: &'static str,
111    /// Other content entries this resource imports from or exports, each as a
112    /// reference kind, optional detail string and target content id.
113    pub references: Option<&'static [(Reference, Option<&'static str>, Id)]>,
114    /// Whether this content has already been injected into the DOM.
115    pub is_loaded: AtomicBool,
116}
117
118// unsafe impl Send for Module {}
119// unsafe impl Sync for Module {}
120
121impl Content {
122    /// Return the object URL of this content's blob once it has been created,
123    /// or `None` if it has not yet been loaded.
124    pub fn url(&self) -> Option<String> {
125        self.url.lock().unwrap().clone()
126    }
127
128    // fn content(&self, ctx: &Context) -> Result<String> {
129    fn content(&self, ctx: &Context) -> Result<String> {
130        let mut text = String::new();
131
132        if let Some(references) = &self.references {
133            let mut imports = Vec::new();
134            let mut exports = Vec::new();
135
136            for (kind, what, id) in references.iter() {
137                let module = ctx
138                    .get(id)
139                    .ok_or(format!("unable to lookup module `{}`", self.ident))?;
140                let url = module
141                    .url()
142                    .ok_or(format!("[{}] module is not loaded `{}`", self.ident, id))?;
143                match kind {
144                    Reference::Module => match what {
145                        Some(detail) => {
146                            imports.push(format!("import {detail} from \"{url}\";"));
147                        }
148                        None => {
149                            imports.push(format!("import \"{url}\";"));
150                        }
151                    },
152                    Reference::Export => {
153                        let module = ctx
154                            .get(id)
155                            .ok_or(format!("unable to lookup module `{}`", self.ident))?;
156                        let url = module
157                            .url()
158                            .ok_or(format!("[{}] module is not loaded `{}`", self.ident, id))?;
159                        exports.push(format!("export {} from \"{}\";", what.unwrap(), url));
160                    }
161                    _ => {}
162                }
163            }
164
165            let imports = imports.join("\n");
166            let exports = exports.join("\n");
167
168            text += &imports;
169            text += self.content;
170            text += &exports;
171            Ok(text)
172        } else {
173            Ok(self.content.to_string())
174        }
175    }
176
177    /// Return `true` if this content has already been injected into the DOM.
178    pub fn is_loaded(&self) -> bool {
179        self.is_loaded.load(Ordering::SeqCst)
180    }
181
182    fn load_deps(self: Arc<Self>, ctx: Arc<Context>) -> BoxFuture<'static, Result<()>> {
183        async move {
184            if let Some(references) = &self.references {
185                let futures = references
186                    .iter()
187                    .filter_map(|(_, _, id)| match ctx.get(id) {
188                        Some(content) => {
189                            if !content.is_loaded.load(Ordering::SeqCst) {
190                                Some(content.load(&ctx))
191                            } else {
192                                None
193                            }
194                        }
195                        _ => {
196                            log_error!("Unable to locate module {}", id);
197                            None
198                        }
199                    })
200                    .collect::<Vec<_>>();
201
202                join_all(futures).await;
203
204                // for future in futures {
205                //     future.await?;
206                // }
207            }
208            Ok(())
209        }
210        .boxed()
211    }
212
213    /// Load this content (and its dependencies) into the DOM through the
214    /// given [`Context`], returning the resulting [`ContentStatus`].
215    pub async fn load(self: Arc<Self>, ctx: &Arc<Context>) -> Result<ContentStatus> {
216        ctx.load_content(self).await
217    }
218
219    fn create_blob_url(&self, ctx: &Arc<Context>) -> Result<String> {
220        let content = self.content(ctx)?;
221        let args = Array::new_with_length(1);
222        args.set(0, unsafe { Uint8Array::view(content.as_bytes()).into() });
223        let options = web_sys::BlobPropertyBag::new();
224        match self.content_type {
225            ContentType::Module | ContentType::Script => {
226                options.set_type("application/javascript");
227            }
228            ContentType::Style => {
229                options.set_type("text/css");
230            }
231        }
232
233        let blob = Blob::new_with_u8_array_sequence_and_options(&args, &options)?;
234        let url = Url::create_object_url_with_blob(&blob)?;
235        self.url.lock().unwrap().replace(url.clone());
236        Ok(url)
237    }
238
239    async fn load_impl(self: &Arc<Self>, ctx: &Arc<Context>) -> Result<ContentStatus> {
240        if self.is_loaded() {
241            return Ok(ContentStatus::Exists);
242        }
243
244        self.clone().load_deps(ctx.clone()).await?;
245        // log_info!("load ... {}", self.ident);
246
247        let (sender, receiver) = oneshot();
248        let url = self.create_blob_url(ctx)?;
249
250        // let ident = self.ident.clone();
251        let callback = callback!(move |_event: web_sys::CustomEvent| {
252            // log_info!("{} ... done", ident);
253            // TODO - analyze event
254            let status = ContentStatus::Loaded;
255            sender.try_send(status).expect("unable to post load event");
256        });
257
258        match &self.content_type {
259            ContentType::Module | ContentType::Script => {
260                self.inject_script(&url, &callback)?;
261            }
262            ContentType::Style => {
263                self.inject_style(&url, &callback)?;
264            }
265        };
266        let status = receiver.recv().await.expect("unable to recv() load event");
267        self.is_loaded.store(true, Ordering::SeqCst);
268        Ok(status)
269    }
270
271    fn inject_script<C>(&self, url: &str, callback: &C) -> Result<()>
272    where
273        C: AsRef<js_sys::Function>,
274    {
275        let script = document().create_element("script")?;
276        script.add_event_listener_with_callback("load", callback.as_ref())?;
277
278        match &self.content_type {
279            ContentType::Module => {
280                script.set_attribute("module", "true")?;
281                script.set_attribute("type", "module")?;
282            }
283            ContentType::Script => {
284                script.set_attribute("type", "application/javascript")?;
285            }
286            _ => {
287                panic!(
288                    "inject_script() unsupported content type `{:?}`",
289                    self.content_type
290                )
291            }
292        }
293        script.set_attribute("src", url)?;
294        script.set_attribute("id", self.ident)?;
295        root().append_child(&script)?;
296        Ok(())
297    }
298
299    fn inject_style<C>(&self, url: &str, callback: &C) -> Result<()>
300    where
301        C: AsRef<js_sys::Function>,
302    {
303        let style = document().create_element("link")?;
304        style.add_event_listener_with_callback("load", callback.as_ref())?;
305        style.set_attribute("type", "text/css")?;
306        style.set_attribute("rel", "stylesheet")?;
307        style.set_attribute("href", url)?;
308        style.set_attribute("id", self.ident)?;
309        root().append_child(&style)?;
310        println!("injecting style `{}`", self.ident);
311        Ok(())
312    }
313}
314
315/// Registry and loading context for DOM content, tracking declared content
316/// entries, de-duplicating in-flight load requests and counting loaded items.
317pub struct Context {
318    /// Map of registered content entries keyed by their id.
319    pub content: Arc<Mutex<ContentMap>>,
320    /// Coordinates concurrent load requests so that each content id is only
321    /// loaded once while other callers await the shared result.
322    pub lookup_handler: LookupHandler<Id, ContentStatus, Error>,
323    /// Running count of content entries that have been loaded.
324    pub loaded: AtomicUsize,
325}
326
327impl Default for Context {
328    fn default() -> Self {
329        Context {
330            content: Arc::new(Mutex::new(ContentMap::new())),
331            lookup_handler: LookupHandler::new(),
332            loaded: AtomicUsize::new(0),
333        }
334    }
335}
336
337impl Context {
338    // pub fn new(content : ContentMap) -> Context {
339    //     Context {
340    //         content : Arc::new(Mutex::new(content)),
341    //         lookup_handler: LookupHandler::new(),
342    //         loaded : AtomicUsize::new(0),
343    //     }
344    // }
345
346    /// Register the given content entries with this context, making them
347    /// available for later loading by id.
348    pub fn declare(&self, content: ContentList) {
349        self.content.lock().unwrap().extend(content.iter().cloned());
350        // let mut map = self.content.lock().unwrap();
351        // for (id, content) in content.iter() {
352        //     map.insert(*id,content.clone());
353        // }
354    }
355
356    /// Return the registered [`Content`] entry for the given id, if any.
357    pub fn get(&self, id: &Id) -> Option<Arc<Content>> {
358        self.content.lock().unwrap().get(id).cloned()
359    }
360
361    /// Load a single content entry, injecting it into the DOM. Concurrent
362    /// requests for the same content are de-duplicated via the lookup handler,
363    /// and already-loaded content resolves immediately as [`ContentStatus::Exists`].
364    pub async fn load_content(self: &Arc<Self>, content: Arc<Content>) -> Result<ContentStatus> {
365        if content.is_loaded() {
366            Ok(ContentStatus::Exists)
367        } else {
368            match self.lookup_handler.queue(&content.id).await {
369                RequestType::New(receiver) => {
370                    self.loaded.fetch_add(1, Ordering::SeqCst);
371                    let result = content.load_impl(self).await;
372                    self.lookup_handler.complete(&content.id, result).await;
373                    receiver.recv().await?
374                }
375                RequestType::Pending(receiver) => receiver.recv().await?,
376            }
377        }
378    }
379
380    /// Load all content entries identified by the given list of ids,
381    /// resolving and injecting each into the DOM, and log the total number
382    /// of references loaded along with the elapsed time.
383    pub async fn load_ids(self: &Arc<Self>, list: &[Id]) -> Result<()> {
384        let start = Instant::now();
385
386        // let mut futures = Vec::with_capacity(list.len());
387        // for id in list {
388        //     if let Some(module) = self.get(id) {
389        //         futures.push(module.load(self));
390        //     }
391        // }
392        let futures = list
393            .iter()
394            .filter_map(|id| {
395                match self.get(id) {
396                    Some(module) => Some(module.load(self)),
397                    _ => {
398                        log_error!("Unable to locate module {}", id);
399                        // TODO: panic
400                        None
401                    }
402                }
403            })
404            .collect::<Vec<_>>();
405
406        for future in futures {
407            match future.await {
408                Ok(_event) => {}
409                Err(err) => {
410                    log_error!("{}", err);
411                }
412            }
413        }
414
415        let elapsed = start.elapsed();
416        let loaded = self.loaded.load(Ordering::SeqCst);
417        log_info!(
418            "Loaded {} references in {} msec",
419            loaded,
420            elapsed.as_millis()
421        );
422
423        Ok(())
424    }
425}
426
427static mut CONTEXT: Option<Arc<Context>> = None;
428
429/// Return the global, lazily-initialized loader [`Context`] singleton.
430pub fn context() -> Arc<Context> {
431    let context_ptr = &raw const CONTEXT;
432    unsafe {
433        if let Some(context) = (*context_ptr).as_ref() {
434            context.clone()
435        } else {
436            let context = Arc::new(Context::default());
437            CONTEXT = Some(context.clone());
438            context
439        }
440    }
441}
442
443/// Declare the given content entries on the global loader [`Context`],
444/// registering them for subsequent loading, and return that context.
445pub fn declare(content: ContentList) -> Arc<Context> {
446    let ctx = context();
447    ctx.declare(content);
448    ctx
449}