Skip to main content

obeli_sk_boa_runtime/store/
mod.rs

1//! Module containing the types related to the [`JsValueStore`].
2use boa_engine::bigint::RawBigInt;
3use boa_engine::builtins::array_buffer::{AlignedVec, SharedArrayBuffer};
4use boa_engine::builtins::error::ErrorKind;
5use boa_engine::builtins::typed_array::TypedArrayKind;
6use boa_engine::value::TryIntoJs;
7use boa_engine::{Context, JsError, JsResult, JsString, JsValue, JsVariant, js_error};
8use rustc_hash::FxHashSet;
9use std::sync::Arc;
10
11mod from;
12mod to;
13
14/// Convenience method to avoid copy-pasting the same message.
15#[inline]
16fn unsupported_type() -> JsError {
17    js_error!(Error: "DataCloneError: unsupported type for structured data")
18}
19
20#[inline]
21fn unsupported_transfer() -> JsError {
22    js_error!(TypeError: "Found an invalid value in transferList")
23}
24
25/// A type to help store [`JsString`]. Because [`JsString`] relies on [`std::rc::Rc`],
26/// it cannot be `Send`, which is a necessary contract for the Store. The [`StringStore`]
27/// can be transformed from and into `JsString`, but owns its data. It is _not_ copy-on-
28/// write.
29#[derive(Debug, Eq, PartialEq, Hash)]
30struct StringStore(Vec<u16>);
31
32impl StringStore {
33    fn to_js_string(&self) -> JsString {
34        JsString::from(self.0.as_slice())
35    }
36}
37
38impl From<JsString> for StringStore {
39    fn from(value: JsString) -> Self {
40        Self(value.to_vec())
41    }
42}
43
44impl From<StringStore> for JsString {
45    fn from(value: StringStore) -> Self {
46        JsString::from(value.0.as_slice())
47    }
48}
49
50/// Inner value for [`JsValueStore`].
51#[derive(Debug)]
52enum ValueStoreInner {
53    /// An Empty value that will be filled later. This is only used during
54    /// construction, and if encountered at other points will result
55    /// in an error.
56    Empty,
57
58    /// Primitive values - `null`.
59    Null,
60
61    /// Primitive values - `undefined`.
62    Undefined,
63
64    /// Primitive values - `Boolean`.
65    Boolean(bool),
66
67    /// Primitive values - `float64`. No need to store integers separately,
68    /// they'll be checked when recreating the `JsValue`.
69    Float(f64),
70
71    /// [`JsString`]s are context-free, but not `Send`. Since we want to be
72    /// `Send`, we'll have to make a copy of the data.
73    String(StringStore),
74
75    /// [`boa_engine::JsBigInt`]s are context-free but not `Send`. The Raw version
76    /// of it is, though.
77    BigInt(RawBigInt),
78
79    /// A dictionary of strings to values which should be reconstructed into
80    /// a `JsObject`. Note: the prototype and constructor are not maintained,
81    /// and during reconstruction the default `Object` prototype will be used.
82    Object(Vec<(StringStore, JsValueStore)>),
83
84    /// A `Map()` object in JavaScript.
85    Map(Vec<(JsValueStore, JsValueStore)>),
86
87    /// A `Set()` object in JavaScript. The elements are already unique at
88    /// construction.
89    Set(Vec<JsValueStore>),
90
91    /// An `Array` object in JavaScript.
92    Array(Vec<Option<JsValueStore>>),
93
94    /// A `Date` object in JavaScript. Although this can be marshaled, it uses
95    /// the system's datetime library to be reconstructed and may diverge.
96    Date(f64),
97
98    /// Allowed error types (see the structured clone algorithm page).
99    #[expect(unused)]
100    Error {
101        kind: ErrorKind,
102        name: StringStore,
103        message: StringStore,
104        stack: StringStore,
105        cause: StringStore,
106    },
107
108    /// Regular expression. We store the expression and its flags. Everything else
109    /// is reset. These are extracted as `String`, so we don't need to use the
110    /// [`StringStore`] type.
111    RegExp { source: String, flags: String },
112
113    /// Array Buffer.
114    ArrayBuffer(AlignedVec<u8>),
115
116    /// Shared Array Buffer.
117    SharedArrayBuffer(SharedArrayBuffer),
118
119    /// Dataview.
120    #[expect(unused)]
121    DataView {
122        buffer: JsValueStore,
123        byte_length: u64,
124        byte_offset: u64,
125    },
126
127    /// Typed Array, including its kind and data.
128    TypedArray {
129        kind: TypedArrayKind,
130        buffer: JsValueStore,
131    },
132}
133
134/// A [`JsValue`]-like structure that can rebuild its value given any [`Context`].
135/// It essentially stores the value itself and its original type. During
136/// reconstruction, the constructors of the new [`Context`] will be used.
137///
138/// This follows the rules of the [structured clone algorithm][sca], but does not
139/// require a [`Context`] to copy/move, and can be [`Send`] between threads.
140///
141/// It is not serializable as it allows recursive values.
142///
143/// To transform a [`JsValue`] into a [`JsValueStore`], the application MUST
144/// pass in the context of the initial value. To transform it back to a
145/// [`JsValue`], the application MUST pass the context that will contain
146/// all prototypes for the new types (e.g. Object).
147///
148/// [sca]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
149#[derive(Debug, Clone)]
150pub struct JsValueStore(Arc<ValueStoreInner>);
151
152impl TryIntoJs for JsValueStore {
153    fn try_into_js(&self, context: &mut Context) -> JsResult<JsValue> {
154        let mut seen = to::ReverseSeenMap::default();
155        to::try_value_into_js(self, &mut seen, context)
156    }
157}
158
159impl JsValueStore {
160    /// Replace the inner content with a new inner. This is necessary as the inner
161    /// content holder must be allocated before its own inner content is created
162    /// (to allow for recursive data). Therefore, the pattern is to create the
163    /// store with an empty inner, then create the sub-content, and replace the
164    /// empty inner with the new inner.
165    ///
166    /// # SAFETY
167    /// This should only be done if the inner content is [`ValueStoreInner::Empty`],
168    /// and only by the creator of the current [`JsValueStore`]. We enforce the first
169    /// rule at runtime (and will panic), and the second rule by requiring a mutable
170    /// reference. This is still unsafe and relies on unsafe pointer access.
171    unsafe fn replace(&mut self, other: ValueStoreInner) {
172        let ptr = Arc::as_ptr(&self.0).cast_mut();
173
174        assert!(!ptr.is_null());
175        unsafe {
176            assert!(
177                matches!(*ptr, ValueStoreInner::Empty),
178                "ValueStoreInner must be empty."
179            );
180
181            *ptr = other;
182        }
183    }
184
185    /// A still-being-constructed value.
186    fn empty() -> Self {
187        Self(Arc::new(ValueStoreInner::Empty))
188    }
189
190    fn new(inner: ValueStoreInner) -> Self {
191        Self(Arc::new(inner))
192    }
193
194    /// Create a context-free [`JsValue`] equivalent from an existing `JsValue` and the
195    /// [`Context`] that was used to create it. The `transfer` argument allows for
196    /// transferring ownership of the inner data to the context-free value instead of
197    /// cloning it. By default, if a value isn't in the transfer vector, it is cloned.
198    ///
199    /// # Errors
200    /// Any errors related to transferring or cloning a value's inner data.
201    pub fn try_from_js(
202        value: &JsValue,
203        context: &mut Context,
204        transfer: Vec<JsValue>,
205    ) -> JsResult<Self> {
206        let mut seen = from::SeenMap::default();
207        // Verify the validity of the transfer list and make it a set.
208        let transfer = transfer
209            .into_iter()
210            .map(|v| match v.variant() {
211                JsVariant::Object(o) if from::is_transferable(&o) => Ok(o),
212                _ => Err(unsupported_transfer()),
213            })
214            .collect::<Result<FxHashSet<_>, _>>()?;
215
216        let v = from::try_from_js_value(value, &transfer, &mut seen, context)?;
217        Ok(v)
218    }
219}