///|
/// JS operation wrapper with try-catch
pub extern "js" fn ffi_wrap_sync(
op : () -> Any,
on_ok : (Any) -> Unit,
on_error : (Any) -> Unit,
) -> Unit =
#| (op, on_ok, on_error) => { try { on_ok(op()); } catch (e) { on_error(e); } }
///|
/// Generic JS error type
pub suberror JsError {
JsError(String)
}
///|
/// Safe wrapper that converts JS exceptions to MoonBit errors
pub fn try_sync(op : () -> Any) -> Any raise JsError {
let result : Ref[Any?] = { val: None }
let error : Ref[String?] = { val: None }
ffi_wrap_sync(op, fn(v) { result.val = Some(v) }, fn(e) {
let msg : String = e["message"].cast()
error.val = Some(msg)
})
match error.val {
Some(msg) => raise JsError(msg)
None =>
match result.val {
Some(v) => v
None => raise JsError("No result")
}
}
}
///|
/// Throw a JS Error with the given message
pub extern "js" fn throw_error(msg : String) -> Unit =
#| (msg) => { throw new Error(msg); }
///|
/// Wrap a MoonBit operation that may raise, converting errors to JS exceptions
/// Usage: export_sync(fn() { may_raise_error() })
pub fn[E : Show + Error] export_sync(op : () -> Any raise E) -> Any {
op() catch {
e => {
throw_error(Show::to_string(e))
undefined()
}
}
}
///|
/// Test helper: assert that op throws a JS exception
/// Returns the error message if thrown, fails if no exception
pub fn assert_throws(op : () -> Any) -> String raise JsError {
try_sync(op) |> ignore
raise JsError("Expected exception but none was thrown")
}
///|
/// Test helper: assert that op throws with specific message (contains check)
pub fn assert_throws_with(op : () -> Any, expected_msg : String) -> Unit {
let msg = assert_throws(op) catch { JsError(m) => m }
if !msg.contains(expected_msg) {
let _ = throw_error(
"Expected error containing \"" +
expected_msg +
"\" but got \"" +
msg +
"\"",
)
}
}
///|
pub suberror ThrowError {
ThrowError(Any)
}
///|
pub impl Show for ThrowError with fn output(self, logger) {
logger.write_string("@js.Error: ")
let ThrowError(inner) = self
logger.write_object(inner)
}
///|
/// Wraps a synchronous function call, converting any thrown JS errors into ThrowError
/// ```moonbit skip
/// let result = throwable(() => undefined()._invoke([]))
/// ```
pub fn[T] throwable(f : () -> T raise?) -> T raise ThrowError {
match throwable_result(f |> identity) {
Ok(result) => result |> identity
Err(e) => raise ThrowError(e |> identity)
}
}
///|
/// Wraps a synchronous function call that returns Result, converting any thrown JS errors into JsError
fn throwable_result(f : () -> Result[Any, Any]) -> Result[Any, Any] {
ffi_wrap_sync_result(identity(f), x => Ok(x), x => Err(x))
}
///|
extern "js" fn ffi_wrap_sync_result(
f : Any,
ok : (Any) -> Result[Any, Any],
err : (Any) -> Result[Any, Any],
) -> Result[Any, Any] =
#|(f, ok, err) => {
#| try {
#| return ok(f())
#| } catch(e) {
#| return err(e)
#| }
#|}