euv-core 0.28.8

A declarative, cross-platform UI framework for Rust with virtual DOM, reactive signals, and HTML macros for WebAssembly.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
use super::*;

/// Resolves (or installs on first call) the batched DOM-op helpers
/// under `globalThis.__euv_dom_ops__`.
///
/// The function table holds three JS functions:
///
/// - `set_attrs(elem, names, values)` — batched `setAttribute`.
/// - `remove_attrs(elem, names)` — batched `removeAttribute`.
/// - `child_ops(parent, ops)` — batched `insertBefore`/`appendChild`/
///   `removeChild`.
///
/// All three are constructed via `js_sys::eval` so the helpers work
/// without any setup beyond a single `globalThis` lookup. The first
/// call performs the installation; subsequent calls return the cached
/// table in a single `Reflect::get` per process.
///
/// Returns `None` if `globalThis` is unreachable (e.g. SSR). Callers
/// fall back to the per-op `web_sys` path in that case.
///
/// # Returns
///
/// - `Option<DomOpTable>` - The function table, or `None` if it could
///   not be resolved.
pub(crate) fn ensure_dom_op_table() -> Option<DomOpTable> {
    // Fast path: a cached table, cloned out so the `RefCell` borrow is
    // released before any JS work happens below.
    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()?;
    // Claim the name set before it is used for anything. `set` wins the race
    // against `get` for the very first caller, so the names are fixed before
    // the first `Reflect::get` and the lookup and install paths below cannot
    // disagree about which name they are talking about. A losing caller just
    // reads the winner's names, which is correct: they are the same set.
    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)?
    };
    // Cache the resolved table. A refused borrow only costs one extra
    // `Reflect::get` on the next patch, so the failure is ignored rather
    // than propagated.
    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)
}

/// Installs the batched DOM-op helpers onto `globalThis.__euv_dom_ops__`.
///
/// The helpers are intentionally tiny — they just loop over the passed
/// arrays calling `setAttribute` / `removeAttribute` / `insertBefore` /
/// `appendChild` / `removeChild` on the element. Equivalent JS cost to
/// the per-op path (N attribute writes inside the function instead of N
/// JS round-trips) but only one crossing to enter the function.
///
/// # Arguments
///
/// - `&JsValue` - The `globalThis` handle the helpers are installed on.
/// - `&DomOpNames` - The JavaScript-side property names the installed
///   helpers are published under.
///
/// # Returns
///
/// - `Option<DomOpTable>` - The installed helper table, or `None` when
///   evaluation or the property install step failed.
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,
    })
}

/// Compiles a JS function body via `js_sys::eval`.
///
/// # Arguments
///
/// - `&str` - The JavaScript function body, wrapped in parentheses
///   before evaluation so it parses as an expression.
///
/// # Returns
///
/// - `Option<Function>` - The compiled function, or `None` when the body
///   failed to evaluate or did not produce a function object.
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())
}

/// Resolves the JS `globalThis` handle.
///
/// Falls back to `window` when `globalThis` is not present (older
/// Safari / non-browser WASM hosts). Returns `None` if neither is
/// available.
///
/// # Returns
///
/// - `Option<JsValue>` - The resolved `globalThis` handle, or `None`
///   when neither `globalThis` nor `window` is reachable.
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())
}

/// Returns `true` when the named attribute requires the form-property
/// dispatch path (i.e. setting `.value` / `.checked` / `.disabled` /
/// `.selected` / `.readonly` / `.multiple` on the DOM element instead
/// of calling `setAttribute`). For these attributes the renderer must
/// keep the per-element direct call to preserve the previous semantics
/// — batched `setAttribute` would silently break input/textarea/etc.
///
/// # Arguments
///
/// - `&str` - The attribute name to classify.
///
/// # Returns
///
/// - `bool` - `true` when the attribute must go through the direct
///   form-property assignment path instead of `setAttribute`.
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
}

/// Applies a batch of `setAttribute(name, value)` writes to `elem` via a
/// single JS-side function call.
///
/// On any failure (table unavailable, JS exception), the function falls
/// back to per-op `web_sys::Element::set_attribute` calls.
///
/// # Arguments
///
/// - `&Element` - The element to mutate.
/// - `&[(String, String)]` - The `(name, value)` pairs to set.
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);
        }
    }
}

/// Applies a batch of `removeAttribute(name)` writes to `elem` via a
/// single JS-side function call.
///
/// On any failure (table unavailable, JS exception), the function falls
/// back to per-op `web_sys::Element::remove_attribute` calls.
///
/// # Arguments
///
/// - `&Element` - The element to mutate.
/// - `&[String]` - The attribute names to remove.
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);
        }
    }
}

/// Applies a batch of child-mutation ops to `parent` via a single
/// JS-side function call.
///
/// Each op is encoded as a 3-element `Array`:
/// - `[0, node, refNode]` → `parent.insertBefore(node, refNode)`
/// - `[1, node, null]` → `parent.appendChild(node)`
/// - `[2, node, null]` → `parent.removeChild(node)`
///
/// On any failure (table unavailable, JS exception), the function falls
/// back to per-op `web_sys` calls.
///
/// # Arguments
///
/// - `&Element` - The parent element whose children are being mutated.
/// - `&[ChildOp]` - The ops to apply, in order.
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);
        }
    }
}

/// Per-op fallback used when the JS-side batched call fails. Mirrors
/// the previous patch-path behaviour exactly.
///
/// # Arguments
///
/// - `&Element` - The parent the child is inserted into or removed from.
/// - `&ChildOp` - The single child operation to apply.
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);
        }
    }
}

/// Builds the per-load names from the current clock.
///
/// The suffix is the microsecond clock, mixed and encoded. Failure of the
/// encoder is not fatal: it can only fail on a charset the crate rejects,
/// and [`JS_DOM_OP_NAME_CHARSET`] is a compile-time constant that satisfies
/// it, so the fallback is a defensive branch rather than a reachable one.
///
/// # Returns
///
/// - `DomOpNames` - The freshly built names.
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),
    }
}

/// Returns the encoded per-load suffix.
///
/// `bin-encode-decode` zero-pads every input byte into a 3-byte group before
/// base-encoding, so each byte fed costs one and a third output characters:
/// six input bytes become a 24-character suffix, not twelve. Six bytes is the
/// right input because 48 bits is already far more than enough to make the
/// name unguessable, and a longer input would only pad the identifier out.
///
/// The bytes are folded into printable ASCII first, because the encoder takes
/// `&str` and a raw byte can be zero or above `0x7F`, neither of which
/// survives the round-trip through `str`.
///
/// # Returns
///
/// - `String` - The encoded suffix, or
///   [`JS_DOM_OP_NAME_FALLBACK_SUFFIX`] if the clock or the encoder failed.
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),
    }
}

/// Returns the current time in microseconds.
///
/// `Date.now()` only reaches millisecond resolution, which is coarse enough
/// that two page loads inside the same millisecond would share a name, so
/// the sub-millisecond remainder is taken from `performance.now()`. Off-wasm
/// neither is available and the host clock is used instead; a host build is
/// not the security target, so its weaker value is accepted.
///
/// # Returns
///
/// - `u64` - Microseconds, or `0` if no clock is available.
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,
        }
    }
}