pub struct TaskScope { /* private fields */ }Expand description
A node in the scope tree that owns spawned tasks and carries a typed context for dependency injection.
§Drop guarantee
When a TaskScope is dropped, all descendant scopes and their
tasks are cancelled iteratively using a work queue — recursion
is never used, so deeply nested UI trees (200+ levels) never
overflow the stack.
§Cancellation is cooperative
Cancellation drops the task’s Future and removes it from the
executor. Like all async Rust, this only takes effect at the next
.await point — a task stuck in a synchronous compute loop cannot
be interrupted mid-execution. This is the same trade-off made by
tokio::task::JoinHandle::abort. For long synchronous work,
insert yield_now at checkpoints.
§Context
Use provide / consume
for lightweight dependency injection that walks up the scope tree.
§Callback lifecycle
CallbackHandles registered via
register_callback_handle are
dropped before spawned tasks are cancelled, ensuring that
signal subscriptions are removed before any task cleanup.
Implementations§
Source§impl TaskScope
impl TaskScope
Sourcepub fn new() -> Self
pub fn new() -> Self
Create a new root scope on the global thread-local executor.
For explicit executor ownership use TaskScope::with_executor.
Examples found in repository?
37fn main() {
38 // One-time executor setup.
39 init_flush_scheduler(Rc::new(CliScheduler));
40 init_time_source(Rc::new(CliClock::new()));
41
42 // ---- Signal basics ----
43 println!("=== Signal basics ===");
44 let count = Signal::new(0);
45 println!("count = {}", count.read());
46 count.set(42);
47 println!("count = {}", count.read());
48
49 // ---- Memo (auto-tracking computed value) ----
50 println!("\n=== Memo ===");
51 let a = Signal::new(2);
52 let b = Signal::new(3);
53 let a2 = a.clone();
54 let b2 = b.clone();
55 let sum = Memo::new(move || a2.read() + b2.read());
56 println!("2 + 3 = {}", sum.read());
57 a.set(10);
58 println!("10 + 3 = {}", sum.read());
59
60 // ---- TaskScope: spawn a task that watches a signal ----
61 println!("\n=== TaskScope (scoped signal watcher) ===");
62 let messages = Signal::new(vec!["hello".to_string()]);
63
64 {
65 let scope = TaskScope::new();
66 let msgs = messages.clone();
67 scope.spawn(async move {
68 loop {
69 let val = msgs.changed().await;
70 println!(" task observed: {:?}", val);
71 }
72 });
73
74 messages.set(vec!["hello".to_string(), "world".to_string()]);
75 messages.set(vec![
76 "hello".to_string(),
77 "world".to_string(),
78 "!".to_string(),
79 ]);
80
81 // Explicit drop — cancels the task and cleans up resources.
82 drop(scope);
83 }
84 println!("(scope dropped — task and subscriptions cleaned up)");
85
86 // ---- Batch: multiple sets, one notification ----
87 println!("\n=== Batch ===");
88 let x = Signal::new(0);
89 let notifications = Rc::new(Cell::new(0u32));
90 let n = Rc::clone(¬ifications);
91 let x2 = x.clone();
92
93 let scope = TaskScope::new();
94 scope.spawn(async move {
95 loop {
96 x2.changed().await;
97 n.set(n.get() + 1);
98 }
99 });
100
101 batch(|| {
102 x.set(1);
103 x.set(2);
104 x.set(3);
105 });
106 println!(
107 "After 3 sets in a batch: {} notification(s)",
108 notifications.get()
109 );
110 // Should be 1 — not 3.
111
112 println!("\nDone.");
113}More examples
68fn main() {
69 println!("=== Auralis Task Scope Benchmarks ===\n");
70
71 // 1. Scope create + destroy (100 tasks)
72 let iterations = 50_000;
73 init();
74 time("scope_100_tasks_create_destroy", iterations, || {
75 let scope = TaskScope::new();
76 for _ in 0..100 {
77 scope.spawn(async {});
78 }
79 drop(scope);
80 });
81
82 // 2. Deep nesting drop (200 levels)
83 init();
84 time("scope_200_levels_deep_drop", 5_000, || {
85 let root = TaskScope::new();
86 {
87 let mut current = TaskScope::new_child(&root);
88 for _ in 0..199 {
89 current.spawn(async {});
90 current = TaskScope::new_child(¤t);
91 }
92 }
93 drop(root);
94 });
95
96 // 3. Priority ordering (batched flush)
97 {
98 let order: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
99 let min_iterations = 5_000;
100 // Warm-up
101 for _ in 0..500 {
102 let sched = BatchedFlush::new();
103 init_flush_scheduler(Rc::clone(&sched) as Rc<dyn ScheduleFlush>);
104 order.borrow_mut().clear();
105 for i in 0..1000 {
106 let o = Rc::clone(&order);
107 auralis_task::spawn_global_with_priority(Priority::Low, async move {
108 o.borrow_mut().push(format!("low_{i}"));
109 });
110 }
111 for i in 0..10 {
112 let o = Rc::clone(&order);
113 auralis_task::spawn_global_with_priority(Priority::High, async move {
114 o.borrow_mut().push(format!("high_{i}"));
115 });
116 }
117 sched.drain();
118 }
119
120 let start = Instant::now();
121 for _ in 0..min_iterations {
122 let sched = BatchedFlush::new();
123 init_flush_scheduler(Rc::clone(&sched) as Rc<dyn ScheduleFlush>);
124 order.borrow_mut().clear();
125 for i in 0..1000 {
126 let o = Rc::clone(&order);
127 auralis_task::spawn_global_with_priority(Priority::Low, async move {
128 o.borrow_mut().push(format!("low_{i}"));
129 });
130 }
131 for i in 0..10 {
132 let o = Rc::clone(&order);
133 auralis_task::spawn_global_with_priority(Priority::High, async move {
134 o.borrow_mut().push(format!("high_{i}"));
135 });
136 }
137 sched.drain();
138 // Verify correctness.
139 let result = order.borrow().clone();
140 debug_assert_eq!(result.len(), 1010);
141 }
142 let elapsed = start.elapsed().as_secs_f64() * 1_000_000.0 / min_iterations as f64;
143 println!(
144 " {:<42} {:>8.2} µs/iter",
145 "priority_1000_low_10_high", elapsed
146 );
147 init(); // restore sync scheduler
148 }
149
150 // 4. Scope churn (100 scopes x 10 tasks, batch drop)
151 init();
152 time("scope_churn_100x10_batch_drop", 5_000, || {
153 let scopes: Vec<TaskScope> = (0..100)
154 .map(|_| {
155 let s = TaskScope::new();
156 for _ in 0..10 {
157 s.spawn(async {});
158 }
159 s
160 })
161 .collect();
162 drop(scopes);
163 });
164
165 // 5. Scope suspend + resume (1000 tasks)
166 init();
167 let scope = TaskScope::new();
168 for _ in 0..1000 {
169 scope.spawn(async {});
170 }
171 time("scope_suspend_resume_1000_tasks", 500_000, || {
172 scope.suspend();
173 scope.resume();
174 });
175
176 // 6. Wide shallow tree drop (50x50, ~2550 tasks)
177 init();
178 time("scope_wide_tree_50x3_drop", 500, || {
179 let root = TaskScope::new();
180 for _ in 0..50 {
181 let child = TaskScope::new_child(&root);
182 child.spawn(async {});
183 for _ in 0..50 {
184 let grandchild = TaskScope::new_child(&child);
185 grandchild.spawn(async {});
186 }
187 }
188 drop(root);
189 });
190
191 println!("\n=== Done ===");
192}Sourcepub fn with_executor(ex: &Rc<RefCell<Executor>>) -> Self
pub fn with_executor(ex: &Rc<RefCell<Executor>>) -> Self
Create a new root scope on the given executor.
All tasks spawned in this scope (and its descendants) run on
ex. The scope holds a strong reference, keeping the executor
alive at least as long as the scope.
Examples found in repository?
28fn handle_request(request_id: u32) -> String {
29 let ex = Executor::new_instance();
30 Executor::install_flush_scheduler(&ex, Rc::new(SyncScheduler));
31
32 let scope = TaskScope::with_executor(&ex);
33 let data = Signal::new(String::new());
34
35 // "route handler" spawns a reactive effect
36 let d = data.clone();
37 scope.spawn(async move {
38 loop {
39 d.changed().await;
40 let val = d.read();
41 if val == "done" {
42 break;
43 }
44 }
45 });
46
47 // "middleware" sets up cleanup
48 let cleanup_called = Rc::new(Cell::new(false));
49 let cc = Rc::clone(&cleanup_called);
50 scope.on_cleanup(move || cc.set(true));
51
52 // "business logic" — would be I/O in real app
53 let result = auralis_task::with_executor(&ex, || {
54 data.set(format!("request {request_id}: processing"));
55 data.set(format!("request {request_id}: done"));
56 data.read()
57 });
58
59 // Drop scope → cancels effect, runs cleanup.
60 drop(scope);
61
62 assert!(cleanup_called.get());
63 result
64}
65
66fn main() {
67 // ---- Pattern 1: sequential requests (same thread, isolated) ----------
68 println!("=== 1. Sequential request isolation ===");
69 let r1 = handle_request(1);
70 let r2 = handle_request(2);
71 println!(" {r1}");
72 println!(" {r2}");
73 assert_eq!(r1, "request 1: done");
74 assert_eq!(r2, "request 2: done");
75
76 // ---- Pattern 2: same-signal isolation test (different executors) -----
77 println!("\n=== 2. Cross-executor signal isolation ===");
78 {
79 let ex_a = Executor::new_instance();
80 Executor::install_flush_scheduler(&ex_a, Rc::new(SyncScheduler));
81 let ex_b = Executor::new_instance();
82 Executor::install_flush_scheduler(&ex_b, Rc::new(SyncScheduler));
83
84 let sig_a1 = Signal::new(0i32);
85 let sig_b1 = Signal::new(0i32);
86
87 // Scope on executor A sets sig_a and reads sig_b.
88 let scope_a = TaskScope::with_executor(&ex_a);
89 let a_val = Rc::new(RefCell::new(Vec::new()));
90 let av = Rc::clone(&a_val);
91 {
92 let s = sig_a1.clone();
93 scope_a.spawn(async move {
94 s.set(42);
95 av.borrow_mut().push(s.read());
96 });
97 }
98 drop(scope_a);
99
100 // Scope on executor B sets sig_b and reads sig_a.
101 let scope_b = TaskScope::with_executor(&ex_b);
102 let b_val = Rc::new(RefCell::new(Vec::new()));
103 let bv = Rc::clone(&b_val);
104 {
105 let s = sig_b1.clone();
106 scope_b.spawn(async move {
107 s.set(99);
108 bv.borrow_mut().push(s.read());
109 });
110 }
111 drop(scope_b);
112
113 // Each executor only sees its own signal changes.
114 assert_eq!(*a_val.borrow(), vec![42]);
115 assert_eq!(*b_val.borrow(), vec![99]);
116 assert_eq!(sig_a1.read(), 42);
117 assert_eq!(sig_b1.read(), 99);
118 }
119
120 // ---- Pattern 3: the SSR mental model (one request = one executor) ----
121 println!("\n=== 3. SSR mental model ===");
122 for i in 0..3 {
123 let ex = Executor::new_instance();
124 Executor::install_flush_scheduler(&ex, Rc::new(SyncScheduler));
125 let counter = Signal::new(0i32);
126
127 let scope = TaskScope::with_executor(&ex);
128 let c = counter.clone();
129 let results = Rc::new(RefCell::new(Vec::new()));
130 let res = Rc::clone(&results);
131
132 scope.spawn(async move {
133 loop {
134 c.changed().await;
135 let v = c.read();
136 res.borrow_mut().push(v);
137 if v >= 3 {
138 break;
139 }
140 }
141 });
142
143 auralis_task::with_executor(&ex, || {
144 counter.set(1);
145 counter.set(2);
146 counter.set(3);
147 });
148
149 drop(scope);
150 assert_eq!(*results.borrow(), vec![1, 2, 3]);
151 println!(" request {i}: signals {:?}", results.borrow());
152 }
153
154 println!("\nAll patterns completed — zero cross-request leakage.");
155}Sourcepub fn new_child(parent: &Self) -> Self
pub fn new_child(parent: &Self) -> Self
Create a child scope that inherits the parent’s executor.
The child is stored in the parent’s children list. This means
dropping all external clones of the child does not immediately
cancel it — the parent’s strong reference keeps it alive. The
child is fully cancelled only when the parent itself is dropped
(or when TaskScope::drop runs on the last reference).
To explicitly cancel a child while the parent is still alive,
call suspend on the child, or use a
JoinHandle to cancel individual tasks.
Examples found in repository?
68fn main() {
69 println!("=== Auralis Task Scope Benchmarks ===\n");
70
71 // 1. Scope create + destroy (100 tasks)
72 let iterations = 50_000;
73 init();
74 time("scope_100_tasks_create_destroy", iterations, || {
75 let scope = TaskScope::new();
76 for _ in 0..100 {
77 scope.spawn(async {});
78 }
79 drop(scope);
80 });
81
82 // 2. Deep nesting drop (200 levels)
83 init();
84 time("scope_200_levels_deep_drop", 5_000, || {
85 let root = TaskScope::new();
86 {
87 let mut current = TaskScope::new_child(&root);
88 for _ in 0..199 {
89 current.spawn(async {});
90 current = TaskScope::new_child(¤t);
91 }
92 }
93 drop(root);
94 });
95
96 // 3. Priority ordering (batched flush)
97 {
98 let order: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
99 let min_iterations = 5_000;
100 // Warm-up
101 for _ in 0..500 {
102 let sched = BatchedFlush::new();
103 init_flush_scheduler(Rc::clone(&sched) as Rc<dyn ScheduleFlush>);
104 order.borrow_mut().clear();
105 for i in 0..1000 {
106 let o = Rc::clone(&order);
107 auralis_task::spawn_global_with_priority(Priority::Low, async move {
108 o.borrow_mut().push(format!("low_{i}"));
109 });
110 }
111 for i in 0..10 {
112 let o = Rc::clone(&order);
113 auralis_task::spawn_global_with_priority(Priority::High, async move {
114 o.borrow_mut().push(format!("high_{i}"));
115 });
116 }
117 sched.drain();
118 }
119
120 let start = Instant::now();
121 for _ in 0..min_iterations {
122 let sched = BatchedFlush::new();
123 init_flush_scheduler(Rc::clone(&sched) as Rc<dyn ScheduleFlush>);
124 order.borrow_mut().clear();
125 for i in 0..1000 {
126 let o = Rc::clone(&order);
127 auralis_task::spawn_global_with_priority(Priority::Low, async move {
128 o.borrow_mut().push(format!("low_{i}"));
129 });
130 }
131 for i in 0..10 {
132 let o = Rc::clone(&order);
133 auralis_task::spawn_global_with_priority(Priority::High, async move {
134 o.borrow_mut().push(format!("high_{i}"));
135 });
136 }
137 sched.drain();
138 // Verify correctness.
139 let result = order.borrow().clone();
140 debug_assert_eq!(result.len(), 1010);
141 }
142 let elapsed = start.elapsed().as_secs_f64() * 1_000_000.0 / min_iterations as f64;
143 println!(
144 " {:<42} {:>8.2} µs/iter",
145 "priority_1000_low_10_high", elapsed
146 );
147 init(); // restore sync scheduler
148 }
149
150 // 4. Scope churn (100 scopes x 10 tasks, batch drop)
151 init();
152 time("scope_churn_100x10_batch_drop", 5_000, || {
153 let scopes: Vec<TaskScope> = (0..100)
154 .map(|_| {
155 let s = TaskScope::new();
156 for _ in 0..10 {
157 s.spawn(async {});
158 }
159 s
160 })
161 .collect();
162 drop(scopes);
163 });
164
165 // 5. Scope suspend + resume (1000 tasks)
166 init();
167 let scope = TaskScope::new();
168 for _ in 0..1000 {
169 scope.spawn(async {});
170 }
171 time("scope_suspend_resume_1000_tasks", 500_000, || {
172 scope.suspend();
173 scope.resume();
174 });
175
176 // 6. Wide shallow tree drop (50x50, ~2550 tasks)
177 init();
178 time("scope_wide_tree_50x3_drop", 500, || {
179 let root = TaskScope::new();
180 for _ in 0..50 {
181 let child = TaskScope::new_child(&root);
182 child.spawn(async {});
183 for _ in 0..50 {
184 let grandchild = TaskScope::new_child(&child);
185 grandchild.spawn(async {});
186 }
187 }
188 drop(root);
189 });
190
191 println!("\n=== Done ===");
192}Sourcepub fn spawn(&self, future: impl Future<Output = ()> + 'static) -> JoinHandle
pub fn spawn(&self, future: impl Future<Output = ()> + 'static) -> JoinHandle
Spawn a future in this scope at low priority.
Returns a JoinHandle that can cancel this individual task.
Drop the handle to detach (the task keeps running until the
scope is dropped).
Examples found in repository?
28fn handle_request(request_id: u32) -> String {
29 let ex = Executor::new_instance();
30 Executor::install_flush_scheduler(&ex, Rc::new(SyncScheduler));
31
32 let scope = TaskScope::with_executor(&ex);
33 let data = Signal::new(String::new());
34
35 // "route handler" spawns a reactive effect
36 let d = data.clone();
37 scope.spawn(async move {
38 loop {
39 d.changed().await;
40 let val = d.read();
41 if val == "done" {
42 break;
43 }
44 }
45 });
46
47 // "middleware" sets up cleanup
48 let cleanup_called = Rc::new(Cell::new(false));
49 let cc = Rc::clone(&cleanup_called);
50 scope.on_cleanup(move || cc.set(true));
51
52 // "business logic" — would be I/O in real app
53 let result = auralis_task::with_executor(&ex, || {
54 data.set(format!("request {request_id}: processing"));
55 data.set(format!("request {request_id}: done"));
56 data.read()
57 });
58
59 // Drop scope → cancels effect, runs cleanup.
60 drop(scope);
61
62 assert!(cleanup_called.get());
63 result
64}
65
66fn main() {
67 // ---- Pattern 1: sequential requests (same thread, isolated) ----------
68 println!("=== 1. Sequential request isolation ===");
69 let r1 = handle_request(1);
70 let r2 = handle_request(2);
71 println!(" {r1}");
72 println!(" {r2}");
73 assert_eq!(r1, "request 1: done");
74 assert_eq!(r2, "request 2: done");
75
76 // ---- Pattern 2: same-signal isolation test (different executors) -----
77 println!("\n=== 2. Cross-executor signal isolation ===");
78 {
79 let ex_a = Executor::new_instance();
80 Executor::install_flush_scheduler(&ex_a, Rc::new(SyncScheduler));
81 let ex_b = Executor::new_instance();
82 Executor::install_flush_scheduler(&ex_b, Rc::new(SyncScheduler));
83
84 let sig_a1 = Signal::new(0i32);
85 let sig_b1 = Signal::new(0i32);
86
87 // Scope on executor A sets sig_a and reads sig_b.
88 let scope_a = TaskScope::with_executor(&ex_a);
89 let a_val = Rc::new(RefCell::new(Vec::new()));
90 let av = Rc::clone(&a_val);
91 {
92 let s = sig_a1.clone();
93 scope_a.spawn(async move {
94 s.set(42);
95 av.borrow_mut().push(s.read());
96 });
97 }
98 drop(scope_a);
99
100 // Scope on executor B sets sig_b and reads sig_a.
101 let scope_b = TaskScope::with_executor(&ex_b);
102 let b_val = Rc::new(RefCell::new(Vec::new()));
103 let bv = Rc::clone(&b_val);
104 {
105 let s = sig_b1.clone();
106 scope_b.spawn(async move {
107 s.set(99);
108 bv.borrow_mut().push(s.read());
109 });
110 }
111 drop(scope_b);
112
113 // Each executor only sees its own signal changes.
114 assert_eq!(*a_val.borrow(), vec![42]);
115 assert_eq!(*b_val.borrow(), vec![99]);
116 assert_eq!(sig_a1.read(), 42);
117 assert_eq!(sig_b1.read(), 99);
118 }
119
120 // ---- Pattern 3: the SSR mental model (one request = one executor) ----
121 println!("\n=== 3. SSR mental model ===");
122 for i in 0..3 {
123 let ex = Executor::new_instance();
124 Executor::install_flush_scheduler(&ex, Rc::new(SyncScheduler));
125 let counter = Signal::new(0i32);
126
127 let scope = TaskScope::with_executor(&ex);
128 let c = counter.clone();
129 let results = Rc::new(RefCell::new(Vec::new()));
130 let res = Rc::clone(&results);
131
132 scope.spawn(async move {
133 loop {
134 c.changed().await;
135 let v = c.read();
136 res.borrow_mut().push(v);
137 if v >= 3 {
138 break;
139 }
140 }
141 });
142
143 auralis_task::with_executor(&ex, || {
144 counter.set(1);
145 counter.set(2);
146 counter.set(3);
147 });
148
149 drop(scope);
150 assert_eq!(*results.borrow(), vec![1, 2, 3]);
151 println!(" request {i}: signals {:?}", results.borrow());
152 }
153
154 println!("\nAll patterns completed — zero cross-request leakage.");
155}More examples
37fn main() {
38 // One-time executor setup.
39 init_flush_scheduler(Rc::new(CliScheduler));
40 init_time_source(Rc::new(CliClock::new()));
41
42 // ---- Signal basics ----
43 println!("=== Signal basics ===");
44 let count = Signal::new(0);
45 println!("count = {}", count.read());
46 count.set(42);
47 println!("count = {}", count.read());
48
49 // ---- Memo (auto-tracking computed value) ----
50 println!("\n=== Memo ===");
51 let a = Signal::new(2);
52 let b = Signal::new(3);
53 let a2 = a.clone();
54 let b2 = b.clone();
55 let sum = Memo::new(move || a2.read() + b2.read());
56 println!("2 + 3 = {}", sum.read());
57 a.set(10);
58 println!("10 + 3 = {}", sum.read());
59
60 // ---- TaskScope: spawn a task that watches a signal ----
61 println!("\n=== TaskScope (scoped signal watcher) ===");
62 let messages = Signal::new(vec!["hello".to_string()]);
63
64 {
65 let scope = TaskScope::new();
66 let msgs = messages.clone();
67 scope.spawn(async move {
68 loop {
69 let val = msgs.changed().await;
70 println!(" task observed: {:?}", val);
71 }
72 });
73
74 messages.set(vec!["hello".to_string(), "world".to_string()]);
75 messages.set(vec![
76 "hello".to_string(),
77 "world".to_string(),
78 "!".to_string(),
79 ]);
80
81 // Explicit drop — cancels the task and cleans up resources.
82 drop(scope);
83 }
84 println!("(scope dropped — task and subscriptions cleaned up)");
85
86 // ---- Batch: multiple sets, one notification ----
87 println!("\n=== Batch ===");
88 let x = Signal::new(0);
89 let notifications = Rc::new(Cell::new(0u32));
90 let n = Rc::clone(¬ifications);
91 let x2 = x.clone();
92
93 let scope = TaskScope::new();
94 scope.spawn(async move {
95 loop {
96 x2.changed().await;
97 n.set(n.get() + 1);
98 }
99 });
100
101 batch(|| {
102 x.set(1);
103 x.set(2);
104 x.set(3);
105 });
106 println!(
107 "After 3 sets in a batch: {} notification(s)",
108 notifications.get()
109 );
110 // Should be 1 — not 3.
111
112 println!("\nDone.");
113}68fn main() {
69 println!("=== Auralis Task Scope Benchmarks ===\n");
70
71 // 1. Scope create + destroy (100 tasks)
72 let iterations = 50_000;
73 init();
74 time("scope_100_tasks_create_destroy", iterations, || {
75 let scope = TaskScope::new();
76 for _ in 0..100 {
77 scope.spawn(async {});
78 }
79 drop(scope);
80 });
81
82 // 2. Deep nesting drop (200 levels)
83 init();
84 time("scope_200_levels_deep_drop", 5_000, || {
85 let root = TaskScope::new();
86 {
87 let mut current = TaskScope::new_child(&root);
88 for _ in 0..199 {
89 current.spawn(async {});
90 current = TaskScope::new_child(¤t);
91 }
92 }
93 drop(root);
94 });
95
96 // 3. Priority ordering (batched flush)
97 {
98 let order: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
99 let min_iterations = 5_000;
100 // Warm-up
101 for _ in 0..500 {
102 let sched = BatchedFlush::new();
103 init_flush_scheduler(Rc::clone(&sched) as Rc<dyn ScheduleFlush>);
104 order.borrow_mut().clear();
105 for i in 0..1000 {
106 let o = Rc::clone(&order);
107 auralis_task::spawn_global_with_priority(Priority::Low, async move {
108 o.borrow_mut().push(format!("low_{i}"));
109 });
110 }
111 for i in 0..10 {
112 let o = Rc::clone(&order);
113 auralis_task::spawn_global_with_priority(Priority::High, async move {
114 o.borrow_mut().push(format!("high_{i}"));
115 });
116 }
117 sched.drain();
118 }
119
120 let start = Instant::now();
121 for _ in 0..min_iterations {
122 let sched = BatchedFlush::new();
123 init_flush_scheduler(Rc::clone(&sched) as Rc<dyn ScheduleFlush>);
124 order.borrow_mut().clear();
125 for i in 0..1000 {
126 let o = Rc::clone(&order);
127 auralis_task::spawn_global_with_priority(Priority::Low, async move {
128 o.borrow_mut().push(format!("low_{i}"));
129 });
130 }
131 for i in 0..10 {
132 let o = Rc::clone(&order);
133 auralis_task::spawn_global_with_priority(Priority::High, async move {
134 o.borrow_mut().push(format!("high_{i}"));
135 });
136 }
137 sched.drain();
138 // Verify correctness.
139 let result = order.borrow().clone();
140 debug_assert_eq!(result.len(), 1010);
141 }
142 let elapsed = start.elapsed().as_secs_f64() * 1_000_000.0 / min_iterations as f64;
143 println!(
144 " {:<42} {:>8.2} µs/iter",
145 "priority_1000_low_10_high", elapsed
146 );
147 init(); // restore sync scheduler
148 }
149
150 // 4. Scope churn (100 scopes x 10 tasks, batch drop)
151 init();
152 time("scope_churn_100x10_batch_drop", 5_000, || {
153 let scopes: Vec<TaskScope> = (0..100)
154 .map(|_| {
155 let s = TaskScope::new();
156 for _ in 0..10 {
157 s.spawn(async {});
158 }
159 s
160 })
161 .collect();
162 drop(scopes);
163 });
164
165 // 5. Scope suspend + resume (1000 tasks)
166 init();
167 let scope = TaskScope::new();
168 for _ in 0..1000 {
169 scope.spawn(async {});
170 }
171 time("scope_suspend_resume_1000_tasks", 500_000, || {
172 scope.suspend();
173 scope.resume();
174 });
175
176 // 6. Wide shallow tree drop (50x50, ~2550 tasks)
177 init();
178 time("scope_wide_tree_50x3_drop", 500, || {
179 let root = TaskScope::new();
180 for _ in 0..50 {
181 let child = TaskScope::new_child(&root);
182 child.spawn(async {});
183 for _ in 0..50 {
184 let grandchild = TaskScope::new_child(&child);
185 grandchild.spawn(async {});
186 }
187 }
188 drop(root);
189 });
190
191 println!("\n=== Done ===");
192}Sourcepub fn spawn_with_priority(
&self,
priority: Priority,
future: impl Future<Output = ()> + 'static,
) -> JoinHandle
pub fn spawn_with_priority( &self, priority: Priority, future: impl Future<Output = ()> + 'static, ) -> JoinHandle
Spawn a future in this scope at the given priority.
The current scope is set to self during the spawn so that any
synchronous work inside the future constructor (e.g. bind_text)
can discover the owning scope via current_scope.
Returns a JoinHandle that can cancel this individual task.
Sourcepub fn watch<T: Clone + 'static>(
&self,
sig: &Signal<T>,
f: impl FnMut(&T) + 'static,
) -> JoinHandle
pub fn watch<T: Clone + 'static>( &self, sig: &Signal<T>, f: impl FnMut(&T) + 'static, ) -> JoinHandle
Spawn a task that calls f with the new value whenever sig changes.
This is a convenience wrapper around the common pattern:
scope.spawn({
let s = sig.clone();
async move { loop { s.changed().await; f(&s.read()); } }
});Returns a JoinHandle for individual cancellation.
Sourcepub fn watch_effect(&self, effect: impl Fn() + 'static) -> JoinHandle
pub fn watch_effect(&self, effect: impl Fn() + 'static) -> JoinHandle
Spawn a task that re-runs effect whenever any Signal read
inside it changes — using a Memo internally
to auto-track dependencies.
The effect is run once immediately to discover its dependencies. Subsequent runs happen on the executor when a dependency changes.
Returns a JoinHandle for individual cancellation.
Sourcepub fn register_callback_handle(&self, handle: CallbackHandle)
pub fn register_callback_handle(&self, handle: CallbackHandle)
Register a CallbackHandle that will be dropped when this scope
is dropped (or when clear_callbacks is called).
Used by bind_* functions to ensure signal subscriptions are
cleaned up when the owning component is destroyed.
Sourcepub fn on_cleanup(&self, f: impl FnOnce() + 'static)
pub fn on_cleanup(&self, f: impl FnOnce() + 'static)
Register a cleanup function that runs when this scope is dropped.
Equivalent to register_callback_handle(CallbackHandle::new(f)).
Cleanup functions run before spawned tasks are cancelled, so they can safely interact with signals and other resources.
If the scope is already cancelled, f is dropped immediately.
Examples found in repository?
28fn handle_request(request_id: u32) -> String {
29 let ex = Executor::new_instance();
30 Executor::install_flush_scheduler(&ex, Rc::new(SyncScheduler));
31
32 let scope = TaskScope::with_executor(&ex);
33 let data = Signal::new(String::new());
34
35 // "route handler" spawns a reactive effect
36 let d = data.clone();
37 scope.spawn(async move {
38 loop {
39 d.changed().await;
40 let val = d.read();
41 if val == "done" {
42 break;
43 }
44 }
45 });
46
47 // "middleware" sets up cleanup
48 let cleanup_called = Rc::new(Cell::new(false));
49 let cc = Rc::clone(&cleanup_called);
50 scope.on_cleanup(move || cc.set(true));
51
52 // "business logic" — would be I/O in real app
53 let result = auralis_task::with_executor(&ex, || {
54 data.set(format!("request {request_id}: processing"));
55 data.set(format!("request {request_id}: done"));
56 data.read()
57 });
58
59 // Drop scope → cancels effect, runs cleanup.
60 drop(scope);
61
62 assert!(cleanup_called.get());
63 result
64}Sourcepub fn consume<T: 'static>(&self) -> Option<Rc<T>>
pub fn consume<T: 'static>(&self) -> Option<Rc<T>>
Look up a value of type T by walking up the scope tree.
Returns None if no ancestor (including self) has provided a
value of this type.
Sourcepub fn expect_context<T: 'static>(&self) -> Rc<T>
pub fn expect_context<T: 'static>(&self) -> Rc<T>
Sourcepub fn is_cancelled(&self) -> bool
pub fn is_cancelled(&self) -> bool
Return true if this scope has been cancelled (dropped).
A cancelled scope silently ignores spawn calls.
Sourcepub fn set_label(&self, label: impl Into<String>)
pub fn set_label(&self, label: impl Into<String>)
Set a human-readable label for this scope.
Labels appear in dump_reactive_graph() output and are useful
for debugging.
Sourcepub fn enter<R>(&self, f: impl FnOnce() -> R) -> R
pub fn enter<R>(&self, f: impl FnOnce() -> R) -> R
Run f with self set as the current scope for the thread.
Used by framework glue code so bind functions can discover the
owning scope via current_scope.
Sourcepub fn suspend(&self)
pub fn suspend(&self)
Suspend all tasks owned by this scope and its descendants.
Suspended tasks are skipped during executor polling. Signal
subscriptions remain registered but their callbacks are not
invoked while the scope is suspended. Use resume
to restart execution.
Used by if_async_cached and match_async_cached to pause
hidden branches.
Examples found in repository?
68fn main() {
69 println!("=== Auralis Task Scope Benchmarks ===\n");
70
71 // 1. Scope create + destroy (100 tasks)
72 let iterations = 50_000;
73 init();
74 time("scope_100_tasks_create_destroy", iterations, || {
75 let scope = TaskScope::new();
76 for _ in 0..100 {
77 scope.spawn(async {});
78 }
79 drop(scope);
80 });
81
82 // 2. Deep nesting drop (200 levels)
83 init();
84 time("scope_200_levels_deep_drop", 5_000, || {
85 let root = TaskScope::new();
86 {
87 let mut current = TaskScope::new_child(&root);
88 for _ in 0..199 {
89 current.spawn(async {});
90 current = TaskScope::new_child(¤t);
91 }
92 }
93 drop(root);
94 });
95
96 // 3. Priority ordering (batched flush)
97 {
98 let order: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
99 let min_iterations = 5_000;
100 // Warm-up
101 for _ in 0..500 {
102 let sched = BatchedFlush::new();
103 init_flush_scheduler(Rc::clone(&sched) as Rc<dyn ScheduleFlush>);
104 order.borrow_mut().clear();
105 for i in 0..1000 {
106 let o = Rc::clone(&order);
107 auralis_task::spawn_global_with_priority(Priority::Low, async move {
108 o.borrow_mut().push(format!("low_{i}"));
109 });
110 }
111 for i in 0..10 {
112 let o = Rc::clone(&order);
113 auralis_task::spawn_global_with_priority(Priority::High, async move {
114 o.borrow_mut().push(format!("high_{i}"));
115 });
116 }
117 sched.drain();
118 }
119
120 let start = Instant::now();
121 for _ in 0..min_iterations {
122 let sched = BatchedFlush::new();
123 init_flush_scheduler(Rc::clone(&sched) as Rc<dyn ScheduleFlush>);
124 order.borrow_mut().clear();
125 for i in 0..1000 {
126 let o = Rc::clone(&order);
127 auralis_task::spawn_global_with_priority(Priority::Low, async move {
128 o.borrow_mut().push(format!("low_{i}"));
129 });
130 }
131 for i in 0..10 {
132 let o = Rc::clone(&order);
133 auralis_task::spawn_global_with_priority(Priority::High, async move {
134 o.borrow_mut().push(format!("high_{i}"));
135 });
136 }
137 sched.drain();
138 // Verify correctness.
139 let result = order.borrow().clone();
140 debug_assert_eq!(result.len(), 1010);
141 }
142 let elapsed = start.elapsed().as_secs_f64() * 1_000_000.0 / min_iterations as f64;
143 println!(
144 " {:<42} {:>8.2} µs/iter",
145 "priority_1000_low_10_high", elapsed
146 );
147 init(); // restore sync scheduler
148 }
149
150 // 4. Scope churn (100 scopes x 10 tasks, batch drop)
151 init();
152 time("scope_churn_100x10_batch_drop", 5_000, || {
153 let scopes: Vec<TaskScope> = (0..100)
154 .map(|_| {
155 let s = TaskScope::new();
156 for _ in 0..10 {
157 s.spawn(async {});
158 }
159 s
160 })
161 .collect();
162 drop(scopes);
163 });
164
165 // 5. Scope suspend + resume (1000 tasks)
166 init();
167 let scope = TaskScope::new();
168 for _ in 0..1000 {
169 scope.spawn(async {});
170 }
171 time("scope_suspend_resume_1000_tasks", 500_000, || {
172 scope.suspend();
173 scope.resume();
174 });
175
176 // 6. Wide shallow tree drop (50x50, ~2550 tasks)
177 init();
178 time("scope_wide_tree_50x3_drop", 500, || {
179 let root = TaskScope::new();
180 for _ in 0..50 {
181 let child = TaskScope::new_child(&root);
182 child.spawn(async {});
183 for _ in 0..50 {
184 let grandchild = TaskScope::new_child(&child);
185 grandchild.spawn(async {});
186 }
187 }
188 drop(root);
189 });
190
191 println!("\n=== Done ===");
192}Sourcepub fn resume(&self)
pub fn resume(&self)
Resume all tasks owned by this scope and its descendants.
This reverses the effect of suspend. Tasks
become eligible for polling again on the next executor flush.
Examples found in repository?
68fn main() {
69 println!("=== Auralis Task Scope Benchmarks ===\n");
70
71 // 1. Scope create + destroy (100 tasks)
72 let iterations = 50_000;
73 init();
74 time("scope_100_tasks_create_destroy", iterations, || {
75 let scope = TaskScope::new();
76 for _ in 0..100 {
77 scope.spawn(async {});
78 }
79 drop(scope);
80 });
81
82 // 2. Deep nesting drop (200 levels)
83 init();
84 time("scope_200_levels_deep_drop", 5_000, || {
85 let root = TaskScope::new();
86 {
87 let mut current = TaskScope::new_child(&root);
88 for _ in 0..199 {
89 current.spawn(async {});
90 current = TaskScope::new_child(¤t);
91 }
92 }
93 drop(root);
94 });
95
96 // 3. Priority ordering (batched flush)
97 {
98 let order: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
99 let min_iterations = 5_000;
100 // Warm-up
101 for _ in 0..500 {
102 let sched = BatchedFlush::new();
103 init_flush_scheduler(Rc::clone(&sched) as Rc<dyn ScheduleFlush>);
104 order.borrow_mut().clear();
105 for i in 0..1000 {
106 let o = Rc::clone(&order);
107 auralis_task::spawn_global_with_priority(Priority::Low, async move {
108 o.borrow_mut().push(format!("low_{i}"));
109 });
110 }
111 for i in 0..10 {
112 let o = Rc::clone(&order);
113 auralis_task::spawn_global_with_priority(Priority::High, async move {
114 o.borrow_mut().push(format!("high_{i}"));
115 });
116 }
117 sched.drain();
118 }
119
120 let start = Instant::now();
121 for _ in 0..min_iterations {
122 let sched = BatchedFlush::new();
123 init_flush_scheduler(Rc::clone(&sched) as Rc<dyn ScheduleFlush>);
124 order.borrow_mut().clear();
125 for i in 0..1000 {
126 let o = Rc::clone(&order);
127 auralis_task::spawn_global_with_priority(Priority::Low, async move {
128 o.borrow_mut().push(format!("low_{i}"));
129 });
130 }
131 for i in 0..10 {
132 let o = Rc::clone(&order);
133 auralis_task::spawn_global_with_priority(Priority::High, async move {
134 o.borrow_mut().push(format!("high_{i}"));
135 });
136 }
137 sched.drain();
138 // Verify correctness.
139 let result = order.borrow().clone();
140 debug_assert_eq!(result.len(), 1010);
141 }
142 let elapsed = start.elapsed().as_secs_f64() * 1_000_000.0 / min_iterations as f64;
143 println!(
144 " {:<42} {:>8.2} µs/iter",
145 "priority_1000_low_10_high", elapsed
146 );
147 init(); // restore sync scheduler
148 }
149
150 // 4. Scope churn (100 scopes x 10 tasks, batch drop)
151 init();
152 time("scope_churn_100x10_batch_drop", 5_000, || {
153 let scopes: Vec<TaskScope> = (0..100)
154 .map(|_| {
155 let s = TaskScope::new();
156 for _ in 0..10 {
157 s.spawn(async {});
158 }
159 s
160 })
161 .collect();
162 drop(scopes);
163 });
164
165 // 5. Scope suspend + resume (1000 tasks)
166 init();
167 let scope = TaskScope::new();
168 for _ in 0..1000 {
169 scope.spawn(async {});
170 }
171 time("scope_suspend_resume_1000_tasks", 500_000, || {
172 scope.suspend();
173 scope.resume();
174 });
175
176 // 6. Wide shallow tree drop (50x50, ~2550 tasks)
177 init();
178 time("scope_wide_tree_50x3_drop", 500, || {
179 let root = TaskScope::new();
180 for _ in 0..50 {
181 let child = TaskScope::new_child(&root);
182 child.spawn(async {});
183 for _ in 0..50 {
184 let grandchild = TaskScope::new_child(&child);
185 grandchild.spawn(async {});
186 }
187 }
188 drop(root);
189 });
190
191 println!("\n=== Done ===");
192}Sourcepub fn is_suspended(&self) -> bool
pub fn is_suspended(&self) -> bool
Return true if this scope is currently suspended.