use std::{
cell::{Cell, RefCell},
slice,
};
use boa_gc::{Ephemeron, Finalize, Gc, Trace, WeakGc};
use crate::{
Context, JsArgs, JsData, JsObject, JsResult, JsSymbol, JsValue, JsVariant,
context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
job::{Job, JobCallback, NativeAsyncJob},
js_error, js_string,
object::{
ErasedVTableObject, JsFunction, VTableObject,
internal_methods::get_prototype_from_constructor,
},
property::Attribute,
realm::Realm,
string::StaticJsStrings,
};
use super::{BuiltInConstructor, BuiltInObject, IntrinsicObject, builder::BuiltInBuilder};
#[cfg(test)]
mod tests;
#[derive(Trace)]
struct CleanupSignaler(#[unsafe_ignore_trace] Cell<Option<async_channel::WeakSender<()>>>);
impl Finalize for CleanupSignaler {
fn finalize(&self) {
if let Some(sender) = self.0.take()
&& let Some(sender) = sender.upgrade()
{
let _ = sender.try_send(());
}
}
}
#[derive(Trace, Finalize)]
pub(crate) struct RegistryCell {
target: Ephemeron<ErasedVTableObject, CleanupSignaler>,
held_value: JsValue,
unregister_token: Option<WeakGc<ErasedVTableObject>>,
}
#[derive(Trace, Finalize, JsData)]
pub(crate) struct FinalizationRegistry {
realm: Realm,
callback: JobCallback,
#[unsafe_ignore_trace]
cleanup_notifier: async_channel::Sender<()>,
cells: Vec<RegistryCell>,
}
impl IntrinsicObject for FinalizationRegistry {
fn get(intrinsics: &Intrinsics) -> JsObject {
Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor()
}
fn init(realm: &Realm) {
BuiltInBuilder::from_standard_constructor::<Self>(realm)
.property(
JsSymbol::to_string_tag(),
js_string!("FinalizationRegistry"),
Attribute::CONFIGURABLE,
)
.method(Self::register, js_string!("register"), 2)
.method(Self::unregister, js_string!("unregister"), 1)
.build();
}
}
impl BuiltInObject for FinalizationRegistry {
const NAME: crate::JsString = StaticJsStrings::FINALIZATION_REGISTRY;
const ATTRIBUTE: Attribute = Attribute::WRITABLE.union(Attribute::CONFIGURABLE);
}
impl BuiltInConstructor for FinalizationRegistry {
const CONSTRUCTOR_ARGUMENTS: usize = 1;
const CONSTRUCTOR_STORAGE_SLOTS: usize = 0;
const PROTOTYPE_STORAGE_SLOTS: usize = 3;
const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor =
StandardConstructors::finalization_registry;
fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
if new_target.is_undefined() {
return Err(js_error!(
TypeError: "FinalizationRegistry: cannot call constructor without `new`"
));
}
let callback = args
.get_or_undefined(0)
.as_object()
.and_then(JsFunction::from_object)
.ok_or_else(|| {
js_error!(
TypeError: "FinalizationRegistry: \
cleanup callback of registry must be callable"
)
})?;
let prototype = get_prototype_from_constructor(
new_target,
StandardConstructors::finalization_registry,
context,
)?;
let realm = context.vm.frame().realm.clone();
let callback = context.host_hooks().make_job_callback(callback, context);
let cells = Vec::new();
let (sender, receiver) = async_channel::bounded(1);
let registry = JsObject::new_unique(
prototype,
FinalizationRegistry {
realm,
callback,
cells,
cleanup_notifier: sender,
},
);
let weak_registry = WeakGc::new(registry.inner());
{
async fn inner_cleanup(
weak_registry: WeakGc<VTableObject<FinalizationRegistry>>,
receiver: async_channel::Receiver<()>,
context: &RefCell<&mut Context>,
) -> JsResult<JsValue> {
let Ok(()) = receiver.recv().await else {
return Ok(JsValue::undefined());
};
let Some(registry) = weak_registry.upgrade().map(JsObject::from_inner) else {
return Ok(JsValue::undefined());
};
let result = FinalizationRegistry::cleanup(®istry, &mut context.borrow_mut());
context
.borrow_mut()
.enqueue_job(Job::FinalizationRegistryCleanupJob(NativeAsyncJob::new(
async move |context| inner_cleanup(weak_registry, receiver, context).await,
)));
result.map(|()| JsValue::undefined())
}
context.enqueue_job(Job::FinalizationRegistryCleanupJob(NativeAsyncJob::new(
async move |ctx| inner_cleanup(weak_registry, receiver, ctx).await,
)));
}
Ok(registry.upcast().into())
}
}
impl FinalizationRegistry {
fn register(this: &JsValue, args: &[JsValue], _context: &mut Context) -> JsResult<JsValue> {
let this = this.as_object();
let mut registry = this
.as_ref()
.and_then(JsObject::downcast_mut::<Self>)
.ok_or_else(|| {
js_error!(
TypeError: "FinalizationRegistry.prototype.register: \
invalid object type for `this`",
)
})?;
let target = args.get_or_undefined(0);
let held_value = args.get_or_undefined(1);
let unregister_token = args.get_or_undefined(2);
let Some(target_obj) = target.as_object() else {
return Err(js_error!(
TypeError: "FinalizationRegistry.prototype.register: \
`target` must be an Object or Symbol",
));
};
if target == held_value {
return Err(js_error!(
TypeError: "FinalizationRegistry.prototype.register: \
`heldValue` cannot be the same as `target`"
));
}
let unregister_token = match unregister_token.variant() {
JsVariant::Object(obj) => Some(WeakGc::new(obj.inner())),
JsVariant::Undefined => None,
_ => {
return Err(js_error!(
TypeError: "FinalizationRegistry.prototype.register: \
`unregisterToken` must be an Object, a Symbol, or undefined",
));
}
};
let cell = RegistryCell {
target: Ephemeron::new(
target_obj.inner(),
CleanupSignaler(Cell::new(Some(
registry.cleanup_notifier.clone().downgrade(),
))),
),
held_value: held_value.clone(),
unregister_token,
};
registry.cells.push(cell);
Ok(JsValue::undefined())
}
fn unregister(this: &JsValue, args: &[JsValue], _context: &mut Context) -> JsResult<JsValue> {
let this = this.as_object();
let mut registry = this
.as_ref()
.and_then(JsObject::downcast_mut::<Self>)
.ok_or_else(|| {
js_error!(
TypeError: "FinalizationRegistry.prototype.register: \
invalid object type for `this`",
)
})?;
let unregister_token = args.get_or_undefined(0).as_object();
let unregister_token = unregister_token
.as_ref()
.map(JsObject::inner)
.ok_or_else(|| {
js_error!(
TypeError: "FinalizationRegistry.prototype.unregister: \
`unregisterToken` must be an Object or a Symbol.",
)
})?;
let mut removed = false;
let mut i = 0;
while i < registry.cells.len() {
let cell = ®istry.cells[i];
if let Some(tok) = cell.unregister_token.as_ref()
&& let Some(tok) = tok.upgrade()
&& Gc::ptr_eq(&tok, unregister_token)
{
let cell = registry.cells.swap_remove(i);
let _key = cell.target.key();
cell.target.value().and_then(|v| v.0.take());
removed = true;
} else {
i += 1;
}
}
Ok(removed.into())
}
pub(crate) fn cleanup(
obj: &JsObject<FinalizationRegistry>,
context: &mut Context,
) -> JsResult<()> {
let callback = std::mem::replace(
&mut obj.borrow_mut().data_mut().callback,
JobCallback::new(context.intrinsics().objects().throw_type_error(), ()),
);
let mut i = 0;
let result = loop {
if i >= obj.borrow().data().cells.len() {
break Ok(());
}
if obj.borrow().data().cells[i].target.has_value() {
i += 1;
} else {
let cell = obj.borrow_mut().data_mut().cells.swap_remove(i);
let result = context.host_hooks().call_job_callback(
&callback,
&JsValue::undefined(),
slice::from_ref(&cell.held_value),
context,
);
if let Err(err) = result {
break Err(err);
}
}
};
obj.borrow_mut().data_mut().callback = callback;
result
}
}