workflow_dom/
loader.rs

1use crate::error::Error;
2use crate::result::Result;
3use futures::future::{join_all, BoxFuture, FutureExt};
4use js_sys::{Array, Uint8Array};
5use std::collections::HashMap;
6use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
7use std::sync::Arc;
8use std::sync::Mutex;
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
16pub type Id = u64;
17pub type ContentMap = HashMap<Id, Arc<Content>>;
18pub type ContentList<'l> = &'l [(Id, Arc<Content>)];
19
20static mut DOCUMENT_ROOT: Option<web_sys::Element> = None;
21
22pub fn document() -> Document {
23    web_sys::window().unwrap().document().unwrap()
24}
25
26pub fn root() -> web_sys::Element {
27    unsafe {
28        match DOCUMENT_ROOT.as_ref() {
29            Some(root) => root.clone(),
30            None => {
31                let root = {
32                    let collection = document().get_elements_by_tag_name("head");
33                    if collection.length() > 0 {
34                        collection.item(0).unwrap()
35                    } else {
36                        document().get_elements_by_tag_name("body").item(0).unwrap()
37                    }
38                };
39                DOCUMENT_ROOT = Some(root.clone());
40                root
41            }
42        }
43    }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum ContentType {
48    Module,
49    Script,
50    Style,
51}
52
53impl ContentType {
54    pub fn is_js(&self) -> bool {
55        self == &ContentType::Script || self == &ContentType::Module
56    }
57}
58
59#[allow(dead_code)]
60pub enum Reference {
61    Module,
62    Script,
63    Style,
64    Export,
65}
66
67#[allow(dead_code)]
68#[derive(Debug, Clone)]
69pub enum ContentStatus {
70    Loaded,
71    Exists,
72    Error,
73}
74
75pub struct Content {
76    pub content_type: ContentType,
77    pub url: Mutex<Option<String>>,
78    pub id: Id,
79    pub ident: &'static str,
80    pub content: &'static str,
81    pub references: Option<&'static [(Reference, Option<&'static str>, Id)]>,
82    pub is_loaded: AtomicBool,
83}
84
85// unsafe impl Send for Module {}
86// unsafe impl Sync for Module {}
87
88impl Content {
89    pub fn url(&self) -> Option<String> {
90        self.url.lock().unwrap().clone()
91    }
92
93    // fn content(&self, ctx: &Context) -> Result<String> {
94    fn content(&self, ctx: &Context) -> Result<String> {
95        let mut text = String::new();
96
97        if let Some(references) = &self.references {
98            let mut imports = Vec::new();
99            let mut exports = Vec::new();
100
101            for (kind, what, id) in references.iter() {
102                let module = ctx
103                    .get(id)
104                    .ok_or(format!("unable to lookup module `{}`", self.ident))?;
105                let url = module
106                    .url()
107                    .ok_or(format!("[{}] module is not loaded `{}`", self.ident, id))?;
108                match kind {
109                    Reference::Module => match what {
110                        Some(detail) => {
111                            imports.push(format!("import {detail} from \"{url}\";"));
112                        }
113                        None => {
114                            imports.push(format!("import \"{url}\";"));
115                        }
116                    },
117                    Reference::Export => {
118                        let module = ctx
119                            .get(id)
120                            .ok_or(format!("unable to lookup module `{}`", self.ident))?;
121                        let url = module
122                            .url()
123                            .ok_or(format!("[{}] module is not loaded `{}`", self.ident, id))?;
124                        exports.push(format!("export {} from \"{}\";", what.unwrap(), url));
125                    }
126                    _ => {}
127                }
128            }
129
130            let imports = imports.join("\n");
131            let exports = exports.join("\n");
132
133            text += &imports;
134            text += self.content;
135            text += &exports;
136            Ok(text)
137        } else {
138            Ok(self.content.to_string())
139        }
140    }
141
142    pub fn is_loaded(&self) -> bool {
143        self.is_loaded.load(Ordering::SeqCst)
144    }
145
146    fn load_deps(self: Arc<Self>, ctx: Arc<Context>) -> BoxFuture<'static, Result<()>> {
147        async move {
148            if let Some(references) = &self.references {
149                let futures = references
150                    .iter()
151                    .filter_map(|(_, _, id)| {
152                        if let Some(content) = ctx.get(id) {
153                            if !content.is_loaded.load(Ordering::SeqCst) {
154                                Some(content.load(&ctx))
155                            } else {
156                                None
157                            }
158                        } else {
159                            log_error!("Unable to locate module {}", id);
160                            None
161                        }
162                    })
163                    .collect::<Vec<_>>();
164
165                join_all(futures).await;
166
167                // for future in futures {
168                //     future.await?;
169                // }
170            }
171            Ok(())
172        }
173        .boxed()
174    }
175
176    pub async fn load(self: Arc<Self>, ctx: &Arc<Context>) -> Result<ContentStatus> {
177        ctx.load_content(self).await
178    }
179
180    fn create_blob_url(&self, ctx: &Arc<Context>) -> Result<String> {
181        let content = self.content(ctx)?;
182        let args = Array::new_with_length(1);
183        args.set(0, unsafe { Uint8Array::view(content.as_bytes()).into() });
184        let options = web_sys::BlobPropertyBag::new();
185        match self.content_type {
186            ContentType::Module | ContentType::Script => {
187                options.set_type("application/javascript");
188            }
189            ContentType::Style => {
190                options.set_type("text/css");
191            }
192        }
193
194        let blob = Blob::new_with_u8_array_sequence_and_options(&args, &options)?;
195        let url = Url::create_object_url_with_blob(&blob)?;
196        self.url.lock().unwrap().replace(url.clone());
197        Ok(url)
198    }
199
200    async fn load_impl(self: &Arc<Self>, ctx: &Arc<Context>) -> Result<ContentStatus> {
201        if self.is_loaded() {
202            return Ok(ContentStatus::Exists);
203        }
204
205        self.clone().load_deps(ctx.clone()).await?;
206        // log_info!("load ... {}", self.ident);
207
208        let (sender, receiver) = oneshot();
209        let url = self.create_blob_url(ctx)?;
210
211        // let ident = self.ident.clone();
212        let callback = callback!(move |_event: web_sys::CustomEvent| {
213            // log_info!("{} ... done", ident);
214            // TODO - analyze event
215            let status = ContentStatus::Loaded;
216            sender.try_send(status).expect("unable to post load event");
217        });
218
219        match &self.content_type {
220            ContentType::Module | ContentType::Script => {
221                self.inject_script(&url, &callback)?;
222            }
223            ContentType::Style => {
224                self.inject_style(&url, &callback)?;
225            }
226        };
227        let status = receiver.recv().await.expect("unable to recv() load event");
228        self.is_loaded.store(true, Ordering::SeqCst);
229        Ok(status)
230    }
231
232    fn inject_script<C>(&self, url: &str, callback: &C) -> Result<()>
233    where
234        C: AsRef<js_sys::Function>,
235    {
236        let script = document().create_element("script")?;
237        script.add_event_listener_with_callback("load", callback.as_ref())?;
238
239        match &self.content_type {
240            ContentType::Module => {
241                script.set_attribute("module", "true")?;
242                script.set_attribute("type", "module")?;
243            }
244            ContentType::Script => {
245                script.set_attribute("type", "application/javascript")?;
246            }
247            _ => {
248                panic!(
249                    "inject_script() unsupported content type `{:?}`",
250                    self.content_type
251                )
252            }
253        }
254        script.set_attribute("src", url)?;
255        script.set_attribute("id", self.ident)?;
256        root().append_child(&script)?;
257        Ok(())
258    }
259
260    fn inject_style<C>(&self, url: &str, callback: &C) -> Result<()>
261    where
262        C: AsRef<js_sys::Function>,
263    {
264        let style = document().create_element("link")?;
265        style.add_event_listener_with_callback("load", callback.as_ref())?;
266        style.set_attribute("type", "text/css")?;
267        style.set_attribute("rel", "stylesheet")?;
268        style.set_attribute("href", url)?;
269        style.set_attribute("id", self.ident)?;
270        root().append_child(&style)?;
271        println!("injecting style `{}`", self.ident);
272        Ok(())
273    }
274}
275
276pub struct Context {
277    pub content: Arc<Mutex<ContentMap>>,
278    pub lookup_handler: LookupHandler<Id, ContentStatus, Error>,
279    pub loaded: AtomicUsize,
280}
281
282impl Default for Context {
283    fn default() -> Self {
284        Context {
285            content: Arc::new(Mutex::new(ContentMap::new())),
286            lookup_handler: LookupHandler::new(),
287            loaded: AtomicUsize::new(0),
288        }
289    }
290}
291
292impl Context {
293    // pub fn new(content : ContentMap) -> Context {
294    //     Context {
295    //         content : Arc::new(Mutex::new(content)),
296    //         lookup_handler: LookupHandler::new(),
297    //         loaded : AtomicUsize::new(0),
298    //     }
299    // }
300
301    pub fn declare(&self, content: ContentList) {
302        self.content.lock().unwrap().extend(content.iter().cloned());
303        // let mut map = self.content.lock().unwrap();
304        // for (id, content) in content.iter() {
305        //     map.insert(*id,content.clone());
306        // }
307    }
308
309    pub fn get(&self, id: &Id) -> Option<Arc<Content>> {
310        self.content.lock().unwrap().get(id).cloned()
311    }
312
313    pub async fn load_content(self: &Arc<Self>, content: Arc<Content>) -> Result<ContentStatus> {
314        if content.is_loaded() {
315            Ok(ContentStatus::Exists)
316        } else {
317            match self.lookup_handler.queue(&content.id).await {
318                RequestType::New(receiver) => {
319                    self.loaded.fetch_add(1, Ordering::SeqCst);
320                    let result = content.load_impl(self).await;
321                    self.lookup_handler.complete(&content.id, result).await;
322                    receiver.recv().await?
323                }
324                RequestType::Pending(receiver) => receiver.recv().await?,
325            }
326        }
327    }
328
329    pub async fn load_ids(self: &Arc<Self>, list: &[Id]) -> Result<()> {
330        let start = Instant::now();
331
332        // let mut futures = Vec::with_capacity(list.len());
333        // for id in list {
334        //     if let Some(module) = self.get(id) {
335        //         futures.push(module.load(self));
336        //     }
337        // }
338        let futures = list
339            .iter()
340            .filter_map(|id| {
341                if let Some(module) = self.get(id) {
342                    Some(module.load(self))
343                } else {
344                    log_error!("Unable to locate module {}", id);
345                    // TODO: panic
346                    None
347                }
348            })
349            .collect::<Vec<_>>();
350
351        for future in futures {
352            match future.await {
353                Ok(_event) => {}
354                Err(err) => {
355                    log_error!("{}", err);
356                }
357            }
358        }
359
360        let elapsed = start.elapsed();
361        let loaded = self.loaded.load(Ordering::SeqCst);
362        log_info!(
363            "Loaded {} references in {} msec",
364            loaded,
365            elapsed.as_millis()
366        );
367
368        Ok(())
369    }
370}
371
372static mut CONTEXT: Option<Arc<Context>> = None;
373
374pub fn context() -> Arc<Context> {
375    unsafe {
376        if let Some(context) = CONTEXT.as_ref() {
377            context.clone()
378        } else {
379            let context = Arc::new(Context::default());
380            CONTEXT = Some(context.clone());
381            context
382        }
383    }
384}
385
386pub fn declare(content: ContentList) -> Arc<Context> {
387    let ctx = context();
388    ctx.declare(content);
389    ctx
390}