tinymist_package/registry/
browser.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
//! Browser proxy registry for tinymist. You should implement interfaces in js.

use std::{io::Read, path::Path};

use js_sys::Uint8Array;
use typst::diag::{eco_format, EcoString};
use wasm_bindgen::{prelude::*, JsValue};

use super::{PackageError, PackageRegistry, PackageSpec};

/// The `ProxyContext` struct is a wrapper around a JavaScript this.
#[wasm_bindgen]
#[derive(Debug, Clone)]
pub struct ProxyContext {
    context: JsValue,
}

#[wasm_bindgen]
impl ProxyContext {
    /// Creates a new `ProxyContext` instance.
    #[wasm_bindgen(constructor)]
    pub fn new(context: JsValue) -> Self {
        Self { context }
    }

    /// Returns the JavaScript this.
    #[wasm_bindgen(getter)]
    pub fn context(&self) -> JsValue {
        self.context.clone()
    }

    /// A convenience function to untar a tarball and call a callback for each
    /// entry.
    pub fn untar(&self, data: &[u8], cb: js_sys::Function) -> Result<(), JsValue> {
        let cb = move |key: String, value: &[u8], mtime: u64| -> Result<(), JsValue> {
            let key = JsValue::from_str(&key);
            let value = Uint8Array::from(value);
            let mtime = JsValue::from_f64(mtime as f64);
            cb.call3(&self.context, &key, &value, &mtime).map(|_| ())
        };

        let decompressed = flate2::read::GzDecoder::new(data);
        let mut reader = tar::Archive::new(decompressed);
        let entries = reader.entries();
        let entries = entries.map_err(|err| {
            let t = PackageError::MalformedArchive(Some(eco_format!("{err}")));
            JsValue::from_str(&format!("{t:?}"))
        })?;

        let mut buf = Vec::with_capacity(1024);
        for entry in entries {
            // Read single entry
            let mut entry = entry.map_err(|e| format!("{e:?}"))?;
            let header = entry.header();

            let is_file = header.entry_type().is_file();
            if !is_file {
                continue;
            }

            let mtime = header.mtime().unwrap_or(0);

            let path = header.path().map_err(|e| format!("{e:?}"))?;
            let path = path.to_string_lossy().as_ref().to_owned();

            let size = header.size().map_err(|e| format!("{e:?}"))?;
            buf.clear();
            buf.reserve(size as usize);
            entry.read_to_end(&mut buf).map_err(|e| format!("{e:?}"))?;

            cb(path, &buf, mtime)?
        }

        Ok(())
    }
}

/// The `JsRegistry` struct is a wrapper around a JavaScript function that
#[derive(Debug)]
pub struct JsRegistry {
    /// The JavaScript this context.
    pub context: ProxyContext,
    /// The JavaScript function to call for resolving packages.
    pub real_resolve_fn: js_sys::Function,
}

impl PackageRegistry for JsRegistry {
    fn resolve(&self, spec: &PackageSpec) -> Result<std::sync::Arc<Path>, PackageError> {
        // prepare js_spec
        let js_spec = js_sys::Object::new();
        js_sys::Reflect::set(&js_spec, &"name".into(), &spec.name.to_string().into()).unwrap();
        js_sys::Reflect::set(
            &js_spec,
            &"namespace".into(),
            &spec.namespace.to_string().into(),
        )
        .unwrap();
        js_sys::Reflect::set(
            &js_spec,
            &"version".into(),
            &spec.version.to_string().into(),
        )
        .unwrap();

        self.real_resolve_fn
            .call1(&self.context.clone().into(), &js_spec)
            .map_err(|e| PackageError::Other(Some(eco_format!("{:?}", e))))
            .and_then(|v| {
                if v.is_undefined() {
                    Err(PackageError::NotFound(spec.clone()))
                } else {
                    Ok(Path::new(&v.as_string().unwrap()).into())
                }
            })
    }

    // todo: provide package list for browser
    fn packages(&self) -> &[(PackageSpec, Option<EcoString>)] {
        &[]
    }
}

// todo
/// Safety: `JsRegistry` is only used in the browser environment, and we cannot
/// share data between workers.
unsafe impl Send for JsRegistry {}
/// Safety: `JsRegistry` is only used in the browser environment, and we cannot
/// share data between workers.
unsafe impl Sync for JsRegistry {}