atmos/os_lib/js/uncaught.rs
1//! 「呼び出し元へ例外を返せない場所」で起きた JS 例外の整形。
2//!
3//! 【2026-07-28】イベントリスナ・`setTimeout` コールバック・オブザーバ通知など、
4//! JS から呼ばれるが呼び出し元(Rust 側)が `Result` を返せない経路では、
5//! 例外を `let _ = it.call_value(...)` で**丸ごと捨てて**いた。
6//! その結果、ページの `click` ハンドラが例外を投げても、コンソールにも
7//! シリアルログにも何も出ず「押しても何も起きない」だけが残っていた。
8//!
9//! 本モジュールは外部依存を持たない純粋な整形処理だけを持ち、
10//! ホスト側の単体試験から直接検証できるようにしてある。
11
12extern crate alloc;
13use alloc::string::String;
14
15/// コンソールへ出す 1 行の最大長。壊れた例外値(巨大な文字列や循環構造の
16/// 文字列化)でページ出力を埋め尽くさないための上限。
17pub const MAX_UNCAUGHT_LEN: usize = 300;
18
19/// 捕捉されなかった例外の 1 行メッセージを組み立てる。
20///
21/// - `context` … どこで起きたか(`"click listener"` 等)。空なら省略する。
22/// - `value_repr` … 例外値の文字列化。空なら `"<no message>"` とする
23/// (空文字のまま出すと「何も起きていない」ように見えてしまうため)。
24///
25/// 長すぎる場合は `MAX_UNCAUGHT_LEN` で切り詰め、末尾に `…` を付ける。
26pub fn format_uncaught(context: &str, value_repr: &str) -> String {
27 let body = if value_repr.trim().is_empty() {
28 "<no message>"
29 } else {
30 value_repr
31 };
32
33 let mut s = String::from("Uncaught");
34 if !context.trim().is_empty() {
35 s.push_str(" (in ");
36 s.push_str(context.trim());
37 s.push(')');
38 }
39 s.push_str(": ");
40
41 if body.chars().count() > MAX_UNCAUGHT_LEN {
42 let truncated: String = body.chars().take(MAX_UNCAUGHT_LEN).collect();
43 s.push_str(&truncated);
44 s.push('…');
45 } else {
46 s.push_str(body);
47 }
48 s
49}