tinymist_package/registry/
browser.rs

1//! Browser proxy registry for tinymist. You should implement interfaces in js.
2
3use std::{io::Read, path::Path};
4
5use js_sys::Uint8Array;
6use tinymist_std::ImmutPath;
7use typst::diag::{EcoString, eco_format};
8use wasm_bindgen::{JsValue, prelude::*};
9
10use super::{PackageError, PackageRegistry, PackageSpec};
11
12/// The `ProxyContext` struct is a wrapper around a JavaScript this.
13#[wasm_bindgen]
14#[derive(Debug, Clone)]
15pub struct ProxyContext {
16    context: JsValue,
17}
18
19#[wasm_bindgen]
20impl ProxyContext {
21    /// Creates a new `ProxyContext` instance.
22    #[wasm_bindgen(constructor)]
23    pub fn new(context: JsValue) -> Self {
24        Self { context }
25    }
26
27    /// Returns the JavaScript this.
28    #[wasm_bindgen(getter)]
29    pub fn context(&self) -> JsValue {
30        self.context.clone()
31    }
32
33    /// A convenience function to untar a tarball and call a callback for each
34    /// entry.
35    pub fn untar(&self, data: &[u8], cb: js_sys::Function) -> Result<(), JsValue> {
36        let cb = move |key: String, value: &[u8], mtime: u64| -> Result<(), JsValue> {
37            let key = JsValue::from_str(&key);
38            let value = Uint8Array::from(value);
39            let mtime = JsValue::from_f64(mtime as f64);
40            cb.call3(&self.context, &key, &value, &mtime).map(|_| ())
41        };
42
43        let decompressed = flate2::read::GzDecoder::new(data);
44        let mut reader = tar::Archive::new(decompressed);
45        let entries = reader.entries();
46        let entries = entries.map_err(|err| {
47            let t = PackageError::MalformedArchive(Some(eco_format!("{err}")));
48            JsValue::from_str(&format!("{t:?}"))
49        })?;
50
51        let mut buf = Vec::with_capacity(1024);
52        for entry in entries {
53            // Read single entry
54            let mut entry = entry.map_err(|e| format!("{e:?}"))?;
55            let header = entry.header();
56
57            let is_file = header.entry_type().is_file();
58            if !is_file {
59                continue;
60            }
61
62            let mtime = header.mtime().unwrap_or(0);
63
64            let path = header.path().map_err(|e| format!("{e:?}"))?;
65            let path = path.to_string_lossy().as_ref().to_owned();
66
67            let size = header.size().map_err(|e| format!("{e:?}"))?;
68            buf.clear();
69            buf.reserve(size as usize);
70            entry.read_to_end(&mut buf).map_err(|e| format!("{e:?}"))?;
71
72            cb(path, &buf, mtime)?
73        }
74
75        Ok(())
76    }
77}
78
79/// The `JsRegistry` struct is a wrapper around a JavaScript function that
80#[derive(Debug)]
81pub struct JsRegistry {
82    /// The JavaScript this context.
83    pub context: ProxyContext,
84    /// The JavaScript function to call for resolving packages.
85    pub real_resolve_fn: js_sys::Function,
86}
87
88impl PackageRegistry for JsRegistry {
89    fn resolve(&self, spec: &PackageSpec) -> Result<std::sync::Arc<Path>, PackageError> {
90        // prepare js_spec
91        let js_spec = js_sys::Object::new();
92        js_sys::Reflect::set(&js_spec, &"name".into(), &spec.name.to_string().into()).unwrap();
93        js_sys::Reflect::set(
94            &js_spec,
95            &"namespace".into(),
96            &spec.namespace.to_string().into(),
97        )
98        .unwrap();
99        js_sys::Reflect::set(
100            &js_spec,
101            &"version".into(),
102            &spec.version.to_string().into(),
103        )
104        .unwrap();
105
106        self.real_resolve_fn
107            .call1(&self.context.clone().into(), &js_spec)
108            .map_err(|e| PackageError::Other(Some(eco_format!("{:?}", e))))
109            .and_then(|v| {
110                if v.is_undefined() {
111                    Err(PackageError::NotFound(spec.clone()))
112                } else {
113                    Ok(Path::new(&v.as_string().unwrap()).into())
114                }
115            })
116    }
117
118    // todo: provide package list for browser
119    fn packages(&self) -> &[(PackageSpec, Option<EcoString>)] {
120        &[]
121    }
122}
123
124impl JsRegistry {
125    /// Returns the path at which non-local packages should be stored when
126    /// downloaded.
127    pub fn package_cache_path(&self) -> Option<&ImmutPath> {
128        None
129    }
130
131    /// Returns the path at which local packages are stored.
132    pub fn package_path(&self) -> Option<&ImmutPath> {
133        None
134    }
135}
136
137// todo
138/// Safety: `JsRegistry` is only used in the browser environment, and we cannot
139/// share data between workers.
140unsafe impl Send for JsRegistry {}
141/// Safety: `JsRegistry` is only used in the browser environment, and we cannot
142/// share data between workers.
143unsafe impl Sync for JsRegistry {}