ferrijs_std/web/js_iterator.rs
1//! The one `{ value, done }` iterator protocol used by every
2//! web-platform collection class (`Headers`, `URLSearchParams`,
3//! `FormData`).
4//!
5//! WHATWG iteration is LIVE: mutations made during a loop are observed,
6//! so `for (const [k] of params) params.delete(k)` behaves as it does in
7//! Node. The iterator therefore holds no snapshot — it re-reads its
8//! parent on every `next()`.
9//!
10//! The parent is carried as a property ON the iterator object, which the
11//! JS GC traces, and re-read per call. The native `next` closure captures
12//! nothing from the JS heap, per the GC-cycle discipline: a closure that
13//! captured the parent `Class` would be invisible to the collector and
14//! could strand the runtime at teardown.
15
16use rquickjs::atom::PredefinedAtom;
17use rquickjs::function::{Func, This};
18use rquickjs::{Class, Ctx, Object, Value, class::JsClass};
19
20/// Yield the entry at `index`, or `None` once the collection is
21/// exhausted. Called with the parent freshly borrowed, so it always sees
22/// current state.
23pub type Project<'js, T> = fn(&Ctx<'js>, &Class<'js, T>, usize) -> rquickjs::Result<Option<Value<'js>>>;
24
25/// Build a live iterator over `parent`, projecting each position through
26/// `project`. The result is itself iterable (`[Symbol.iterator]` returns
27/// `this`), so it works with `for..of`, spread and `Array.from`.
28pub fn live_iterator<'js, T>(
29 ctx: &Ctx<'js>,
30 parent: Class<'js, T>,
31 project: Project<'js, T>,
32) -> rquickjs::Result<Object<'js>>
33where
34 T: JsClass<'js> + 'js,
35{
36 let it = Object::new(ctx.clone())?;
37 it.set("position", 0usize)?;
38 it.set("target", parent)?;
39 it.set(
40 PredefinedAtom::SymbolIterator,
41 Func::from(|it: This<Object<'js>>| -> rquickjs::Result<Object<'js>> { Ok(it.0) }),
42 )?;
43 it.set(
44 PredefinedAtom::Next,
45 Func::from(
46 move |ctx: Ctx<'js>, it: This<Object<'js>>| -> rquickjs::Result<Object<'js>> {
47 let position = it.get::<_, usize>("position")?;
48 let parent: Class<'js, T> = it.get("target")?;
49 let res = Object::new(ctx.clone())?;
50 match project(&ctx, &parent, position)? {
51 None => {
52 res.set(PredefinedAtom::Value, Value::new_undefined(ctx))?;
53 res.set(PredefinedAtom::Done, true)?;
54 },
55 Some(value) => {
56 res.set(PredefinedAtom::Value, value)?;
57 res.set(PredefinedAtom::Done, false)?;
58 it.set("position", position + 1)?;
59 },
60 }
61 Ok(res)
62 },
63 ),
64 )?;
65 Ok(it)
66}