mirror of
https://github.com/lexmount/moli.git
synced 2026-10-07 16:01:01 +00:00
feat(worklet): expose native interfaces and independent module promises
Expose Worklet and AudioWorklet with shared branded prototypes and the readonly BaseAudioContext.audioWorklet attribute. Native factories bypass replaceable public constructors; each addModule call gets its own promise and rejection state. Tests cover WebIDL conversion order, forged/proxy receivers, intrinsic factories, cross-realm results and independent module completion promises, including the existing Worker-backed module loader. AudioWorklet execution still uses the existing per-module Worker backend. A shared AudioWorkletGlobalScope, real DSP/message ports, Blob dependency loading and complete parser/AbortError handling remain follow-up. Tests cover top-level Window and same-origin iframe realms, plus applicable Worker globals. Popup exposure remains with the separate popup-realm migration on the integration branch; main still uses a legacy Window shell without a complete realm. Rebased onto main, with separate exposure-list and Worker regression insertion points from WebGL, Cache and ServiceWorker. An initial full test run hit the existing synchronous-XHR watchdog wall-clock assertion at 4.208s (limit 4s). The unchanged-tree focused recheck and subsequent full run passed, with no automatic retries; the initial log is preserved.
This commit is contained in:
@@ -8,6 +8,7 @@ use super::materialize::{
|
||||
enum RealmDependentFinalizer {
|
||||
NodeMixinUnscopables,
|
||||
CryptoSecureContextSurface,
|
||||
BaseAudioContextSecureContextSurface,
|
||||
XmlHttpRequestEventTargetState,
|
||||
NotificationPermission,
|
||||
PointerEventSecureContextSurface,
|
||||
@@ -33,6 +34,10 @@ const REALM_DEPENDENT_FINALIZER_ALLOWLIST: &[(&str, RealmDependentFinalizer)] =
|
||||
),
|
||||
// These entries install private realm state, context-conditional surface,
|
||||
// or constructor data whose JavaScript identity must be realm-local.
|
||||
(
|
||||
"BaseAudioContext",
|
||||
RealmDependentFinalizer::BaseAudioContextSecureContextSurface,
|
||||
),
|
||||
(
|
||||
"Crypto",
|
||||
RealmDependentFinalizer::CryptoSecureContextSurface,
|
||||
@@ -80,6 +85,9 @@ pub(super) fn finalize_materialized_interface(
|
||||
RealmDependentFinalizer::NodeMixinUnscopables => {
|
||||
finalize_node_mixin_unscopables(scope, prototype);
|
||||
}
|
||||
RealmDependentFinalizer::BaseAudioContextSecureContextSurface => {
|
||||
crate::context_bootstrap::web_audio_runtime::finalize_base_audio_context_realm_bindings(scope, prototype)?;
|
||||
}
|
||||
RealmDependentFinalizer::CryptoSecureContextSurface => {
|
||||
crate::context_bootstrap::crypto::finalize_crypto_realm_bindings(scope, prototype)?;
|
||||
}
|
||||
@@ -140,6 +148,7 @@ mod tests {
|
||||
"Element",
|
||||
"DocumentType",
|
||||
"CharacterData",
|
||||
"BaseAudioContext",
|
||||
"Crypto",
|
||||
"XMLHttpRequestEventTarget",
|
||||
"Notification",
|
||||
|
||||
@@ -102,6 +102,8 @@ const SECURE_CONTEXT_ONLY_INTERFACE_NAMES: &[&str] = &[
|
||||
"SubtleCrypto",
|
||||
"CryptoKey",
|
||||
"IdleDetector",
|
||||
"Worklet",
|
||||
"AudioWorklet",
|
||||
"Clipboard",
|
||||
"ClipboardItem",
|
||||
"Cache",
|
||||
|
||||
@@ -2063,6 +2063,14 @@ pub(crate) fn set_interface_prototype_constructor<'s>(
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn window_realm_secure_context_available<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
window: v8::Local<'s, v8::Object>,
|
||||
) -> bool {
|
||||
get_private_value(scope, window, WINDOW_SECURE_CONTEXT_AVAILABLE_SLOT)
|
||||
.is_some_and(|value| value.is_true())
|
||||
}
|
||||
|
||||
fn install_window_runtime_state<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
global: v8::Local<'s, v8::Object>,
|
||||
|
||||
@@ -1183,6 +1183,14 @@ const CONSTRUCTOR_SPECS_AFTER_STREAMS: &[ConstructorSpec] = &[
|
||||
interface: web_api_interfaces::AudioContext::DESCRIPTOR,
|
||||
kind: ConstructorKind::AudioContext,
|
||||
},
|
||||
ConstructorSpec {
|
||||
interface: web_api_interfaces::Worklet::DESCRIPTOR,
|
||||
kind: ConstructorKind::Illegal,
|
||||
},
|
||||
ConstructorSpec {
|
||||
interface: web_api_interfaces::AudioWorklet::DESCRIPTOR,
|
||||
kind: ConstructorKind::Illegal,
|
||||
},
|
||||
ConstructorSpec {
|
||||
interface: web_api_interfaces::AudioWorkletNode::DESCRIPTOR,
|
||||
kind: ConstructorKind::AudioWorkletNode,
|
||||
|
||||
@@ -6,8 +6,7 @@ use super::media_queries::{
|
||||
use super::*;
|
||||
use crate::native_bridge::throw_dom_exception;
|
||||
use crate::util::{
|
||||
array_push_value, call_object_method, get_private_value, object_string_property,
|
||||
set_private_value, set_symbol_to_string_tag,
|
||||
call_object_method, get_private_value, object_string_property, set_private_value,
|
||||
};
|
||||
use crate::web_api_interfaces;
|
||||
use crate::webidl;
|
||||
@@ -23,11 +22,12 @@ const AUDIO_CONTEXT_LISTENERS_SLOT: &str = "__moliAudioContextListeners";
|
||||
const AUDIO_CONTEXT_MODULES_SLOT: &str = "__moliAudioContextModules";
|
||||
const AUDIO_CONTEXT_MODULE_LIST_SLOT: &str = "__moliAudioContextModuleList";
|
||||
const AUDIO_CONTEXT_PROCESSORS_SLOT: &str = "__moliAudioContextProcessors";
|
||||
const AUDIO_CONTEXT_WORKLET_SLOT: &str = "__moliAudioContextWorklet";
|
||||
const AUDIO_WORKLET_CONTEXT_SLOT: &str = "__moliAudioWorkletContext";
|
||||
const AUDIO_WORKLET_MODULE_CONTEXT_SLOT: &str = "__moliAudioWorkletModuleContext";
|
||||
const AUDIO_WORKLET_MODULE_WORKER_SLOT: &str = "__moliAudioWorkletModuleWorker";
|
||||
const AUDIO_WORKLET_MODULE_PROMISE_SLOT: &str = "__moliAudioWorkletModulePromise";
|
||||
const AUDIO_WORKLET_MODULE_RESOLVER_SLOT: &str = "__moliAudioWorkletModuleResolver";
|
||||
const AUDIO_WORKLET_MODULE_RESOLVERS_SLOT: &str = "__moliAudioWorkletModuleResolvers";
|
||||
const AUDIO_WORKLET_MODULE_ERROR_SLOT: &str = "__moliAudioWorkletModuleError";
|
||||
const AUDIO_WORKLET_MODULE_LOADED_SLOT: &str = "__moliAudioWorkletModuleLoaded";
|
||||
const AUDIO_WORKLET_MODULE_SETTLED_SLOT: &str = "__moliAudioWorkletModuleSettled";
|
||||
const AUDIO_WORKLET_CALLBACK_MODULE_SLOT: &str = "__moliAudioWorkletCallbackModule";
|
||||
@@ -51,8 +51,6 @@ struct AudioContextObjectDeclaration<'scope> {
|
||||
state: &'static str,
|
||||
#[webapi(data_property)]
|
||||
destination: v8::Local<'scope, v8::Object>,
|
||||
#[webapi(data_property = "audioWorklet")]
|
||||
audio_worklet: v8::Local<'scope, v8::Object>,
|
||||
#[webapi(slot = SIMPLE_EVENT_TARGET_SLOT, value = AUDIO_CONTEXT_LISTENERS_SLOT)]
|
||||
event_target_slot: (),
|
||||
#[webapi(slot = AUDIO_CONTEXT_MODULES_SLOT)]
|
||||
@@ -66,10 +64,10 @@ struct AudioContextObjectDeclaration<'scope> {
|
||||
#[derive(WebApiObject)]
|
||||
#[webapi(interface = web_api_interfaces::AudioWorklet)]
|
||||
struct AudioWorkletObjectDeclaration<'scope> {
|
||||
#[webapi(prototype)]
|
||||
prototype: v8::Local<'scope, v8::Object>,
|
||||
#[webapi(slot = AUDIO_WORKLET_CONTEXT_SLOT)]
|
||||
context: v8::Local<'scope, v8::Object>,
|
||||
#[webapi(method = "addModule", length = 1, callback = audio_worklet_add_module_callback)]
|
||||
add_module: (),
|
||||
}
|
||||
|
||||
#[derive(WebApiObject)]
|
||||
@@ -88,10 +86,8 @@ struct AudioWorkletModuleStateDeclaration<'scope> {
|
||||
context: v8::Local<'scope, v8::Object>,
|
||||
#[webapi(slot = AUDIO_WORKLET_MODULE_WORKER_SLOT)]
|
||||
worker: v8::Local<'scope, v8::Object>,
|
||||
#[webapi(slot = AUDIO_WORKLET_MODULE_PROMISE_SLOT)]
|
||||
promise: v8::Local<'scope, v8::Promise>,
|
||||
#[webapi(slot = AUDIO_WORKLET_MODULE_RESOLVER_SLOT)]
|
||||
resolver: v8::Local<'scope, v8::PromiseResolver>,
|
||||
#[webapi(slot = AUDIO_WORKLET_MODULE_RESOLVERS_SLOT)]
|
||||
resolvers: v8::Local<'scope, v8::Array>,
|
||||
#[webapi(slot = AUDIO_WORKLET_MODULE_LOADED_SLOT)]
|
||||
loaded: bool,
|
||||
#[webapi(slot = AUDIO_WORKLET_MODULE_SETTLED_SLOT)]
|
||||
@@ -480,6 +476,10 @@ pub(in crate::context_bootstrap) fn install_web_audio_template_bindings<'s>(
|
||||
graph::install(scope, template);
|
||||
}
|
||||
match interface_name {
|
||||
"Worklet" => WorkletPrototypeDeclaration::initialize_prototype_template(
|
||||
scope,
|
||||
template.prototype_template(scope),
|
||||
),
|
||||
"AudioParam" => audio_param::install(scope, template),
|
||||
"BiquadFilterNode" => biquad::install(scope, template),
|
||||
"BaseAudioContext" => {
|
||||
@@ -528,17 +528,12 @@ fn audio_context_constructor_callback<'s>(
|
||||
let modules = new_web_audio_map_object(scope);
|
||||
let module_list = v8::Array::new(scope, 0);
|
||||
let processors = new_web_audio_map_object(scope);
|
||||
let audio_worklet = AudioWorkletObjectDeclaration::new(context)
|
||||
.bind(scope)
|
||||
.expect("AudioWorklet declaration should bind");
|
||||
set_symbol_to_string_tag(scope, audio_worklet, "AudioWorklet");
|
||||
|
||||
AudioContextObjectDeclaration::new(
|
||||
0.0,
|
||||
44_100.0,
|
||||
"running",
|
||||
destination,
|
||||
audio_worklet,
|
||||
modules,
|
||||
module_list,
|
||||
processors,
|
||||
@@ -579,20 +574,12 @@ fn audio_context_close_callback<'s>(
|
||||
module_state,
|
||||
AUDIO_WORKLET_MODULE_SETTLED_SLOT,
|
||||
) {
|
||||
set_audio_worklet_module_bool_slot(
|
||||
let error = new_dom_exception_value(
|
||||
scope,
|
||||
module_state,
|
||||
AUDIO_WORKLET_MODULE_SETTLED_SLOT,
|
||||
true,
|
||||
"AudioWorklet module loading was aborted.",
|
||||
"AbortError",
|
||||
);
|
||||
if let Some(resolver) = audio_worklet_module_resolver(scope, module_state) {
|
||||
let error = new_dom_exception_value(
|
||||
scope,
|
||||
"AudioWorklet module loading was aborted.",
|
||||
"AbortError",
|
||||
);
|
||||
let _ = resolver.reject(scope, error);
|
||||
}
|
||||
settle_audio_worklet_module(scope, module_state, Err(error));
|
||||
}
|
||||
if let Some(worker) =
|
||||
web_audio_object_slot(scope, module_state, AUDIO_WORKLET_MODULE_WORKER_SLOT)
|
||||
@@ -627,76 +614,184 @@ fn audio_context_close_callback<'s>(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(interface = web_api_interfaces::Worklet, enumerable, receiver)]
|
||||
struct WorkletPrototypeDeclaration {
|
||||
#[webapi(method = "addModule", length = 1, returns_promise, callback = audio_worklet_add_module_callback)]
|
||||
add_module: (),
|
||||
}
|
||||
|
||||
#[derive(Default, WebApiObject)]
|
||||
#[webapi(fragment, prototype = "BaseAudioContext", enumerable, receiver = web_api_interfaces::BaseAudioContext::is_instance)]
|
||||
struct BaseAudioContextSecureAttributes {
|
||||
#[webapi(accessor_property = "audioWorklet", getter = audio_worklet_getter)]
|
||||
audio_worklet: (),
|
||||
}
|
||||
|
||||
pub(super) fn finalize_base_audio_context_realm_bindings<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
prototype: v8::Local<'s, v8::Object>,
|
||||
) -> Result<()> {
|
||||
let global = scope.get_current_context().global(scope);
|
||||
if super::runtime_state::window_realm_secure_context_available(scope, global) {
|
||||
BaseAudioContextSecureAttributes::default()
|
||||
.initialize(scope, prototype)
|
||||
.map_err(|error| anyhow!(error))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn audio_worklet_getter<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
let context = args.this();
|
||||
if let Some(worklet) = web_audio_object_slot(scope, context, AUDIO_CONTEXT_WORKLET_SLOT) {
|
||||
rv.set(worklet.into());
|
||||
return;
|
||||
}
|
||||
let Some(realm) = context.get_creation_context(scope) else {
|
||||
return;
|
||||
};
|
||||
let scope = &mut v8::ContextScope::new(scope, realm);
|
||||
let Ok(prototype) = ensure_intrinsic_interface_prototype(scope, "AudioWorklet") else {
|
||||
return;
|
||||
};
|
||||
let worklet = AudioWorkletObjectDeclaration::new(prototype, context)
|
||||
.bind(scope)
|
||||
.expect("AudioWorklet declaration should bind");
|
||||
set_private_value(scope, context, AUDIO_CONTEXT_WORKLET_SLOT, worklet.into());
|
||||
rv.set(worklet.into());
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default, webidl::WebIdlEnum)]
|
||||
#[webidl(name = "RequestCredentials", rename_all = "kebab-case")]
|
||||
enum WorkletCredentials {
|
||||
Omit,
|
||||
#[default]
|
||||
SameOrigin,
|
||||
Include,
|
||||
}
|
||||
|
||||
impl WorkletCredentials {
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Omit => "omit",
|
||||
Self::SameOrigin => "same-origin",
|
||||
Self::Include => "include",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, webidl::WebIdlDictionary)]
|
||||
#[webidl(prefix = "WorkletOptions")]
|
||||
struct WorkletOptions {
|
||||
#[webidl(converter = "enum", default = WorkletCredentials::SameOrigin)]
|
||||
credentials: WorkletCredentials,
|
||||
}
|
||||
|
||||
#[derive(webidl::WebIdlArgs)]
|
||||
#[webidl(prefix = "Worklet.addModule")]
|
||||
struct WorkletAddModuleArgs {
|
||||
#[webidl(required, converter = "usv_string")]
|
||||
module_url: String,
|
||||
#[webidl(with = worklet_options_arg)]
|
||||
options: WorkletOptions,
|
||||
}
|
||||
|
||||
fn worklet_options_arg<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: &v8::FunctionCallbackArguments<'s>,
|
||||
index: i32,
|
||||
) -> Result<WorkletOptions, webidl::WebIdlError> {
|
||||
webidl::parse_dictionary(
|
||||
scope,
|
||||
args.get(index),
|
||||
webidl::Context::argument("Worklet.addModule", 2),
|
||||
)
|
||||
.map(Option::unwrap_or_default)
|
||||
}
|
||||
|
||||
fn audio_worklet_add_module_callback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
let Some(parsed) = webidl::parse_args::<WorkletAddModuleArgs>(scope, &args) else {
|
||||
return;
|
||||
};
|
||||
let worklet = args.this();
|
||||
let Some(context) = web_audio_object_slot(scope, worklet, AUDIO_WORKLET_CONTEXT_SLOT) else {
|
||||
let error = type_error_value(
|
||||
scope,
|
||||
"AudioWorklet.addModule called on incompatible receiver.",
|
||||
)
|
||||
.unwrap_or_else(|| v8::undefined(scope).into());
|
||||
set_rejected_promise_return(scope, &mut rv, error);
|
||||
throw_type_error(scope, "AudioWorklet context state is unavailable.");
|
||||
return;
|
||||
};
|
||||
let Some(module_url) = resolve_audio_worklet_module_url(scope, args.get(0)) else {
|
||||
let error = type_error_value(scope, "AudioWorklet.addModule module URL is invalid.")
|
||||
.unwrap_or_else(|| v8::undefined(scope).into());
|
||||
set_rejected_promise_return(scope, &mut rv, error);
|
||||
// Argument conversion failures use the method's realm. The operation's
|
||||
// Promise, URL resolution and workers use the Worklet's relevant realm.
|
||||
let Some(realm) = worklet.get_creation_context(scope) else {
|
||||
return;
|
||||
};
|
||||
let credentials = match audio_worklet_credentials(scope, &args) {
|
||||
Ok(credentials) => credentials,
|
||||
Err(message) => {
|
||||
let error =
|
||||
type_error_value(scope, &message).unwrap_or_else(|| v8::undefined(scope).into());
|
||||
set_rejected_promise_return(scope, &mut rv, error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let Some(modules) = web_audio_object_slot(scope, context, AUDIO_CONTEXT_MODULES_SLOT) else {
|
||||
let error = type_error_value(scope, "AudioWorklet context state is unavailable.")
|
||||
.unwrap_or_else(|| v8::undefined(scope).into());
|
||||
set_rejected_promise_return(scope, &mut rv, error);
|
||||
return;
|
||||
};
|
||||
if let Some(existing_module) = map_get_object(scope, modules, &module_url)
|
||||
&& let Some(promise) =
|
||||
get_private_value(scope, existing_module, AUDIO_WORKLET_MODULE_PROMISE_SLOT)
|
||||
.and_then(|value| v8::Local::<v8::Promise>::try_from(value).ok())
|
||||
{
|
||||
rv.set(promise.into());
|
||||
return;
|
||||
}
|
||||
|
||||
let scope = &mut v8::ContextScope::new(scope, realm);
|
||||
let Some(resolver) = v8::PromiseResolver::new(scope) else {
|
||||
rv.set_undefined();
|
||||
return;
|
||||
};
|
||||
let promise = resolver.get_promise(scope);
|
||||
let Some(worker) = create_audio_worklet_module_worker(scope, &module_url, credentials) else {
|
||||
rv.set(promise.into());
|
||||
let Some(module_url) = resolve_audio_worklet_module_url(scope, &parsed.module_url) else {
|
||||
let error = new_dom_exception_value(
|
||||
scope,
|
||||
"AudioWorklet.addModule module URL is invalid.",
|
||||
"SyntaxError",
|
||||
);
|
||||
let _ = resolver.reject(scope, error);
|
||||
return;
|
||||
};
|
||||
let Some(modules) = web_audio_object_slot(scope, context, AUDIO_CONTEXT_MODULES_SLOT) else {
|
||||
let error = v8::Exception::type_error(
|
||||
scope,
|
||||
v8str(scope, "AudioWorklet context state is unavailable."),
|
||||
);
|
||||
let _ = resolver.reject(scope, error);
|
||||
return;
|
||||
};
|
||||
if let Some(module_state) = map_get_object(scope, modules, &module_url) {
|
||||
if audio_worklet_module_bool_slot(scope, module_state, AUDIO_WORKLET_MODULE_SETTLED_SLOT) {
|
||||
if audio_worklet_module_bool_slot(scope, module_state, AUDIO_WORKLET_MODULE_LOADED_SLOT)
|
||||
{
|
||||
let _ = resolver.resolve(scope, v8::undefined(scope).into());
|
||||
} else if let Some(error) =
|
||||
get_private_value(scope, module_state, AUDIO_WORKLET_MODULE_ERROR_SLOT)
|
||||
{
|
||||
let _ = resolver.reject(scope, error);
|
||||
}
|
||||
} else if let Some(resolvers) =
|
||||
web_audio_array_slot(scope, module_state, AUDIO_WORKLET_MODULE_RESOLVERS_SLOT)
|
||||
{
|
||||
let key = v8_string(scope, &resolvers.length().to_string()).expect("array index");
|
||||
let _ = resolvers.create_data_property(scope, key.into(), resolver.into());
|
||||
}
|
||||
return;
|
||||
}
|
||||
let Some(worker) =
|
||||
create_audio_worklet_module_worker(scope, &module_url, parsed.options.credentials.as_str())
|
||||
else {
|
||||
let error = type_error_value(scope, "AudioWorklet module failed.")
|
||||
.unwrap_or_else(|| v8::undefined(scope).into());
|
||||
let _ = resolver.reject(scope, error);
|
||||
rv.set(promise.into());
|
||||
return;
|
||||
};
|
||||
|
||||
let resolvers = v8::Array::new_with_elements(scope, &[resolver.into()]);
|
||||
let module_state =
|
||||
AudioWorkletModuleStateDeclaration::new(context, worker, promise, resolver, false, false)
|
||||
AudioWorkletModuleStateDeclaration::new(context, worker, resolvers, false, false)
|
||||
.bind(scope)
|
||||
.expect("AudioWorklet module state declaration should bind");
|
||||
install_audio_worklet_worker_callbacks(scope, worker, module_state);
|
||||
let _ = map_set_object(scope, modules, &module_url, module_state);
|
||||
if let Some(module_list) = web_audio_array_slot(scope, context, AUDIO_CONTEXT_MODULE_LIST_SLOT)
|
||||
{
|
||||
array_push_value(scope, module_list, module_state.into());
|
||||
let key = v8_string(scope, &module_list.length().to_string()).expect("array index");
|
||||
let _ = module_list.create_data_property(scope, key.into(), module_state.into());
|
||||
}
|
||||
|
||||
rv.set(promise.into());
|
||||
}
|
||||
|
||||
fn install_audio_worklet_worker_callbacks<'s>(
|
||||
@@ -756,35 +851,12 @@ fn audio_worklet_worker_message_callback<'s>(
|
||||
};
|
||||
let _ = map_set_object(scope, processors, &name, module_state);
|
||||
}
|
||||
"module-loaded" => {
|
||||
if audio_worklet_module_bool_slot(
|
||||
scope,
|
||||
module_state,
|
||||
AUDIO_WORKLET_MODULE_SETTLED_SLOT,
|
||||
) {
|
||||
return;
|
||||
}
|
||||
set_audio_worklet_module_bool_slot(
|
||||
scope,
|
||||
module_state,
|
||||
AUDIO_WORKLET_MODULE_LOADED_SLOT,
|
||||
true,
|
||||
);
|
||||
set_audio_worklet_module_bool_slot(
|
||||
scope,
|
||||
module_state,
|
||||
AUDIO_WORKLET_MODULE_SETTLED_SLOT,
|
||||
true,
|
||||
);
|
||||
if let Some(resolver) = audio_worklet_module_resolver(scope, module_state) {
|
||||
let _ = resolver.resolve(scope, v8::undefined(scope).into());
|
||||
}
|
||||
}
|
||||
"module-loaded" => settle_audio_worklet_module(scope, module_state, Ok(())),
|
||||
"processor-error" => {
|
||||
let message = object_string_property(scope, message, "message")
|
||||
.unwrap_or_else(|| "AudioWorklet processor failed.".to_owned());
|
||||
let error = error_value(scope, &message).unwrap_or_else(|| v8::undefined(scope).into());
|
||||
fail_audio_worklet_module(scope, module_state, error);
|
||||
settle_audio_worklet_module(scope, module_state, Err(error));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -793,7 +865,7 @@ fn audio_worklet_worker_message_callback<'s>(
|
||||
fn audio_worklet_worker_error_callback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
_rv: v8::ReturnValue<'s, v8::Value>,
|
||||
mut rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
let Some(module_state) = audio_worklet_callback_module_state(scope, &args) else {
|
||||
return;
|
||||
@@ -807,7 +879,10 @@ fn audio_worklet_worker_error_callback<'s>(
|
||||
})
|
||||
.unwrap_or_else(|| "AudioWorklet module failed.".to_owned());
|
||||
let error = error_value(scope, &message).unwrap_or_else(|| v8::undefined(scope).into());
|
||||
fail_audio_worklet_module(scope, module_state, error);
|
||||
settle_audio_worklet_module(scope, module_state, Err(error));
|
||||
// This internal worker failure is reported through addModule's Promise.
|
||||
// Cancel the handled error instead of reporting it again on Window.
|
||||
rv.set_bool(false);
|
||||
}
|
||||
|
||||
fn audio_worklet_node_constructor_callback<'s>(
|
||||
@@ -884,28 +959,6 @@ fn audio_worklet_node_constructor_callback<'s>(
|
||||
rv.set(node.into());
|
||||
}
|
||||
|
||||
fn set_rejected_promise_return<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
rv: &mut v8::ReturnValue<'s, v8::Value>,
|
||||
error: v8::Local<'s, v8::Value>,
|
||||
) {
|
||||
if let Some(promise) = rejected_promise(scope, error) {
|
||||
rv.set(promise.into());
|
||||
} else {
|
||||
rv.set(v8::undefined(scope).into());
|
||||
}
|
||||
}
|
||||
|
||||
fn rejected_promise<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
error: v8::Local<'s, v8::Value>,
|
||||
) -> Option<v8::Local<'s, v8::Promise>> {
|
||||
let resolver = v8::PromiseResolver::new(scope)?;
|
||||
let promise = resolver.get_promise(scope);
|
||||
let _ = resolver.reject(scope, error);
|
||||
Some(promise)
|
||||
}
|
||||
|
||||
fn resolved_undefined_promise<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
) -> Option<v8::Local<'s, v8::Promise>> {
|
||||
@@ -958,50 +1011,23 @@ fn map_set_object<'s>(
|
||||
|
||||
fn resolve_audio_worklet_module_url(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
value: v8::Local<'_, v8::Value>,
|
||||
input: &str,
|
||||
) -> Option<String> {
|
||||
let input = value.to_string(scope)?.to_rust_string_lossy(scope);
|
||||
let base_url = if let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) {
|
||||
let host = unsafe { &*host_ptr };
|
||||
super::worker_host::worker_constructor_base_url(host)
|
||||
} else {
|
||||
let global = scope.get_current_context().global(scope);
|
||||
let location = object_property_as_object(scope, global, "location")?;
|
||||
let href = object_string_property(scope, location, "href")?;
|
||||
url::Url::parse(&href).ok()?
|
||||
};
|
||||
base_url.join(&input).ok().map(|url| url.to_string())
|
||||
}
|
||||
use crate::native_bridge::OwnerDispatchScope;
|
||||
|
||||
fn audio_worklet_credentials(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
args: &v8::FunctionCallbackArguments<'_>,
|
||||
) -> std::result::Result<&'static str, String> {
|
||||
let value = args.get(1);
|
||||
if value.is_undefined() || value.is_null() {
|
||||
return Ok("same-origin");
|
||||
}
|
||||
let Ok(options) = v8::Local::<v8::Object>::try_from(value) else {
|
||||
return Ok("same-origin");
|
||||
let host_ptr = context_host_ptr_from_global_bridge(scope)?;
|
||||
// SAFETY: the bridge owns the host for the lifetime of the callback.
|
||||
let host = unsafe { &*host_ptr };
|
||||
let global = scope.get_current_context().global(scope);
|
||||
let document = match super::navigation_window::runtime_window_dispatch_scope(scope, global)? {
|
||||
OwnerDispatchScope::Top => host.document_handle(),
|
||||
OwnerDispatchScope::Child(handle) => host.child_browsing_context_document_handle(handle)?,
|
||||
OwnerDispatchScope::LightweightPopup(id) => host.lightweight_popup_document_handle(id)?,
|
||||
};
|
||||
let Some(credentials) = options.get(scope, v8str(scope, "credentials").into()) else {
|
||||
return Ok("same-origin");
|
||||
};
|
||||
if credentials.is_undefined() {
|
||||
return Ok("same-origin");
|
||||
}
|
||||
let Some(credentials) = credentials.to_string(scope) else {
|
||||
return Err("AudioWorklet.addModule options.credentials is invalid.".to_owned());
|
||||
};
|
||||
let credentials = credentials.to_rust_string_lossy(scope);
|
||||
match credentials.as_str() {
|
||||
"omit" => Ok("omit"),
|
||||
"same-origin" => Ok("same-origin"),
|
||||
"include" => Ok("include"),
|
||||
_ => Err(format!(
|
||||
"The provided value '{credentials}' is not a valid enum value of type RequestCredentials."
|
||||
)),
|
||||
}
|
||||
host.document_base_url_for_handle(document)
|
||||
.join(input)
|
||||
.ok()
|
||||
.map(|url| url.to_string())
|
||||
}
|
||||
|
||||
fn create_audio_worklet_module_worker<'s>(
|
||||
@@ -1176,10 +1202,10 @@ fn audio_worklet_callback_module_state<'s>(
|
||||
web_audio_object_slot(scope, data, AUDIO_WORKLET_CALLBACK_MODULE_SLOT)
|
||||
}
|
||||
|
||||
fn fail_audio_worklet_module<'s>(
|
||||
fn settle_audio_worklet_module<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
module_state: v8::Local<'s, v8::Object>,
|
||||
error: v8::Local<'s, v8::Value>,
|
||||
result: Result<(), v8::Local<'s, v8::Value>>,
|
||||
) {
|
||||
if audio_worklet_module_bool_slot(scope, module_state, AUDIO_WORKLET_MODULE_SETTLED_SLOT) {
|
||||
return;
|
||||
@@ -1190,8 +1216,45 @@ fn fail_audio_worklet_module<'s>(
|
||||
AUDIO_WORKLET_MODULE_SETTLED_SLOT,
|
||||
true,
|
||||
);
|
||||
if let Some(resolver) = audio_worklet_module_resolver(scope, module_state) {
|
||||
let _ = resolver.reject(scope, error);
|
||||
set_audio_worklet_module_bool_slot(
|
||||
scope,
|
||||
module_state,
|
||||
AUDIO_WORKLET_MODULE_LOADED_SLOT,
|
||||
result.is_ok(),
|
||||
);
|
||||
if let Err(error) = result {
|
||||
set_private_value(scope, module_state, AUDIO_WORKLET_MODULE_ERROR_SLOT, error);
|
||||
}
|
||||
let Some(resolvers) =
|
||||
web_audio_array_slot(scope, module_state, AUDIO_WORKLET_MODULE_RESOLVERS_SLOT)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
// Drop the state's references before settling, and never invoke author
|
||||
// Promise.then / constructor properties to join a module response.
|
||||
set_private_value(
|
||||
scope,
|
||||
module_state,
|
||||
AUDIO_WORKLET_MODULE_RESOLVERS_SLOT,
|
||||
v8::undefined(scope).into(),
|
||||
);
|
||||
for index in 0..resolvers.length() {
|
||||
let Some(value) = resolvers.get_index(scope, index) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(object) = v8::Local::<v8::Object>::try_from(value) else {
|
||||
continue;
|
||||
};
|
||||
// Only native PromiseResolvers are written to this private array.
|
||||
let resolver = unsafe { v8::Local::<v8::PromiseResolver>::cast_unchecked(object) };
|
||||
match result {
|
||||
Ok(()) => {
|
||||
let _ = resolver.resolve(scope, v8::undefined(scope).into());
|
||||
}
|
||||
Err(error) => {
|
||||
let _ = resolver.reject(scope, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1215,15 +1278,6 @@ fn set_audio_worklet_module_bool_slot<'s>(
|
||||
set_private_value(scope, object, slot, value.into());
|
||||
}
|
||||
|
||||
fn audio_worklet_module_resolver<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
) -> Option<v8::Local<'s, v8::PromiseResolver>> {
|
||||
get_private_value(scope, object, AUDIO_WORKLET_MODULE_RESOLVER_SLOT)
|
||||
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
|
||||
.map(|object| unsafe { v8::Local::<v8::PromiseResolver>::cast_unchecked(object) })
|
||||
}
|
||||
|
||||
fn dynamics_compressor_reduction_getter_callback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
@@ -1272,6 +1326,22 @@ pub(in crate::context_bootstrap) fn offline_audio_context_constructor_callback<'
|
||||
let context = args.this();
|
||||
let destination = audio_destination_node(scope, context);
|
||||
let compressors = v8::Array::new(scope, 0);
|
||||
let modules = new_web_audio_map_object(scope);
|
||||
let module_list = v8::Array::new(scope, 0);
|
||||
let processors = new_web_audio_map_object(scope);
|
||||
set_private_value(scope, context, AUDIO_CONTEXT_MODULES_SLOT, modules.into());
|
||||
set_private_value(
|
||||
scope,
|
||||
context,
|
||||
AUDIO_CONTEXT_MODULE_LIST_SLOT,
|
||||
module_list.into(),
|
||||
);
|
||||
set_private_value(
|
||||
scope,
|
||||
context,
|
||||
AUDIO_CONTEXT_PROCESSORS_SLOT,
|
||||
processors.into(),
|
||||
);
|
||||
OfflineAudioContextObjectDeclaration::new(
|
||||
0.0,
|
||||
length,
|
||||
|
||||
+52
-1
@@ -227,7 +227,14 @@ async fn audio_worklet_close_rejects_pending_add_module_response() {
|
||||
globalThis.__audioWorkletCloseDone = false;
|
||||
const context = new AudioContext();
|
||||
globalThis.__audioWorkletCloseContext = context;
|
||||
context.audioWorklet.addModule({module_url_literal}).then(
|
||||
const first = context.audioWorklet.addModule({module_url_literal});
|
||||
const second = context.audioWorklet.addModule({module_url_literal});
|
||||
Promise.allSettled([first, second]).then(results => {{
|
||||
if (first === second || results.some(result => result.status !== "rejected" || result.reason.name !== "AbortError")) {{
|
||||
throw new Error("Every pending caller must independently reject on close");
|
||||
}}
|
||||
throw results[0].reason;
|
||||
}}).then(
|
||||
() => {{
|
||||
globalThis.__audioWorkletCloseResult = "resolved";
|
||||
globalThis.__audioWorkletCloseDone = true;
|
||||
@@ -1447,3 +1454,47 @@ async fn worker_owned_messageport_options_transfer_null_clone_path_responds() {
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn worklet_module_promises_preserve_independent_native_waiters() {
|
||||
run_page_vm_async_test(async move {
|
||||
let mut page_vm = test_page_vm_with_document_url(Url::parse("https://worklet-promises.test/").unwrap());
|
||||
let local_executor = page_vm.local_executor.clone();
|
||||
let result = local_executor.run(async move {
|
||||
page_vm.vm_mut().eval(&format!("({}).then(value => globalThis.__workletDone = value, error => globalThis.__workletDone = String(error));", include_str!("../../../../script_vm/tests/worklet_module_promises.js")))?;
|
||||
drive_websocket_until_done(&mut page_vm, "String(globalThis.__workletDone !== undefined)", "Worklet module waiters should settle").await?;
|
||||
page_vm.vm_mut().eval("JSON.stringify(__nodeReplacementResults.failures)")
|
||||
}).await.expect("Worklet module fixture should complete");
|
||||
assert_eq!(result, "[]");
|
||||
}).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn borrowed_worklet_add_module_uses_receiver_document_base() {
|
||||
run_page_vm_async_test(async move {
|
||||
let (base_url, request_rx, server) = spawn_shared_worker_script_capture_http_server(
|
||||
"registerProcessor('receiver-base', class extends AudioWorkletProcessor {});",
|
||||
).await;
|
||||
let mut page_vm = test_page_vm_with_document_url(Url::parse(&format!("{base_url}/main/page.html")).unwrap());
|
||||
let base = serde_json::to_string(&format!("{base_url}/worklet/")).unwrap();
|
||||
let local_executor = page_vm.local_executor.clone();
|
||||
let result = local_executor.run(async move {
|
||||
page_vm.vm_mut().eval(&format!(r#"
|
||||
const frame = document.createElement('iframe'); document.body.append(frame);
|
||||
const other = frame.contentWindow;
|
||||
const base = other.document.createElement('base'); base.href = {base}; other.document.head.append(base);
|
||||
const context = new other.AudioContext();
|
||||
Worklet.prototype.addModule.call(context.audioWorklet, 'entry.js').then(() => {{
|
||||
globalThis.__receiverBaseResult = String(new other.AudioWorkletNode(context, 'receiver-base') instanceof other.AudioWorkletNode);
|
||||
return context.close();
|
||||
}}).catch(error => globalThis.__receiverBaseResult = String(error));
|
||||
"#))?;
|
||||
drive_websocket_until_done(&mut page_vm, "String(globalThis.__receiverBaseResult !== undefined)", "borrowed addModule should settle").await?;
|
||||
page_vm.vm_mut().eval("__receiverBaseResult")
|
||||
}).await.expect("receiver base URL test should complete");
|
||||
assert_eq!(result, "true");
|
||||
let request = request_rx.await.expect("module request");
|
||||
assert!(request.starts_with("GET /worklet/entry.js "), "{request}");
|
||||
server.await.expect("server should finish");
|
||||
}).await;
|
||||
}
|
||||
|
||||
@@ -2156,6 +2156,7 @@ mod webidl_receivers;
|
||||
mod webidl_trusted_types;
|
||||
mod websocket;
|
||||
mod window_execution_context;
|
||||
mod worklet_interfaces;
|
||||
|
||||
mod string_timers;
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
(async () => {
|
||||
const rows = [], assert = (ok, message) => { if (!ok) throw Error(message); };
|
||||
const check = async (name, run) => { try { await run(); rows.push({ name, pass: true }); } catch (error) { rows.push({ name, pass: false, message: String(error) }); } };
|
||||
const realms = [['main', window], ['child', document.getElementById('child').contentWindow]];
|
||||
for (const [label, w] of realms) {
|
||||
if (!w.isSecureContext) {
|
||||
await check(label + '/insecure', () => assert(!('Worklet' in w) && !('AudioWorklet' in w) && !('audioWorklet' in w.BaseAudioContext.prototype) && !('audioWorklet' in new w.OfflineAudioContext(1,1,44100)), 'secure globals and attribute hidden'));
|
||||
continue;
|
||||
}
|
||||
for (const name of ['Worklet', 'AudioWorklet']) {
|
||||
await check(label + '/' + name, () => {
|
||||
const C = w[name], parent = name === 'AudioWorklet' ? w.Worklet : w.Object;
|
||||
const d = Object.getOwnPropertyDescriptor(w, name);
|
||||
assert(typeof C === 'function' && C.name === name && C.length === 0, 'constructor metadata');
|
||||
assert(d.writable && d.configurable && !d.enumerable, 'global descriptor');
|
||||
assert(Object.getPrototypeOf(C.prototype) === parent.prototype && Object.getPrototypeOf(C) === (parent === w.Object ? w.Function.prototype : parent), 'native inheritance');
|
||||
const tag = Object.getOwnPropertyDescriptor(C.prototype, Symbol.toStringTag);
|
||||
assert(tag.value === name && tag.configurable && !tag.writable && !tag.enumerable, 'prototype tag');
|
||||
for (const call of [() => C(), () => new C()]) { let error; try { call(); } catch (e) { error = e; } assert(error instanceof w.TypeError, 'illegal constructor'); }
|
||||
});
|
||||
}
|
||||
await check(label + '/receiver', async () => {
|
||||
const method = w.Worklet.prototype.addModule, d = Object.getOwnPropertyDescriptor(w.Worklet.prototype, 'addModule');
|
||||
assert(method.length === 1 && d.enumerable && d.configurable && d.writable, 'shared operation descriptor');
|
||||
let conversions = 0, traps = 0; const trap = () => { traps++; throw Error('author trap'); };
|
||||
const url = { toString() { conversions++; return 'data:text/javascript,'; } }, options = { get credentials() { conversions++; return 'same-origin'; } };
|
||||
const revoked = Proxy.revocable({}, {}); revoked.revoke();
|
||||
for (const receiver of [null, {}, w.Worklet.prototype, w.AudioWorklet.prototype, Object.create(w.Worklet.prototype), new Proxy({}, { get: trap, getPrototypeOf: trap }), revoked.proxy]) {
|
||||
const promise = method.call(receiver, url, options); assert(promise instanceof w.Promise, 'callee Promise');
|
||||
let error; try { await promise; } catch (e) { error = e; } assert(error instanceof w.TypeError, 'callee TypeError rejection');
|
||||
}
|
||||
assert(conversions === 0 && traps === 0, 'receiver validation precedes author callbacks');
|
||||
});
|
||||
let context;
|
||||
try {
|
||||
await check(label + '/native-factory', () => {
|
||||
const C = w.AudioWorklet, descriptor = Object.getOwnPropertyDescriptor(w, 'AudioWorklet'); let reads = 0;
|
||||
Object.defineProperty(w, 'AudioWorklet', { configurable: true, get() { reads++; throw Error('author constructor'); } });
|
||||
try { context = new w.AudioContext(); } finally { if (descriptor) Object.defineProperty(w, 'AudioWorklet', descriptor); else delete w.AudioWorklet; }
|
||||
const worklet = context.audioWorklet;
|
||||
const d = Object.getOwnPropertyDescriptor(w.BaseAudioContext.prototype, 'audioWorklet');
|
||||
assert(d && typeof d.get === 'function' && d.set === undefined && d.enumerable && d.configurable && !Object.hasOwn(context, 'audioWorklet'), 'shared readonly attribute');
|
||||
const offline = new w.OfflineAudioContext(1, 1, 44100);
|
||||
assert(Object.getPrototypeOf(offline.audioWorklet) === C.prototype && offline.audioWorklet === offline.audioWorklet, 'offline worklet');
|
||||
assert(d.get.call(context) === worklet, 'genuine receiver');
|
||||
for (const value of [{}, Object.create(context), new Proxy(context, {})]) { let error; try {d.get.call(value);} catch(e) {error=e;} assert(error instanceof w.TypeError, 'getter receiver'); }
|
||||
assert(Object.getPrototypeOf(worklet) === C.prototype && worklet instanceof w.Worklet, 'native derived prototype');
|
||||
assert(worklet === context.audioWorklet && !Object.hasOwn(worklet, 'addModule') && worklet.addModule === w.Worklet.prototype.addModule && reads === 0, 'shared operation and intrinsic factory');
|
||||
});
|
||||
await check(label + '/argument-conversion', async () => {
|
||||
const worklet = context.audioWorklet;
|
||||
for (const args of [[], [Symbol()], ['data:text/javascript,', 7], ['data:text/javascript,', {credentials:'invalid'}]]) {
|
||||
let error; const promise=worklet.addModule(...args); assert(promise instanceof w.Promise, 'conversion rejection is Promise');
|
||||
try { await promise; } catch (e) { error=e; } assert(error instanceof w.TypeError, 'invalid arguments');
|
||||
}
|
||||
const sentinel = {}, order=[]; let error;
|
||||
const promise=worklet.addModule({toString(){order.push('url');return 'data:text/javascript,';}},{get credentials(){order.push('credentials');return {toString(){order.push('string');throw sentinel;}};}});
|
||||
try { await promise; } catch (e) {error=e;}
|
||||
assert(error===sentinel && order.join()==='url,credentials,string', 'dictionary order and original exception');
|
||||
});
|
||||
await check(label + '/invalid-url', async () => {
|
||||
let error;
|
||||
try { await context.audioWorklet.addModule('https://['); } catch(e) {error=e;}
|
||||
assert(error instanceof w.DOMException && error.name === 'SyntaxError', 'invalid URL DOMException');
|
||||
const sentinel = {};
|
||||
try { await context.audioWorklet.addModule('https://[', {get credentials(){throw sentinel;}}); } catch(e) {error=e;}
|
||||
assert(error === sentinel, 'dictionary conversion before URL parsing');
|
||||
});
|
||||
await check(label + '/cross-realm', async () => {
|
||||
const other = realms.find(([,r]) => r !== w)[1];
|
||||
const otherContext = new other.AudioContext();
|
||||
try {
|
||||
const getter = Object.getOwnPropertyDescriptor(w.BaseAudioContext.prototype, 'audioWorklet').get;
|
||||
const receiver = getter.call(otherContext);
|
||||
assert(Object.getPrototypeOf(receiver) === other.AudioWorklet.prototype, 'getter uses receiver realm');
|
||||
const p = w.Worklet.prototype.addModule.call(receiver, 'https://[');
|
||||
let error; try {await p;} catch(e) {error=e;}
|
||||
assert(p instanceof other.Promise && error instanceof other.DOMException && error.name === 'SyntaxError', 'valid receiver realm');
|
||||
const invalid = w.Worklet.prototype.addModule.call(receiver, Symbol());
|
||||
try {await invalid;} catch(e) {error=e;}
|
||||
assert(invalid instanceof w.Promise && error instanceof w.TypeError, 'conversion rejection callee realm');
|
||||
} finally {await otherContext.close();}
|
||||
});
|
||||
} finally { if (context) await context.close(); }
|
||||
}
|
||||
globalThis.__nodeReplacementResults={rows,failures:rows.filter(row=>!row.pass),passed:rows.filter(row=>row.pass).length,total:rows.length};
|
||||
return rows.every(row=>row.pass);
|
||||
})()
|
||||
@@ -0,0 +1,21 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn worklet_interfaces_preserve_realms_brands_and_secure_exposure() {
|
||||
for url in [
|
||||
"https://worklet-interfaces.test/",
|
||||
"http://worklet-interfaces.test/",
|
||||
] {
|
||||
let mut vm = new_storage_page_task_executor_test_vm(url);
|
||||
vm.eval("document.body.innerHTML = '<iframe id=child></iframe>'")
|
||||
.unwrap();
|
||||
vm.eval(&format!("({}).then(value => globalThis.__workletDone = value, error => globalThis.__workletDone = String(error));", include_str!("worklet_interfaces.js"))).unwrap();
|
||||
assert_eq!(
|
||||
vm.eval_after_selected_page_tasks("JSON.stringify(__nodeReplacementResults.failures)")
|
||||
.unwrap(),
|
||||
"[]",
|
||||
"{url}"
|
||||
);
|
||||
assert_eq!(vm.eval("__workletDone").unwrap(), "true", "{url}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
(async () => {
|
||||
const rows = [], assert = (ok, message) => { if (!ok) throw Error(message); };
|
||||
const check = async (name, run) => { try { await run(); rows.push({ name, pass: true }); } catch (error) { rows.push({ name, pass: false, message: String(error) }); } };
|
||||
const context = new AudioContext(), worklet = context.audioWorklet;
|
||||
const source = text => 'data:text/javascript,' + encodeURIComponent(text);
|
||||
const errors = [], onError = event => errors.push(event.message);
|
||||
addEventListener('error', onError);
|
||||
try {
|
||||
await check('pending-and-completed-module-promises', async () => {
|
||||
const url = source('registerProcessor("fresh-promise", class extends AudioWorkletProcessor {});');
|
||||
const first = worklet.addModule(url); let reads=0;
|
||||
const poison = () => {reads++; throw Error('public Promise property');};
|
||||
Object.defineProperties(first, {then:{get:poison,configurable:true}, constructor:{get:poison,configurable:true}});
|
||||
let second;
|
||||
// A second call appends a native waiter; inherited array setters must not run.
|
||||
Object.defineProperty(Array.prototype, '1', {configurable:true, set:poison});
|
||||
try {second = worklet.addModule(url);} finally {delete Array.prototype[1];delete first.then;delete first.constructor;}
|
||||
assert(reads===0 && first!==second, 'independent pending Promise without author hooks');
|
||||
const values = await Promise.all([first, second]);
|
||||
assert(values.every(value => value===undefined), 'all callers fulfilled');
|
||||
Object.defineProperties(first, {then:{get:poison,configurable:true}, constructor:{get:poison,configurable:true}});
|
||||
let third;
|
||||
try {third=worklet.addModule(url);} finally {delete first.then;delete first.constructor;}
|
||||
assert(reads===0 && third!==first && third!==second, 'independent completed Promise');
|
||||
assert(await third === undefined, 'completed response reused');
|
||||
assert(new AudioWorkletNode(context, 'fresh-promise') instanceof AudioWorkletNode, 'processor registration retained');
|
||||
});
|
||||
await check('failed-module-promises', async () => {
|
||||
const url=source('export {');
|
||||
const first=worklet.addModule(url), second=worklet.addModule(url);
|
||||
assert(first!==second, 'independent failed pending Promise');
|
||||
const results=await Promise.allSettled([first,second]);
|
||||
assert(results.every(result=>result.status==='rejected'), 'all failed waiters settle');
|
||||
const third=worklet.addModule(url);assert(third!==first && third!==second, 'independent rejected cached Promise');
|
||||
const [result]=await Promise.allSettled([third]);assert(result.status==='rejected', 'cached failure settles');
|
||||
assert(errors.length === 0, 'module rejection does not report internal Worker error on Window');
|
||||
});
|
||||
await check('offline-worklet-module', async () => {
|
||||
const offline=new OfflineAudioContext(1,1,44100);
|
||||
const url=source('registerProcessor("offline-module", class extends AudioWorkletProcessor {});');
|
||||
await offline.audioWorklet.addModule(url);
|
||||
const node=new AudioWorkletNode(offline, 'offline-module');
|
||||
assert(node instanceof AudioWorkletNode, 'offline processor registered');
|
||||
node.connect(offline.destination);
|
||||
});
|
||||
} finally {removeEventListener('error', onError);await context.close();}
|
||||
globalThis.__nodeReplacementResults={rows,failures:rows.filter(row=>!row.pass),passed:rows.filter(row=>row.pass).length,total:rows.length};
|
||||
return rows.every(row=>row.pass);
|
||||
})()
|
||||
@@ -13,8 +13,8 @@ pub use moli_v8_util::{
|
||||
object_own_static_string_property, object_property_as_array, object_property_as_object,
|
||||
object_string_property, private_key, register_intrinsic_interface,
|
||||
registered_intrinsic_constructor, registered_intrinsic_prototype, set_null_prototype,
|
||||
set_private_value, set_symbol_to_string_tag, throw_range_error, throw_type_error,
|
||||
v8_json_parse, v8_string, v8str, walk_object_chain,
|
||||
set_private_value, throw_range_error, throw_type_error, v8_json_parse, v8_string, v8str,
|
||||
walk_object_chain,
|
||||
};
|
||||
use moli_webapi_declare::WebApiValue;
|
||||
pub(crate) use moli_webapi_declare::define_array_data_property as define_v8_array_data_property;
|
||||
|
||||
@@ -35,7 +35,8 @@ interfaces! {
|
||||
AudioContext: BaseAudioContext;
|
||||
AudioDestinationNode;
|
||||
AudioParam;
|
||||
AudioWorklet;
|
||||
Worklet;
|
||||
AudioWorklet: Worklet;
|
||||
AudioWorkletNode;
|
||||
BaseAudioContext: EventTarget;
|
||||
BatteryManager;
|
||||
|
||||
@@ -1889,6 +1889,24 @@ async fn worker_self_close() {
|
||||
|
||||
// ─── Error propagation ──────────────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
async fn worker_does_not_expose_window_worklet_interfaces() {
|
||||
ensure_v8();
|
||||
let mut handle = spawn_worker(
|
||||
"postMessage({Worklet: 'Worklet' in self, AudioWorklet: 'AudioWorklet' in self}); close();"
|
||||
.to_owned(),
|
||||
"https://worklet-worker.test/worker.js".into(),
|
||||
);
|
||||
let message = timeout(TIMEOUT, handle.recv())
|
||||
.await
|
||||
.expect("timed out")
|
||||
.expect("channel closed");
|
||||
assert_eq!(
|
||||
expect_post_json(message),
|
||||
r#"{"Worklet":false,"AudioWorklet":false}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn worker_error_propagation() {
|
||||
ensure_v8();
|
||||
|
||||
Reference in New Issue
Block a user