fix(fonts): lazily create FontFace loaded promise

This commit is contained in:
ldm0
2026-09-23 00:11:51 +08:00
parent 484b4dea54
commit 89225032ef
5 changed files with 146 additions and 40 deletions
@@ -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";
@@ -33,6 +33,8 @@ struct FontFaceObjectDeclaration<'s> {
loaded: Option<v8::Local<'s, v8::Promise>>,
#[webapi(slot = FONT_FACE_LOADED_RESOLVER_SLOT)]
loaded_resolver: Option<v8::Local<'s, v8::PromiseResolver>>,
#[webapi(slot = FONT_FACE_ERROR_SLOT)]
error: Option<v8::Local<'s, v8::Value>>,
#[webapi(slot = FONT_FACE_SET_OWNERS_SLOT, constructor_default = Vec::new())]
owner_sets: Vec<v8::Local<'s, v8::Value>>,
#[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<v8::Local<'s, v8::Value>> {
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::<v8::PromiseResolver>::cast_unchecked(object) })
}
fn ensure_font_face_loaded_promise<'s>(
scope: &mut v8::PinScope<'s, '_>,
face: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Promise>> {
if let Some(loaded) = font_face_slot_value(scope, face, FONT_FACE_LOADED_SLOT)
.and_then(|value| v8::Local::<v8::Promise>::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<v8::Local<'_, v8::Object>>,
@@ -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,
@@ -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 {
@@ -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/");