Skip to content

Commit 811773a

Browse files
committed
feat(clone): add structuredClone support for Error objects and related tests
1 parent f5e88de commit 811773a

File tree

6 files changed

+178
-7
lines changed

6 files changed

+178
-7
lines changed

core/runtime/src/clone/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ use boa_engine::realm::Realm;
77
use boa_engine::value::TryFromJs;
88
use boa_engine::{Context, JsResult, JsValue, boa_module};
99

10+
#[cfg(test)]
11+
mod tests;
12+
1013
/// Options used by `structuredClone`. This is currently unused.
1114
#[derive(Debug, Clone, TryFromJs)]
1215
pub struct StructuredCloneOptions {

core/runtime/src/clone/tests.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//! Tests for the `structuredClone` extension.
2+
3+
use crate::test::{TestAction, run_test_actions};
4+
5+
#[test]
6+
fn clones_error_objects() {
7+
run_test_actions([
8+
TestAction::harness(),
9+
TestAction::run(
10+
r#"
11+
const original = new Error("boom");
12+
const cloned = structuredClone(original);
13+
14+
assert(cloned instanceof Error);
15+
assert(cloned !== original);
16+
assertEq(cloned.name, "Error");
17+
assertEq(cloned.message, "boom");
18+
"#,
19+
),
20+
]);
21+
}

core/runtime/src/message/tests.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,49 @@ fn basic() {
5353
);
5454
}
5555

56+
#[test]
57+
fn basic_error_message() {
58+
let context = &mut Context::default();
59+
60+
let sender = OnMessageQueueSender::create(context);
61+
message::register(sender, None, context).unwrap();
62+
63+
run_test_actions_with(
64+
[
65+
TestAction::harness(),
66+
TestAction::run(
67+
r#"
68+
let latestMessage = null;
69+
function onMessageQueue(message) {
70+
latestMessage = message;
71+
}
72+
73+
const message = new Error("boom");
74+
postMessage(message);
75+
assert(latestMessage === null);
76+
"#,
77+
),
78+
TestAction::inspect_context(move |context| {
79+
drop(future::block_on(future::poll_once(
80+
context
81+
.downcast_job_executor::<SimpleJobExecutor>()
82+
.expect("")
83+
.run_jobs_async(&RefCell::new(context)),
84+
)));
85+
}),
86+
TestAction::run(
87+
r#"
88+
assert(latestMessage instanceof Error);
89+
assert(latestMessage !== message);
90+
assertEq(latestMessage.name, "Error");
91+
assertEq(latestMessage.message, "boom");
92+
"#,
93+
),
94+
],
95+
context,
96+
);
97+
}
98+
5699
#[test]
57100
fn shared_multi_thread() {
58101
let (sender, receiver) = std::sync::mpsc::channel::<OnMessageQueueSender>();

core/runtime/src/store/from.rs

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22
33
use crate::store::{JsValueStore, StringStore, ValueStoreInner, unsupported_type};
44
use boa_engine::builtins::array_buffer::{AlignedVec, ArrayBuffer};
5-
use boa_engine::builtins::error::Error;
5+
use boa_engine::builtins::error::{Error, ErrorKind};
66
use boa_engine::object::builtins::{
77
JsArray, JsArrayBuffer, JsDataView, JsDate, JsMap, JsRegExp, JsSet, JsSharedArrayBuffer,
88
JsTypedArray,
99
};
1010
use boa_engine::property::PropertyKey;
11-
use boa_engine::{Context, JsError, JsObject, JsResult, JsString, JsValue, JsVariant, js_error};
11+
use boa_engine::{
12+
Context, JsError, JsNativeErrorKind, JsObject, JsResult, JsString, JsValue, JsVariant,
13+
js_error, js_string,
14+
};
1215
use std::collections::{HashMap, HashSet};
1316

1417
/// A Map of seen objects when walking through the value. We use the address
@@ -181,6 +184,50 @@ fn clone_regexp(
181184
Ok(stored)
182185
}
183186

187+
fn clone_error(
188+
original: &JsObject,
189+
seen: &mut SeenMap,
190+
context: &mut Context,
191+
) -> JsResult<JsValueStore> {
192+
let native = JsError::from_opaque(JsValue::from(original.clone()))
193+
.try_native(context)
194+
.map_err(|_| unsupported_type())?;
195+
196+
let kind = match native.kind() {
197+
JsNativeErrorKind::Aggregate(_) => ErrorKind::Aggregate,
198+
JsNativeErrorKind::Eval => ErrorKind::Eval,
199+
JsNativeErrorKind::Type => ErrorKind::Type,
200+
JsNativeErrorKind::Range => ErrorKind::Range,
201+
JsNativeErrorKind::Reference => ErrorKind::Reference,
202+
JsNativeErrorKind::Syntax => ErrorKind::Syntax,
203+
JsNativeErrorKind::Uri => ErrorKind::Uri,
204+
_ => ErrorKind::Error,
205+
};
206+
207+
let to_optional_string = |key: &str, context: &mut Context| -> JsResult<JsString> {
208+
let value = original.get(js_string!(key), context)?;
209+
if value.is_undefined() {
210+
Ok(js_string!())
211+
} else {
212+
value.to_string(context)
213+
}
214+
};
215+
216+
let name = to_optional_string("name", context)?;
217+
let stack = to_optional_string("stack", context)?;
218+
let cause = to_optional_string("cause", context)?;
219+
220+
let stored = JsValueStore::new(ValueStoreInner::Error {
221+
kind,
222+
name: name.into(),
223+
message: JsString::from(native.message()).into(),
224+
stack: stack.into(),
225+
cause: cause.into(),
226+
});
227+
seen.insert(original, stored.clone());
228+
Ok(stored)
229+
}
230+
184231
fn try_from_map(
185232
original: &JsObject,
186233
map: &JsMap,
@@ -258,8 +305,8 @@ fn try_from_js_object_clone(
258305
return clone_typed_array(object, typed_array, transfer, seen, context);
259306
} else if let Ok(ref date) = JsDate::from_object(object.clone()) {
260307
return clone_date(object, date, seen, context);
261-
} else if let Ok(_error) = object.clone().downcast::<Error>() {
262-
return Err(js_error!(TypeError: "Errors are not supported yet."));
308+
} else if object.downcast_ref::<Error>().is_some() {
309+
return clone_error(object, seen, context);
263310
} else if let Ok(ref regexp) = JsRegExp::from_object(object.clone()) {
264311
return clone_regexp(object, regexp, seen, context);
265312
} else if let Ok(_dataview) = JsDataView::from_object(object.clone()) {

core/runtime/src/store/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,6 @@ enum ValueStoreInner {
9696
Date(f64),
9797

9898
/// Allowed error types (see the structured clone algorithm page).
99-
#[expect(unused)]
10099
Error {
101100
kind: ErrorKind,
102101
name: StringStore,

core/runtime/src/store/to.rs

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,28 @@
11
//! All methods for deserializing a [`JsValueStore`] into a [`JsValue`].
22
use crate::store::{JsValueStore, StringStore, ValueStoreInner, unsupported_type};
33
use boa_engine::builtins::array_buffer::{AlignedVec, SharedArrayBuffer};
4+
use boa_engine::builtins::error::ErrorKind;
45
use boa_engine::builtins::typed_array::TypedArrayKind;
56
use boa_engine::object::builtins::{
67
JsArray, JsArrayBuffer, JsDataView, JsDate, JsMap, JsRegExp, JsSet, JsSharedArrayBuffer,
78
js_typed_array_from_kind,
89
};
9-
use boa_engine::{Context, JsBigInt, JsObject, JsResult, JsString, JsValue, js_error};
10+
use boa_engine::{
11+
Context, JsBigInt, JsNativeError, JsObject, JsResult, JsString, JsValue, js_string,
12+
};
1013
use std::collections::HashMap;
1114

1215
#[derive(Default)]
1316
pub(super) struct ReverseSeenMap(HashMap<usize, JsObject>);
1417

18+
type ErrorStoreFields<'a> = (
19+
&'a ErrorKind,
20+
&'a StringStore,
21+
&'a StringStore,
22+
&'a StringStore,
23+
&'a StringStore,
24+
);
25+
1526
impl ReverseSeenMap {
1627
fn get(&self, object: &JsValueStore) -> Option<JsObject> {
1728
let addr = std::ptr::from_ref(object.0.as_ref()).addr();
@@ -150,6 +161,47 @@ fn try_into_js_date(
150161
Ok(JsValue::from(date))
151162
}
152163

164+
fn try_into_js_error(
165+
store: &JsValueStore,
166+
error_data: ErrorStoreFields<'_>,
167+
seen: &mut ReverseSeenMap,
168+
context: &mut Context,
169+
) -> JsResult<JsValue> {
170+
let (kind, name, message, stack, cause) = error_data;
171+
let message = message.to_js_string().to_std_string_escaped();
172+
let native = match kind {
173+
ErrorKind::Aggregate => JsNativeError::aggregate(Vec::new()),
174+
ErrorKind::Eval => JsNativeError::eval(),
175+
ErrorKind::Type => JsNativeError::typ(),
176+
ErrorKind::Range => JsNativeError::range(),
177+
ErrorKind::Reference => JsNativeError::reference(),
178+
ErrorKind::Syntax => JsNativeError::syntax(),
179+
ErrorKind::Uri => JsNativeError::uri(),
180+
_ => JsNativeError::error(),
181+
}
182+
.with_message(message);
183+
184+
let error = native.into_opaque(context);
185+
seen.insert(store, error.clone());
186+
187+
let name = name.to_js_string();
188+
if !name.is_empty() {
189+
error.set(js_string!("name"), name, true, context)?;
190+
}
191+
192+
let stack = stack.to_js_string();
193+
if !stack.is_empty() {
194+
error.set(js_string!("stack"), stack, true, context)?;
195+
}
196+
197+
let cause = cause.to_js_string();
198+
if !cause.is_empty() {
199+
error.set(js_string!("cause"), cause, true, context)?;
200+
}
201+
202+
Ok(JsValue::from(error))
203+
}
204+
153205
fn try_into_regexp(
154206
store: &JsValueStore,
155207
source: &str,
@@ -207,7 +259,13 @@ pub(super) fn try_value_into_js(
207259
ValueStoreInner::Set(values) => try_into_js_set(store, values, seen, context),
208260
ValueStoreInner::Array(items) => try_items_into_js_array(store, items, seen, context),
209261
ValueStoreInner::Date(msec) => try_into_js_date(store, *msec, seen, context),
210-
ValueStoreInner::Error { .. } => Err(js_error!("Not yet implemented.")),
262+
ValueStoreInner::Error {
263+
kind,
264+
name,
265+
message,
266+
stack,
267+
cause,
268+
} => try_into_js_error(store, (kind, name, message, stack, cause), seen, context),
211269
ValueStoreInner::RegExp { source, flags } => {
212270
try_into_regexp(store, source, flags, seen, context)
213271
}

0 commit comments

Comments
 (0)