Skip to content

Commit 0f3a392

Browse files
committed
fix(clone): enhance error handling by supporting cause and errors in JsValueStore
1 parent cd37c5c commit 0f3a392

File tree

5 files changed

+218
-18
lines changed

5 files changed

+218
-18
lines changed

core/runtime/src/clone/tests.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,56 @@ fn clones_error_objects() {
1919
),
2020
]);
2121
}
22+
23+
#[test]
24+
fn clones_error_object_cause() {
25+
run_test_actions([
26+
TestAction::harness(),
27+
TestAction::run(
28+
r#"
29+
const original = new Error("boom", { cause: { code: 7 } });
30+
const cloned = structuredClone(original);
31+
32+
assert(cloned instanceof Error);
33+
assert(cloned.cause !== original.cause);
34+
assertEq(cloned.cause.code, 7);
35+
"#,
36+
),
37+
]);
38+
}
39+
40+
#[test]
41+
fn clones_aggregate_error_entries() {
42+
run_test_actions([
43+
TestAction::harness(),
44+
TestAction::run(
45+
r#"
46+
const original = new AggregateError([new Error("inner")], "agg");
47+
const cloned = structuredClone(original);
48+
49+
assert(cloned instanceof AggregateError);
50+
assertEq(cloned.message, "agg");
51+
assertEq(cloned.errors.length, 1);
52+
assert(cloned.errors[0] instanceof Error);
53+
assertEq(cloned.errors[0].message, "inner");
54+
"#,
55+
),
56+
]);
57+
}
58+
59+
#[test]
60+
fn clones_error_with_undefined_cause_property() {
61+
run_test_actions([
62+
TestAction::harness(),
63+
TestAction::run(
64+
r#"
65+
const original = new Error("boom", { cause: undefined });
66+
const cloned = structuredClone(original);
67+
68+
assert(Object.hasOwn(original, "cause"));
69+
assert(Object.hasOwn(cloned, "cause"));
70+
assertEq(cloned.cause, undefined);
71+
"#,
72+
),
73+
]);
74+
}

core/runtime/src/message/tests.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,92 @@ fn basic_error_message() {
9696
);
9797
}
9898

99+
#[test]
100+
fn basic_error_with_object_cause() {
101+
let context = &mut Context::default();
102+
103+
let sender = OnMessageQueueSender::create(context);
104+
message::register(sender, None, context).unwrap();
105+
106+
run_test_actions_with(
107+
[
108+
TestAction::harness(),
109+
TestAction::run(
110+
r#"
111+
let latestMessage = null;
112+
function onMessageQueue(message) {
113+
latestMessage = message;
114+
}
115+
116+
const message = new Error("boom", { cause: { code: 7 } });
117+
postMessage(message);
118+
assert(latestMessage === null);
119+
"#,
120+
),
121+
TestAction::inspect_context(move |context| {
122+
drop(future::block_on(future::poll_once(
123+
context
124+
.downcast_job_executor::<SimpleJobExecutor>()
125+
.expect("")
126+
.run_jobs_async(&RefCell::new(context)),
127+
)));
128+
}),
129+
TestAction::run(
130+
r#"
131+
assert(latestMessage instanceof Error);
132+
assert(latestMessage !== message);
133+
assertEq(latestMessage.message, "boom");
134+
assertEq(latestMessage.cause.code, 7);
135+
"#,
136+
),
137+
],
138+
context,
139+
);
140+
}
141+
142+
#[test]
143+
fn basic_error_with_undefined_cause_property() {
144+
let context = &mut Context::default();
145+
146+
let sender = OnMessageQueueSender::create(context);
147+
message::register(sender, None, context).unwrap();
148+
149+
run_test_actions_with(
150+
[
151+
TestAction::harness(),
152+
TestAction::run(
153+
r#"
154+
let latestMessage = null;
155+
function onMessageQueue(message) {
156+
latestMessage = message;
157+
}
158+
159+
const message = new Error("boom", { cause: undefined });
160+
postMessage(message);
161+
assert(latestMessage === null);
162+
"#,
163+
),
164+
TestAction::inspect_context(move |context| {
165+
drop(future::block_on(future::poll_once(
166+
context
167+
.downcast_job_executor::<SimpleJobExecutor>()
168+
.expect("")
169+
.run_jobs_async(&RefCell::new(context)),
170+
)));
171+
}),
172+
TestAction::run(
173+
r#"
174+
assert(latestMessage instanceof Error);
175+
assert(latestMessage !== message);
176+
assert(Object.hasOwn(latestMessage, "cause"));
177+
assertEq(latestMessage.cause, undefined);
178+
"#,
179+
),
180+
],
181+
context,
182+
);
183+
}
184+
99185
#[test]
100186
fn shared_multi_thread() {
101187
let (sender, receiver) = std::sync::mpsc::channel::<OnMessageQueueSender>();

core/runtime/src/store/from.rs

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,13 @@ fn clone_regexp(
186186

187187
fn clone_error(
188188
original: &JsObject,
189+
transfer: &HashSet<JsObject>,
189190
seen: &mut SeenMap,
190191
context: &mut Context,
191192
) -> JsResult<JsValueStore> {
193+
let mut store = JsValueStore::empty();
194+
seen.insert(original, store.clone());
195+
192196
let native = JsError::from_opaque(JsValue::from(original.clone()))
193197
.try_native(context)
194198
.map_err(|_| unsupported_type())?;
@@ -215,17 +219,51 @@ fn clone_error(
215219

216220
let name = to_optional_string("name", context)?;
217221
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)
222+
223+
let cause = if original.has_own_property(js_string!("cause"), context)? {
224+
let cause = original.get(js_string!("cause"), context)?;
225+
Some(try_from_js_value(&cause, transfer, seen, context)?)
226+
} else {
227+
None
228+
};
229+
230+
let errors = if matches!(kind, ErrorKind::Aggregate) {
231+
let errors = original.get(js_string!("errors"), context)?;
232+
if errors.is_undefined() {
233+
Vec::new()
234+
} else {
235+
let Some(errors) = errors.as_object() else {
236+
return Err(unsupported_type());
237+
};
238+
239+
let errors = JsArray::from_object(errors).map_err(|_| unsupported_type())?;
240+
241+
let length = errors.length(context)?;
242+
let length = usize::try_from(length).map_err(JsError::from_rust)?;
243+
let mut values = Vec::with_capacity(length);
244+
for i in 0..length {
245+
let value = errors.get(i, context)?;
246+
values.push(try_from_js_value(&value, transfer, seen, context)?);
247+
}
248+
values
249+
}
250+
} else {
251+
Vec::new()
252+
};
253+
254+
// SAFETY: This is safe as this function is the sole owner of the store.
255+
unsafe {
256+
store.replace(ValueStoreInner::Error {
257+
kind,
258+
name: name.into(),
259+
message: JsString::from(native.message()).into(),
260+
stack: stack.into(),
261+
cause,
262+
errors,
263+
});
264+
}
265+
266+
Ok(store)
229267
}
230268

231269
fn try_from_map(
@@ -306,7 +344,7 @@ fn try_from_js_object_clone(
306344
} else if let Ok(ref date) = JsDate::from_object(object.clone()) {
307345
return clone_date(object, date, seen, context);
308346
} else if object.downcast_ref::<Error>().is_some() {
309-
return clone_error(object, seen, context);
347+
return clone_error(object, transfer, seen, context);
310348
} else if let Ok(ref regexp) = JsRegExp::from_object(object.clone()) {
311349
return clone_regexp(object, regexp, seen, context);
312350
} else if let Ok(_dataview) = JsDataView::from_object(object.clone()) {

core/runtime/src/store/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@ enum ValueStoreInner {
101101
name: StringStore,
102102
message: StringStore,
103103
stack: StringStore,
104-
cause: StringStore,
104+
cause: Option<JsValueStore>,
105+
errors: Vec<JsValueStore>,
105106
},
106107

107108
/// Regular expression. We store the expression and its flags. Everything else

core/runtime/src/store/to.rs

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ type ErrorStoreFields<'a> = (
2020
&'a StringStore,
2121
&'a StringStore,
2222
&'a StringStore,
23-
&'a StringStore,
23+
&'a Option<JsValueStore>,
24+
&'a [JsValueStore],
2425
);
2526

2627
impl ReverseSeenMap {
@@ -167,7 +168,7 @@ fn try_into_js_error(
167168
seen: &mut ReverseSeenMap,
168169
context: &mut Context,
169170
) -> JsResult<JsValue> {
170-
let (kind, name, message, stack, cause) = error_data;
171+
let (kind, name, message, stack, cause, errors) = error_data;
171172
let message = message.to_js_string().to_std_string_escaped();
172173
let native = match kind {
173174
ErrorKind::Aggregate => JsNativeError::aggregate(Vec::new()),
@@ -194,11 +195,26 @@ fn try_into_js_error(
194195
error.set(js_string!("stack"), stack, true, context)?;
195196
}
196197

197-
let cause = cause.to_js_string();
198-
if !cause.is_empty() {
198+
if let Some(cause) = cause {
199+
let cause = try_value_into_js(cause, seen, context)?;
199200
error.set(js_string!("cause"), cause, true, context)?;
200201
}
201202

203+
if !errors.is_empty() {
204+
let errors_array = JsArray::new(context)?;
205+
for (index, value) in errors.iter().enumerate() {
206+
let value = try_value_into_js(value, seen, context)?;
207+
errors_array.set(index, value, true, context)?;
208+
}
209+
210+
error.set(
211+
js_string!("errors"),
212+
JsValue::from(errors_array),
213+
true,
214+
context,
215+
)?;
216+
}
217+
202218
Ok(JsValue::from(error))
203219
}
204220

@@ -265,7 +281,13 @@ pub(super) fn try_value_into_js(
265281
message,
266282
stack,
267283
cause,
268-
} => try_into_js_error(store, (kind, name, message, stack, cause), seen, context),
284+
errors,
285+
} => try_into_js_error(
286+
store,
287+
(kind, name, message, stack, cause, errors),
288+
seen,
289+
context,
290+
),
269291
ValueStoreInner::RegExp { source, flags } => {
270292
try_into_regexp(store, source, flags, seen, context)
271293
}

0 commit comments

Comments
 (0)