use ::std::cell::RefCell;
use ::std::collections::HashSet;
use ::std::ptr::NonNull;
use bun_core::ZBox;
use mozjs::jsapi::*;
use mozjs::jsval::{JSVal, ObjectValue, UndefinedValue};
use mozjs::rooted;
struct GcStore {
keys: HashSet<String>,
}
impl GcStore {
fn new() -> Self {
GcStore {
keys: HashSet::new(),
}
}
fn prop_name(namespace: &str, key: &str) -> ZBox {
if namespace.is_empty() {
ZBox::from_vec(format!("__gc_cache_{}", key).into_bytes())
} else {
ZBox::from_vec(format!("__gc_{}_{}", namespace, key).into_bytes())
}
}
fn tracking_key(namespace: &str, key: &str) -> String {
if namespace.is_empty() {
key.to_string()
} else {
format!("{}::{}", namespace, key)
}
}
fn insert(&mut self, cx: *mut JSContext, namespace: &str, key: &str, obj: *mut JSObject) {
if obj.is_null() {
return;
}
let global = unsafe { CurrentGlobalOrNull(cx) };
if global.is_null() {
return;
}
let cx_ref = unsafe { mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx)) };
rooted!(&in(cx_ref) let global_root = global);
rooted!(&in(cx_ref) let obj_val_root = ObjectValue(obj));
let prop_name = Self::prop_name(namespace, key);
unsafe {
JS_DefineProperty(
cx,
global_root.handle().into(),
prop_name.as_ptr(),
obj_val_root.handle().into(),
(JSPROP_READONLY) as u32,
);
}
self.keys.insert(Self::tracking_key(namespace, key));
}
fn get(&self, cx: *mut JSContext, namespace: &str, key: &str) -> Option<*mut JSObject> {
if !self.keys.contains(&Self::tracking_key(namespace, key)) {
return None;
}
let global = unsafe { CurrentGlobalOrNull(cx) };
if global.is_null() {
return None;
}
let cx_ref = unsafe { mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx)) };
rooted!(&in(cx_ref) let global_root = global);
let prop_name = Self::prop_name(namespace, key);
let mut val = UndefinedValue();
unsafe {
JS_GetProperty(
cx,
global_root.handle().into(),
prop_name.as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut val,
},
);
}
if val.is_object() {
Some(val.to_object())
} else {
None
}
}
fn remove(&mut self, cx: *mut JSContext, namespace: &str, key: &str) {
if !self.keys.remove(&Self::tracking_key(namespace, key)) {
return;
}
let global = unsafe { CurrentGlobalOrNull(cx) };
if global.is_null() {
return;
}
let cx_ref = unsafe { mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx)) };
rooted!(&in(cx_ref) let global_root = global);
let prop_name = Self::prop_name(namespace, key);
unsafe {
JS_DeleteProperty1(cx, global_root.handle().into(), prop_name.as_ptr());
}
}
}
thread_local! {
static GC_STORE: RefCell<GcStore> = RefCell::new(GcStore::new());
}
pub fn gc_store_insert(cx: *mut JSContext, key: &str, obj: *mut JSObject) {
GC_STORE.with(|s| {
s.borrow_mut().insert(cx, "", key, obj);
});
}
pub fn gc_store_get(cx: *mut JSContext, key: &str) -> Option<*mut JSObject> {
GC_STORE.with(|s| s.borrow().get(cx, "", key))
}
pub fn gc_store_remove(cx: *mut JSContext, key: &str) {
GC_STORE.with(|s| {
s.borrow_mut().remove(cx, "", key);
});
}
pub fn gc_store_insert_ns(cx: *mut JSContext, namespace: &str, key: &str, obj: *mut JSObject) {
GC_STORE.with(|s| {
s.borrow_mut().insert(cx, namespace, key, obj);
});
}
pub fn gc_store_get_ns(cx: *mut JSContext, namespace: &str, key: &str) -> Option<*mut JSObject> {
GC_STORE.with(|s| s.borrow().get(cx, namespace, key))
}
pub fn gc_store_remove_ns(cx: *mut JSContext, namespace: &str, key: &str) {
GC_STORE.with(|s| {
s.borrow_mut().remove(cx, namespace, key);
});
}
pub fn gc_store_key(namespace: &str, id: u64) -> String {
format!("__gc_{}_{}", namespace, id)
}
use ::std::sync::atomic::{AtomicU64, Ordering};
static GC_KEY_COUNTER: AtomicU64 = AtomicU64::new(1);
pub fn gc_store_unique_key(namespace: &str) -> String {
let id = GC_KEY_COUNTER.fetch_add(1, Ordering::Relaxed);
gc_store_key(namespace, id)
}
#[cfg(test)]
mod tests {
use super::*;
use bun_core::ByteSlice;
#[test]
fn prop_name_empty_namespace_uses_cache_prefix() {
let c = GcStore::prop_name("", "foo");
let s = c.to_str().unwrap();
assert_eq!(s, "__gc_cache_foo");
}
#[test]
fn prop_name_with_namespace() {
let c = GcStore::prop_name("ServerUserData", "handler");
let s = c.to_str().unwrap();
assert_eq!(s, "__gc_ServerUserData_handler");
}
#[test]
fn tracking_key_empty_namespace() {
assert_eq!(GcStore::tracking_key("", "foo"), "foo");
}
#[test]
fn tracking_key_with_namespace() {
assert_eq!(
GcStore::tracking_key("EmitterState", "data:0"),
"EmitterState::data:0"
);
}
#[test]
fn tracking_key_uniqueness() {
let a = GcStore::tracking_key("ServerUserData", "handler");
let b = GcStore::tracking_key("BunServeUserData", "handler");
assert_ne!(a, b, "same key in different namespaces must be distinct");
}
#[test]
fn gc_store_new_is_empty() {
let store = GcStore::new();
assert!(store.keys.is_empty());
}
#[test]
fn gc_store_key_format() {
assert_eq!(gc_store_key("timer", 42), "__gc_timer_42");
}
#[test]
fn gc_store_unique_key_format() {
let k1 = gc_store_unique_key("http");
let k2 = gc_store_unique_key("http");
assert!(k1.starts_with("__gc_http_"));
assert!(k2.starts_with("__gc_http_"));
assert_ne!(k1, k2);
}
#[test]
fn gc_store_unique_key_counter_increments() {
let before = GC_KEY_COUNTER.fetch_add(0, Ordering::SeqCst);
let _ = gc_store_unique_key("test_ns");
let after = GC_KEY_COUNTER.fetch_add(0, Ordering::SeqCst);
assert!(
after > before,
"counter must increment: before={before}, after={after}"
);
}
}