Skip to content
File

Blob: src/rust/jsg-test/tests/collections_gc.rs

rust921 lines
1// Copyright (c) 2026 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5//! GC tracing tests for collection fields in `#[jsg_resource]` structs.
6//!
7//! Verifies that `Vec<jsg::Rc<T>>`, `HashMap<K, jsg::Rc<T>>`, `BTreeMap<K, jsg::Rc<T>>`,
8//! `HashSet<jsg::Rc<T>>`, `BTreeSet<jsg::Rc<T>>`, and their `Cell<…>` variants all
9//! produce correct GC trace edges so that children are kept alive as long as the parent
10//! is reachable, and collected once the parent is collected.
11//!
12//! Each test follows the same pattern:
13//! 1. Wrap the parent for JavaScript (gives it a JS wrapper held by a context global).
14//! 2. Drop all Rust `Rc` handles — the parent is now only alive via the JS wrapper.
15//! 3. Run a minor + major GC while the global is reachable → children must survive.
16//! 4. Move to a fresh context (global is gone) → GC must collect parent and all children.
17 
18use std::cell::Cell;
19use std::collections::BTreeMap;
20use std::collections::BTreeSet;
21use std::collections::HashMap;
22use std::collections::HashSet;
23use std::sync::atomic::AtomicUsize;
24use std::sync::atomic::Ordering;
25 
26use jsg::ToJS;
27use jsg_macros::jsg_method;
28use jsg_macros::jsg_resource;
29 
30// =============================================================================
31// Shared leaf resource
32// =============================================================================
33 
34static LEAF_DROPS: AtomicUsize = AtomicUsize::new(0);
35 
36#[jsg_resource]
37struct Leaf {
38 pub value: u32,
39}
40 
41impl Drop for Leaf {
42 fn drop(&mut self) {
43 LEAF_DROPS.fetch_add(1, Ordering::SeqCst);
44 }
45}
46 
47#[jsg_resource]
48impl Leaf {
49 #[jsg_method]
50 fn get_value(&self) -> jsg::Number {
51 jsg::Number::from(self.value)
52 }
53}
54 
55// =============================================================================
56// Vec<jsg::Rc<T>>
57// =============================================================================
58 
59static VEC_PARENT_DROPS: AtomicUsize = AtomicUsize::new(0);
60 
61#[jsg_resource]
62struct VecParent {
63 pub children: Vec<jsg::Rc<Leaf>>,
64}
65 
66impl Drop for VecParent {
67 fn drop(&mut self) {
68 VEC_PARENT_DROPS.fetch_add(1, Ordering::SeqCst);
69 }
70}
71 
72#[jsg_resource]
73impl VecParent {}
74 
75/// `Vec<jsg::Rc<T>>` children are kept alive while parent JS wrapper is reachable.
76#[test]
77fn vec_rc_children_kept_alive_through_gc() {
78 LEAF_DROPS.store(0, Ordering::SeqCst);
79 VEC_PARENT_DROPS.store(0, Ordering::SeqCst);
80 
81 let harness = crate::Harness::new();
82 harness.run_in_context(|lock, ctx| {
83 let c1 = jsg::Rc::new(Leaf { value: 1 });
84 let c2 = jsg::Rc::new(Leaf { value: 2 });
85 let c3 = jsg::Rc::new(Leaf { value: 3 });
86 
87 let parent = jsg::Rc::new(VecParent {
88 children: vec![c1.clone(), c2.clone(), c3.clone()],
89 });
90 let wrapped = parent.clone().to_js(lock);
91 ctx.set_global("parent", wrapped);
92 
93 // Drop all Rust refs.
94 std::mem::drop(c1);
95 std::mem::drop(c2);
96 std::mem::drop(c3);
97 std::mem::drop(parent);
98 
99 // GC while global is reachable — parent and all children must survive.
100 crate::Harness::request_gc(lock);
101 assert_eq!(VEC_PARENT_DROPS.load(Ordering::SeqCst), 0);
102 assert_eq!(LEAF_DROPS.load(Ordering::SeqCst), 0);
103 Ok(())
104 });
105 
106 // Fresh context — global is gone, everything should be collected.
107 harness.run_in_context(|lock, _ctx| {
108 crate::Harness::request_gc(lock);
109 assert_eq!(VEC_PARENT_DROPS.load(Ordering::SeqCst), 1);
110 assert_eq!(LEAF_DROPS.load(Ordering::SeqCst), 3);
111 Ok(())
112 });
113}
114 
115/// An empty `Vec<jsg::Rc<T>>` does not crash during GC.
116#[test]
117fn vec_rc_empty_does_not_crash_during_gc() {
118 VEC_PARENT_DROPS.store(0, Ordering::SeqCst);
119 
120 let harness = crate::Harness::new();
121 harness.run_in_context(|lock, ctx| {
122 let parent = jsg::Rc::new(VecParent { children: vec![] });
123 let wrapped = parent.clone().to_js(lock);
124 ctx.set_global("parent", wrapped);
125 std::mem::drop(parent);
126 
127 crate::Harness::request_gc(lock);
128 assert_eq!(VEC_PARENT_DROPS.load(Ordering::SeqCst), 0);
129 Ok(())
130 });
131 
132 harness.run_in_context(|lock, _ctx| {
133 crate::Harness::request_gc(lock);
134 assert_eq!(VEC_PARENT_DROPS.load(Ordering::SeqCst), 1);
135 Ok(())
136 });
137}
138 
139// =============================================================================
140// HashMap<K, jsg::Rc<T>>
141// =============================================================================
142 
143static HASHMAP_PARENT_DROPS: AtomicUsize = AtomicUsize::new(0);
144 
145#[jsg_resource]
146struct HashMapParent {
147 pub children: HashMap<String, jsg::Rc<Leaf>>,
148}
149 
150impl Drop for HashMapParent {
151 fn drop(&mut self) {
152 HASHMAP_PARENT_DROPS.fetch_add(1, Ordering::SeqCst);
153 }
154}
155 
156#[jsg_resource]
157impl HashMapParent {}
158 
159/// `HashMap<K, jsg::Rc<T>>` values are kept alive while parent JS wrapper is reachable.
160#[test]
161fn hashmap_rc_values_kept_alive_through_gc() {
162 LEAF_DROPS.store(0, Ordering::SeqCst);
163 HASHMAP_PARENT_DROPS.store(0, Ordering::SeqCst);
164 
165 let harness = crate::Harness::new();
166 harness.run_in_context(|lock, ctx| {
167 let a = jsg::Rc::new(Leaf { value: 10 });
168 let b = jsg::Rc::new(Leaf { value: 20 });
169 
170 let mut map = HashMap::new();
171 map.insert("a".to_owned(), a.clone());
172 map.insert("b".to_owned(), b.clone());
173 
174 let parent = jsg::Rc::new(HashMapParent { children: map });
175 let wrapped = parent.clone().to_js(lock);
176 ctx.set_global("parent", wrapped);
177 
178 std::mem::drop(a);
179 std::mem::drop(b);
180 std::mem::drop(parent);
181 
182 crate::Harness::request_gc(lock);
183 assert_eq!(HASHMAP_PARENT_DROPS.load(Ordering::SeqCst), 0);
184 assert_eq!(LEAF_DROPS.load(Ordering::SeqCst), 0);
185 Ok(())
186 });
187 
188 harness.run_in_context(|lock, _ctx| {
189 crate::Harness::request_gc(lock);
190 assert_eq!(HASHMAP_PARENT_DROPS.load(Ordering::SeqCst), 1);
191 assert_eq!(LEAF_DROPS.load(Ordering::SeqCst), 2);
192 Ok(())
193 });
194}
195 
196/// An empty `HashMap<K, jsg::Rc<T>>` does not crash during GC.
197#[test]
198fn hashmap_rc_empty_does_not_crash_during_gc() {
199 HASHMAP_PARENT_DROPS.store(0, Ordering::SeqCst);
200 
201 let harness = crate::Harness::new();
202 harness.run_in_context(|lock, ctx| {
203 let parent = jsg::Rc::new(HashMapParent {
204 children: HashMap::new(),
205 });
206 let wrapped = parent.clone().to_js(lock);
207 ctx.set_global("parent", wrapped);
208 std::mem::drop(parent);
209 
210 crate::Harness::request_gc(lock);
211 assert_eq!(HASHMAP_PARENT_DROPS.load(Ordering::SeqCst), 0);
212 Ok(())
213 });
214 
215 harness.run_in_context(|lock, _ctx| {
216 crate::Harness::request_gc(lock);
217 assert_eq!(HASHMAP_PARENT_DROPS.load(Ordering::SeqCst), 1);
218 Ok(())
219 });
220}
221 
222// =============================================================================
223// BTreeMap<K, jsg::Rc<T>>
224// =============================================================================
225 
226static BTREEMAP_PARENT_DROPS: AtomicUsize = AtomicUsize::new(0);
227 
228#[jsg_resource]
229struct BTreeMapParent {
230 pub children: BTreeMap<String, jsg::Rc<Leaf>>,
231}
232 
233impl Drop for BTreeMapParent {
234 fn drop(&mut self) {
235 BTREEMAP_PARENT_DROPS.fetch_add(1, Ordering::SeqCst);
236 }
237}
238 
239#[jsg_resource]
240impl BTreeMapParent {}
241 
242/// `BTreeMap<K, jsg::Rc<T>>` values are kept alive while parent JS wrapper is reachable.
243#[test]
244fn btreemap_rc_values_kept_alive_through_gc() {
245 LEAF_DROPS.store(0, Ordering::SeqCst);
246 BTREEMAP_PARENT_DROPS.store(0, Ordering::SeqCst);
247 
248 let harness = crate::Harness::new();
249 harness.run_in_context(|lock, ctx| {
250 let x = jsg::Rc::new(Leaf { value: 100 });
251 let y = jsg::Rc::new(Leaf { value: 200 });
252 
253 let mut map = BTreeMap::new();
254 map.insert("x".to_owned(), x.clone());
255 map.insert("y".to_owned(), y.clone());
256 
257 let parent = jsg::Rc::new(BTreeMapParent { children: map });
258 let wrapped = parent.clone().to_js(lock);
259 ctx.set_global("parent", wrapped);
260 
261 std::mem::drop(x);
262 std::mem::drop(y);
263 std::mem::drop(parent);
264 
265 crate::Harness::request_gc(lock);
266 assert_eq!(BTREEMAP_PARENT_DROPS.load(Ordering::SeqCst), 0);
267 assert_eq!(LEAF_DROPS.load(Ordering::SeqCst), 0);
268 Ok(())
269 });
270 
271 harness.run_in_context(|lock, _ctx| {
272 crate::Harness::request_gc(lock);
273 assert_eq!(BTREEMAP_PARENT_DROPS.load(Ordering::SeqCst), 1);
274 assert_eq!(LEAF_DROPS.load(Ordering::SeqCst), 2);
275 Ok(())
276 });
277}
278 
279/// An empty `BTreeMap<K, jsg::Rc<T>>` does not crash during GC.
280#[test]
281fn btreemap_rc_empty_does_not_crash_during_gc() {
282 BTREEMAP_PARENT_DROPS.store(0, Ordering::SeqCst);
283 
284 let harness = crate::Harness::new();
285 harness.run_in_context(|lock, ctx| {
286 let parent = jsg::Rc::new(BTreeMapParent {
287 children: BTreeMap::new(),
288 });
289 let wrapped = parent.clone().to_js(lock);
290 ctx.set_global("parent", wrapped);
291 std::mem::drop(parent);
292 
293 crate::Harness::request_gc(lock);
294 assert_eq!(BTREEMAP_PARENT_DROPS.load(Ordering::SeqCst), 0);
295 Ok(())
296 });
297 
298 harness.run_in_context(|lock, _ctx| {
299 crate::Harness::request_gc(lock);
300 assert_eq!(BTREEMAP_PARENT_DROPS.load(Ordering::SeqCst), 1);
301 Ok(())
302 });
303}
304 
305// =============================================================================
306// HashSet<jsg::Rc<T>>
307// =============================================================================
308 
309static HASHSET_PARENT_DROPS: AtomicUsize = AtomicUsize::new(0);
310 
311// jsg::Rc<T> implements Hash + Eq by pointer identity (matching std::rc::Rc).
312#[jsg_resource]
313struct HashSetParent {
314 pub children: HashSet<jsg::Rc<Leaf>>,
315}
316 
317impl Drop for HashSetParent {
318 fn drop(&mut self) {
319 HASHSET_PARENT_DROPS.fetch_add(1, Ordering::SeqCst);
320 }
321}
322 
323#[jsg_resource]
324impl HashSetParent {}
325 
326/// `HashSet<jsg::Rc<T>>` elements are kept alive while parent JS wrapper is reachable.
327// jsg::Rc uses pointer-identity Hash+Eq, so interior mutability (Cell fields) doesn't
328// affect correctness. Suppress the mutable_key_type lint that fires on HashSet::new().
329#[expect(
330 clippy::mutable_key_type,
331 reason = "jsg::Rc hashes by pointer address, not interior state"
332)]
333#[test]
334fn hashset_rc_elements_kept_alive_through_gc() {
335 LEAF_DROPS.store(0, Ordering::SeqCst);
336 HASHSET_PARENT_DROPS.store(0, Ordering::SeqCst);
337 
338 let harness = crate::Harness::new();
339 harness.run_in_context(|lock, ctx| {
340 let p = jsg::Rc::new(Leaf { value: 1 });
341 let q = jsg::Rc::new(Leaf { value: 2 });
342 
343 let mut set = HashSet::new();
344 set.insert(p.clone());
345 set.insert(q.clone());
346 
347 let parent = jsg::Rc::new(HashSetParent { children: set });
348 let wrapped = parent.clone().to_js(lock);
349 ctx.set_global("parent", wrapped);
350 
351 std::mem::drop(p);
352 std::mem::drop(q);
353 std::mem::drop(parent);
354 
355 crate::Harness::request_gc(lock);
356 assert_eq!(HASHSET_PARENT_DROPS.load(Ordering::SeqCst), 0);
357 assert_eq!(LEAF_DROPS.load(Ordering::SeqCst), 0);
358 Ok(())
359 });
360 
361 harness.run_in_context(|lock, _ctx| {
362 crate::Harness::request_gc(lock);
363 assert_eq!(HASHSET_PARENT_DROPS.load(Ordering::SeqCst), 1);
364 assert_eq!(LEAF_DROPS.load(Ordering::SeqCst), 2);
365 Ok(())
366 });
367}
368 
369/// An empty `HashSet<jsg::Rc<T>>` does not crash during GC.
370#[test]
371fn hashset_rc_empty_does_not_crash_during_gc() {
372 HASHSET_PARENT_DROPS.store(0, Ordering::SeqCst);
373 
374 let harness = crate::Harness::new();
375 harness.run_in_context(|lock, ctx| {
376 let parent = jsg::Rc::new(HashSetParent {
377 children: HashSet::new(),
378 });
379 let wrapped = parent.clone().to_js(lock);
380 ctx.set_global("parent", wrapped);
381 std::mem::drop(parent);
382 
383 crate::Harness::request_gc(lock);
384 assert_eq!(HASHSET_PARENT_DROPS.load(Ordering::SeqCst), 0);
385 Ok(())
386 });
387 
388 harness.run_in_context(|lock, _ctx| {
389 crate::Harness::request_gc(lock);
390 assert_eq!(HASHSET_PARENT_DROPS.load(Ordering::SeqCst), 1);
391 Ok(())
392 });
393}
394 
395// =============================================================================
396// BTreeSet<jsg::Rc<T>>
397// =============================================================================
398 
399static BTREESET_PARENT_DROPS: AtomicUsize = AtomicUsize::new(0);
400 
401#[jsg_resource]
402struct BTreeSetParent {
403 pub children: BTreeSet<jsg::Rc<Leaf>>,
404}
405 
406impl Drop for BTreeSetParent {
407 fn drop(&mut self) {
408 BTREESET_PARENT_DROPS.fetch_add(1, Ordering::SeqCst);
409 }
410}
411 
412#[jsg_resource]
413impl BTreeSetParent {}
414 
415/// `BTreeSet<jsg::Rc<T>>` elements are kept alive while parent JS wrapper is reachable.
416// Same pointer-identity rationale as the HashSet test above.
417#[expect(
418 clippy::mutable_key_type,
419 reason = "jsg::Rc orders by pointer address, not interior state"
420)]
421#[test]
422fn btreeset_rc_elements_kept_alive_through_gc() {
423 LEAF_DROPS.store(0, Ordering::SeqCst);
424 BTREESET_PARENT_DROPS.store(0, Ordering::SeqCst);
425 
426 let harness = crate::Harness::new();
427 harness.run_in_context(|lock, ctx| {
428 let r = jsg::Rc::new(Leaf { value: 7 });
429 let s = jsg::Rc::new(Leaf { value: 8 });
430 
431 let mut set = BTreeSet::new();
432 set.insert(r.clone());
433 set.insert(s.clone());
434 
435 let parent = jsg::Rc::new(BTreeSetParent { children: set });
436 let wrapped = parent.clone().to_js(lock);
437 ctx.set_global("parent", wrapped);
438 
439 std::mem::drop(r);
440 std::mem::drop(s);
441 std::mem::drop(parent);
442 
443 crate::Harness::request_gc(lock);
444 assert_eq!(BTREESET_PARENT_DROPS.load(Ordering::SeqCst), 0);
445 assert_eq!(LEAF_DROPS.load(Ordering::SeqCst), 0);
446 Ok(())
447 });
448 
449 harness.run_in_context(|lock, _ctx| {
450 crate::Harness::request_gc(lock);
451 assert_eq!(BTREESET_PARENT_DROPS.load(Ordering::SeqCst), 1);
452 assert_eq!(LEAF_DROPS.load(Ordering::SeqCst), 2);
453 Ok(())
454 });
455}
456 
457/// An empty `BTreeSet<jsg::Rc<T>>` does not crash during GC.
458#[test]
459fn btreeset_rc_empty_does_not_crash_during_gc() {
460 BTREESET_PARENT_DROPS.store(0, Ordering::SeqCst);
461 
462 let harness = crate::Harness::new();
463 harness.run_in_context(|lock, ctx| {
464 let parent = jsg::Rc::new(BTreeSetParent {
465 children: BTreeSet::new(),
466 });
467 let wrapped = parent.clone().to_js(lock);
468 ctx.set_global("parent", wrapped);
469 std::mem::drop(parent);
470 
471 crate::Harness::request_gc(lock);
472 assert_eq!(BTREESET_PARENT_DROPS.load(Ordering::SeqCst), 0);
473 Ok(())
474 });
475 
476 harness.run_in_context(|lock, _ctx| {
477 crate::Harness::request_gc(lock);
478 assert_eq!(BTREESET_PARENT_DROPS.load(Ordering::SeqCst), 1);
479 Ok(())
480 });
481}
482 
483// =============================================================================
484// Cell<Vec<jsg::Rc<T>>>
485// =============================================================================
486 
487static CELL_VEC_PARENT_DROPS: AtomicUsize = AtomicUsize::new(0);
488 
489#[jsg_resource]
490struct CellVecParent {
491 pub children: Cell<Vec<jsg::Rc<Leaf>>>,
492}
493 
494impl Drop for CellVecParent {
495 fn drop(&mut self) {
496 CELL_VEC_PARENT_DROPS.fetch_add(1, Ordering::SeqCst);
497 }
498}
499 
500#[jsg_resource]
501impl CellVecParent {}
502 
503/// `Cell<Vec<jsg::Rc<T>>>` children are kept alive while parent JS wrapper is reachable.
504#[test]
505fn cell_vec_rc_children_kept_alive_through_gc() {
506 LEAF_DROPS.store(0, Ordering::SeqCst);
507 CELL_VEC_PARENT_DROPS.store(0, Ordering::SeqCst);
508 
509 let harness = crate::Harness::new();
510 harness.run_in_context(|lock, ctx| {
511 let c1 = jsg::Rc::new(Leaf { value: 11 });
512 let c2 = jsg::Rc::new(Leaf { value: 22 });
513 
514 let parent = jsg::Rc::new(CellVecParent {
515 children: Cell::new(vec![c1.clone(), c2.clone()]),
516 });
517 let wrapped = parent.clone().to_js(lock);
518 ctx.set_global("parent", wrapped);
519 
520 std::mem::drop(c1);
521 std::mem::drop(c2);
522 std::mem::drop(parent);
523 
524 crate::Harness::request_gc(lock);
525 assert_eq!(CELL_VEC_PARENT_DROPS.load(Ordering::SeqCst), 0);
526 assert_eq!(LEAF_DROPS.load(Ordering::SeqCst), 0);
527 Ok(())
528 });
529 
530 harness.run_in_context(|lock, _ctx| {
531 crate::Harness::request_gc(lock);
532 assert_eq!(CELL_VEC_PARENT_DROPS.load(Ordering::SeqCst), 1);
533 assert_eq!(LEAF_DROPS.load(Ordering::SeqCst), 2);
534 Ok(())
535 });
536}
537 
538/// An empty `Cell<Vec<jsg::Rc<T>>>` does not crash during GC.
539#[test]
540fn cell_vec_rc_empty_does_not_crash_during_gc() {
541 CELL_VEC_PARENT_DROPS.store(0, Ordering::SeqCst);
542 
543 let harness = crate::Harness::new();
544 harness.run_in_context(|lock, ctx| {
545 let parent = jsg::Rc::new(CellVecParent {
546 children: Cell::new(vec![]),
547 });
548 let wrapped = parent.clone().to_js(lock);
549 ctx.set_global("parent", wrapped);
550 std::mem::drop(parent);
551 
552 crate::Harness::request_gc(lock);
553 assert_eq!(CELL_VEC_PARENT_DROPS.load(Ordering::SeqCst), 0);
554 Ok(())
555 });
556 
557 harness.run_in_context(|lock, _ctx| {
558 crate::Harness::request_gc(lock);
559 assert_eq!(CELL_VEC_PARENT_DROPS.load(Ordering::SeqCst), 1);
560 Ok(())
561 });
562}
563 
564// =============================================================================
565// Cell<HashMap<K, jsg::Rc<T>>>
566// =============================================================================
567 
568static CELL_MAP_PARENT_DROPS: AtomicUsize = AtomicUsize::new(0);
569 
570#[jsg_resource]
571struct CellHashMapParent {
572 pub children: Cell<HashMap<String, jsg::Rc<Leaf>>>,
573}
574 
575impl Drop for CellHashMapParent {
576 fn drop(&mut self) {
577 CELL_MAP_PARENT_DROPS.fetch_add(1, Ordering::SeqCst);
578 }
579}
580 
581#[jsg_resource]
582impl CellHashMapParent {}
583 
584/// `Cell<HashMap<K, jsg::Rc<T>>>` values are kept alive while parent JS wrapper is reachable.
585#[test]
586fn cell_hashmap_rc_values_kept_alive_through_gc() {
587 LEAF_DROPS.store(0, Ordering::SeqCst);
588 CELL_MAP_PARENT_DROPS.store(0, Ordering::SeqCst);
589 
590 let harness = crate::Harness::new();
591 harness.run_in_context(|lock, ctx| {
592 let m = jsg::Rc::new(Leaf { value: 99 });
593 
594 let mut map = HashMap::new();
595 map.insert("m".to_owned(), m.clone());
596 
597 let parent = jsg::Rc::new(CellHashMapParent {
598 children: Cell::new(map),
599 });
600 let wrapped = parent.clone().to_js(lock);
601 ctx.set_global("parent", wrapped);
602 
603 std::mem::drop(m);
604 std::mem::drop(parent);
605 
606 crate::Harness::request_gc(lock);
607 assert_eq!(CELL_MAP_PARENT_DROPS.load(Ordering::SeqCst), 0);
608 assert_eq!(LEAF_DROPS.load(Ordering::SeqCst), 0);
609 Ok(())
610 });
611 
612 harness.run_in_context(|lock, _ctx| {
613 crate::Harness::request_gc(lock);
614 assert_eq!(CELL_MAP_PARENT_DROPS.load(Ordering::SeqCst), 1);
615 assert_eq!(LEAF_DROPS.load(Ordering::SeqCst), 1);
616 Ok(())
617 });
618}
619 
620// =============================================================================
621// Mixed field resource — Vec + HashMap + bare Rc all in one struct
622// =============================================================================
623 
624static MIXED_PARENT_DROPS: AtomicUsize = AtomicUsize::new(0);
625 
626#[jsg_resource]
627struct MixedParent {
628 pub singles: Vec<jsg::Rc<Leaf>>,
629 pub named: HashMap<String, jsg::Rc<Leaf>>,
630 pub direct: jsg::Rc<Leaf>,
631}
632 
633impl Drop for MixedParent {
634 fn drop(&mut self) {
635 MIXED_PARENT_DROPS.fetch_add(1, Ordering::SeqCst);
636 }
637}
638 
639#[jsg_resource]
640impl MixedParent {}
641 
642/// A resource with `Vec`, `HashMap`, and bare `Rc` fields — all children traced.
643#[test]
644fn mixed_collection_fields_all_traced() {
645 LEAF_DROPS.store(0, Ordering::SeqCst);
646 MIXED_PARENT_DROPS.store(0, Ordering::SeqCst);
647 
648 let harness = crate::Harness::new();
649 harness.run_in_context(|lock, ctx| {
650 let vec_child = jsg::Rc::new(Leaf { value: 1 });
651 let map_child = jsg::Rc::new(Leaf { value: 2 });
652 let direct_child = jsg::Rc::new(Leaf { value: 3 });
653 
654 let mut named = HashMap::new();
655 named.insert("key".to_owned(), map_child.clone());
656 
657 let parent = jsg::Rc::new(MixedParent {
658 singles: vec![vec_child.clone()],
659 named,
660 direct: direct_child.clone(),
661 });
662 let wrapped = parent.clone().to_js(lock);
663 ctx.set_global("parent", wrapped);
664 
665 std::mem::drop(vec_child);
666 std::mem::drop(map_child);
667 std::mem::drop(direct_child);
668 std::mem::drop(parent);
669 
670 // All 3 children must survive (1 from vec, 1 from map, 1 direct).
671 crate::Harness::request_gc(lock);
672 assert_eq!(MIXED_PARENT_DROPS.load(Ordering::SeqCst), 0);
673 assert_eq!(LEAF_DROPS.load(Ordering::SeqCst), 0);
674 Ok(())
675 });
676 
677 harness.run_in_context(|lock, _ctx| {
678 crate::Harness::request_gc(lock);
679 assert_eq!(MIXED_PARENT_DROPS.load(Ordering::SeqCst), 1);
680 assert_eq!(LEAF_DROPS.load(Ordering::SeqCst), 3);
681 Ok(())
682 });
683}
684 
685// =============================================================================
686// Vec<jsg::Rc<T>> — child kept alive by vec even after outer Rust ref is dropped
687// =============================================================================
688 
689/// Verifies that a child held only by a `Vec` inside a JS-wrapped parent is NOT
690/// collected while the parent is reachable, even after its own Rust `Rc` is gone.
691#[test]
692fn vec_rc_child_kept_alive_by_parent_after_rust_ref_dropped() {
693 LEAF_DROPS.store(0, Ordering::SeqCst);
694 VEC_PARENT_DROPS.store(0, Ordering::SeqCst);
695 
696 let harness = crate::Harness::new();
697 harness.run_in_context(|lock, _ctx| {
698 let child = jsg::Rc::new(Leaf { value: 42 });
699 
700 let parent = jsg::Rc::new(VecParent {
701 children: vec![child.clone()],
702 });
703 
704 // Wrap parent so it gets a JS object.
705 let _ = parent.clone().to_js(lock);
706 
707 // Drop the child Rust ref — vec inside parent is the only holder.
708 std::mem::drop(child);
709 
710 // Force a GC: parent wrapper is unreachable (no global), but the local
711 // handle keeps it alive in this scope.
712 crate::Harness::request_gc(lock);
713 // Child must NOT have been collected yet — the parent's vec still holds it.
714 assert_eq!(LEAF_DROPS.load(Ordering::SeqCst), 0);
715 
716 std::mem::drop(parent);
717 Ok(())
718 });
719 
720 harness.run_in_context(|lock, _ctx| {
721 crate::Harness::request_gc(lock);
722 assert_eq!(VEC_PARENT_DROPS.load(Ordering::SeqCst), 1);
723 assert_eq!(LEAF_DROPS.load(Ordering::SeqCst), 1);
724 Ok(())
725 });
726}
727 
728// =============================================================================
729// HashMap with integer key — u32 key, jsg::Rc<T> value
730// =============================================================================
731 
732static INT_KEY_MAP_PARENT_DROPS: AtomicUsize = AtomicUsize::new(0);
733 
734#[jsg_resource]
735struct IntKeyMapParent {
736 pub children: HashMap<u32, jsg::Rc<Leaf>>,
737}
738 
739impl Drop for IntKeyMapParent {
740 fn drop(&mut self) {
741 INT_KEY_MAP_PARENT_DROPS.fetch_add(1, Ordering::SeqCst);
742 }
743}
744 
745#[jsg_resource]
746impl IntKeyMapParent {}
747 
748/// `HashMap<u32, jsg::Rc<T>>` — integer key does not affect tracing.
749#[test]
750fn hashmap_integer_key_rc_values_traced() {
751 LEAF_DROPS.store(0, Ordering::SeqCst);
752 INT_KEY_MAP_PARENT_DROPS.store(0, Ordering::SeqCst);
753 
754 let harness = crate::Harness::new();
755 harness.run_in_context(|lock, ctx| {
756 let v = jsg::Rc::new(Leaf { value: 55 });
757 
758 let mut map = HashMap::new();
759 map.insert(0u32, v.clone());
760 map.insert(1u32, v.clone()); // same child twice
761 
762 let parent = jsg::Rc::new(IntKeyMapParent { children: map });
763 let wrapped = parent.clone().to_js(lock);
764 ctx.set_global("parent", wrapped);
765 
766 std::mem::drop(v);
767 std::mem::drop(parent);
768 
769 crate::Harness::request_gc(lock);
770 assert_eq!(INT_KEY_MAP_PARENT_DROPS.load(Ordering::SeqCst), 0);
771 assert_eq!(LEAF_DROPS.load(Ordering::SeqCst), 0);
772 Ok(())
773 });
774 
775 harness.run_in_context(|lock, _ctx| {
776 crate::Harness::request_gc(lock);
777 assert_eq!(INT_KEY_MAP_PARENT_DROPS.load(Ordering::SeqCst), 1);
778 // One unique Leaf despite two map entries (both were clones of the same Rc).
779 assert_eq!(LEAF_DROPS.load(Ordering::SeqCst), 1);
780 Ok(())
781 });
782}
783 
784// =============================================================================
785// Nested struct tracing via `Traced`
786//
787// Mirrors the C++ pattern:
788// struct PrivateData { void visitForGc(jsg::GcVisitor&) { ... } };
789// class Foo : public jsg::Object { PrivateData privateData_; ... };
790// =============================================================================
791 
792static TRACE_DELEGATION_PARENT_DROPS: AtomicUsize = AtomicUsize::new(0);
793 
794/// A plain Rust struct (not a jsg resource) that holds traceable children.
795/// It manually implements `Traced`.
796struct PrivateData {
797 child: jsg::Rc<Leaf>,
798}
799 
800impl jsg::Traced for PrivateData {
801 fn trace(&self, visitor: &mut jsg::GcVisitor) {
802 visitor.visit_rc(&self.child);
803 }
804}
805 
806/// Resource with a plain nested field — auto-generated tracing delegates to
807/// `Traced::trace` for each field, including this one.
808#[jsg_resource]
809struct TraceDelegationParent {
810 pub data: PrivateData,
811}
812 
813impl Drop for TraceDelegationParent {
814 fn drop(&mut self) {
815 TRACE_DELEGATION_PARENT_DROPS.fetch_add(1, Ordering::SeqCst);
816 }
817}
818 
819#[jsg_resource]
820impl TraceDelegationParent {}
821 
822/// Children held inside a nested `Traced` struct are kept alive while the
823/// parent JS wrapper is reachable.
824#[test]
825fn trace_delegation_child_kept_alive_through_gc() {
826 LEAF_DROPS.store(0, Ordering::SeqCst);
827 TRACE_DELEGATION_PARENT_DROPS.store(0, Ordering::SeqCst);
828 
829 let harness = crate::Harness::new();
830 harness.run_in_context(|lock, ctx| {
831 let child = jsg::Rc::new(Leaf { value: 77 });
832 
833 let parent = jsg::Rc::new(TraceDelegationParent {
834 data: PrivateData {
835 child: child.clone(),
836 },
837 });
838 let wrapped = parent.clone().to_js(lock);
839 ctx.set_global("parent", wrapped);
840 
841 std::mem::drop(child);
842 std::mem::drop(parent);
843 
844 // GC while global reachable — child inside PrivateData must survive.
845 crate::Harness::request_gc(lock);
846 assert_eq!(TRACE_DELEGATION_PARENT_DROPS.load(Ordering::SeqCst), 0);
847 assert_eq!(LEAF_DROPS.load(Ordering::SeqCst), 0);
848 Ok(())
849 });
850 
851 harness.run_in_context(|lock, _ctx| {
852 crate::Harness::request_gc(lock);
853 assert_eq!(TRACE_DELEGATION_PARENT_DROPS.load(Ordering::SeqCst), 1);
854 assert_eq!(LEAF_DROPS.load(Ordering::SeqCst), 1);
855 Ok(())
856 });
857}
858 
859// =============================================================================
860// custom_trace — user-written Traced impl
861// =============================================================================
862 
863static CUSTOM_TRACE_PARENT_DROPS: AtomicUsize = AtomicUsize::new(0);
864 
865/// Resource that opts out of macro-generated tracing via `custom_trace`.
866/// The user provides their own `Traced` impl with custom logic.
867#[jsg_resource(custom_trace)]
868struct CustomTraceParent {
869 pub child: jsg::Rc<Leaf>,
870}
871 
872impl Drop for CustomTraceParent {
873 fn drop(&mut self) {
874 CUSTOM_TRACE_PARENT_DROPS.fetch_add(1, Ordering::SeqCst);
875 }
876}
877 
878// User-supplied Traced impl — not generated by the macro because of custom_trace.
879impl jsg::Traced for CustomTraceParent {
880 fn trace(&self, visitor: &mut jsg::GcVisitor) {
881 visitor.visit_rc(&self.child);
882 }
883}
884 
885#[jsg_resource]
886impl CustomTraceParent {}
887 
888/// With `custom_trace`, the user-supplied `Traced` impl is used and
889/// children are traced correctly.
890#[test]
891fn custom_trace_child_kept_alive_through_gc() {
892 LEAF_DROPS.store(0, Ordering::SeqCst);
893 CUSTOM_TRACE_PARENT_DROPS.store(0, Ordering::SeqCst);
894 
895 let harness = crate::Harness::new();
896 harness.run_in_context(|lock, ctx| {
897 let child = jsg::Rc::new(Leaf { value: 88 });
898 
899 let parent = jsg::Rc::new(CustomTraceParent {
900 child: child.clone(),
901 });
902 let wrapped = parent.clone().to_js(lock);
903 ctx.set_global("parent", wrapped);
904 
905 std::mem::drop(child);
906 std::mem::drop(parent);
907 
908 crate::Harness::request_gc(lock);
909 assert_eq!(CUSTOM_TRACE_PARENT_DROPS.load(Ordering::SeqCst), 0);
910 assert_eq!(LEAF_DROPS.load(Ordering::SeqCst), 0);
911 Ok(())
912 });
913 
914 harness.run_in_context(|lock, _ctx| {
915 crate::Harness::request_gc(lock);
916 assert_eq!(CUSTOM_TRACE_PARENT_DROPS.load(Ordering::SeqCst), 1);
917 assert_eq!(LEAF_DROPS.load(Ordering::SeqCst), 1);
918 Ok(())
919 });
920}