use super::*;
pub(crate) fn ensure_dom_op_table() -> Option<DomOpTable> {
if let Some(cached) = DOM_OP_TABLE
.try_with(|cell: &RefCell<Option<DomOpTable>>| {
cell.try_borrow()
.ok()
.and_then(|guard: Ref<Option<DomOpTable>>| guard.clone())
})
.ok()
.flatten()
{
return Some(cached);
}
let global_value: JsValue = global_this()?;
let _: bool = DomOpNames::set(DomOpNames::build());
let names: &'static DomOpNames = DomOpNames::get();
let table_value: JsValue = match Reflect::get(&global_value, &JsValue::from_str(&names.table)) {
Ok(existing) => existing,
Err(_err) => JsValue::UNDEFINED,
};
let table: DomOpTable = if table_value.is_object() {
let set_attrs: Function =
match Reflect::get(&table_value, &JsValue::from_str(&names.set_attrs)) {
Ok(value) => match value.dyn_into::<Function>() {
Ok(function) => function,
Err(_) => return None,
},
Err(_err) => return None,
};
let remove_attrs: Function =
match Reflect::get(&table_value, &JsValue::from_str(&names.remove_attrs)) {
Ok(value) => match value.dyn_into::<Function>() {
Ok(function) => function,
Err(_) => return None,
},
Err(_err) => return None,
};
let child_ops: Function =
match Reflect::get(&table_value, &JsValue::from_str(&names.child_ops)) {
Ok(value) => match value.dyn_into::<Function>() {
Ok(function) => function,
Err(_) => return None,
},
Err(_err) => return None,
};
DomOpTable {
set_attrs,
remove_attrs,
child_ops,
}
} else {
install_dom_op_table(&global_value, names)?
};
let _: Result<(), std::thread::AccessError> =
DOM_OP_TABLE.try_with(|cell: &RefCell<Option<DomOpTable>>| {
if let Ok(mut guard) = cell.try_borrow_mut() {
*guard = Some(table.clone());
}
});
Some(table)
}
fn install_dom_op_table(global_value: &JsValue, names: &DomOpNames) -> Option<DomOpTable> {
let set_attrs_source: &str = "function(elem, names, values) { \
for (var i = 0; i < names.length; i++) { \
elem.setAttribute(names[i], values[i]); \
} \
}";
let remove_attrs_source: &str = "function(elem, names) { \
for (var i = 0; i < names.length; i++) { \
elem.removeAttribute(names[i]); \
} \
}";
let child_ops_source: &str = "function(parent, ops) { \
for (var i = 0; i < ops.length; i++) { \
var op = ops[i]; \
try { \
if (op[0] === 0) { \
parent.insertBefore(op[1], op[2]); \
} else if (op[0] === 1) { \
parent.appendChild(op[1]); \
} else { \
parent.removeChild(op[1]); \
} \
} catch (e) { \
// One failing op (e.g. a stale reference node) must not \
// abort the rest of the batch; the Rust fallback path \
// drops per-op errors the same way. \
} \
} \
}";
let set_attrs: Function = eval_function(set_attrs_source)?;
let remove_attrs: Function = eval_function(remove_attrs_source)?;
let child_ops: Function = eval_function(child_ops_source)?;
let table_value: JsValue = js_sys::Object::new().into();
let _: Result<bool, JsValue> = Reflect::set(
&table_value,
&JsValue::from_str(&names.set_attrs),
set_attrs.as_ref(),
);
let _: Result<bool, JsValue> = Reflect::set(
&table_value,
&JsValue::from_str(&names.remove_attrs),
remove_attrs.as_ref(),
);
let _: Result<bool, JsValue> = Reflect::set(
&table_value,
&JsValue::from_str(&names.child_ops),
child_ops.as_ref(),
);
let _: Result<bool, JsValue> =
Reflect::set(global_value, &JsValue::from_str(&names.table), &table_value);
Some(DomOpTable {
set_attrs,
remove_attrs,
child_ops,
})
}
fn eval_function(body: &str) -> Option<Function> {
let wrapped: String = format!("({})", body);
js_sys::eval(&wrapped)
.ok()
.and_then(|value: JsValue| value.dyn_into::<Function>().ok())
}
fn global_this() -> Option<JsValue> {
if let Ok(value) = js_sys::eval(JS_GLOBAL_THIS)
&& !value.is_undefined()
{
return Some(value);
}
let window: Window = window()?;
Some(window.into())
}
pub(crate) fn is_property_attr(name: &str) -> bool {
name == ATTR_VALUE
|| name == ATTR_CHECKED
|| name == ATTR_DISABLED
|| name == ATTR_SELECTED
|| name == ATTR_READONLY
|| name == ATTR_MULTIPLE
}
pub(crate) fn apply_set_attr_batch(element: &Element, ops: &[(String, String)]) {
if ops.is_empty() {
return;
}
let Some(table) = ensure_dom_op_table() else {
for (name, value) in ops {
let _: Result<(), JsValue> = element.set_attribute(name, value);
}
return;
};
let names: js_sys::Array = js_sys::Array::new_with_length(ops.len() as u32);
let values: js_sys::Array = js_sys::Array::new_with_length(ops.len() as u32);
for (index, (name, value)) in ops.iter().enumerate() {
names.set(index as u32, JsValue::from_str(name));
values.set(index as u32, JsValue::from_str(value));
}
let element_value: JsValue = element.clone().into();
let result: Result<JsValue, JsValue> = table.set_attrs.call3(
&JsValue::UNDEFINED,
&element_value,
names.as_ref(),
values.as_ref(),
);
if result.is_err() {
for (name, value) in ops {
let _: Result<(), JsValue> = element.set_attribute(name, value);
}
}
}
pub(crate) fn apply_remove_attr_batch(element: &Element, ops: &[String]) {
if ops.is_empty() {
return;
}
let Some(table) = ensure_dom_op_table() else {
for name in ops {
let _: Result<(), JsValue> = element.remove_attribute(name);
}
return;
};
let names: js_sys::Array = js_sys::Array::new_with_length(ops.len() as u32);
for (index, name) in ops.iter().enumerate() {
names.set(index as u32, JsValue::from_str(name));
}
let element_value: JsValue = element.clone().into();
let result: Result<JsValue, JsValue> =
table
.remove_attrs
.call2(&JsValue::UNDEFINED, &element_value, names.as_ref());
if result.is_err() {
for name in ops {
let _: Result<(), JsValue> = element.remove_attribute(name);
}
}
}
pub(crate) fn apply_child_ops_batch(parent: &Element, ops: &[ChildOp]) {
if ops.is_empty() {
return;
}
let Some(table) = ensure_dom_op_table() else {
for op in ops {
apply_child_op_fallback(parent, op);
}
return;
};
let ops_array: js_sys::Array = js_sys::Array::new_with_length(ops.len() as u32);
for (index, op) in ops.iter().enumerate() {
let tuple: js_sys::Array = js_sys::Array::new_with_length(3);
let kind: u32 = match op {
ChildOp::InsertBefore { .. } => 0,
ChildOp::AppendChild(_) => 1,
ChildOp::RemoveChild(_) => 2,
};
let primary: JsValue = match op {
ChildOp::InsertBefore { node, .. } => node.clone().into(),
ChildOp::AppendChild(node) => node.clone().into(),
ChildOp::RemoveChild(node) => node.clone().into(),
};
let reference: JsValue = match op {
ChildOp::InsertBefore { reference, .. } => {
reference.clone().map(Node::into).unwrap_or(JsValue::NULL)
}
_ => JsValue::NULL,
};
tuple.set(0, JsValue::from(kind));
tuple.set(1, primary);
tuple.set(2, reference);
ops_array.set(index as u32, tuple.into());
}
let parent_value: JsValue = parent.clone().into();
let result: Result<JsValue, JsValue> =
table
.child_ops
.call2(&JsValue::UNDEFINED, &parent_value, ops_array.as_ref());
if result.is_err() {
for op in ops {
apply_child_op_fallback(parent, op);
}
}
}
fn apply_child_op_fallback(parent: &Element, op: &ChildOp) {
match op {
ChildOp::InsertBefore { node, reference } => {
let _: Result<Node, JsValue> = match reference {
Some(reference_node) => parent.insert_before(node, Some(reference_node)),
None => parent.append_child(node),
};
}
ChildOp::AppendChild(node) => {
let _: Result<Node, JsValue> = parent.append_child(node);
}
ChildOp::RemoveChild(node) => {
let _: Result<Node, JsValue> = parent.remove_child(node);
}
}
}
pub(crate) fn build_dom_op_names() -> DomOpNames {
let suffix: String = encoded_name_suffix();
DomOpNames {
table: format!("{}{}", JS_DOM_OP_NAME_PREFIX, suffix),
set_attrs: format!("{}dom_op_set_attrs_{}", JS_DOM_OP_NAME_PREFIX, suffix),
remove_attrs: format!("{}dom_op_remove_attrs_{}", JS_DOM_OP_NAME_PREFIX, suffix),
child_ops: format!("{}dom_op_child_ops_{}", JS_DOM_OP_NAME_PREFIX, suffix),
}
}
pub(crate) fn encoded_name_suffix() -> String {
let micros: u64 = now_micros();
let mixed: u64 = micros.wrapping_mul(JS_DOM_OP_NAME_MIX);
let printable: [u8; 6] = {
let bytes: [u8; 8] = mixed.to_le_bytes();
let mut masked: [u8; 6] = [b'0'; 6];
for (slot, byte) in masked.iter_mut().zip(bytes.iter().take(6)) {
*slot = b'!' + (byte % 94);
}
masked
};
let raw: &str = std::str::from_utf8(&printable).unwrap_or(JS_DOM_OP_NAME_FALLBACK_SUFFIX);
let encoded: Result<String, EncodeError> =
Charset::new().charset(JS_DOM_OP_NAME_CHARSET).encode(raw);
match encoded {
Ok(suffix) => suffix,
Err(_) => String::from(JS_DOM_OP_NAME_FALLBACK_SUFFIX),
}
}
pub(crate) fn now_micros() -> u64 {
#[cfg(target_arch = "wasm32")]
{
let millis: f64 = js_sys::Date::now();
let fraction: f64 = js_sys::eval(JS_PERFORMANCE_NOW_FRACTION)
.ok()
.and_then(|value: JsValue| value.as_f64())
.unwrap_or(0.0);
let total_micros: f64 = millis * 1000.0 + fraction * 1000.0;
if total_micros.is_finite() && total_micros > 0.0 {
return total_micros as u64;
}
0
}
#[cfg(not(target_arch = "wasm32"))]
{
match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(elapsed) => elapsed.as_micros() as u64,
Err(_) => 0,
}
}
}