From 89225032efd18e02f4808eb62c110b847447490c Mon Sep 17 00:00:00 2001 From: ldm0 Date: Thu, 16 Jul 2026 19:36:08 +0800 Subject: [PATCH] fix(fonts): lazily create FontFace loaded promise --- .../context_bootstrap/css_fontface_runtime.rs | 1 + .../css_fontface_runtime/font_face.rs | 118 ++++++++++++------ .../css_fontface_runtime/font_face_set.rs | 2 +- .../font_face_set/loading.rs | 2 +- .../src/script_vm/tests/browser_api/misc.rs | 63 ++++++++++ 5 files changed, 146 insertions(+), 40 deletions(-) diff --git a/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime.rs b/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime.rs index 3829770c82..65f7d6b4eb 100644 --- a/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime.rs +++ b/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime.rs @@ -21,6 +21,7 @@ pub(super) const FONT_FACE_DISPLAY_SLOT: &str = "__moliFontFaceDisplay"; pub(super) const FONT_FACE_STATUS_SLOT: &str = "__moliFontFaceStatus"; pub(super) const FONT_FACE_LOADED_SLOT: &str = "__moliFontFaceLoaded"; pub(super) const FONT_FACE_LOADED_RESOLVER_SLOT: &str = "__moliFontFaceLoadedResolver"; +pub(super) const FONT_FACE_ERROR_SLOT: &str = "__moliFontFaceError"; pub(super) const FONT_FACE_SET_OWNERS_SLOT: &str = "__moliFontFaceSetOwners"; pub(super) const FONT_FACE_LOAD_NOTIFICATION_SENT_SLOT: &str = "__moliFontFaceLoadNotificationSent"; pub(super) const FONT_FACE_SET_MANUAL_FACES_SLOT: &str = "__moliFontFaceSetManualFaces"; diff --git a/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/font_face.rs b/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/font_face.rs index 0718ee999c..2e8255abb0 100644 --- a/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/font_face.rs +++ b/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/font_face.rs @@ -33,6 +33,8 @@ struct FontFaceObjectDeclaration<'s> { loaded: Option>, #[webapi(slot = FONT_FACE_LOADED_RESOLVER_SLOT)] loaded_resolver: Option>, + #[webapi(slot = FONT_FACE_ERROR_SLOT)] + error: Option>, #[webapi(slot = FONT_FACE_SET_OWNERS_SLOT, constructor_default = Vec::new())] owner_sets: Vec>, #[webapi(slot = FONT_FACE_LOAD_NOTIFICATION_SENT_SLOT, constructor_default = false)] @@ -122,8 +124,7 @@ struct FontFacePrototypeAccessorsDeclaration { status: (), #[webapi( accessor_property, - getter = font_face_readonly_attribute_getter_callback, - data = callback_data_index_value(scope, 2), + getter = font_face_loaded_getter_callback, returns_promise, enumerable )] @@ -190,6 +191,18 @@ fn font_face_readonly_attribute_getter_callback<'s>( rv.set(value); } +fn font_face_loaded_getter_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'s, v8::Value>, +) { + let Some(loaded) = ensure_font_face_loaded_promise(scope, args.this()) else { + rv.set_undefined(); + return; + }; + rv.set(loaded.into()); +} + fn font_face_attribute_setter_callback<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -254,36 +267,32 @@ pub(in crate::context_bootstrap) fn font_face_constructor_callback<'s>( return; }; let display = descriptor_string_property(scope, descriptors, "display", "auto"); - let (source, status, loaded, loaded_resolver) = match parsed.source { + let (source, status, error) = match parsed.source { FontFaceConstructorSource::Css(source) if moli_css_parse::normalize_font_face_src(&source).is_some() => { - let resolver = v8::PromiseResolver::new(scope) - .expect("FontFace loaded promise resolver should allocate"); - let loaded = resolver.get_promise(scope); - (source, "unloaded", Some(loaded), Some(resolver)) + (source, "unloaded", None) } FontFaceConstructorSource::Css(source) => { - let loaded = super::query::make_rejected_dom_exception_promise( + let error = crate::context_bootstrap::new_dom_exception_value( scope, - "SyntaxError", "Invalid FontFace source descriptor.", + "SyntaxError", ); - (source, "error", Some(loaded), None) + (source, "error", Some(error)) } FontFaceConstructorSource::Binary(bytes) if moli_web_mime::sniff_font_mime_type(&bytes).is_some() => { - let loaded = resolved_promise(scope, this.into()); - (String::new(), "loaded", loaded, None) + (String::new(), "loaded", None) } FontFaceConstructorSource::Binary(_) => { - let loaded = super::query::make_rejected_dom_exception_promise( + let error = crate::context_bootstrap::new_dom_exception_value( scope, - "SyntaxError", "Invalid font data in ArrayBuffer.", + "SyntaxError", ); - (String::new(), "error", Some(loaded), None) + (String::new(), "error", Some(error)) } }; FontFaceObjectDeclaration::new( @@ -297,8 +306,9 @@ pub(in crate::context_bootstrap) fn font_face_constructor_callback<'s>( variation_settings, display, status, - loaded, - loaded_resolver, + None, + None, + error, ) .initialize(scope, this) .expect("FontFace declaration should initialize object"); @@ -338,11 +348,7 @@ const FONT_FACE_WRITABLE_ATTRIBUTE_SLOTS: &[&str] = &[ FONT_FACE_DISPLAY_SLOT, ]; -const FONT_FACE_READONLY_ATTRIBUTE_SLOTS: &[&str] = &[ - FONT_FACE_SOURCE_SLOT, - FONT_FACE_STATUS_SLOT, - FONT_FACE_LOADED_SLOT, -]; +const FONT_FACE_READONLY_ATTRIBUTE_SLOTS: &[&str] = &[FONT_FACE_SOURCE_SLOT, FONT_FACE_STATUS_SLOT]; pub(in crate::context_bootstrap) fn font_face_load_callback<'s>( scope: &mut v8::PinScope<'s, '_>, @@ -350,8 +356,10 @@ pub(in crate::context_bootstrap) fn font_face_load_callback<'s>( mut rv: v8::ReturnValue<'s, v8::Value>, ) { let this = args.this(); - if let Some(loaded) = load_font_face(scope, this) { - rv.set(loaded); + let loaded = ensure_font_face_loaded_promise(scope, this); + start_font_face_load(scope, this); + if let Some(loaded) = loaded { + rv.set(loaded.into()); return; } match resolved_promise(scope, this.into()) { @@ -360,14 +368,13 @@ pub(in crate::context_bootstrap) fn font_face_load_callback<'s>( } } -pub(super) fn load_font_face<'s>( +pub(super) fn start_font_face_load<'s>( scope: &mut v8::PinScope<'s, '_>, face: v8::Local<'s, v8::Object>, -) -> Option> { - let loaded = font_face_slot_value(scope, face, FONT_FACE_LOADED_SLOT); +) { if font_face_status(scope, face).as_deref() != Some("unloaded") { super::events::notify_font_face_set_owners_of_load(scope, face); - return loaded; + return; } set_font_face_status(scope, face, "loading"); @@ -379,20 +386,22 @@ pub(super) fn load_font_face<'s>( set_font_face_status(scope, face, "error"); } - if let Some(resolver) = font_face_loaded_resolver(scope, face) { - if succeeds { + if succeeds { + if let Some(resolver) = font_face_loaded_resolver(scope, face) { let _ = resolver.resolve(scope, face.into()); - } else { - let exception = crate::context_bootstrap::new_dom_exception_value( - scope, - "No source in the FontFace src list could be loaded.", - "NetworkError", - ); + } + } else { + let exception = crate::context_bootstrap::new_dom_exception_value( + scope, + "No source in the FontFace src list could be loaded.", + "NetworkError", + ); + set_font_face_slot_value(scope, face, FONT_FACE_ERROR_SLOT, exception); + if let Some(resolver) = font_face_loaded_resolver(scope, face) { let _ = resolver.reject(scope, exception); } } super::events::notify_font_face_set_owners_of_load(scope, face); - loaded } pub(super) fn font_face_load_failed<'s>( @@ -422,7 +431,7 @@ pub(crate) fn load_font_faces_for_family<'s>( { continue; } - let _ = load_font_face(scope, face); + start_font_face_load(scope, face); } } @@ -471,6 +480,39 @@ fn font_face_loaded_resolver<'s>( .map(|object| unsafe { v8::Local::::cast_unchecked(object) }) } +fn ensure_font_face_loaded_promise<'s>( + scope: &mut v8::PinScope<'s, '_>, + face: v8::Local<'s, v8::Object>, +) -> Option> { + if let Some(loaded) = font_face_slot_value(scope, face, FONT_FACE_LOADED_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + { + return Some(loaded); + } + let status = font_face_status(scope, face)?; + let resolver = v8::PromiseResolver::new(scope)?; + let loaded = resolver.get_promise(scope); + set_font_face_slot_value(scope, face, FONT_FACE_LOADED_SLOT, loaded.into()); + match status.as_str() { + "loaded" => { + let _ = resolver.resolve(scope, face.into()); + } + "error" => { + let error = + font_face_slot_value(scope, face, FONT_FACE_ERROR_SLOT).unwrap_or_else(|| { + crate::context_bootstrap::new_dom_exception_value( + scope, + "The FontFace failed to load.", + "NetworkError", + ) + }); + let _ = resolver.reject(scope, error); + } + _ => set_font_face_slot_value(scope, face, FONT_FACE_LOADED_RESOLVER_SLOT, resolver.into()), + } + Some(loaded) +} + fn descriptor_string_property( scope: &mut v8::PinScope<'_, '_>, object: Option>, diff --git a/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/font_face_set.rs b/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/font_face_set.rs index 2f4468cb95..cfc28f4791 100644 --- a/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/font_face_set.rs +++ b/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/font_face_set.rs @@ -1,4 +1,4 @@ -use super::font_face::{font_face_load_failed, load_font_face}; +use super::font_face::{font_face_load_failed, start_font_face_load}; use super::query::{ font_face_set_matching_faces_array, font_load_query_contains_css_wide_keyword, make_rejected_dom_exception_promise, diff --git a/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/font_face_set/loading.rs b/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/font_face_set/loading.rs index aa6192164d..6954e6a571 100644 --- a/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/font_face_set/loading.rs +++ b/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/font_face_set/loading.rs @@ -64,7 +64,7 @@ pub(in crate::context_bootstrap) fn font_face_set_load_callback<'s>( else { continue; }; - let _ = load_font_face(scope, face); + start_font_face_load(scope, face); failed |= font_face_load_failed(scope, face); } if failed { diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs index 3210813cf0..b65a13f918 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs @@ -1791,6 +1791,69 @@ fn font_face_declared_slots_ignore_prototype_spoofing() { assert_eq!(vm.eval("fakeLoadedResult").unwrap(), "rejected:TypeError"); } +#[test] +fn invalid_font_face_defers_loaded_rejection_until_promise_is_observed() { + let mut vm = new_storage_test_vm("https://font-face-lazy-loaded-promise.test/"); + + let initial = vm + .eval( + r#" +(() => { + globalThis.__fontFaceUnhandled = []; + window.addEventListener('unhandledrejection', event => { + __fontFaceUnhandled.push(event.reason && event.reason.name); + event.preventDefault(); + }); + globalThis.__invalidLazyFontFace = new FontFace( + 'InvalidLazyFace', + 'not a font source' + ); + return JSON.stringify({ + status: __invalidLazyFontFace.status, + unhandled: __fontFaceUnhandled + }); +})() +"#, + ) + .expect("invalid FontFace construction should not throw"); + assert_eq!(initial, r#"{"status":"error","unhandled":[]}"#); + + for _ in 0..4 { + vm.eval("JSON.stringify(__fontFaceUnhandled)") + .expect("unobserved FontFace rejection checkpoints should drain"); + } + assert_eq!( + vm.eval("JSON.stringify(__fontFaceUnhandled)") + .expect("unobserved FontFace rejection result should evaluate"), + "[]", + ); + + let observed = vm + .eval( + r#" +(() => { + const first = __invalidLazyFontFace.loaded; + const second = __invalidLazyFontFace.loaded; + globalThis.__invalidLazyFontFaceRejection = 'pending'; + first.catch(error => { + __invalidLazyFontFaceRejection = error.name; + }); + return JSON.stringify({ + stable: first === second, + loadReturnsLoaded: __invalidLazyFontFace.load() === first + }); +})() +"#, + ) + .expect("observed invalid FontFace loaded promise should evaluate"); + assert_eq!(observed, r#"{"stable":true,"loadReturnsLoaded":true}"#,); + assert_eq!( + vm.eval("__invalidLazyFontFaceRejection") + .expect("invalid FontFace loaded rejection should settle"), + "SyntaxError", + ); +} + #[test] fn font_face_string_sources_follow_load_state_and_connected_style_use() { let mut vm = new_storage_test_vm("https://font-face-string-source-state.test/");