pub fn with_executor<R>(ex: &Rc<RefCell<Executor>>, f: impl FnOnce() -> R) -> RExpand description
Run f with ex set as the current executor.
Signal callbacks and spawn_global calls inside f will be routed
to ex instead of the global thread-local executor. Restores the
previous executor afterward.
§Signal routing constraints
Auralis uses a single global schedule hook (installed once by the
first call to init_flush_scheduler) that decides where signal
notifications land by checking the current executor at the time the
notification fires, not at the time Signal::set is called.
This design implies two hard requirements for multi-instance users:
init_flush_schedulermust be called at least once — without it,Signal::setfalls back to synchronous callback execution, which breaks the deferred-notification model and can cause re-entrant borrow panics.- The instance executor must still be “current” when the flush
runs — if
with_executorhas already exited, deferred callbacks from signals set insidefwill be routed to the global executor (or synchronously if no global hook is installed).
For the typical single-threaded case (Wasm, game loop, CLI), both
requirements are satisfied trivially: call init_flush_scheduler
once at startup and never use with_executor. For SSR / multi-tenant
servers, ensure that with_executor wraps the entire request
lifecycle — from signal creation through the final flush.
§Example
use auralis_task::Executor;
let ex = Executor::new_instance();
Executor::install_flush_scheduler(&ex, my_scheduler);
auralis_task::with_executor(&ex, || {
// Signal notifications and task spawns here go to `ex`.
});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}