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
16pub type Id = u64;
18pub type ContentMap = HashMap<Id, Arc<Content>>;
20pub type ContentList<'l> = &'l [(Id, Arc<Content>)];
22
23static mut DOCUMENT_ROOT: Option<web_sys::Element> = None;
24
25pub fn document() -> Document {
27 web_sys::window().unwrap().document().unwrap()
28}
29
30pub 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)]
54pub enum ContentType {
56 Module,
58 Script,
60 Style,
62}
63
64impl ContentType {
65 pub fn is_js(&self) -> bool {
67 self == &ContentType::Script || self == &ContentType::Module
68 }
69}
70
71#[allow(dead_code)]
72pub enum Reference {
75 Module,
77 Script,
79 Style,
81 Export,
83}
84
85#[allow(dead_code)]
86#[derive(Debug, Clone)]
87pub enum ContentStatus {
89 Loaded,
91 Exists,
93 Error,
95}
96
97pub struct Content {
101 pub content_type: ContentType,
103 pub url: Mutex<Option<String>>,
105 pub id: Id,
107 pub ident: &'static str,
109 pub content: &'static str,
111 pub references: Option<&'static [(Reference, Option<&'static str>, Id)]>,
114 pub is_loaded: AtomicBool,
116}
117
118impl Content {
122 pub fn url(&self) -> Option<String> {
125 self.url.lock().unwrap().clone()
126 }
127
128 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 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 }
208 Ok(())
209 }
210 .boxed()
211 }
212
213 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 let (sender, receiver) = oneshot();
248 let url = self.create_blob_url(ctx)?;
249
250 let callback = callback!(move |_event: web_sys::CustomEvent| {
252 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
315pub struct Context {
318 pub content: Arc<Mutex<ContentMap>>,
320 pub lookup_handler: LookupHandler<Id, ContentStatus, Error>,
323 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 declare(&self, content: ContentList) {
349 self.content.lock().unwrap().extend(content.iter().cloned());
350 }
355
356 pub fn get(&self, id: &Id) -> Option<Arc<Content>> {
358 self.content.lock().unwrap().get(id).cloned()
359 }
360
361 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 pub async fn load_ids(self: &Arc<Self>, list: &[Id]) -> Result<()> {
384 let start = Instant::now();
385
386 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 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
429pub 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
443pub fn declare(content: ContentList) -> Arc<Context> {
446 let ctx = context();
447 ctx.declare(content);
448 ctx
449}