Skip to main content

atmos/os_lib/js/builtins/
window_api.rs

1// 分割: builtins.rs より機械的に移動(2026-07-16 リファクタ フェーズ2)。
2// ロジック不変。可視性のみ pub(crate) へ昇格し、親が pub(crate) use で再エクスポート。
3use super::*;
4
5// ===== window.confirm / window.prompt =====
6/// `window.confirm(message)`(丸ごと未対応だった。`window.alert`は`console_
7/// log`で無害に代替されていたが、対になる`confirm`/`prompt`自体がグローバル
8/// にも`window`にも一切登録されておらず、呼び出すと`ReferenceError`/
9/// `TypeError`になっていた。2026-07-16 発見・実装)。この処理系にはブロッキング
10/// モーダルダイアログ機構が無いため、メッセージを`console_log`と同じ経路へ
11/// 出力したうえで、常に「OK」を選択したものとして`true`を返す(`alert`と
12/// 同じ「実際のUIは出さないが呼び出し自体は安全に完了させる」簡略方針)。
13pub(crate) fn window_confirm(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
14    let _ = console_log(it, this, a);
15    Ok(Value::Bool(true))
16}
17/// `window.prompt(message, default)`(丸ごと未対応だった。理由は`window_
18/// confirm`と同じ)。`default`引数があればそれをそのまま返す(ユーザーが
19/// 何も変更せず「OK」を押した相当)。無指定時は仕様どおり`null`。
20pub(crate) fn window_prompt(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
21    let _ = console_log(it, this, a);
22    match a.get(1) {
23        Some(v) if !matches!(v, Value::Undefined) => Ok(v.clone()),
24        _ => Ok(Value::Null),
25    }
26}
27
28// ===== window.open / window.close / window.focus / window.blur =====
29/// `window.open(url, target, features)`(丸ごと未対応だった。`confirm`/
30/// `prompt`と同じ監査で発見。`typeof window.open === 'function'`という
31/// 定番のフィーチャー検出ガードや、ポップアップを試みるコードが
32/// `TypeError`で丸ごと落ちる状態だった。2026-07-16 発見・実装)。この処理系
33/// には複数ウィンドウ/ポップアップを新規に開く仕組みが無いため、実際には
34/// 何も開かず、仕様上「開けなかった(ポップアップブロック等)」場合と
35/// 同じ`null`を返す誠実な簡略実装(ブラウザのポップアップブロッカーが
36/// `window.open()`を拒否した場合の正規の戻り値と同じ形)。
37pub(crate) fn window_open(_it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
38    Ok(Value::Null)
39}
40/// `window.close()`(丸ごと未対応だった。理由は`window_open`と同じ)。
41/// この処理系にはウィンドウを閉じる操作自体が無いため無害な no-op
42/// (`window.print()`と同方針)。
43pub(crate) fn window_close(_it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
44    Ok(Value::Undefined)
45}
46/// `window.stop()`(丸ごと未対応だった。ブラウザの「読み込み中止」ボタン
47/// に相当する定番API。2026-07-17 発見)。この処理系はページ全体を同期的に
48/// パースしてから`<script>`を実行するため、途中で中断できる非同期の
49/// 読み込み処理自体が無く、`window.close`/`.print`と同じ無害なno-op。
50pub(crate) fn window_stop(_it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
51    Ok(Value::Undefined)
52}
53/// `window.focus()`/`.blur()`(丸ごと未対応だった。理由は`window_open`と
54/// 同じ)。この処理系にはウィンドウ/タブという概念自体が無く(
55/// `document.hasFocus()`が常に`true`固定なのと同じ理由)、フォーカス要求
56/// 先も存在しないため無害な no-op。
57pub(crate) fn window_focus(_it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
58    Ok(Value::Undefined)
59}
60
61// ===== window.scrollY / scrollTo / scrollBy / scrollIntoView =====
62
63/// `window.scrollTo(x, y)`/`window.scroll(x, y)`(丸ごと未対応だった。以前は
64/// `SCROLL_Y` という孤立した `static` へ書き込むだけの実装で、`window.
65/// scrollY`(`fire_scroll`経由でレンダラが更新する別経路)にも実際の描画位置
66/// にも一切反映されない死んだ経路だった。2026-07-16 発見・修正)。
67/// `DomBridge::pending_scroll_abs_y`へ保留し、`render.rs`の`draw()`冒頭で
68/// 消費・`WebEngine::scroll_y`へ適用・`fire_scroll()`呼び出しまで行う。
69pub(crate) fn window_scroll_to(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
70    let y = if let Some(Value::Object(o)) = a.first() {
71        o.borrow()
72            .props
73            .get("top")
74            .map(|v| v.to_number())
75            .unwrap_or(0.0)
76    } else {
77        arg(a, 1).to_number()
78    };
79    it.dom.borrow_mut().pending_scroll_abs_y = Some(y as i32);
80    Ok(Value::Undefined)
81}
82/// `window.scrollBy(x, y)`(丸ごと未対応だった。`dom_noop`のまま何もせず、
83/// 相対スクロールが一切機能しなかった。2026-07-16 発見・実装)。複数回
84/// 呼ばれた場合は次の描画までの分を加算する。
85pub(crate) fn window_scroll_by(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
86    let dy = if let Some(Value::Object(o)) = a.first() {
87        o.borrow()
88            .props
89            .get("top")
90            .map(|v| v.to_number())
91            .unwrap_or(0.0)
92    } else {
93        arg(a, 1).to_number()
94    };
95    let mut dom = it.dom.borrow_mut();
96    dom.pending_scroll_by_y = Some(dom.pending_scroll_by_y.unwrap_or(0) + dy as i32);
97    Ok(Value::Undefined)
98}
99pub(crate) fn element_scroll_into_view(_it: &mut Interp, _t: Value, _a: &[Value]) -> Result<Value, Value> {
100    Ok(Value::Undefined) // ページ内スクロール:AtmOS web_engine が管理するため no-op
101}
102
103// ===== window.matchMedia =====
104pub(crate) fn window_match_media(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
105    let query = arg(a, 0).to_js_string();
106    // `window.matchMedia()` は `max-width`/`min-width` の狭い専用判定のみで、
107    // 同じ `@media` 構文を対象とする CSS 側の `css::media_query_matches`
108    // (`prefers-color-scheme`/`hover`/`pointer`/`resolution`/`monochrome`/
109    // `forced-colors`/`inverted-colors`/`color-gamut`/複合条件/カンマ区切り等、
110    // 多数のバグ修正を経て充実済み)と食い違う非対称なバグだった。同じ判定
111    // ロジックを再利用して一致させる。
112    let matches = crate::os_lib::css::media_query_matches(&query);
113    let mql = Obj::plain();
114    {
115        let mut b = mql.borrow_mut();
116        b.props.insert("matches".into(), Value::Bool(matches));
117        b.props.insert("media".into(), Value::str(query));
118        b.props
119            .insert("addEventListener".into(), nv("addEventListener", dom_noop));
120        b.props
121            .insert("addListener".into(), nv("addListener", dom_noop));
122        b.props
123            .insert("removeListener".into(), nv("removeListener", dom_noop));
124        b.props.insert(
125            "removeEventListener".into(),
126            nv("removeEventListener", dom_noop),
127        );
128    }
129    Ok(Value::Object(mql))
130}
131
132/// window の隠し props 配列(ObjKind::Array)へリスナを追記する。
133/// scroll/popstate など、eval をまたいで永続させたいリスナの保存に使う
134/// (Interp ローカルだと eval 毎に破棄されるため window オブジェクトに退避)。
135/// エントリは `addEventListener` の `{signal}` 対応(2026-07-14 発見・実装)
136/// のため `[cb, signal]` 形式で格納する(`event_target_*` 系の `[cb, once,
137/// signal]` と同系統。`window` 系イベントには `once` 概念が無いためこちら
138/// は2要素)。`window_listeners`(interp.rs)/`_message_listeners`/
139/// `_error_listeners` の発火経路側で `event_target_listener_cb`/
140/// `window_listener_aborted` を使って読み解く。
141pub(crate) fn push_window_listener(it: &mut Interp, key: &str, cb: Value, signal: Option<Value>) {
142    let w = match it.global.borrow().vars.get("window") {
143        Some(Value::Object(w)) => w.clone(),
144        _ => return,
145    };
146    let entry = Value::Object(Obj::array(alloc::vec![cb, signal.unwrap_or(Value::Undefined)]));
147    let existing = w.borrow().props.get(key).cloned();
148    match existing {
149        Some(Value::Object(arr)) if matches!(arr.borrow().kind, ObjKind::Array(_)) => {
150            if let ObjKind::Array(items) = &mut arr.borrow_mut().kind {
151                items.push(entry);
152            }
153        }
154        _ => {
155            let arr = Obj::array(alloc::vec![entry]);
156            w.borrow_mut().props.insert(key.into(), Value::Object(arr));
157        }
158    }
159}
160/// `push_window_listener` の格納エントリ `[cb, signal]` の `signal` が
161/// abort 済みかどうか。
162pub(crate) fn window_listener_aborted(entry: &Value) -> bool {
163    if let Value::Object(o) = entry {
164        if let ObjKind::Array(items) = &o.borrow().kind {
165            if let Some(sig) = items.get(1) {
166                return super::super::dom_bridge::is_signal_aborted(sig);
167            }
168        }
169    }
170    false
171}
172
173/// window.addEventListener('load'|'DOMContentLoaded', cb) → load は即時発火。
174/// 'scroll' は window の隠し props `_scroll_listeners`、'popstate' は `_popstate_listeners`、
175/// 'hashchange' は `_hashchange_listeners`、'resize' は `_resize_listeners`
176/// 配列へ永続記録(該当イベント発生時にホストが発火)。
177pub(crate) fn window_add_event_listener(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
178    let event = arg(a, 0).to_js_string();
179    let cb = arg(a, 1);
180    // `{signal}`(丸ごと未対応だった。`AbortController` でリスナを一括解除する
181    // 定番パターン。2026-07-14 発見・実装)。既に abort 済みなら登録自体を
182    // 行わない(DOM 仕様どおり)。
183    let signal = parse_listener_signal(&arg(a, 2));
184    if signal.as_ref().is_some_and(super::super::dom_bridge::is_signal_aborted) {
185        return Ok(Value::Undefined);
186    }
187    match event.as_str() {
188        // `readystatechange`(丸ごと未対応だった。`document.readyState`は
189        // 常に `"complete"` を返す簡略実装のため、`load`/`DOMContentLoaded`
190        // と同じく登録時点で即時発火する)。
191        "load" | "DOMContentLoaded" | "readystatechange" => {
192            // 文書がその段階に達していなければ保留し、`JsRuntime::set_document_ready_state` が発火する。
193            // 達していれば従来どおり即時に呼ぶ(このエンジンは読み込みを同期的に進めるため)。
194            // 【2026-10-20】YouTube は読み込み完了(readystatechange → complete)で monomer 部品を接続する。
195            let state = match it.global.borrow().vars.get("document").cloned() {
196                Some(d) => obj_prop(&d, "readyState").map(|v| v.to_js_string()).unwrap_or_default(),
197                None => String::new(),
198            };
199            let reached = match event.as_str() {
200                "DOMContentLoaded" => state != "loading",
201                _ => state.is_empty() || state == "complete",
202            };
203            crate::warn!("[TMPRS] add {} state={} reached={}", event, state, reached); // TMPRS
204            if reached {
205                // 例外を捨てるとページ初期化が途中で止まった理由が消える。
206                it.call_listener(&cb, Value::Undefined, &[], "window load listener");
207            } else {
208                let key = alloc::format!("_evt_{}_listeners", event);
209                push_window_listener(it, &key, cb, signal);
210            }
211        }
212        "scroll" => {
213            push_window_listener(it, "_scroll_listeners", cb, signal);
214        }
215        "popstate" => {
216            push_window_listener(it, "_popstate_listeners", cb, signal);
217        }
218        "hashchange" => {
219            push_window_listener(it, "_hashchange_listeners", cb, signal);
220        }
221        "resize" => {
222            push_window_listener(it, "_resize_listeners", cb, signal);
223        }
224        // `online`/`offline`(丸ごと未対応だった。`navigator.onLine` は既に
225        // 実装済みだったが、対応するイベント種別自体が登録経路すら無く
226        // `addEventListener('online', ...)` が黙って何もしないバグだった。
227        // この処理系には実際のネットワーク接続状態変化検知機構が無いため、
228        // 実発火は無い(`window.dispatchEvent()` 経由の手動シミュレーション
229        // のみサポート)。
230        "online" => {
231            push_window_listener(it, "_online_listeners", cb, signal);
232        }
233        "offline" => {
234            push_window_listener(it, "_offline_listeners", cb, signal);
235        }
236        // `beforeunload`/`unload`/`pageshow`/`pagehide`/`visibilitychange`/
237        // `languagechange`(丸ごと未対応だった。`online`/`offline` と同じ
238        // 理由で登録経路自体が無かった)。この処理系にはページ遷移/タブ
239        // 可視性変化/言語設定変更のいずれの検知機構も無いため、`online`/
240        // `offline` と同じく実発火は無く手動シミュレーションのみサポート。
241        "beforeunload" => {
242            push_window_listener(it, "_beforeunload_listeners", cb, signal);
243        }
244        "unload" => {
245            push_window_listener(it, "_unload_listeners", cb, signal);
246        }
247        "pageshow" => {
248            push_window_listener(it, "_pageshow_listeners", cb, signal);
249        }
250        "pagehide" => {
251            push_window_listener(it, "_pagehide_listeners", cb, signal);
252        }
253        "visibilitychange" => {
254            push_window_listener(it, "_visibilitychange_listeners", cb, signal);
255        }
256        "languagechange" => {
257            push_window_listener(it, "_languagechange_listeners", cb, signal);
258        }
259        // `storage`(他タブ/ウィンドウでの `localStorage`/`sessionStorage`
260        // 変更通知。丸ごと未対応だった。`StorageEvent` コンストラクタ自体は
261        // 既に対応済みだったが、対応するイベント種別の登録経路が無かった)。
262        // この処理系には実際の複数タブ間通知機構が無いため、`online`/
263        // `offline` と同じく手動シミュレーションのみサポート。
264        "storage" => {
265            push_window_listener(it, "_storage_listeners", cb, signal);
266        }
267        // `beforeprint`/`afterprint`(`window.print()`の前後に発火する
268        // 印刷ダイアログ連動イベント。丸ごと未対応だった)。この処理系には
269        // 実際の印刷パイプライン自体が無いため、`online`/`offline` と同じく
270        // 手動シミュレーションのみサポート。
271        "beforeprint" => {
272            push_window_listener(it, "_beforeprint_listeners", cb, signal);
273        }
274        "afterprint" => {
275            push_window_listener(it, "_afterprint_listeners", cb, signal);
276        }
277        // `window.postMessage()` の受信側。`_message_listeners` は
278        // `window_post_message` が同期的に配送する。
279        "message" => {
280            push_window_listener(it, "_message_listeners", cb, signal);
281        }
282        // `reportError()`/未捕捉例外の受信側。`_error_listeners` は
283        // `report_error` が同期的に配送する。
284        "error" => {
285            push_window_listener(it, "_error_listeners", cb, signal);
286        }
287        // 【2026-10-19】その他の型(YouTube の yt-navigate-finish 等の独自イベント)も保持する。
288        // 従来は黙って捨てていた。
289        other => {
290            let key = alloc::format!("_evt_{}_listeners", other);
291            push_window_listener(it, &key, cb, signal);
292        }
293    }
294    Ok(Value::Undefined)
295}
296
297/// `window.removeEventListener(type, cb)`(丸ごと未対応だった。以前は
298/// 無害な `domNoop` のままで、`window.addEventListener` で登録した
299/// リスナが `AbortSignal` 経由以外では二度と解除できないバグだった)。
300/// `window_add_event_listener` と同じ6種の固定イベント種別に限り対応する
301/// 隠し配列から、参照が一致するエントリを取り除く(`event_target_remove_
302/// event_listener` と同じ `Rc::ptr_eq` 比較パターン)。
303pub(crate) fn window_remove_event_listener(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
304    let event = arg(a, 0).to_js_string();
305    let cb = arg(a, 1);
306    let custom_key = alloc::format!("_evt_{}_listeners", event);
307    let key = match event.as_str() {
308        "scroll" => "_scroll_listeners",
309        "popstate" => "_popstate_listeners",
310        "hashchange" => "_hashchange_listeners",
311        "resize" => "_resize_listeners",
312        "online" => "_online_listeners",
313        "offline" => "_offline_listeners",
314        "beforeunload" => "_beforeunload_listeners",
315        "unload" => "_unload_listeners",
316        "pageshow" => "_pageshow_listeners",
317        "pagehide" => "_pagehide_listeners",
318        "visibilitychange" => "_visibilitychange_listeners",
319        "languagechange" => "_languagechange_listeners",
320        "storage" => "_storage_listeners",
321        "beforeprint" => "_beforeprint_listeners",
322        "afterprint" => "_afterprint_listeners",
323        "message" => "_message_listeners",
324        "error" => "_error_listeners",
325        _ => custom_key.as_str(),
326    };
327    let w = match it.global.borrow().vars.get("window") {
328        Some(Value::Object(w)) => w.clone(),
329        _ => return Ok(Value::Undefined),
330    };
331    let arr = match w.borrow().props.get(key) {
332        Some(Value::Object(arr)) => arr.clone(),
333        _ => return Ok(Value::Undefined),
334    };
335    if let ObjKind::Array(items) = &mut arr.borrow_mut().kind {
336        items.retain(|entry| {
337            let stored_cb = match entry {
338                Value::Object(o) => match &o.borrow().kind {
339                    ObjKind::Array(pair) => pair.first().cloned().unwrap_or(Value::Undefined),
340                    _ => entry.clone(),
341                },
342                _ => entry.clone(),
343            };
344            match (&stored_cb, &cb) {
345                (Value::Object(x), Value::Object(y)) => !Rc::ptr_eq(x, y),
346                _ => true,
347            }
348        });
349    }
350    Ok(Value::Undefined)
351}
352/// `window.dispatchEvent(event)`(丸ごと未対応だった。`window` オブジェクトに
353/// `addEventListener`/`removeEventListener` はあったが `dispatchEvent` 自体が
354/// 存在せず、`window.dispatchEvent(new HashChangeEvent(...))` のような JS 側
355/// からのイベントシミュレーションが一切できなかった。`window_add_event_
356/// listener` が対応する6種の固定イベント種別(`scroll`/`popstate`/
357/// `hashchange`/`resize`/`message`/`error`)に限り、対応する隠し配列
358/// (`push_window_listener`が書き込む先)からリスナを読み出し、渡された
359/// `event` オブジェクトをそのまま転送して呼び出す(`fire_window_listeners`
360/// のような独自 Event 再構築は行わず、JS 側が構築した実際のオブジェクトを
361/// そのまま渡す)。対応外の種別は無害に無視する(`addEventListener`側の
362/// 未対応種別の扱いと同じ)。`window.onerror`(`report_error`側で既に
363/// 対応済み)と同様に、`window.on<type>`(`window.onhashchange = fn`等の
364/// IDL属性スタイルのハンドラ)が設定されていればそれも合わせて呼び出す。
365pub(crate) fn window_dispatch_event(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
366    let event = arg(a, 0);
367    let event_type = obj_prop(&event, "type").map(|v| v.to_js_string()).unwrap_or_default();
368    let custom_key = alloc::format!("_evt_{}_listeners", event_type);
369    let key = match event_type.as_str() {
370        "scroll" => "_scroll_listeners",
371        "popstate" => "_popstate_listeners",
372        "hashchange" => "_hashchange_listeners",
373        "resize" => "_resize_listeners",
374        "online" => "_online_listeners",
375        "offline" => "_offline_listeners",
376        "beforeunload" => "_beforeunload_listeners",
377        "unload" => "_unload_listeners",
378        "pageshow" => "_pageshow_listeners",
379        "pagehide" => "_pagehide_listeners",
380        "visibilitychange" => "_visibilitychange_listeners",
381        "languagechange" => "_languagechange_listeners",
382        "storage" => "_storage_listeners",
383        "beforeprint" => "_beforeprint_listeners",
384        "afterprint" => "_afterprint_listeners",
385        "message" => "_message_listeners",
386        "error" => "_error_listeners",
387        _ => custom_key.as_str(),
388    };
389    let w = match it.global.borrow().vars.get("window") {
390        Some(Value::Object(w)) => w.clone(),
391        _ => return Ok(Value::Bool(true)),
392    };
393    // dispatch 対象(`window.dispatchEvent` なら window、`document.dispatchEvent`
394    // なら document)を `event.target`/`srcElement`/`currentTarget` に設定する
395    // (`event_target_dispatch_event` と同じイディオム。以前はこれらを一切
396    // 設定せず、リスナ内で `e.target` を読むと常に `undefined` だった)。
397    // `this` が取れない経路では window にフォールバックする。
398    let target = if matches!(this, Value::Object(_)) {
399        this.clone()
400    } else {
401        Value::Object(w.clone())
402    };
403    if let Value::Object(eo) = &event {
404        let mut e = eo.borrow_mut();
405        e.props.insert("target".into(), target.clone());
406        e.props.insert("srcElement".into(), target.clone());
407        e.props.insert("currentTarget".into(), target);
408        // window/document には DOM ツリーの伝播フェーズが無いため AT_TARGET(2)。
409        e.props.insert("eventPhase".into(), Value::Number(2.0));
410    }
411    let on_handler = w.borrow().props.get(&alloc::format!("on{event_type}")).cloned();
412    if let Some(f) = on_handler.and_then(callable_or_none) {
413        // `window.onerror` は HTML 仕様上 `GlobalEventHandlers.onerror` として
414        // 特別扱いされ、`ErrorEvent` 経由でディスパッチされた場合もレガシー
415        // 5引数シグネチャ `(message, source, lineno, colno, error)` で呼ばれる
416        // (`reportError()`〔`report_error`〕は既にこの形で呼んでいたが、
417        // `window.dispatchEvent(new ErrorEvent(...))`/`document.dispatchEvent
418        // (new ErrorEvent(...))` 経由では Event オブジェクトをそのまま1引数で
419        // 渡してしまい、2経路で `onerror` の呼び出し形が食い違っていた。
420        // 2026-07-16 発見・修正)。
421        if event_type == "error" {
422            let (message, filename, lineno, colno, err) = if let Value::Object(eo) = &event {
423                let b = eo.borrow();
424                (
425                    b.props.get("message").cloned().unwrap_or_else(|| Value::str("")),
426                    b.props.get("filename").cloned().unwrap_or_else(|| Value::str("")),
427                    b.props.get("lineno").cloned().unwrap_or(Value::Number(0.0)),
428                    b.props.get("colno").cloned().unwrap_or(Value::Number(0.0)),
429                    b.props.get("error").cloned().unwrap_or(Value::Null),
430                )
431            } else {
432                (Value::str(""), Value::str(""), Value::Number(0.0), Value::Number(0.0), Value::Null)
433            };
434            it.call_listener(
435                &f,
436                Value::Undefined,
437                &[message, filename, lineno, colno, err],
438                "window.onerror",
439            );
440        } else {
441            it.call_listener(
442                &f,
443                Value::Undefined,
444                core::slice::from_ref(&event),
445                "window.onerror",
446            );
447        }
448    }
449    let entries = match w.borrow().props.get(key) {
450        Some(Value::Object(arr)) => match &arr.borrow().kind {
451            ObjKind::Array(items) => items.clone(),
452            _ => alloc::vec::Vec::new(),
453        },
454        _ => alloc::vec::Vec::new(),
455    };
456    for entry in entries {
457        let (cb, signal) = match &entry {
458            Value::Object(o) => match &o.borrow().kind {
459                ObjKind::Array(items) => (items.first().cloned().unwrap_or(Value::Undefined), items.get(1).cloned()),
460                _ => (entry.clone(), None),
461            },
462            _ => (entry.clone(), None),
463        };
464        if signal.as_ref().is_some_and(super::super::dom_bridge::is_signal_aborted) {
465            continue;
466        }
467        it.call_listener(
468            &cb,
469            Value::Undefined,
470            core::slice::from_ref(&event),
471            "window listener",
472        );
473    }
474    // 発火完了後は仕様どおり `currentTarget: null` / `eventPhase: NONE(0)` へ戻す
475    // (`target` は発火後の `event.target` 参照用にそのまま残す)。
476    if let Value::Object(eo) = &event {
477        let mut e = eo.borrow_mut();
478        e.props.insert("currentTarget".into(), Value::Null);
479        e.props.insert("eventPhase".into(), Value::Number(0.0));
480    }
481    // 仕様どおり `!event.defaultPrevented` を返す。以前は無条件 `true` を返して
482    // おり、リスナが `preventDefault()` を呼んでも `window.dispatchEvent()`/
483    // `document.dispatchEvent()` が常に `true` を返すバグだった(要素用
484    // `dom_dispatch_event`/`event_target_dispatch_event` は正しく実装済みで、
485    // window/document 経路にだけ同型バグが残っていた)。
486    let default_prevented = if let Value::Object(eo) = &event {
487        matches!(eo.borrow().props.get("defaultPrevented"), Some(Value::Bool(true)))
488    } else {
489        false
490    };
491    Ok(Value::Bool(!default_prevented))
492}
493
494/// `document.addEventListener`/`removeEventListener`/`dispatchEvent` が
495/// `window` と共有する固定イベント種別の集合(`window_add_event_listener`の
496/// match armと同じ)。
497fn is_window_shared_event_type(t: &str) -> bool {
498    matches!(
499        t,
500        "load"
501            | "DOMContentLoaded"
502            | "readystatechange"
503            | "scroll"
504            | "popstate"
505            | "hashchange"
506            | "resize"
507            | "online"
508            | "offline"
509            | "beforeunload"
510            | "unload"
511            | "pageshow"
512            | "pagehide"
513            | "visibilitychange"
514            | "languagechange"
515            | "storage"
516            | "beforeprint"
517            | "afterprint"
518            | "message"
519            | "error"
520    )
521}
522
523/// `document.addEventListener(type, fn, options)`。`window` 固有の固定
524/// イベント種別(`scroll`/`hashchange`/`error`等)は従来どおり `window`と
525/// 共有する隠し配列へ登録するが、それ以外の汎用種別(`click`/`keydown`等)は
526/// `document`自身が持つ`EventTarget`風の`_listeners`(`event_target_add_
527/// event_listener`と同じ格納形式)へ登録する。実クリック等の実DOMイベント
528/// バブリングの最終到達点として`dispatch_event_in_interp`
529/// (`interp/calls.rs`)がこちらを読み出す(丸ごと未対応だった。以前は
530/// 固定種別以外を`window_add_event_listener`の`_ => {}`アームで黙って
531/// 破棄しており、`document.addEventListener('click', fn)`という定番の
532/// イベント委譲パターンが登録すら行われず一切機能しなかった。2026-07-16
533/// 発見・実装)。
534pub(crate) fn document_add_event_listener(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
535    let etype = arg(a, 0).to_js_string();
536    { static N: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0); if N.fetch_add(1, core::sync::atomic::Ordering::Relaxed) < 60 { crate::warn!("[TMPRS] doc.add {} stack={:?}", etype, it.call_names.iter().rev().take(3).collect::<Vec<_>>()); } } // TMPRS
537    if is_window_shared_event_type(&etype) {
538        return window_add_event_listener(it, this, a);
539    }
540    event_target_add_event_listener(it, this, a)
541}
542/// `document.removeEventListener`。`document_add_event_listener`と対になる
543/// 振り分け(丸ごと未対応だった。2026-07-16 発見・実装)。
544pub(crate) fn document_remove_event_listener(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
545    let etype = arg(a, 0).to_js_string();
546    if is_window_shared_event_type(&etype) {
547        return window_remove_event_listener(it, this, a);
548    }
549    event_target_remove_event_listener(it, this, a)
550}
551/// `document.dispatchEvent(event)`。JS側から直接`document.dispatchEvent(new
552/// Event('click'))`のように合成発火する経路(実クリックのDOMバブリング経由の
553/// 到達は`dispatch_event_in_interp`側の別経路)。固定種別以外は`document`
554/// 自身をターゲットとして`event_target_dispatch_event`と同じロジックで
555/// 発火する(丸ごと未対応だった。2026-07-16 発見・実装)。
556pub(crate) fn document_dispatch_event(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
557    let event = arg(a, 0);
558    let etype = obj_prop(&event, "type").map(|v| v.to_js_string()).unwrap_or_default();
559    if is_window_shared_event_type(&etype) {
560        return window_dispatch_event(it, this, a);
561    }
562    event_target_dispatch_event(it, this, a)
563}
564
565/// `element.requestFullscreen()`(Fullscreen API + ベンダープレフィックス互換)。
566/// `document.fullscreenElement`(および `webkitFullscreenElement` 等)と CSS `:fullscreen`
567/// 疑似クラス用の内部属性 `_fullscreen` を更新し、`fullscreenchange` / `webkitfullscreenchange`
568/// イベントを対象要素および `document` へ発火する。
569pub fn dom_request_fullscreen(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
570    if let Some(idx) = this_dom_idx(&this) {
571        {
572            let mut dom = it.dom.borrow_mut();
573            let len = dom.nodes.len();
574            for i in 0..len {
575                dom.remove_attr(i, "_fullscreen");
576            }
577            dom.set_attr(idx, "_fullscreen", "1");
578        }
579        if let Some(Value::Object(doc)) = it.global.borrow().vars.get("document").cloned() {
580            let mut d = doc.borrow_mut();
581            d.props.insert("fullscreenElement".into(), this.clone());
582            d.props.insert("webkitFullscreenElement".into(), this.clone());
583            d.props.insert("mozFullScreenElement".into(), this.clone());
584            d.props.insert("msFullscreenElement".into(), this.clone());
585        }
586        fire_fullscreen_change_events(it, idx);
587    }
588    Ok(resolved_promise(it, Value::Undefined))
589}
590
591/// `document.exitFullscreen()`(対をなす解除メソッド + ベンダープレフィックス互換)。
592pub(crate) fn document_exit_fullscreen(it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
593    let mut target_idx: Option<usize> = None;
594    if let Some(Value::Object(doc)) = it.global.borrow().vars.get("document").cloned() {
595        let prev = doc.borrow().props.get("fullscreenElement").cloned();
596        if let Some(prev_val) = prev {
597            if let Some(idx) = this_dom_idx(&prev_val) {
598                target_idx = Some(idx);
599            }
600        }
601        let mut d = doc.borrow_mut();
602        d.props.insert("fullscreenElement".into(), Value::Null);
603        d.props.insert("webkitFullscreenElement".into(), Value::Null);
604        d.props.insert("mozFullScreenElement".into(), Value::Null);
605        d.props.insert("msFullscreenElement".into(), Value::Null);
606    }
607    {
608        let mut dom = it.dom.borrow_mut();
609        let len = dom.nodes.len();
610        for i in 0..len {
611            if dom.has_attr(i, "_fullscreen") {
612                if target_idx.is_none() {
613                    target_idx = Some(i);
614                }
615                dom.remove_attr(i, "_fullscreen");
616            }
617        }
618    }
619    let idx = target_idx.unwrap_or(0);
620    fire_fullscreen_change_events(it, idx);
621    Ok(resolved_promise(it, Value::Undefined))
622}
623
624fn fire_fullscreen_change_events(it: &mut Interp, idx: usize) {
625    let has_node = idx < it.dom.borrow().nodes.len();
626    if has_node {
627        let _ = it.dispatch_event_in_interp(idx, "fullscreenchange", &[]);
628        let _ = it.dispatch_event_in_interp(idx, "webkitfullscreenchange", &[]);
629    }
630    let doc_opt = it.global.borrow().vars.get("document").cloned();
631    if let Some(Value::Object(doc)) = doc_opt {
632        let doc_val = Value::Object(doc.clone());
633        for (ev_type, prop_name) in [
634            ("fullscreenchange", "onfullscreenchange"),
635            ("webkitfullscreenchange", "onwebkitfullscreenchange"),
636        ] {
637            let handler = doc.borrow().props.get(prop_name).cloned();
638            if let Some(h) = handler.filter(|h| matches!(h, Value::Object(o) if o.borrow().is_callable())) {
639                let ev = Obj::plain();
640                {
641                    let mut e = ev.borrow_mut();
642                    e.props.insert("type".into(), Value::str(ev_type));
643                    e.props.insert("target".into(), doc_val.clone());
644                    e.props.insert("currentTarget".into(), doc_val.clone());
645                    e.props.insert("bubbles".into(), Value::Bool(true));
646                }
647                it.call_listener(&h, doc_val.clone(), &[Value::Object(ev)], prop_name);
648            }
649        }
650    }
651}
652
653/// `element.requestPointerLock()`(Pointer Lock API。丸ごと未対応だった)。
654/// `requestFullscreen`/`fullscreenElement` と全く同じ「実際のロック/画面占有
655/// までは行わず状態のみ追跡する」簡略方針(`pointerlockchange` イベントは
656/// 対象外)。
657pub fn dom_request_pointer_lock(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
658    if this_dom_idx(&this).is_some() {
659        if let Some(Value::Object(doc)) = it.global.borrow().vars.get("document").cloned() {
660            doc.borrow_mut().props.insert("pointerLockElement".into(), this.clone());
661        }
662    }
663    Ok(Value::Undefined)
664}
665/// `document.exitPointerLock()`(対をなす解除メソッド。丸ごと未対応だった)。
666pub(crate) fn document_exit_pointer_lock(it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
667    if let Some(Value::Object(doc)) = it.global.borrow().vars.get("document").cloned() {
668        doc.borrow_mut().props.insert("pointerLockElement".into(), Value::Null);
669    }
670    Ok(Value::Undefined)
671}
672
673/// `<video>` の `.requestPictureInPicture()`(Picture-in-Picture API。丸ごと
674/// 未対応だった)。`requestFullscreen`/`requestPointerLock` と全く同じ
675/// 「状態のみ追跡する」簡略方針。仕様どおり解決済み Promise(
676/// `PictureInPictureWindow` 相当のダミーオブジェクトで解決)を返す。
677pub fn dom_request_picture_in_picture(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
678    if this_dom_idx(&this).is_some() {
679        if let Some(Value::Object(doc)) = it.global.borrow().vars.get("document").cloned() {
680            doc.borrow_mut()
681                .props
682                .insert("pictureInPictureElement".into(), this.clone());
683        }
684    }
685    Ok(resolved_promise(it, Value::Object(Obj::plain())))
686}
687/// `document.exitPictureInPicture()`(対をなす解除メソッド。丸ごと未対応
688/// だった)。
689pub(crate) fn document_exit_picture_in_picture(it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
690    if let Some(Value::Object(doc)) = it.global.borrow().vars.get("document").cloned() {
691        doc.borrow_mut()
692            .props
693            .insert("pictureInPictureElement".into(), Value::Null);
694    }
695    Ok(resolved_promise(it, Value::Undefined))
696}
697
698/// `document.createTreeWalker(root, whatToShow?, filter?)`(DOM 標準。
699/// 丸ごと未対応だった。DOM 部分木を走査する定番パターン)。`whatToShow`/
700/// `filter` は保持のみで実際の絞り込みは行わない簡略実装(`:where()` を
701/// `:is()` と同一視するのと同じ「フィルタは常に通過」方針)。
702pub(crate) fn document_create_tree_walker(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
703    let root = arg(a, 0);
704    let root_idx = this_dom_idx(&root).unwrap_or(0);
705    let tw = Obj::plain();
706    {
707        let mut b = tw.borrow_mut();
708        b.props.insert("root".into(), root.clone());
709        b.props.insert("currentNode".into(), root);
710        b.props.insert("_tw_root".into(), Value::Number(root_idx as f64));
711        b.props.insert(
712            "whatToShow".into(),
713            if a.len() > 1 { arg(a, 1) } else { Value::Number(0xFFFF_FFFFu32 as f64) },
714        );
715        b.props.insert("filter".into(), arg(a, 2));
716        b.props.insert("nextNode".into(), nv("nextNode", tree_walker_next_node));
717        b.props.insert("previousNode".into(), nv("previousNode", tree_walker_previous_node));
718        b.props.insert("parentNode".into(), nv("parentNode", tree_walker_parent_node));
719        b.props.insert("firstChild".into(), nv("firstChild", tree_walker_first_child));
720        b.props.insert("lastChild".into(), nv("lastChild", tree_walker_last_child));
721        b.props.insert("nextSibling".into(), nv("nextSibling", tree_walker_next_sibling));
722        b.props
723            .insert("previousSibling".into(), nv("previousSibling", tree_walker_previous_sibling));
724    }
725    Ok(Value::Object(tw))
726}
727pub(crate) fn tw_current_idx(this: &Value) -> Option<usize> {
728    let Value::Object(o) = this else { return None };
729    let cur = o.borrow().props.get("currentNode").cloned();
730    cur.and_then(|v| this_dom_idx(&v))
731}
732pub(crate) fn tw_root_idx(this: &Value) -> Option<usize> {
733    let Value::Object(o) = this else { return None };
734    o.borrow().props.get("_tw_root").map(|v| v.to_number() as usize)
735}
736pub(crate) fn tw_set_current(this: &Value, idx: usize) {
737    if let Value::Object(o) = this {
738        o.borrow_mut().props.insert("currentNode".into(), Value::Object(Obj::dom(idx)));
739    }
740}
741pub(crate) fn tw_what_to_show(this: &Value) -> u32 {
742    let Value::Object(o) = this else { return crate::os_lib::dom_traversal::SHOW_ALL };
743    o.borrow()
744        .props
745        .get("whatToShow")
746        .map(|v| v.to_number() as u32)
747        .unwrap_or(crate::os_lib::dom_traversal::SHOW_ALL)
748}
749pub(crate) fn tw_get_filter(this: &Value) -> Option<Value> {
750    let Value::Object(o) = this else { return None };
751    let f = o.borrow().props.get("filter").cloned()?;
752    match f {
753        Value::Null | Value::Undefined => None,
754        _ => Some(f),
755    }
756}
757
758struct DomTreeRef(alloc::rc::Rc<core::cell::RefCell<crate::os_lib::js::dom_bridge::DomBridge>>);
759
760impl crate::os_lib::dom_traversal::TraversalTree for DomTreeRef {
761    fn parent(&self, idx: usize) -> Option<usize> {
762        self.0.borrow().nodes.get(idx).and_then(|n| n.parent)
763    }
764
765    fn first_child(&self, idx: usize) -> Option<usize> {
766        self.0.borrow().nodes.get(idx).and_then(|n| n.children.first().copied())
767    }
768
769    fn last_child(&self, idx: usize) -> Option<usize> {
770        self.0.borrow().nodes.get(idx).and_then(|n| n.children.last().copied())
771    }
772
773    fn next_sibling(&self, idx: usize) -> Option<usize> {
774        let b = self.0.borrow();
775        let p = b.nodes.get(idx).and_then(|n| n.parent)?;
776        let kids = &b.nodes.get(p)?.children;
777        let pos = kids.iter().position(|&c| c == idx)?;
778        kids.get(pos + 1).copied()
779    }
780
781    fn previous_sibling(&self, idx: usize) -> Option<usize> {
782        let b = self.0.borrow();
783        let p = b.nodes.get(idx).and_then(|n| n.parent)?;
784        let kids = &b.nodes.get(p)?.children;
785        let pos = kids.iter().position(|&c| c == idx)?;
786        if pos > 0 {
787            kids.get(pos - 1).copied()
788        } else {
789            None
790        }
791    }
792
793    fn node_type(&self, idx: usize) -> u16 {
794        let b = self.0.borrow();
795        if let Some(n) = b.nodes.get(idx) {
796            if n.is_text {
797                3
798            } else if n.tag == "#comment" {
799                8
800            } else if n.tag == "#document-fragment" || n.tag == "#fragment" {
801                11
802            } else if n.tag == "#document" {
803                9
804            } else {
805                1
806            }
807        } else {
808            1
809        }
810    }
811}
812
813fn run_traversal_filter(
814    it: &mut Interp,
815    filter_val: &Option<Value>,
816    node_idx: usize,
817) -> crate::os_lib::dom_traversal::FilterResult {
818    let Some(filter) = filter_val else {
819        return crate::os_lib::dom_traversal::FilterResult::Accept;
820    };
821    let node_obj = Value::Object(Obj::dom(node_idx));
822    match filter {
823        Value::Object(o) if o.borrow().is_callable() => {
824            match it.call_value(filter, Value::Undefined, &[node_obj]) {
825                Ok(v) => crate::os_lib::dom_traversal::FilterResult::from_u32(v.to_number() as u32),
826                Err(_) => crate::os_lib::dom_traversal::FilterResult::Reject,
827            }
828        }
829        Value::Object(o) => {
830            let accept = o.borrow().props.get("acceptNode").cloned();
831            if let Some(f) = accept {
832                let is_call = match &f {
833                    Value::Object(fo) => fo.borrow().is_callable(),
834                    _ => false,
835                };
836                if is_call {
837                    match it.call_value(&f, filter.clone(), &[node_obj]) {
838                        Ok(v) => {
839                            crate::os_lib::dom_traversal::FilterResult::from_u32(v.to_number() as u32)
840                        }
841                        Err(_) => crate::os_lib::dom_traversal::FilterResult::Reject,
842                    }
843                } else {
844                    crate::os_lib::dom_traversal::FilterResult::Accept
845                }
846            } else {
847                crate::os_lib::dom_traversal::FilterResult::Accept
848            }
849        }
850        _ => crate::os_lib::dom_traversal::FilterResult::Accept,
851    }
852}
853
854/// `TreeWalker.prototype.nextNode()`。
855pub(crate) fn tree_walker_next_node(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
856    let Some(cur) = tw_current_idx(&this) else { return Ok(Value::Null) };
857    let root = tw_root_idx(&this).unwrap_or(cur);
858    let what_to_show = tw_what_to_show(&this);
859    let filter = tw_get_filter(&this);
860
861    let tree = DomTreeRef(it.dom.clone());
862    let res = crate::os_lib::dom_traversal::tw_next_node(&tree, root, cur, what_to_show, &mut |idx| {
863        run_traversal_filter(it, &filter, idx)
864    });
865
866    if let Some(next) = res {
867        tw_set_current(&this, next);
868        Ok(Value::Object(Obj::dom(next)))
869    } else {
870        Ok(Value::Null)
871    }
872}
873
874/// `TreeWalker.prototype.previousNode()`。
875pub(crate) fn tree_walker_previous_node(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
876    let Some(cur) = tw_current_idx(&this) else { return Ok(Value::Null) };
877    let root = tw_root_idx(&this).unwrap_or(cur);
878    let what_to_show = tw_what_to_show(&this);
879    let filter = tw_get_filter(&this);
880
881    let tree = DomTreeRef(it.dom.clone());
882    let res = crate::os_lib::dom_traversal::tw_previous_node(&tree, root, cur, what_to_show, &mut |idx| {
883        run_traversal_filter(it, &filter, idx)
884    });
885
886    if let Some(prev) = res {
887        tw_set_current(&this, prev);
888        Ok(Value::Object(Obj::dom(prev)))
889    } else {
890        Ok(Value::Null)
891    }
892}
893
894/// `TreeWalker.prototype.parentNode()`。
895pub(crate) fn tree_walker_parent_node(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
896    let Some(cur) = tw_current_idx(&this) else { return Ok(Value::Null) };
897    let root = tw_root_idx(&this).unwrap_or(cur);
898    let what_to_show = tw_what_to_show(&this);
899    let filter = tw_get_filter(&this);
900
901    let tree = DomTreeRef(it.dom.clone());
902    let res = crate::os_lib::dom_traversal::tw_parent_node(&tree, root, cur, what_to_show, &mut |idx| {
903        run_traversal_filter(it, &filter, idx)
904    });
905
906    if let Some(p) = res {
907        tw_set_current(&this, p);
908        Ok(Value::Object(Obj::dom(p)))
909    } else {
910        Ok(Value::Null)
911    }
912}
913
914/// `TreeWalker.prototype.firstChild()`。
915pub(crate) fn tree_walker_first_child(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
916    let Some(cur) = tw_current_idx(&this) else { return Ok(Value::Null) };
917    let what_to_show = tw_what_to_show(&this);
918    let filter = tw_get_filter(&this);
919
920    let tree = DomTreeRef(it.dom.clone());
921    let res = crate::os_lib::dom_traversal::tw_first_child(&tree, cur, what_to_show, &mut |idx| {
922        run_traversal_filter(it, &filter, idx)
923    });
924
925    if let Some(c) = res {
926        tw_set_current(&this, c);
927        Ok(Value::Object(Obj::dom(c)))
928    } else {
929        Ok(Value::Null)
930    }
931}
932
933/// `TreeWalker.prototype.lastChild()`。
934pub(crate) fn tree_walker_last_child(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
935    let Some(cur) = tw_current_idx(&this) else { return Ok(Value::Null) };
936    let what_to_show = tw_what_to_show(&this);
937    let filter = tw_get_filter(&this);
938
939    let tree = DomTreeRef(it.dom.clone());
940    let res = crate::os_lib::dom_traversal::tw_last_child(&tree, cur, what_to_show, &mut |idx| {
941        run_traversal_filter(it, &filter, idx)
942    });
943
944    if let Some(c) = res {
945        tw_set_current(&this, c);
946        Ok(Value::Object(Obj::dom(c)))
947    } else {
948        Ok(Value::Null)
949    }
950}
951
952/// `TreeWalker.prototype.nextSibling()`。
953pub(crate) fn tree_walker_next_sibling(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
954    let Some(cur) = tw_current_idx(&this) else { return Ok(Value::Null) };
955    let root = tw_root_idx(&this).unwrap_or(cur);
956    let what_to_show = tw_what_to_show(&this);
957    let filter = tw_get_filter(&this);
958
959    let tree = DomTreeRef(it.dom.clone());
960    let res = crate::os_lib::dom_traversal::tw_next_sibling(&tree, root, cur, what_to_show, &mut |idx| {
961        run_traversal_filter(it, &filter, idx)
962    });
963
964    if let Some(s) = res {
965        tw_set_current(&this, s);
966        Ok(Value::Object(Obj::dom(s)))
967    } else {
968        Ok(Value::Null)
969    }
970}
971
972/// `TreeWalker.prototype.previousSibling()`。
973pub(crate) fn tree_walker_previous_sibling(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
974    let Some(cur) = tw_current_idx(&this) else { return Ok(Value::Null) };
975    let root = tw_root_idx(&this).unwrap_or(cur);
976    let what_to_show = tw_what_to_show(&this);
977    let filter = tw_get_filter(&this);
978
979    let tree = DomTreeRef(it.dom.clone());
980    let res = crate::os_lib::dom_traversal::tw_previous_sibling(&tree, root, cur, what_to_show, &mut |idx| {
981        run_traversal_filter(it, &filter, idx)
982    });
983
984    if let Some(s) = res {
985        tw_set_current(&this, s);
986        Ok(Value::Object(Obj::dom(s)))
987    } else {
988        Ok(Value::Null)
989    }
990}
991
992/// `document.createNodeIterator(root, whatToShow?, filter?)`。
993pub(crate) fn document_create_node_iterator(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
994    let root = arg(a, 0);
995    let root_idx = this_dom_idx(&root).unwrap_or(0);
996    let ni = Obj::plain();
997    {
998        let mut b = ni.borrow_mut();
999        b.props.insert("root".into(), root.clone());
1000        b.props.insert("referenceNode".into(), root);
1001        b.props.insert("pointerBeforeReferenceNode".into(), Value::Bool(true));
1002        b.props.insert("_ni_root".into(), Value::Number(root_idx as f64));
1003        b.props.insert(
1004            "whatToShow".into(),
1005            if a.len() > 1 { arg(a, 1) } else { Value::Number(0xFFFF_FFFFu32 as f64) },
1006        );
1007        b.props.insert("filter".into(), arg(a, 2));
1008        b.props
1009            .insert("nextNode".into(), nv("nextNode", node_iterator_next_node));
1010        b.props.insert(
1011            "previousNode".into(),
1012            nv("previousNode", node_iterator_previous_node),
1013        );
1014        b.props.insert("detach".into(), nv("detach", dom_noop));
1015    }
1016    Ok(Value::Object(ni))
1017}
1018pub(crate) fn ni_ref_idx(this: &Value) -> Option<usize> {
1019    let Value::Object(o) = this else { return None };
1020    let r = o.borrow().props.get("referenceNode").cloned();
1021    r.and_then(|v| this_dom_idx(&v))
1022}
1023pub(crate) fn ni_root_idx(this: &Value) -> Option<usize> {
1024    let Value::Object(o) = this else { return None };
1025    o.borrow()
1026        .props
1027        .get("_ni_root")
1028        .map(|v| v.to_number() as usize)
1029}
1030pub(crate) fn ni_pointer_before(this: &Value) -> bool {
1031    let Value::Object(o) = this else { return false };
1032    matches!(
1033        o.borrow().props.get("pointerBeforeReferenceNode"),
1034        Some(Value::Bool(true))
1035    )
1036}
1037pub(crate) fn ni_set_state(this: &Value, idx: usize, before: bool) {
1038    if let Value::Object(o) = this {
1039        let mut b = o.borrow_mut();
1040        b.props
1041            .insert("referenceNode".into(), Value::Object(Obj::dom(idx)));
1042        b.props
1043            .insert("pointerBeforeReferenceNode".into(), Value::Bool(before));
1044    }
1045}
1046
1047/// `NodeIterator.prototype.nextNode()`。
1048pub(crate) fn node_iterator_next_node(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
1049    let Some(cur) = ni_ref_idx(&this) else {
1050        return Ok(Value::Null);
1051    };
1052    let root = ni_root_idx(&this).unwrap_or(cur);
1053    let what_to_show = tw_what_to_show(&this);
1054    let filter = tw_get_filter(&this);
1055
1056    if ni_pointer_before(&this) {
1057        ni_set_state(&this, cur, false);
1058        let tree = DomTreeRef(it.dom.clone());
1059        if crate::os_lib::dom_traversal::evaluate_node_filter(&tree, cur, what_to_show, &mut |idx| {
1060            run_traversal_filter(it, &filter, idx)
1061        }) == crate::os_lib::dom_traversal::FilterResult::Accept {
1062            return Ok(Value::Object(Obj::dom(cur)));
1063        }
1064    }
1065
1066    let tree = DomTreeRef(it.dom.clone());
1067    let next = crate::os_lib::dom_traversal::ni_step_next(&tree, root, cur, what_to_show, &mut |idx| {
1068        run_traversal_filter(it, &filter, idx)
1069    });
1070
1071    match next {
1072        Some(n) => {
1073            ni_set_state(&this, n, false);
1074            Ok(Value::Object(Obj::dom(n)))
1075        }
1076        None => Ok(Value::Null),
1077    }
1078}
1079
1080/// `NodeIterator.prototype.previousNode()`。
1081pub(crate) fn node_iterator_previous_node(
1082    it: &mut Interp,
1083    this: Value,
1084    _a: &[Value],
1085) -> Result<Value, Value> {
1086    let Some(cur) = ni_ref_idx(&this) else {
1087        return Ok(Value::Null);
1088    };
1089    let root = ni_root_idx(&this).unwrap_or(cur);
1090    let what_to_show = tw_what_to_show(&this);
1091    let filter = tw_get_filter(&this);
1092
1093    if !ni_pointer_before(&this) {
1094        ni_set_state(&this, cur, true);
1095        let tree = DomTreeRef(it.dom.clone());
1096        if crate::os_lib::dom_traversal::evaluate_node_filter(&tree, cur, what_to_show, &mut |idx| {
1097            run_traversal_filter(it, &filter, idx)
1098        }) == crate::os_lib::dom_traversal::FilterResult::Accept {
1099            return Ok(Value::Object(Obj::dom(cur)));
1100        }
1101    }
1102
1103    let tree = DomTreeRef(it.dom.clone());
1104    let prev = crate::os_lib::dom_traversal::ni_step_previous(&tree, root, cur, what_to_show, &mut |idx| {
1105        run_traversal_filter(it, &filter, idx)
1106    });
1107
1108    match prev {
1109        Some(p) => {
1110            ni_set_state(&this, p, true);
1111            Ok(Value::Object(Obj::dom(p)))
1112        }
1113        None => Ok(Value::Null),
1114    }
1115}
1116
1117/// `document.hasStorageAccess()`(Storage Access API。丸ごと未対応
1118/// だった)。この処理系にはサードパーティ Cookie 分離/ブロック機構自体が
1119/// 無くストレージは常にアクセス可能なため、常に `true` を返す簡略実装。
1120pub(crate) fn document_has_storage_access(it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
1121    Ok(resolved_promise(it, Value::Bool(true)))
1122}
1123/// `document.requestStorageAccess()`(対をなす要求メソッド。丸ごと未対応
1124/// だった)。理由は `hasStorageAccess` と同じで、常に成功を返す。
1125pub(crate) fn document_request_storage_access(it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
1126    Ok(resolved_promise(it, Value::Undefined))
1127}
1128
1129/// `screen.orientation.lock(orientation)`(Screen Orientation API。丸ごと
1130/// 未対応だった)。実際にディスプレイの向きを固定する機構は無いため、
1131/// `fullscreenElement` 等と同じ「要求された値へ `type` を書き換えるだけ」の
1132/// 簡略実装。
1133pub(crate) fn screen_orientation_lock(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1134    if let Value::Object(o) = &this {
1135        o.borrow_mut().props.insert("type".into(), Value::str(arg(a, 0).to_js_string()));
1136    }
1137    Ok(resolved_promise(it, Value::Undefined))
1138}
1139/// `screen.orientation.unlock()`(対をなす解除メソッド。丸ごと未対応
1140/// だった)。現在のビューポート寸法から求めた自然な向きへ `type` を戻す
1141/// (`window.screen` 初期化時の判定ロジックと同じ)。
1142pub(crate) fn screen_orientation_unlock(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
1143    if let Value::Object(o) = &this {
1144        let vw = super::super::super::layout::VIEWPORT_HINT_W.load(core::sync::atomic::Ordering::Relaxed);
1145        let vh = super::super::super::layout::VIEWPORT_HINT_H.load(core::sync::atomic::Ordering::Relaxed);
1146        let otype = if vw >= vh { "landscape-primary" } else { "portrait-primary" };
1147        o.borrow_mut().props.insert("type".into(), Value::str(otype));
1148    }
1149    Ok(Value::Undefined)
1150}
1151
1152/// `window.postMessage(data, targetOrigin)`(および `iframe.contentWindow.postMessage`)。
1153/// 同一フレーム内の `_message_listeners` を発火するとともに、文書内に `<iframe src="...">` が
1154/// ある場合は子フレームへの配送キュー(`pending_child_messages`)にも積む。
1155pub(crate) fn window_post_message(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
1156    let data = structured_clone(it, Value::Undefined, a)?;
1157    let href = location_href(&it.global).unwrap_or_default();
1158    let origin = location_components(&href)
1159        .into_iter()
1160        .find(|(k, _)| *k == "origin")
1161        .map(|(_, v)| v)
1162        .unwrap_or_default();
1163    {
1164        let mut dom = it.dom.borrow_mut();
1165        let iframe_srcs: Vec<String> = dom
1166            .nodes
1167            .iter()
1168            .filter(|n| n.tag == "iframe")
1169            .filter_map(|n| n.attrs.get("src").cloned())
1170            .filter(|s| !s.is_empty())
1171            .collect();
1172        for src in iframe_srcs {
1173            dom.pending_child_messages.push((src, data.clone(), origin.clone()));
1174        }
1175    }
1176    deliver_window_message(it, data, &origin);
1177    Ok(Value::Undefined)
1178}
1179
1180/// 子 `<iframe>` 内の `window.parent.postMessage(data, targetOrigin)` / `window.top.postMessage(...)`。
1181/// 親フレームの `WebEngine` が次フレームで受け取れるよう `pending_parent_messages` に積む。
1182pub(crate) fn parent_window_post_message(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
1183    let data = structured_clone(it, Value::Undefined, a)?;
1184    let href = location_href(&it.global).unwrap_or_default();
1185    let origin = location_components(&href)
1186        .into_iter()
1187        .find(|(k, _)| *k == "origin")
1188        .map(|(_, v)| v)
1189        .unwrap_or_default();
1190    it.dom.borrow_mut().pending_parent_messages.push((data, origin));
1191    Ok(Value::Undefined)
1192}
1193
1194/// フレーム階層深さ(`0` = 最上位ページ、`1..` = 子 `<iframe>`)に応じて `window.parent` / `window.top` を設定する。
1195/// 子 `<iframe>` 内では `window.self !== window.parent` および `window.self !== window.top` が真となり、
1196/// `window.parent.postMessage(...)` で親フレームへメッセージを送れる(YouTube 埋め込みの IFrame Player API 等)。
1197pub(crate) fn configure_frame_depth(global: &Rc<RefCell<Scope>>, depth: u32) {
1198    let win_opt = global.borrow().vars.get("window").cloned();
1199    let Some(Value::Object(win)) = win_opt else { return };
1200    if depth == 0 {
1201        let win_val = Value::Object(win.clone());
1202        win.borrow_mut().props.insert("parent".into(), win_val.clone());
1203        win.borrow_mut().props.insert("top".into(), win_val.clone());
1204        global.borrow_mut().vars.insert("parent".into(), win_val.clone());
1205        global.borrow_mut().vars.insert("top".into(), win_val);
1206    } else {
1207        let parent_win = Obj::plain();
1208        {
1209            let mut p = parent_win.borrow_mut();
1210            p.props.insert("_is_parent_window".into(), Value::Bool(true));
1211            p.props.insert(
1212                "postMessage".into(),
1213                nv("parent.postMessage", parent_window_post_message),
1214            );
1215            p.props.insert("frames".into(), Value::Object(Obj::array(alloc::vec![])));
1216            p.props.insert("length".into(), Value::Number(0.0));
1217        }
1218        let parent_val = Value::Object(parent_win.clone());
1219        parent_win.borrow_mut().props.insert("self".into(), parent_val.clone());
1220        parent_win.borrow_mut().props.insert("window".into(), parent_val.clone());
1221        parent_win.borrow_mut().props.insert("parent".into(), parent_val.clone());
1222        parent_win.borrow_mut().props.insert("top".into(), parent_val.clone());
1223        win.borrow_mut().props.insert("parent".into(), parent_val.clone());
1224        win.borrow_mut().props.insert("top".into(), parent_val.clone());
1225        global.borrow_mut().vars.insert("parent".into(), parent_val.clone());
1226        global.borrow_mut().vars.insert("top".into(), parent_val);
1227    }
1228}
1229
1230/// `window` 上の `message` イベントリスナおよび `window.onmessage` を発火する。
1231pub(crate) fn deliver_window_message(it: &mut Interp, data: Value, origin: &str) {
1232    let ev = Obj::plain();
1233    ev.borrow_mut().props.insert("type".into(), Value::str("message"));
1234    ev.borrow_mut().props.insert("data".into(), data);
1235    ev.borrow_mut().props.insert("origin".into(), Value::str(origin));
1236    ev.borrow_mut().props.insert("source".into(), Value::Null);
1237    let (listeners, onmessage, win_val) = match it.global.borrow().vars.get("window") {
1238        Some(Value::Object(w)) => {
1239            let b = w.borrow();
1240            (
1241                b.props.get("_message_listeners").cloned(),
1242                b.props.get("onmessage").cloned(),
1243                Value::Object(w.clone()),
1244            )
1245        }
1246        _ => (None, None, Value::Undefined),
1247    };
1248    if let Some(h) = onmessage.filter(|h| matches!(h, Value::Object(o) if o.borrow().is_callable())) {
1249        it.call_listener(&h, win_val, &[Value::Object(ev.clone())], "window.onmessage");
1250    }
1251    if let Some(Value::Object(arr)) = listeners {
1252        let entries: Vec<Value> = match &arr.borrow().kind {
1253            ObjKind::Array(items) => items.clone(),
1254            _ => Vec::new(),
1255        };
1256        for entry in entries {
1257            if window_listener_aborted(&entry) {
1258                continue;
1259            }
1260            let cb = event_target_listener_cb(&entry);
1261            it.call_listener(
1262                &cb,
1263                Value::Undefined,
1264                &[Value::Object(ev.clone())],
1265                "window listener",
1266            );
1267        }
1268    }
1269}
1270
1271/// `reportError(error)`(丸ごと未対応だった。未捕捉例外と同じ経路で
1272/// `window.onerror`(レガシー5引数シグネチャ)/
1273/// `window.addEventListener('error', fn)`(ErrorEvent 風オブジェクト)へ
1274/// 同期的に通知する。仕様上のイベントループ経由の非同期報告・
1275/// `event.preventDefault()` によるコンソール抑制は簡略化し、常に
1276/// `console.error` へも出力する)。
1277pub(crate) fn report_error(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
1278    let err = arg(a, 0);
1279    let message = if let Value::Object(o) = &err {
1280        o.borrow()
1281            .props
1282            .get("message")
1283            .map(|v| v.to_js_string())
1284            .unwrap_or_else(|| err.to_js_string())
1285    } else {
1286        err.to_js_string()
1287    };
1288    let _ = console_log(it, Value::Undefined, &[Value::str(&message)]);
1289    let ev = Obj::plain();
1290    {
1291        let mut e = ev.borrow_mut();
1292        e.props.insert("type".into(), Value::str("error"));
1293        e.props.insert("message".into(), Value::str(&message));
1294        e.props.insert("error".into(), err.clone());
1295        e.props.insert("filename".into(), Value::str(""));
1296        e.props.insert("lineno".into(), Value::Number(0.0));
1297        e.props.insert("colno".into(), Value::Number(0.0));
1298    }
1299    let window = match it.global.borrow().vars.get("window") {
1300        Some(Value::Object(w)) => Some(w.clone()),
1301        _ => None,
1302    };
1303    if let Some(w) = &window {
1304        let onerror = w.borrow().props.get("onerror").cloned();
1305        if let Some(f) = onerror.and_then(callable_or_none) {
1306            it.call_listener(
1307                &f,
1308                Value::Undefined,
1309                &[
1310                    Value::str(&message),
1311                    Value::str(""),
1312                    Value::Number(0.0),
1313                    Value::Number(0.0),
1314                    err.clone(),
1315                ],
1316                "window.onerror",
1317            );
1318        }
1319        let listeners = w.borrow().props.get("_error_listeners").cloned();
1320        if let Some(Value::Object(arr)) = listeners {
1321            let entries: Vec<Value> = match &arr.borrow().kind {
1322                ObjKind::Array(items) => items.clone(),
1323                _ => Vec::new(),
1324            };
1325            for entry in entries {
1326                if window_listener_aborted(&entry) {
1327                    continue;
1328                }
1329                let cb = event_target_listener_cb(&entry);
1330                it.call_listener(
1331                &cb,
1332                Value::Undefined,
1333                &[Value::Object(ev.clone())],
1334                "window listener",
1335            );
1336            }
1337        }
1338    }
1339    Ok(Value::Undefined)
1340}
1341
1342/// `AbortSignal` 用の既定の中断理由(`DOMException("AbortError")` の簡易近似)。
1343pub(crate) fn make_abort_error() -> Value {
1344    make_dom_exception("AbortError", "signal is aborted without reason")
1345}
1346/// `new DOMException(message?, name?)`。仕様どおり既定値は `message=""`/
1347/// `name="Error"`。
1348pub(crate) fn dom_exception_ctor(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
1349    let message = match arg(a, 0) {
1350        Value::Undefined => String::new(),
1351        v => v.to_js_string(),
1352    };
1353    let name = match arg(a, 1) {
1354        Value::Undefined => String::from("Error"),
1355        v => v.to_js_string(),
1356    };
1357    Ok(make_dom_exception(&name, &message))
1358}
1359/// `name` から DOM Level 4 のレガシー数値定数(`err.code`)を引く。未知の
1360/// 名前は仕様どおり `0`。
1361pub(crate) fn dom_exception_legacy_code(name: &str) -> f64 {
1362    match name {
1363        "IndexSizeError" => 1.0,
1364        "HierarchyRequestError" => 3.0,
1365        "WrongDocumentError" => 4.0,
1366        "InvalidCharacterError" => 5.0,
1367        "NoModificationAllowedError" => 7.0,
1368        "NotFoundError" => 8.0,
1369        "NotSupportedError" => 9.0,
1370        "InUseAttributeError" => 10.0,
1371        "InvalidStateError" => 11.0,
1372        "SyntaxError" => 12.0,
1373        "InvalidModificationError" => 13.0,
1374        "NamespaceError" => 14.0,
1375        "InvalidAccessError" => 15.0,
1376        "SecurityError" => 18.0,
1377        "NetworkError" => 19.0,
1378        "AbortError" => 20.0,
1379        "TimeoutError" => 23.0,
1380        "DataCloneError" => 25.0,
1381        _ => 0.0,
1382    }
1383}
1384/// `DOMException` インスタンスを構築する共通ヘルパー。`AbortController.
1385/// abort()` 等、エンジン内部で例外を作る箇所からも再利用する(詳細は
1386/// `dom_exception_ctor` 参照)。
1387pub(crate) fn make_dom_exception(name: &str, message: &str) -> Value {
1388    let o = Obj::plain();
1389    {
1390        let mut b = o.borrow_mut();
1391        b.props.insert("name".into(), Value::str(name));
1392        b.props.insert("message".into(), Value::str(message));
1393        b.props
1394            .insert("code".into(), Value::Number(dom_exception_legacy_code(name)));
1395        b.props.insert("toString".into(), nv("toString", dom_exception_to_string));
1396    }
1397    Value::Object(o)
1398}
1399pub(crate) fn dom_exception_to_string(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
1400    if let Value::Object(o) = &this {
1401        let b = o.borrow();
1402        let name = b.props.get("name").map(|v| v.to_js_string()).unwrap_or_default();
1403        let message = b.props.get("message").map(|v| v.to_js_string()).unwrap_or_default();
1404        return Ok(Value::str(if message.is_empty() {
1405            name
1406        } else {
1407            alloc::format!("{}: {}", name, message)
1408        }));
1409    }
1410    Ok(Value::str("DOMException"))
1411}
1412/// `controller.abort(reason?)`。以前は `signal.aborted` を立てるだけで、(1) 既に中断済み
1413/// の場合の二重発火防止、(2) 仕様どおりの `signal.reason`、(3) `signal.addEventListener
1414/// ('abort', fn)`/`signal.onabort` によるコールバック通知、のいずれも欠けていた
1415/// (`addEventListener` 自体が単なる no-op だったため、`abort()` を呼んでも登録した
1416/// リスナが一切発火しないサイレントなバグ)。
1417pub(crate) fn abort_controller_abort(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1418    if let Value::Object(o) = &this {
1419        let sig = o.borrow().props.get("signal").cloned();
1420        if let Some(Value::Object(s)) = sig {
1421            let already_aborted = matches!(s.borrow().props.get("aborted"), Some(Value::Bool(true)));
1422            if already_aborted {
1423                return Ok(Value::Undefined);
1424            }
1425            let reason = match arg(a, 0) {
1426                Value::Undefined => make_abort_error(),
1427                r => r,
1428            };
1429            {
1430                let mut b = s.borrow_mut();
1431                b.props.insert("aborted".into(), Value::Bool(true));
1432                b.props.insert("reason".into(), reason);
1433            }
1434            let cb = s.borrow().props.get("onabort").cloned();
1435            if let Some(f) = cb.and_then(callable_or_none) {
1436                it.call_listener(&f, Value::Object(s.clone()), &[], "AbortSignal onabort");
1437            }
1438        }
1439    }
1440    Ok(Value::Undefined)
1441}
1442/// `signal.addEventListener('abort', fn)`。以前は無条件 no-op で、リスナが一切
1443/// 保持/発火されなかった。他の `EventTarget` 風オブジェクト(XHR 等)と同じ
1444/// 「`on<type>` プロパティへ格納する単一リスナ」簡略方式で対応する。
1445pub(crate) fn abort_signal_add_event_listener(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1446    if let Value::Object(o) = &this {
1447        o.borrow_mut()
1448            .props
1449            .insert(format!("on{}", arg(a, 0).to_js_string()), arg(a, 1));
1450    }
1451    Ok(Value::Undefined)
1452}
1453/// `signal.removeEventListener('abort', fn)`。`addEventListener` は実装済みなのに
1454/// 対をなす削除側が無条件 no-op のままだったバグ(登録したリスナを外すつもりが
1455/// 何も起きず、後から `abort()` すると外したはずのリスナがまだ発火する)。
1456/// 「`on<type>` に単一リスナを格納する」既存の簡略方式に合わせ、渡された関数が
1457/// 現在登録済みのものと同一参照の場合のみ消す(別の関数参照を渡した場合は
1458/// 仕様どおり no-op のまま)。
1459pub(crate) fn abort_signal_remove_event_listener(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1460    if let Value::Object(o) = &this {
1461        let key = format!("on{}", arg(a, 0).to_js_string());
1462        let target = arg(a, 1);
1463        let matches = match (o.borrow().props.get(&key), &target) {
1464            (Some(Value::Object(stored)), Value::Object(t)) => Rc::ptr_eq(stored, t),
1465            _ => false,
1466        };
1467        if matches {
1468            o.borrow_mut().props.shift_remove(&key);
1469        }
1470    }
1471    Ok(Value::Undefined)
1472}
1473/// `new AbortSignal()`(仕様上直接は構築できない)。
1474pub(crate) fn abort_signal_illegal_ctor(it: &mut Interp, _t: Value, _a: &[Value]) -> Result<Value, Value> {
1475    Err(it.type_error("Illegal constructor"))
1476}
1477
1478pub(crate) fn abort_controller_ctor(_it: &mut Interp, _t: Value, _a: &[Value]) -> Result<Value, Value> {
1479    let signal = Obj::plain();
1480    {
1481        let mut s = signal.borrow_mut();
1482        s.props.insert("aborted".into(), Value::Bool(false));
1483        s.props.insert("reason".into(), Value::Undefined);
1484        s.props.insert(
1485            "addEventListener".into(),
1486            nv("addEventListener", abort_signal_add_event_listener),
1487        );
1488        s.props.insert(
1489            "removeEventListener".into(),
1490            nv("removeEventListener", abort_signal_remove_event_listener),
1491        );
1492        s.props.insert(
1493            "throwIfAborted".into(),
1494            nv("throwIfAborted", abort_signal_throw_if_aborted),
1495        );
1496    }
1497    let ctrl = Obj::plain();
1498    {
1499        let mut c = ctrl.borrow_mut();
1500        c.props.insert("signal".into(), Value::Object(signal));
1501        c.props
1502            .insert("abort".into(), nv("abort", abort_controller_abort));
1503    }
1504    Ok(Value::Object(ctrl))
1505}
1506/// `signal.throwIfAborted()`(ES2022/DOM。既に中断済みなら `reason` を投げ、
1507/// そうでなければ何もしない、fetch 等のループ内チェックで頻出のイディオム)が
1508/// 丸ごと未対応だった。`AbortSignal.timeout()`/`.any()` は内部で
1509/// `new AbortController()` を経由するため、`abort_controller_ctor`/
1510/// `abort_signal_abort_static` の2箇所にさえ追加すれば全生成経路をカバーする。
1511pub(crate) fn abort_signal_throw_if_aborted(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
1512    if let Value::Object(o) = &this {
1513        let aborted = matches!(o.borrow().props.get("aborted"), Some(Value::Bool(true)));
1514        if aborted {
1515            let reason = o.borrow().props.get("reason").cloned().unwrap_or(Value::Undefined);
1516            return Err(reason);
1517        }
1518    }
1519    Ok(Value::Undefined)
1520}
1521/// `AbortSignal.abort(reason?)`(ES2022)。既に中断済みの状態で signal を返す
1522/// 静的ファクトリ。`AbortController` を経由せず即座に中断済み signal が欲しい
1523/// 場合(引数バリデーション失敗時の早期中断等)に使われる。
1524pub(crate) fn abort_signal_abort_static(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
1525    let signal = Obj::plain();
1526    {
1527        let mut s = signal.borrow_mut();
1528        s.props.insert("aborted".into(), Value::Bool(true));
1529        let reason = match arg(a, 0) {
1530            Value::Undefined => make_abort_error(),
1531            r => r,
1532        };
1533        s.props.insert("reason".into(), reason);
1534        s.props.insert(
1535            "addEventListener".into(),
1536            nv("addEventListener", abort_signal_add_event_listener),
1537        );
1538        s.props.insert(
1539            "removeEventListener".into(),
1540            nv("removeEventListener", abort_signal_remove_event_listener),
1541        );
1542        s.props.insert(
1543            "throwIfAborted".into(),
1544            nv("throwIfAborted", abort_signal_throw_if_aborted),
1545        );
1546    }
1547    Ok(Value::Object(signal))
1548}
1549/// `AbortSignal.timeout(ms)`(ES2022)。指定ミリ秒後に自動的に abort する
1550/// AbortSignal を返す、fetch のタイムアウト実装で最頻出のイディオム。この
1551/// エンジンの `setTimeout` は実時間を計測せずマクロタスクキューに積むだけの
1552/// 簡略実装のため、正確な実時間待機ではなくキュー順序に従った近似になる
1553/// (既存の `setTimeout` 全体の簡略化と同じ水準)。ネイティブ関数はクロージャで
1554/// `ms` を束縛できないため、`AbortController`/`setTimeout` を組み合わせる
1555/// JS ソースを組み立てて `eval_source` で実行する。
1556/// `AbortSignal.timeout(ms)`。以前は `c.abort()` を引数無しで呼んでおり、
1557/// `signal.reason` が仕様どおりの `TimeoutError`(DOM Level 4 legacy code
1558/// 23)ではなく既定の `AbortError`(code 20)になっていたバグ。
1559/// `abort_signal_any` と同じ「グローバル一時変数経由で Rust 側の値を JS へ
1560/// 渡す」パターンで、事前に構築した `TimeoutError` の `DOMException` を
1561/// 束縛してから `c.abort(reason)` を呼ぶ。
1562pub(crate) fn abort_signal_timeout(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
1563    let ms = arg(a, 0).to_number();
1564    let reason = make_dom_exception("TimeoutError", "signal timed out");
1565    it.global
1566        .borrow_mut()
1567        .vars
1568        .insert("__abort_signal_timeout_reason".into(), reason);
1569    let src = alloc::format!(
1570        "(function(){{ var c = new AbortController(); var r = __abort_signal_timeout_reason; \
1571         setTimeout(function(){{ c.abort(r); }}, {}); return c.signal; }})()",
1572        ms
1573    );
1574    let result = it.eval_source(&src);
1575    it.global.borrow_mut().vars.remove("__abort_signal_timeout_reason");
1576    result
1577}
1578/// `AbortSignal.any(iterable)`(ES2024)。渡した signal 群のいずれかが中断された
1579/// 時点で中断される合成 signal を返す(複数の中断条件を1つの signal にまとめる
1580/// 定番パターン)。既存の `addEventListener('abort', fn)` は「`on<type>` に単一
1581/// リスナを格納する」簡略実装のため、入力 signal 側に別のリスナが既に登録済みだと
1582/// 上書きしてしまう制約がある(この処理系の EventTarget 系オブジェクト全般に
1583/// 共通の既知の簡略化)。`abort_signal_timeout` と同じ理由(ネイティブ関数は
1584/// クロージャで値を束縛できない)で、入力配列をグローバルの一時変数経由で
1585/// 渡した上で `eval_source` に組み立てた JS を実行する。
1586pub(crate) fn abort_signal_any(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
1587    let signals = it.iter_to_vec(&arg(a, 0));
1588    it.global
1589        .borrow_mut()
1590        .vars
1591        .insert("__abort_signal_any_input".into(), Value::Object(Obj::array(signals)));
1592    let src = "(function(){ \
1593        var c = new AbortController(); \
1594        var arr = __abort_signal_any_input; \
1595        for (var i = 0; i < arr.length; i++) { \
1596            if (arr[i].aborted) { c.abort(arr[i].reason); return c.signal; } \
1597        } \
1598        for (var i = 0; i < arr.length; i++) { \
1599            (function(s){ \
1600                s.addEventListener('abort', function(){ if (!c.signal.aborted) c.abort(s.reason); }); \
1601            })(arr[i]); \
1602        } \
1603        return c.signal; \
1604    })()";
1605    let result = it.eval_source(src);
1606    it.global.borrow_mut().vars.remove("__abort_signal_any_input");
1607    result
1608}
1609