Skip to main content

atmos/os_lib/js/builtins/
promise.rs

1// 分割: builtins.rs より機械的に移動(2026-07-16 リファクタ フェーズ2)。
2// ロジック不変。可視性のみ pub(crate) へ昇格し、親が pub(crate) use で再エクスポート。
3use super::*;
4
5// ============ Promise ============
6
7pub(crate) use alloc::rc::Rc as PromiseRc;
8pub(crate) use core::cell::RefCell as PromiseRefCell;
9
10pub(crate) fn new_pending() -> PromiseRc<PromiseRefCell<PromiseState>> {
11    PromiseRc::new(PromiseRefCell::new(PromiseState::pending()))
12}
13
14pub(crate) fn this_promise_state(this: &Value) -> Option<PromiseRc<PromiseRefCell<PromiseState>>> {
15    if let Value::Object(o) = this {
16        let o = unwrap_proxy_target(o);
17        let b = o.borrow();
18        if let ObjKind::PromiseObj(s) = &b.kind {
19            return Some(s.clone());
20        }
21    }
22    None
23}
24
25pub(crate) fn callable_or_none(v: Value) -> Option<Value> {
26    if let Value::Object(o) = &v {
27        if o.borrow().is_callable() {
28            return Some(v);
29        }
30    }
31    None
32}
33
34/// `new Promise((resolve, reject) => {...})`。executor を同期実行する。
35pub(crate) fn promise_ctor(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
36    let state = new_pending();
37    let executor = arg(a, 0);
38    if let Some(exec) = callable_or_none(executor) {
39        let resolve = Value::Object(Obj::resolver(state.clone(), false));
40        let reject = Value::Object(Obj::resolver(state.clone(), true));
41        if let Err(e) = it.call_value(&exec, Value::Undefined, &[resolve, reject]) {
42            if !it.aborted {
43                it.promise_reject(&state, e);
44            }
45        }
46    }
47    Ok(Value::Object(Obj::promise(state)))
48}
49
50pub(crate) fn promise_resolve_static(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
51    let v = arg(a, 0);
52    // 既に Promise ならそのまま返す。
53    if this_promise_state(&v).is_some() {
54        return Ok(v);
55    }
56    let state = new_pending();
57    it.promise_resolve(&state, v);
58    Ok(Value::Object(Obj::promise(state)))
59}
60
61pub(crate) fn promise_reject_static(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
62    let state = new_pending();
63    it.promise_reject(&state, arg(a, 0));
64    Ok(Value::Object(Obj::promise(state)))
65}
66
67/// `Promise.withResolvers()`(ES2024): `{ promise, resolve, reject }` を返す。
68pub(crate) fn promise_with_resolvers_static(_it: &mut Interp, _t: Value, _a: &[Value]) -> Result<Value, Value> {
69    let state = new_pending();
70    let promise = Value::Object(Obj::promise(state.clone()));
71    let resolve = Value::Object(Obj::resolver(state.clone(), false));
72    let reject = Value::Object(Obj::resolver(state, true));
73    let o = Obj::plain();
74    {
75        let mut b = o.borrow_mut();
76        b.props.insert("promise".into(), promise);
77        b.props.insert("resolve".into(), resolve);
78        b.props.insert("reject".into(), reject);
79    }
80    Ok(Value::Object(o))
81}
82
83/// `Promise.try(callbackFn, ...args)`(ES2025): callbackFn を同期実行し、
84/// 同期例外なら reject、戻り値が thenable ならその状態を採用、それ以外は resolve する。
85/// `new Promise(...)` と違い、callbackFn は非 async 関数でも同期/非同期どちらの結果も統一的に扱える。
86pub(crate) fn promise_try_static(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
87    let state = new_pending();
88    let callback = arg(a, 0);
89    let rest: Vec<Value> = if a.len() > 1 { a[1..].to_vec() } else { Vec::new() };
90    match it.call_value(&callback, Value::Undefined, &rest) {
91        Ok(v) => it.promise_resolve(&state, v),
92        Err(e) => {
93            if !it.aborted {
94                it.promise_reject(&state, e);
95            }
96        }
97    }
98    Ok(Value::Object(Obj::promise(state)))
99}
100
101/// 既に解決済みの Promise を作る(同期取得した値を await 互換で返すため)。
102pub(crate) fn resolved_promise(it: &mut Interp, v: Value) -> Value {
103    let state = new_pending();
104    it.promise_resolve(&state, v);
105    Value::Object(Obj::promise(state))
106}
107/// `TimeRanges`(`buffered`/`seekable`用)の常に空の実装。`length:0` に
108/// 加え、範囲外アクセスとして仕様どおり例外を投げる `start`/`end` を持つ。
109pub fn make_empty_time_ranges() -> super::super::value::ObjRef {
110    let o = Obj::plain();
111    {
112        let mut b = o.borrow_mut();
113        b.props.insert("length".into(), Value::Number(0.0));
114        b.props.insert("start".into(), nv("start", time_ranges_out_of_bounds));
115        b.props.insert("end".into(), nv("end", time_ranges_out_of_bounds));
116    }
117    o
118}
119pub(crate) fn time_ranges_out_of_bounds(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
120    Err(it.error(alloc::format!(
121        "Failed to execute on 'TimeRanges': The index provided ({}) is greater than or equal to the maximum bound (0).",
122        arg(a, 0).to_number() as i64
123    )))
124}
125/// `<audio>`/`<video>` の `.play()`(HTMLMediaElement。丸ごと未対応だった)。
126/// 実際の再生までは配線しない無害な簡略実装だが、仕様どおり Promise を返す
127/// (`await el.play()`/`el.play().catch(...)` という定番パターンで
128/// 「Promise ではない」別種のエラーになるのを防ぐ)。
129pub fn dom_media_play(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
130    if let Some(idx) = this_dom_idx(&this) {
131        let was_paused = it.dom.borrow().get_attr(idx, "_paused").as_deref() != Some("false");
132        it.dom.borrow_mut().set_attr(idx, "_paused", "false");
133        // 仕様どおり `play`/`playing` イベントを発火(既に再生中なら発火しない)。
134        // `.pause()` の `pause` イベントと対(`dialog.close()` の `close` イベント
135        // 発火と同じパターン)。
136        if was_paused {
137            let _ = it.dispatch_event_in_interp(idx, "play", &[]);
138            let _ = it.dispatch_event_in_interp(idx, "playing", &[]);
139        }
140    }
141    Ok(resolved_promise(it, Value::Undefined))
142}
143/// `<audio>`/`<video>.pause()`。`.play()` と対になり、内部専用属性
144/// `_paused` を切り替えるだけの簡略実装(`.paused` getter と状態を共有)。
145/// 仕様どおり `pause` イベントを発火する(既に一時停止中なら発火しない)。
146pub fn dom_media_pause(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
147    if let Some(idx) = this_dom_idx(&this) {
148        let was_playing = it.dom.borrow().get_attr(idx, "_paused").as_deref() == Some("false");
149        it.dom.borrow_mut().set_attr(idx, "_paused", "true");
150        if was_playing {
151            let _ = it.dispatch_event_in_interp(idx, "pause", &[]);
152        }
153    }
154    Ok(Value::Undefined)
155}
156/// `navigator.clipboard.writeText(text)`。この OS 全体で共有される実クリップボードは
157/// 無いため、`navigator.clipboard` オブジェクト自身が持つ `_clipboard` prop への
158/// 単純な文字列保持で近似する。
159pub(crate) fn clipboard_write_text(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
160    if let Value::Object(o) = &this {
161        o.borrow_mut().props.insert("_clipboard".into(), Value::str(arg(a, 0).to_js_string()));
162    }
163    Ok(resolved_promise(it, Value::Undefined))
164}
165/// `navigator.clipboard.readText()`。
166pub(crate) fn clipboard_read_text(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
167    let text = if let Value::Object(o) = &this {
168        o.borrow().props.get("_clipboard").map(|v| v.to_js_string()).unwrap_or_default()
169    } else {
170        String::new()
171    };
172    Ok(resolved_promise(it, Value::str(text)))
173}
174/// `new ClipboardItem({mimeType: blobOrValue, ...})`(丸ごと未対応だった)。
175/// `.types`(キー一覧)と `.getType(type)`(対応する Blob/値を解決済み
176/// Promise で返す)のみの簡略実装。
177pub(crate) fn clipboard_item_ctor(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
178    let data = arg(a, 0);
179    let types: Vec<Value> = if let Value::Object(d) = &data {
180        d.borrow().props.keys().map(Value::str).collect()
181    } else {
182        Vec::new()
183    };
184    let o = Obj::plain();
185    {
186        let mut b = o.borrow_mut();
187        b.props.insert("_clipboard_item_data".into(), data);
188        b.props.insert("types".into(), Value::Object(Obj::array(types)));
189        b.props
190            .insert("getType".into(), nv("ClipboardItem.getType", clipboard_item_get_type));
191    }
192    Ok(Value::Object(o))
193}
194/// `ClipboardItem.prototype.getType(type)`。
195pub(crate) fn clipboard_item_get_type(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
196    let ty = arg(a, 0).to_js_string();
197    let value = if let Value::Object(o) = &this {
198        match o.borrow().props.get("_clipboard_item_data").cloned() {
199            Some(Value::Object(d)) => d.borrow().props.get(&ty).cloned().unwrap_or(Value::Undefined),
200            _ => Value::Undefined,
201        }
202    } else {
203        Value::Undefined
204    };
205    Ok(resolved_promise(it, value))
206}
207/// Blob の隠しプロパティ `_blob_bytes`(`ObjKind::Array` の数値配列)を UTF-8
208/// 文字列へ同期的に変換する(クリップボード書き込みで `.text()` の Promise を
209/// 経由せず値を取り出すためのヘルパー)。
210pub(crate) fn blob_bytes_to_string(blob: &Value) -> Option<String> {
211    let Value::Object(o) = blob else { return None };
212    let bytes_obj = o.borrow().props.get("_blob_bytes").cloned();
213    let Some(Value::Object(bytes)) = bytes_obj else { return None };
214    let byte_vals: alloc::vec::Vec<u8> = match &bytes.borrow().kind {
215        ObjKind::Array(items) => items.iter().map(|v| v.to_number() as u8).collect(),
216        _ => return None,
217    };
218    Some(String::from_utf8_lossy(&byte_vals).into_owned())
219}
220/// `navigator.clipboard.write(items)`(丸ごと未対応だった。`ClipboardItem`
221/// 配列を受け取り、内部の `text/plain` エントリを既存の `_clipboard`
222/// 文字列プロパティへ書き込む簡略実装。画像等の他 MIME タイプは対象外)。
223pub(crate) fn navigator_clipboard_write(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
224    if let Some(Value::Object(item)) = this_items(&arg(a, 0)).into_iter().next() {
225        let data = item.borrow().props.get("_clipboard_item_data").cloned();
226        if let Some(Value::Object(d)) = data {
227            let plain = d.borrow().props.get("text/plain").cloned();
228            if let Some(v) = plain {
229                let text = blob_bytes_to_string(&v).unwrap_or_else(|| v.to_js_string());
230                if let Value::Object(o) = &this {
231                    o.borrow_mut().props.insert("_clipboard".into(), Value::str(text));
232                }
233            }
234        }
235    }
236    Ok(resolved_promise(it, Value::Undefined))
237}
238/// `navigator.userAgentData.getHighEntropyValues(hints)`(丸ごと未対応
239/// だった。低エントロピー値(`brands`/`mobile`/`platform`)は常に含み、
240/// 要求されたヒントに応じて残りの固定値を追加で返す誠実な簡略実装。
241/// 実データを収集する仕組みが無いため全て固定値だが、既存の
242/// `navigator.userAgent`/`.platform`と整合する値を返す)。
243pub(crate) fn navigator_get_high_entropy_values(
244    it: &mut Interp,
245    this: Value,
246    a: &[Value],
247) -> Result<Value, Value> {
248    let hints: alloc::vec::Vec<String> = this_items(&arg(a, 0)).iter().map(|v| v.to_js_string()).collect();
249    let result = Obj::plain();
250    {
251        let mut r = result.borrow_mut();
252        if let Value::Object(ua) = &this {
253            for key in ["brands", "mobile", "platform"] {
254                if let Some(v) = ua.borrow().props.get(key).cloned() {
255                    r.props.insert(key.into(), v);
256                }
257            }
258        }
259        for hint in &hints {
260            match hint.as_str() {
261                "architecture" => {
262                    r.props.insert("architecture".into(), Value::str("arm"));
263                }
264                "bitness" => {
265                    r.props.insert("bitness".into(), Value::str("64"));
266                }
267                "model" => {
268                    r.props.insert("model".into(), Value::str(""));
269                }
270                "platformVersion" => {
271                    r.props.insert("platformVersion".into(), Value::str("1.0"));
272                }
273                "uaFullVersion" | "fullVersionList" => {
274                    if let Value::Object(ua) = &this {
275                        if let Some(v) = ua.borrow().props.get("brands").cloned() {
276                            r.props.insert(hint.clone(), v);
277                        }
278                    }
279                }
280                _ => {}
281            }
282        }
283    }
284    Ok(resolved_promise(it, Value::Object(result)))
285}
286/// `navigator.storage.estimate()`(StorageManager API。丸ごと未対応
287/// だった)。実際のディスク使用量計測機構が無いため`usage: 0`(誠実な
288/// 未計測値)、`quota`は固定の代表値(1GB)を返す簡略実装。
289pub(crate) fn navigator_storage_estimate(it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
290    let result = Obj::plain();
291    {
292        let mut r = result.borrow_mut();
293        r.props.insert("usage".into(), Value::Number(0.0));
294        r.props.insert("quota".into(), Value::Number(1024.0 * 1024.0 * 1024.0));
295    }
296    Ok(resolved_promise(it, Value::Object(result)))
297}
298/// `navigator.storage.persist()`/`.persisted()`(丸ごと未対応だった)。
299/// この処理系はSylFSへの書き込みが常に永続化されるため、両方とも常に
300/// `true`を返す誠実な簡略実装。
301pub(crate) fn navigator_storage_persist(it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
302    Ok(resolved_promise(it, Value::Bool(true)))
303}
304/// `navigator.clipboard.read()`(丸ごと未対応だった)。現在の `_clipboard`
305/// 文字列を `text/plain` の `Blob` として包んだ `ClipboardItem` 1件を返す。
306pub(crate) fn navigator_clipboard_read(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
307    let text = if let Value::Object(o) = &this {
308        o.borrow().props.get("_clipboard").map(|v| v.to_js_string()).unwrap_or_default()
309    } else {
310        String::new()
311    };
312    let blob = make_blob(text.bytes().map(|b| b as f64).collect(), String::from("text/plain"));
313    let data_obj = Obj::plain();
314    data_obj.borrow_mut().props.insert("text/plain".into(), blob);
315    let item = clipboard_item_ctor(it, Value::Undefined, &[Value::Object(data_obj)])?;
316    Ok(resolved_promise(it, Value::Object(Obj::array(alloc::vec![item]))))
317}
318
319/// 拒否済みの Promise を作る(fetch 失敗時)。
320pub(crate) fn rejected_promise(it: &mut Interp, reason: Value) -> Value {
321    let state = new_pending();
322    it.promise_reject(&state, reason);
323    Value::Object(Obj::promise(state))
324}
325
326/// URL を (is_https, host, path) に分解。スキーム無しは弾く(絶対のみ)。
327/// `string_slice` lint を避けるため添字スライスは使わず split_once/strip_prefix で処理。
328pub(crate) fn split_http_url(url: &str) -> Option<(bool, String, String)> {
329    let (is_https, rest) = if let Some(r) = url.strip_prefix("https://") {
330        (true, r)
331    } else if let Some(r) = url.strip_prefix("http://") {
332        (false, r)
333    } else {
334        return None;
335    };
336    let (host, path) = match rest.split_once('/') {
337        Some((h, p)) => (h.to_string(), format!("/{}", p)),
338        None => (rest.to_string(), String::from("/")),
339    };
340    if host.is_empty() {
341        return None;
342    }
343    Some((is_https, host, path))
344}
345
346/// `data:[<mime>][;base64],<data>` の本文部分(カンマ以降)を取り出す。base64 は未対応。
347pub(crate) fn data_url_body(url: &str) -> Option<String> {
348    let rest = url.strip_prefix("data:")?;
349    let (_meta, body) = rest.split_once(',')?;
350    Some(body.to_string())
351}
352
353/// Response オブジェクトを生成(status/ok/_body と text()/json() メソッドを持つ)。
354/// `Response.prototype.statusText`(丸ごと未対応で常に `undefined` だった。`if
355/// (!response.ok) console.log(response.status, response.statusText)` のような
356/// エラーログの定番パターンでよく参照される)。標準的な HTTP ステータス文言の
357/// 主要なもののみのテーブルで近似する(`Reason-Phrase` はサーバ実装依存で仕様上も
358/// クライアント側で正確に再現できるものではない)。
359pub(crate) fn http_status_text(status: u16) -> &'static str {
360    match status {
361        200 => "OK",
362        201 => "Created",
363        202 => "Accepted",
364        204 => "No Content",
365        301 => "Moved Permanently",
366        302 => "Found",
367        304 => "Not Modified",
368        400 => "Bad Request",
369        401 => "Unauthorized",
370        403 => "Forbidden",
371        404 => "Not Found",
372        405 => "Method Not Allowed",
373        408 => "Request Timeout",
374        409 => "Conflict",
375        410 => "Gone",
376        429 => "Too Many Requests",
377        500 => "Internal Server Error",
378        501 => "Not Implemented",
379        502 => "Bad Gateway",
380        503 => "Service Unavailable",
381        504 => "Gateway Timeout",
382        _ => "",
383    }
384}
385pub(crate) fn make_response(status: u16, body: String) -> Value {
386    make_response_with_url(status, body, String::new())
387}
388/// `init.status`(`{status, headers}` 形の `ResponseInit`)を読む共通ヘルパ。
389pub(crate) fn response_init_status(init: &Value, default: u16) -> u16 {
390    match init {
391        Value::Object(o) => o
392            .borrow()
393            .props
394            .get("status")
395            .map(|v| v.to_number() as u16)
396            .unwrap_or(default),
397        _ => default,
398    }
399}
400/// `new Response(body, init)`(Fetch API)。
401pub(crate) fn response_ctor(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
402    let body = match arg(a, 0) {
403        Value::Undefined => String::new(),
404        v => v.to_js_string(),
405    };
406    let init = arg(a, 1);
407    let status = response_init_status(&init, 200);
408    let headers_init = match &init {
409        Value::Object(o) => o.borrow().props.get("headers").cloned().unwrap_or(Value::Undefined),
410        _ => Value::Undefined,
411    };
412    let resp = make_response(status, body);
413    if let Value::Object(r) = &resp {
414        r.borrow_mut().props.insert("headers".into(), make_headers(headers_init));
415    }
416    Ok(resp)
417}
418/// `Response.json(data, init)`(ES2022 static factory。`JSON.stringify` +
419/// `content-type: application/json` ヘッダ付与を1回で行う)。
420pub(crate) fn response_json_static(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
421    let body = json_stringify(it, Value::Undefined, &[arg(a, 0)])?.to_js_string();
422    let status = response_init_status(&arg(a, 1), 200);
423    let resp = make_response(status, body);
424    if let Value::Object(r) = &resp {
425        let headers = make_headers(Value::Undefined);
426        if let Value::Object(h) = &headers {
427            h.borrow_mut()
428                .props
429                .insert("content-type".into(), Value::str("application/json"));
430        }
431        r.borrow_mut().props.insert("headers".into(), headers);
432    }
433    Ok(resp)
434}
435/// `Response.error()`(ネットワークエラーを表す、`ok:false`/`type:"error"` の
436/// 特殊な不透明 Response)。
437pub(crate) fn response_error_static(_it: &mut Interp, _t: Value, _a: &[Value]) -> Result<Value, Value> {
438    let resp = make_response(0, String::new());
439    if let Value::Object(r) = &resp {
440        let mut b = r.borrow_mut();
441        b.props.insert("type".into(), Value::str("error"));
442        b.props.insert("ok".into(), Value::Bool(false));
443    }
444    Ok(resp)
445}
446/// `Response.redirect(url, status?)`(既定 302。`Location` ヘッダ付きの
447/// リダイレクト応答を表す Response を返す)。
448pub(crate) fn response_redirect_static(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
449    let url = arg(a, 0).to_js_string();
450    let status = match arg(a, 1) {
451        Value::Undefined => 302,
452        v => v.to_number() as u16,
453    };
454    let resp = make_response(status, String::new());
455    if let Value::Object(r) = &resp {
456        let headers = make_headers(Value::Undefined);
457        if let Value::Object(h) = &headers {
458            h.borrow_mut().props.insert("location".into(), Value::str(url));
459        }
460        r.borrow_mut().props.insert("headers".into(), headers);
461    }
462    Ok(resp)
463}
464
465/// `Response.prototype.url`(`fetch()` が実際に取得した絶対 URL。相対 URL 指定時
466/// でも解決済みの絶対形が返る、リダイレクト後の最終 URL 確認等で使われる定番
467/// プロパティ)が丸ごと未対応で常に `undefined` だった。`make_response` の呼び出し元
468/// が URL を知っている場合はこちらを使う。
469pub(crate) fn make_response_with_url(status: u16, body: String, url: String) -> Value {
470    let resp = Obj::plain();
471    {
472        let mut r = resp.borrow_mut();
473        r.props
474            .insert("status".into(), Value::Number(status as f64));
475        r.props
476            .insert("ok".into(), Value::Bool((200..300).contains(&status)));
477        r.props
478            .insert("statusText".into(), Value::str(http_status_text(status)));
479        r.props.insert("url".into(), Value::str(url));
480        // `response.type`/`.redirected` が丸ごと未対応で常に `undefined` だった。
481        // この処理系に CORS モデルは無くリダイレクトも透過的に処理される(追跡は
482        // していない)ため、常に妥当な既定値(`"basic"`/`false`)を返す簡略実装。
483        r.props.insert("type".into(), Value::str("basic"));
484        r.props.insert("redirected".into(), Value::Bool(false));
485        // `response.headers`(`Headers` 未対応だった当時は丸ごと欠落していた)。
486        // この処理系の HTTP クライアントは応答ヘッダを捕捉していないため常に空だが、
487        // `get`/`has` 等のメソッドを持つ本物の `Headers` 形(`undefined` ではない)
488        // を返す点に意味がある。
489        r.props.insert("headers".into(), make_headers(Value::Undefined));
490        r.props.insert("_body".into(), Value::str(body));
491        r.props.insert("text".into(), nv("text", response_text));
492        r.props.insert("json".into(), nv("json", response_json));
493        // `Response.prototype.blob()`(Fetch API。`fetch(...).then(r => r.blob())` という
494        // 定番パターンが `blob is not defined` になっていた)が丸ごと未対応だった。
495        r.props.insert("blob".into(), nv("blob", response_blob));
496        // `Response.prototype.arrayBuffer()` も `blob()` と同種の欠落だった
497        // (バイナリレスポンスを扱う `fetch(...).then(r => r.arrayBuffer())` パターン)。
498        r.props
499            .insert("arrayBuffer".into(), nv("arrayBuffer", response_array_buffer));
500        // `Response.prototype.clone()`(`response.clone().json()` のように本文を
501        // 2回読みたい場合に使う定番パターン)が丸ごと未対応で、`clone is not a
502        // function` になっていた。
503        r.props.insert("clone".into(), nv("clone", response_clone));
504    }
505    Value::Object(resp)
506}
507
508/// `Response.prototype.clone()`。同じ `status`/本文を持つ新しい `Response`
509/// オブジェクトを返す(この処理系は本文を消費済みにする `bodyUsed` 追跡が無く
510/// `.text()`/`.json()` 等を複数回呼べる簡略実装のため、`clone()` は主に
511/// 「呼び出し自体が壊れない」ことを保証する意味合いが強い)。
512pub(crate) fn response_clone(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
513    if let Value::Object(o) = &this {
514        let b = o.borrow();
515        let status = b.props.get("status").map(|v| v.to_number() as u16).unwrap_or(200);
516        let body = b.props.get("_body").map(|v| v.to_js_string()).unwrap_or_default();
517        let url = b.props.get("url").map(|v| v.to_js_string()).unwrap_or_default();
518        drop(b);
519        return Ok(make_response_with_url(status, body, url));
520    }
521    Ok(this)
522}
523
524/// this._body を取り出す共通処理。
525pub(crate) fn response_body_string(this: &Value) -> String {
526    if let Value::Object(o) = this {
527        if let Some(v) = o.borrow().props.get("_body") {
528            return v.to_js_string();
529        }
530    }
531    String::new()
532}
533
534/// Response.text(): 本文文字列で解決する Promise。
535pub(crate) fn response_text(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
536    let body = response_body_string(&this);
537    Ok(resolved_promise(it, Value::str(body)))
538}
539
540/// Response.json(): 本文を JSON.parse して解決する Promise。
541pub(crate) fn response_json(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
542    let body = response_body_string(&this);
543    let parsed = json_parse(it, Value::Undefined, &[Value::str(body)])?;
544    Ok(resolved_promise(it, parsed))
545}
546
547/// Response.blob(): 本文を UTF-8 バイト列化した `Blob` で解決する Promise。
548pub(crate) fn response_blob(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
549    let body = response_body_string(&this);
550    let bytes: alloc::vec::Vec<f64> = body.as_bytes().iter().map(|b| *b as f64).collect();
551    let blob = make_blob(bytes, String::new());
552    Ok(resolved_promise(it, blob))
553}
554
555/// Response.arrayBuffer(): 本文を UTF-8 バイト列化した実バイト列付き `ArrayBuffer`
556/// (`make_arraybuffer_with_bytes()`)で解決する Promise。
557pub(crate) fn response_array_buffer(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
558    let body = response_body_string(&this);
559    let bytes: alloc::vec::Vec<f64> = body.as_bytes().iter().map(|b| *b as f64).collect();
560    let buf = make_arraybuffer_with_bytes(bytes);
561    Ok(resolved_promise(it, buf))
562}
563
564/// location.toString(): href を返す(`String(location)` / 文字列連結用)。
565pub(crate) fn location_to_string(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
566    if let Value::Object(o) = &this {
567        if let Some(v) = o.borrow().props.get("href") {
568            return Ok(v.clone());
569        }
570    }
571    Ok(Value::str(""))
572}
573
574/// location オブジェクトにナビゲーション要求を積む共通ヘルパ。
575/// `mode`: "assign"(履歴あり) / "replace"(履歴置換) / "reload"(再読込)。
576/// ホストが take_pending_location() で回収して実ナビゲーションする。
577pub(crate) fn queue_location_nav(this: &Value, url: &str, mode: &str) {
578    if let Value::Object(o) = this {
579        let mut b = o.borrow_mut();
580        b.props
581            .insert(String::from("_pending_location"), Value::str(url));
582        b.props
583            .insert(String::from("_pending_location_mode"), Value::str(mode));
584    }
585}
586
587/// location.assign(url): 指定URLへ遷移(履歴に新エントリを積む)。
588pub(crate) fn location_assign(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
589    let url = a.first().map(|v| v.to_js_string()).unwrap_or_default();
590    if !url.is_empty() {
591        queue_location_nav(&this, &url, "assign");
592    }
593    Ok(Value::Undefined)
594}
595
596/// location.replace(url): 指定URLへ遷移(現在の履歴エントリを置換)。
597pub(crate) fn location_replace(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
598    let url = a.first().map(|v| v.to_js_string()).unwrap_or_default();
599    if !url.is_empty() {
600        queue_location_nav(&this, &url, "replace");
601    }
602    Ok(Value::Undefined)
603}
604
605/// location.reload(): 現在のページを再読込する。
606pub(crate) fn location_reload(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
607    // 現在の href を再読込要求として積む(ホスト側で現URLを使うため空でも可)。
608    let cur = if let Value::Object(o) = &this {
609        o.borrow()
610            .props
611            .get("href")
612            .map(|v| v.to_js_string())
613            .unwrap_or_default()
614    } else {
615        String::new()
616    };
617    queue_location_nav(&this, &cur, "reload");
618    Ok(Value::Undefined)
619}
620
621/// 絶対URL `scheme://host[:port]/path[?query][#hash]` を location の各成分へ分解。
622/// `string_slice` lint 回避で split_once / strip_prefix のみ使用。
623pub(crate) fn location_components(url: &str) -> Vec<(&'static str, String)> {
624    let (protocol, after) = if let Some(a) = url.strip_prefix("https://") {
625        ("https:", a)
626    } else if let Some(a) = url.strip_prefix("http://") {
627        ("http:", a)
628    } else {
629        ("", url)
630    };
631    let (hostport, pathpart) = match after.split_once('/') {
632        Some((h, p)) => (String::from(h), format!("/{}", p)),
633        None => (String::from(after), String::from("/")),
634    };
635    // `user:pass@host` 形式のユーザー情報(userinfo)が丸ごと未対応で、
636    // `hostport`(= `host`/`hostname`/`port`/`origin` の元)にそのまま
637    // 混入していたバグ。`https://user:pass@example.com/` の `hostname` が
638    // 仕様上の `example.com` ではなく `pass@example.com` という壊れた値に
639    // なっていた(`:` での2分割で `user` が誤って `hostname` に化ける)。
640    // userinfo を `host` 系の解析より先に切り離す。
641    let (userinfo, hostport) = match hostport.split_once('@') {
642        Some((ui, hp)) => (String::from(ui), String::from(hp)),
643        None => (String::new(), hostport),
644    };
645    let (username, password) = match userinfo.split_once(':') {
646        Some((u, p)) => (String::from(u), String::from(p)),
647        None => (userinfo.clone(), String::new()),
648    };
649    let (before_hash, hash) = match pathpart.split_once('#') {
650        Some((b, h)) => (String::from(b), format!("#{}", h)),
651        None => (pathpart.clone(), String::new()),
652    };
653    let (pathname, search) = match before_hash.split_once('?') {
654        Some((p, q)) => (String::from(p), format!("?{}", q)),
655        None => (before_hash.clone(), String::new()),
656    };
657    let (hostname, port) = match hostport.split_once(':') {
658        Some((h, p)) => (String::from(h), String::from(p)),
659        None => (hostport.clone(), String::new()),
660    };
661    let origin = if protocol.is_empty() {
662        String::new()
663    } else {
664        format!("{}//{}", protocol, hostport)
665    };
666    alloc::vec![
667        ("href", String::from(url)),
668        ("protocol", String::from(protocol)),
669        ("username", username),
670        ("password", password),
671        ("host", hostport),
672        ("hostname", hostname),
673        ("port", port),
674        ("origin", origin),
675        ("pathname", pathname),
676        ("search", search),
677        ("hash", hash),
678    ]
679}
680
681/// グローバルの location.href を読む(未設定/空なら None)。
682pub fn location_href(global: &Rc<RefCell<Scope>>) -> Option<String> {
683    let loc = match global.borrow().vars.get("location") {
684        Some(Value::Object(o)) => o.clone(),
685        _ => return None,
686    };
687    let href = loc
688        .borrow()
689        .props
690        .get("href")
691        .map(|v| v.to_js_string())
692        .unwrap_or_default();
693    if href.is_empty() {
694        None
695    } else {
696        Some(href)
697    }
698}
699
700/// グローバルの location.hostname を読む(未設定なら空文字列。`document.domain`
701/// の読み取り専用近似が呼ぶ)。
702pub fn location_hostname(global: &Rc<RefCell<Scope>>) -> Option<String> {
703    let loc = match global.borrow().vars.get("location") {
704        Some(Value::Object(o)) => o.clone(),
705        _ => return None,
706    };
707    let hostname = loc.borrow().props.get("hostname").map(|v| v.to_js_string());
708    hostname
709}
710
711/// `document.lastModified` を仕様どおりの `"MM/DD/YYYY HH:MM:SS"` 形式へ整形する。
712/// 実ファイルの mtime を追跡する仕組みが無いため、`epoch_ms_now()`(`Date.now()`
713/// と同じ現在時刻ソース)をそのまま使う簡略実装。
714pub fn format_last_modified() -> String {
715    let (y, mo, d, hh, mm, ss, ..) = decompose_ms(epoch_ms_now());
716    alloc::format!("{mo:02}/{d:02}/{y:04} {hh:02}:{mm:02}:{ss:02}")
717}
718
719/// history.pushState / replaceState 共通: url 指定があれば location を書換え、state を保存。
720/// pushState のみ length をインクリメント。popstate は仕様通り発火しない。
721pub(crate) fn history_change_state(
722    it: &mut Interp,
723    this: &Value,
724    a: &[Value],
725    push: bool,
726) -> Result<Value, Value> {
727    let new_state = arg(a, 0);
728    let url_arg = arg(a, 2);
729
730    if let Value::Object(o) = this {
731        if push {
732            // pushState: 現在の URL・state を _spa_back_stack へ退避してから更新。
733            let cur_url = location_href(&it.global).unwrap_or_default();
734            let cur_state = o.borrow().props.get("state").cloned().unwrap_or(Value::Null);
735            let stack_val = o.borrow().props.get("_spa_back_stack").cloned();
736            let stack = match stack_val {
737                Some(Value::Object(arr)) => arr,
738                _ => Obj::array(alloc::vec![]),
739            };
740            let entry = Obj::plain();
741            {
742                let mut eb = entry.borrow_mut();
743                eb.props.insert("url".into(), Value::str(cur_url));
744                eb.props.insert("state".into(), cur_state);
745            }
746            if let ObjKind::Array(items) = &mut stack.borrow_mut().kind {
747                items.push(Value::Object(entry));
748            }
749            o.borrow_mut().props.insert("_spa_back_stack".into(), Value::Object(stack));
750            // pushState 時は forward スタックをクリア(仕様通り)。
751            o.borrow_mut().props.shift_remove("_spa_fwd_stack");
752        }
753
754        if !matches!(url_arg, Value::Undefined | Value::Null) {
755            let url = url_arg.to_js_string();
756            if !url.is_empty() {
757                let base = location_href(&it.global).unwrap_or_default();
758                let resolved = resolve_url(&base, &url);
759                update_location(&it.global, &resolved);
760                it.base_url = resolved;
761            }
762        }
763
764        let mut b = o.borrow_mut();
765        b.props.insert("state".into(), new_state);
766        if push {
767            let len = match b.props.get("length") {
768                Some(Value::Number(n)) => *n,
769                _ => 1.0,
770            };
771            b.props.insert("length".into(), Value::Number(len + 1.0));
772        }
773    } else {
774        if !matches!(url_arg, Value::Undefined | Value::Null) {
775            let url = url_arg.to_js_string();
776            if !url.is_empty() {
777                let base = location_href(&it.global).unwrap_or_default();
778                let resolved = resolve_url(&base, &url);
779                update_location(&it.global, &resolved);
780                it.base_url = resolved;
781            }
782        }
783    }
784    Ok(Value::Undefined)
785}
786
787pub(crate) fn history_push_state(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
788    history_change_state(it, &this, a, true)
789}
790
791pub(crate) fn history_replace_state(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
792    history_change_state(it, &this, a, false)
793}
794
795/// history のナビゲーション要求を history オブジェクトの隠し props `_pending_nav` に積む。
796/// 値は相対移動量(負=戻る / 正=進む)。エンジンが JS 実行後に take_pending_nav で回収し、
797/// go_back / go_forward を実行してから popstate を発火する。
798/// (SPA だけでなく実ページ遷移にも対応するため、実ナビはホスト側に委譲する)。
799pub(crate) fn set_pending_nav(global: &Rc<RefCell<Scope>>, delta: i64) {
800    if delta == 0 {
801        return;
802    }
803    if let Some(Value::Object(h)) = global.borrow().vars.get("history") {
804        let cur = match h.borrow().props.get("_pending_nav") {
805            Some(Value::Number(n)) => *n as i64,
806            _ => 0,
807        };
808        h.borrow_mut()
809            .props
810            .insert("_pending_nav".into(), Value::Number((cur + delta) as f64));
811    }
812}
813
814pub(crate) fn history_back(it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
815    set_pending_nav(&it.global, -1);
816    Ok(Value::Undefined)
817}
818
819pub(crate) fn history_forward(it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
820    set_pending_nav(&it.global, 1);
821    Ok(Value::Undefined)
822}
823
824pub(crate) fn history_go(it: &mut Interp, _this: Value, a: &[Value]) -> Result<Value, Value> {
825    // go(0) はリロード相当だが当面は no-op。go(-n)/go(n) を相対移動として扱う。
826    let n = arg(a, 0).to_number();
827    if n.is_finite() {
828        set_pending_nav(&it.global, n as i64);
829    }
830    Ok(Value::Undefined)
831}
832