Throw JS errors

This commit is contained in:
Andras Schmelczer 2026-10-28 23:20:32 +00:00
commit 80c2ef142d
3 changed files with 29 additions and 8 deletions

View file

@ -254,6 +254,7 @@ export interface ReconcileApi {
* @param diff - The compact diff array (inserts as strings, deletes as negative integers, retained spans as positive integers) * @param diff - The compact diff array (inserts as strings, deletes as negative integers, retained spans as positive integers)
* @param tokenizer - The tokenisation strategy, which is the same as used in `reconcile`. * @param tokenizer - The tokenisation strategy, which is the same as used in `reconcile`.
* @returns The reconstructed changed text as a string. * @returns The reconstructed changed text as a string.
* @throws {Error} If the diff contains a non-integer, or spans more characters than `original` has.
*/ */
undiff( undiff(
original: string, original: string,

View file

@ -91,6 +91,27 @@ describe.each(backends)('reconcile [$name]', ({ api }) => {
expect(withBigints.some((item) => typeof item === 'bigint')).toBe(true); expect(withBigints.some((item) => typeof item === 'bigint')).toBe(true);
expect(undiff(original, withBigints)).toEqual(changed); expect(undiff(original, withBigints)).toEqual(changed);
}); });
// A trap would also be an `Error` (`RuntimeError: unreachable`), so check the message
it.each([
{ invalidDiff: [10], message: 'Invalid diff' },
{ invalidDiff: [2 ** 32], message: 'Invalid diff' },
{ invalidDiff: [-(2 ** 40)], message: 'Invalid diff' },
{ invalidDiff: [2n ** 40n], message: 'Invalid diff' },
{ invalidDiff: [1.5], message: 'must be an integer' },
{ invalidDiff: [-0.4, 'X', 3], message: 'must be an integer' },
{ invalidDiff: [NaN], message: 'NaN' },
])('undiff rejects $invalidDiff with an Error', ({ invalidDiff, message }) => {
let thrown: unknown;
try {
undiff('abc', invalidDiff);
} catch (error) {
thrown = error;
}
expect(thrown).toBeInstanceOf(Error);
expect((thrown as Error).message).toContain(message);
});
}); });
describe.each(backends)('WASM cleanup after exceptions [$name]', ({ api, bindings }) => { describe.each(backends)('WASM cleanup after exceptions [$name]', ({ api, bindings }) => {

View file

@ -81,45 +81,44 @@ pub fn generic_reconcile(
/// ///
/// # Errors /// # Errors
/// ///
/// Returns a JS error if integer overflow occurs during diff computation. /// Throws a JS `Error` if integer overflow occurs during diff computation.
#[wasm_bindgen(js_name = diff)] #[wasm_bindgen(js_name = diff)]
pub fn diff( pub fn diff(
parent: &str, parent: &str,
changed: &TextWithCursors, changed: &TextWithCursors,
tokenizer: BuiltinTokenizer, tokenizer: BuiltinTokenizer,
) -> Result<Vec<JsValue>, JsValue> { ) -> Result<Vec<JsValue>, JsError> {
set_panic_hook(); set_panic_hook();
let edited_text = EditedText::from_strings_with_tokenizer(parent, changed, &*tokenizer); let edited_text = EditedText::from_strings_with_tokenizer(parent, changed, &*tokenizer);
edited_text edited_text
.to_diff() .to_diff()
.map(|diff| diff.into_iter().map(std::convert::Into::into).collect()) .map(|diff| diff.into_iter().map(std::convert::Into::into).collect())
.map_err(|e| JsValue::from_str(&e.to_string())) .map_err(JsError::from)
} }
/// Inverse of `diff`, applies a compact diff representation to a parent text /// Inverse of `diff`, applies a compact diff representation to a parent text
/// ///
/// # Errors /// # Errors
/// ///
/// Returns a JS error if the diff format is invalid or references ranges /// Throws a JS `Error` if the diff format is invalid or references ranges
/// exceeding the original text length. /// exceeding the original text length.
#[wasm_bindgen(js_name = undiff)] #[wasm_bindgen(js_name = undiff)]
pub fn undiff( pub fn undiff(
parent: &str, parent: &str,
diff: Vec<JsValue>, diff: Vec<JsValue>,
tokenizer: BuiltinTokenizer, tokenizer: BuiltinTokenizer,
) -> Result<String, JsValue> { ) -> Result<String, JsError> {
set_panic_hook(); set_panic_hook();
let parsed_diff: Vec<_> = diff let parsed_diff: Vec<_> = diff
.into_iter() .into_iter()
.map(std::convert::TryInto::try_into) .map(std::convert::TryInto::try_into)
.collect::<Result<_, _>>() .collect::<Result<_, crate::types::number_or_text::DeserialisationError>>()?;
.map_err(|e: crate::types::number_or_text::DeserialisationError| -> JsValue { e.into() })?;
EditedText::from_diff(parent, parsed_diff, &*tokenizer) EditedText::from_diff(parent, parsed_diff, &*tokenizer)
.map(|edited_text| edited_text.apply().text()) .map(|edited_text| edited_text.apply().text())
.map_err(|e| JsValue::from_str(&e.to_string())) .map_err(JsError::from)
} }
fn set_panic_hook() { fn set_panic_hook() {