<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: chh-itt</title>
    <description>The latest articles on DEV Community by chh-itt (@chhitt).</description>
    <link>https://dev.to/chhitt</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3922989%2F3523963a-36a8-47bf-9a9e-3b8aec65e527.png</url>
      <title>DEV Community: chh-itt</title>
      <link>https://dev.to/chhitt</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chhitt"/>
    <language>en</language>
    <item>
      <title>What if reactive effects are just pausable async tasks?</title>
      <dc:creator>chh-itt</dc:creator>
      <pubDate>Wed, 13 May 2026 01:24:44 +0000</pubDate>
      <link>https://dev.to/chhitt/what-if-reactive-effects-are-just-pausable-async-tasks-5gal</link>
      <guid>https://dev.to/chhitt/what-if-reactive-effects-are-just-pausable-async-tasks-5gal</guid>
      <description>&lt;p&gt;I've been thinking about reactive programming from the runtime layer up. The question: what's the minimum machinery you actually need to add on top of Rust's async/await to get a working reactive system?&lt;/p&gt;

&lt;p&gt;Three things Rust already gives you for free&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pin&amp;gt; = a suspended computation&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Think about it: a reactive effect is "a piece of code paused, waiting for its dependencies to change, then re-executing." That's exactly what a future stuck at Poll::Pending is.&lt;/p&gt;

&lt;p&gt;scope.spawn(async move {&lt;br&gt;
    loop {&lt;br&gt;
        count.changed().await;  // suspends here&lt;br&gt;
        println!("count = {}", count.read());&lt;br&gt;
    }&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;The SignalChangedFuture::poll() checks a monotonic version number. If unchanged, it subscribes a callback and returns Poll::Pending. The async executor polls other tasks. When the signal changes, the version bumps, the callback fires, the waker wakes, the executor re-polls. That's the entire effect lifecycle — no custom scheduler, no effect graph traversal, no create_effect() abstraction.&lt;/p&gt;

&lt;p&gt;The executor's poll loop IS the effect scheduler. You don't need a separate one.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Waker = the notification dispatcher&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A reactive library has to decide "which effects need to re-run when signal X changes." In the async model, this maps directly to "which futures should be re-polled." The waker IS that dispatch mechanism:&lt;/p&gt;

&lt;p&gt;fn poll(self: Pin&amp;lt;&amp;amp;mut Self&amp;gt;, cx: &amp;amp;mut Context&amp;lt;'_&amp;gt;) -&amp;gt; Poll&lt;a&gt;Self::Output&lt;/a&gt; {&lt;br&gt;
    if version_changed {&lt;br&gt;
        return Poll::Ready(self.signal.read());&lt;br&gt;
    }&lt;br&gt;
    // subscribe a callback that calls cx.waker().wake_by_ref()&lt;br&gt;
    // executor re-polls this future on next flush&lt;br&gt;
    Poll::Pending&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;No separate notification queue. No topological sort of dependents. Just "wake the waker, executor picks it up."&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Drop = cleanup&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Drop the scope that owns the tasks → all futures drop → each future's Drop impl proactively unsubscribes from its signals → zero dangling subscribers. No manual on_cleanup() tokens, no effect disposal bookkeeping. Rust's ownership does the work.&lt;/p&gt;

&lt;p&gt;For deeply nested scopes (think UI component trees with hundreds of levels), cancellation uses BFS to collect all descendants, then iterates leaf-to-root — no recursive drop, no stack overflow.&lt;/p&gt;




&lt;p&gt;The 20% Rust doesn't give you: re-entrancy prevention&lt;/p&gt;

&lt;p&gt;Here's where pure async breaks down. If Signal::set() calls subscriber callbacks synchronously:&lt;/p&gt;

&lt;p&gt;set() → callback → set() on same signal → callback → infinite loop&lt;br&gt;
set() → callback → subscribe new callback → mutate subscriber list during iteration → RefCell panic&lt;br&gt;
set() → callback → drop owning scope → borrow conflict&lt;/p&gt;

&lt;p&gt;The async model alone can't prevent this. You need a deferred notification state machine.&lt;/p&gt;

&lt;p&gt;The approach: Signal::set() never invokes callbacks directly. It bumps the version, snapshots the subscriber list, and pushes a closure into the executor's deferred callback queue. The executor drains this queue at the start of every flush, before polling any tasks.&lt;/p&gt;

&lt;p&gt;Three states, two flags:&lt;/p&gt;

&lt;p&gt;notifying = false, dirty = false → normal. Snapshot subscribers, push notification, set dirty.&lt;br&gt;
notifying = true              → re-entrant set during callback. Just set dirty, no new notification.&lt;br&gt;
dirty = true, notifying=false → notification already queued. No-op.&lt;/p&gt;

&lt;p&gt;After callbacks complete: check dirty. If a re-entrant set() happened during the callback round, schedule a follow-up notification using a fresh subscriber snapshot (so newly-added subscribers from the callback round are included).&lt;/p&gt;

&lt;p&gt;This covers every edge case: re-entrant set, subscribe/unsubscribe during callback iteration, scope drop during callback (cancelled flag is a Cell outside the RefCell, always writable), set-from-Drop (deferred to next flush via set_deferred()).&lt;/p&gt;




&lt;p&gt;The tradeoff&lt;/p&gt;

&lt;p&gt;A traditional reactive scheduler can run effects in topological order — dependencies before dependents, guaranteeing each memo recomputes at most once. An async executor runs tasks in whatever order they wake up. If two effects both read a dirty memo, they might each trigger its recomputation (the second read sees it's already clean and skips the work, but the first poll of each effect triggers the check).&lt;/p&gt;

&lt;p&gt;Correctness is preserved — version checking + lazy memo recompute guarantees eventual consistency. But optimality is best-effort, not guaranteed.&lt;/p&gt;

&lt;p&gt;For UI frame-level reactivity this is fine. For a compiler's incremental analysis it probably isn't. I think the simplicity trade is worth it for most applications, but I'd be curious to hear counterarguments.&lt;/p&gt;




&lt;p&gt;Why bother?&lt;/p&gt;

&lt;p&gt;If you squint, reactive programming and async programming are solving the same problem from different directions. One suspends computation waiting for data changes; the other suspends computation waiting for I/O. The primitives overlap almost completely. The question isn't "can you build reactivity on async" — it's "why wouldn't you?"&lt;/p&gt;

&lt;p&gt;The only genuinely novel piece needed is a safe way to fire subscriber callbacks without re-entrancy. Everything else is already in the language.&lt;/p&gt;




&lt;p&gt;I built a working implementation of this (~1800 lines across two crates, #![forbid(unsafe_code)], zero external deps for the signal layer). I’d genuinely love technical feedback — especially on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether the lack of topological ordering has bitten anyone in real UI projects, or if it’s mostly a theoretical concern.&lt;/li&gt;
&lt;li&gt;Whether there’s a simpler alternative to the generational slot table I’m using for executor routing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I’ve missed an edge case, or if any of the tradeoffs feel wrong to you, I’m all ears. This is still my own learning process, and I’d rather get corrected now than find out later.&lt;/p&gt;

</description>
      <category>rust</category>
    </item>
    <item>
      <title>Reactive Programming Doesn't Need a Framework — Ownership Is Enough</title>
      <dc:creator>chh-itt</dc:creator>
      <pubDate>Sun, 10 May 2026 08:14:27 +0000</pubDate>
      <link>https://dev.to/chhitt/reactive-programming-doesnt-need-a-framework-ownership-is-enough-3e70</link>
      <guid>https://dev.to/chhitt/reactive-programming-doesnt-need-a-framework-ownership-is-enough-3e70</guid>
      <description>&lt;h1&gt;
  
  
  Reactive Programming Doesn't Need a Framework — Ownership Is Enough
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;How Auralis, a two-crate Rust kernel, reduces reactive programming to things Rust programmers already know.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I spent the last few months building &lt;a href="https://github.com/chh-itt/auralis" rel="noopener noreferrer"&gt;Auralis&lt;/a&gt;, a reactive kernel for Rust. Two crates, zero unsafe, signal crate has zero dependencies. It's not a web framework — no &lt;code&gt;view!&lt;/code&gt; macro, no DOM, no hydration. Just reactive primitives you pair with whatever you already have.&lt;/p&gt;

&lt;p&gt;The core idea fits in one sentence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Reactive = pausable async tasks. Lifecycle = ownership + structured concurrency.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Everything else follows from this. Let me walk through the design decisions that make it work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 1: &lt;code&gt;Signal::set()&lt;/code&gt; can't call subscribers directly
&lt;/h2&gt;

&lt;p&gt;In most reactive systems, changing a signal's value immediately notifies everyone watching it. This seems natural, but it creates a cascade of problems:&lt;/p&gt;

&lt;p&gt;A subscriber callback calls &lt;code&gt;set()&lt;/code&gt; on another signal → that signal's callback calls &lt;code&gt;set()&lt;/code&gt; on the first signal → infinite loop. Or a subscriber tries to subscribe to a new signal during a callback → the subscriber list is being iterated → undefined behavior. Or a subscriber drops itself during a callback → the list iterator points to freed memory.&lt;/p&gt;

&lt;p&gt;Every reactive system eventually invents mechanisms to deal with these: &lt;code&gt;pending_unsubscriptions&lt;/code&gt; lists, &lt;code&gt;traversal_depth&lt;/code&gt; counters, &lt;code&gt;dirty_subscribers&lt;/code&gt; sets.&lt;/p&gt;

&lt;p&gt;Auralis's answer: &lt;strong&gt;don't call subscribers synchronously.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Signal::set does NOT call callbacks.&lt;/span&gt;
&lt;span class="c1"&gt;// It pushes a notification closure to the executor's deferred queue.&lt;/span&gt;
&lt;span class="c1"&gt;// The executor drains this queue at the start of the next flush.&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.state&lt;/span&gt;&lt;span class="nf"&gt;.borrow_mut&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="py"&gt;.value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="py"&gt;.version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="py"&gt;.version&lt;/span&gt;&lt;span class="nf"&gt;.wrapping_add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// monotonic version&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;subs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;prepare_notification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;schedule_notification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The notification closure is a simple &lt;code&gt;Box&amp;lt;dyn FnOnce()&amp;gt;&lt;/code&gt;. It captures a snapshot of the subscriber list at the time &lt;code&gt;set()&lt;/code&gt; was called. When the executor drains the deferred queue (during the next flush, before polling any tasks), it fires each notification. If a subscriber was added after the snapshot, it won't be called for this change. If a subscriber unsubscribes before the notification fires, an &lt;code&gt;alive&lt;/code&gt; flag prevents the callback from running.&lt;/p&gt;

&lt;p&gt;This eliminates re-entrancy entirely. No depth counters, no pending-lists, no edge cases. The cost is one microtask of latency — negligible for any actual use case.&lt;/p&gt;

&lt;p&gt;The state machine for a single signal notification looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;set() → prepare_notification → schedule_notification
         ↓ (already notifying?)    ↓ (inside batch?)
         dirty=true, return        buffer, flush at batch end
                                   ↓
                              executor flush
                                   ↓
                              call subscribers
                                   ↓ (re-entrant set during callback?)
                              dirty=true, schedule follow-up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Problem 2: Cancelling background work is hard. Dropping a scope is easy.
&lt;/h2&gt;

&lt;p&gt;Every Rust UI eventually hits the same problem: the user navigates away, but the HTTP request is still in flight. The component is unmounted, but the timer is still ticking. You need cancellation tokens, &lt;code&gt;AbortHandle&lt;/code&gt;s, or manual &lt;code&gt;Arc&amp;lt;AtomicBool&amp;gt;&lt;/code&gt; flags scattered everywhere.&lt;/p&gt;

&lt;p&gt;Auralis replaces all of that with &lt;code&gt;TaskScope&lt;/code&gt;. When a scope is dropped, all tasks spawned inside it — and all descendant scopes — are cancelled. No tokens, no handles, no manual cleanup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;TaskScope&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with_executor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Spawn a task that listens to a signal.&lt;/span&gt;
&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="nf"&gt;.spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;loop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="nf"&gt;.changed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Spawn another task that runs a timer.&lt;/span&gt;
&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="nf"&gt;.spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_secs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Later, when the UI component is unmounted:&lt;/span&gt;
&lt;span class="nf"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Both tasks are cancelled. No tokens, no handles.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cancellation itself is iterative, not recursive. &lt;code&gt;TaskScope::drop&lt;/code&gt; does a BFS traversal of the scope tree, collects all descendants, then cancels them leaf-to-root. This means a 200-level nested UI tree drops without stack overflow — no &lt;code&gt;#[rustc_recursion_limit]&lt;/code&gt; hacks needed.&lt;/p&gt;

&lt;p&gt;The cancellation uses the same &lt;code&gt;Rc&amp;lt;RefCell&amp;lt;TaskScopeInner&amp;gt;&amp;gt;&lt;/code&gt; that &lt;code&gt;Memo&lt;/code&gt; uses. When the last &lt;code&gt;TaskScope&lt;/code&gt; clone is dropped (&lt;code&gt;Rc::strong_count == 1&lt;/code&gt;), the cancellation runs. Temporary clones — from &lt;code&gt;find_scope&lt;/code&gt; during executor flush, from &lt;code&gt;with_current_scope&lt;/code&gt; during spawn — are harmless. They share the same &lt;code&gt;Rc&lt;/code&gt; and don't trigger cancellation. This is a bug we actually found and fixed in development — a temporary clone was setting &lt;code&gt;cancelled = true&lt;/code&gt; on the shared inner, and subsequent spawns silently returned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 3: Derived state goes stale when you forget to update it
&lt;/h2&gt;

&lt;p&gt;In immediate-mode GUIs (like egui), everything is recomputed every frame. This is simple but wasteful. In retained-mode UIs, derived state must be manually updated when dependencies change. This is efficient but error-prone — one missed update and the UI shows stale data.&lt;/p&gt;

&lt;p&gt;Memo solves this by being both lazy and automatic. You give it a closure. It runs the closure once, discovers which signals were read, and subscribes to them. When a source signal changes, the memo just flips a &lt;code&gt;dirty&lt;/code&gt; flag. The actual recomputation is deferred until someone calls &lt;code&gt;memo.read()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Signal&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;load_large_dataset&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;filter_params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Signal&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;FilterParams&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="c1"&gt;// Memo auto-tracks which signals it reads.&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;filtered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;memo!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filter_params&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt;&lt;span class="nf"&gt;.read&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;filter_params&lt;/span&gt;&lt;span class="nf"&gt;.read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;aggregated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;memo!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filtered&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;aggregate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;filtered&lt;/span&gt;&lt;span class="nf"&gt;.read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;formatted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;memo!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aggregated&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;format_results&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;aggregated&lt;/span&gt;&lt;span class="nf"&gt;.read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Cache hit when params haven't changed: &amp;lt; 0.01 ms&lt;/span&gt;
&lt;span class="c1"&gt;// Cache miss when params changed: ~14 ms (500K records)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;memo!&lt;/code&gt; macro handles the boilerplate of cloning signals before the &lt;code&gt;move&lt;/code&gt; closure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Without macro:&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Memo&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="nf"&gt;.read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="nf"&gt;.read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// With macro:&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;memo!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="nf"&gt;.read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="nf"&gt;.read&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We benchmarked this against two alternatives on a 500K-record 3-stage pipeline:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change rate&lt;/th&gt;
&lt;th&gt;Recompute every frame&lt;/th&gt;
&lt;th&gt;Manual cache&lt;/th&gt;
&lt;th&gt;Auralis Memo&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1% (typical UI)&lt;/td&gt;
&lt;td&gt;~14 ms/fr&lt;/td&gt;
&lt;td&gt;0.14 ms/fr&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.16 ms/fr&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;~14 ms/fr&lt;/td&gt;
&lt;td&gt;1.45 ms/fr&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.78 ms/fr&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50% (worst case)&lt;/td&gt;
&lt;td&gt;~14 ms/fr&lt;/td&gt;
&lt;td&gt;7.33 ms/fr&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9.09 ms/fr&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At realistic UI change rates (&amp;lt; 1%), Memo provides ~90x speedup. Cache-hit cost is literally a &lt;code&gt;Cell::get&lt;/code&gt; + &lt;code&gt;Rc&lt;/code&gt; deref — under 10 microseconds.&lt;/p&gt;

&lt;p&gt;The 50% row is the interesting one. Memo has ~24% overhead vs. a hand-written cache (9.09 vs 7.33 ms/fr). This is the cost of &lt;em&gt;not writing invalidation logic by hand&lt;/em&gt;. A hand-written cache for a 3-stage pipeline needs ~20 lines of &lt;code&gt;if filter_changed || agg_changed || fmt_changed&lt;/code&gt; cascade. Add a stage, you add 2–3 more condition checks. Miss one, you get stale data.&lt;/p&gt;

&lt;p&gt;With Memo, adding a stage is one line: &lt;code&gt;memo!(prev_stage =&amp;gt; compute(&amp;amp;prev_stage.read()))&lt;/code&gt;. The dependency graph is maintained automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 4: &lt;code&gt;Arc&amp;lt;Mutex&amp;lt;T&amp;gt;&amp;gt;&lt;/code&gt; is wasteful when you're single-threaded
&lt;/h2&gt;

&lt;p&gt;Most Rust reactive libraries reach for &lt;code&gt;Arc&amp;lt;Mutex&amp;lt;T&amp;gt;&amp;gt;&lt;/code&gt; so they can be &lt;code&gt;Send + Sync&lt;/code&gt;. On single-threaded Wasm — which is the primary target for many Rust UI frameworks — atomics are pure overhead. Every &lt;code&gt;lock()&lt;/code&gt; is effectively a no-op, but you pay the code size and mental overhead anyway.&lt;/p&gt;

&lt;p&gt;Auralis goes the other way: &lt;code&gt;Signal&amp;lt;T&amp;gt;&lt;/code&gt; and &lt;code&gt;TaskScope&lt;/code&gt; are deliberately &lt;code&gt;!Send + !Sync&lt;/code&gt;. They use &lt;code&gt;Rc&amp;lt;RefCell&amp;lt;T&amp;gt;&amp;gt;&lt;/code&gt; throughout — zero atomics, zero lock overhead. The Wasm binary for a minimal reactive counter compiles to a few tens of KB.&lt;/p&gt;

&lt;p&gt;For multi-threaded scenarios, the escape hatch is &lt;strong&gt;instance isolation&lt;/strong&gt;, not shared mutable state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Thread 1&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;ex1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Executor&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new_instance&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;TaskScope&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with_executor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ex1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// scope holds a strong reference to ex1&lt;/span&gt;

&lt;span class="c1"&gt;// Thread 2 — completely independent&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;ex2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Executor&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new_instance&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;scope2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;TaskScope&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with_executor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ex2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Communication between threads: channels, not shared signals.&lt;/span&gt;
&lt;span class="c1"&gt;// See examples/multi_thread_bridge.rs for a 6-line pattern.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This mirrors tokio's single-threaded runtime model: each executor is a self-contained event loop. Tasks in different executors don't interact. Cross-executor communication uses channels.&lt;/p&gt;

&lt;p&gt;TaskWakers carry a &lt;code&gt;(slot_id, generation)&lt;/code&gt; pair — a thread-local slot table maps these to &lt;code&gt;Weak&amp;lt;RefCell&amp;lt;Executor&amp;gt;&amp;gt;&lt;/code&gt;. The generation counter invalidates stale wakers after an executor is destroyed. Zero unsafe code in the entire routing system.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Deferred callbacks are worth the microtask.&lt;/strong&gt; The signal notification state machine went through multiple iterations, but the core idea — never call subscribers synchronously — survived every rewrite. It eliminates an entire class of bugs at the architectural level.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ownership is a better lifecycle manager than any framework API.&lt;/strong&gt; &lt;code&gt;drop(scope)&lt;/code&gt; is simpler, more composable, and harder to misuse than &lt;code&gt;on_cleanup&lt;/code&gt;, &lt;code&gt;AbortHandle&lt;/code&gt;, &lt;code&gt;CancellationToken&lt;/code&gt;, or any other explicit cancellation mechanism. The compiler enforces it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Build the kernel, not the framework.&lt;/strong&gt; Auralis has no DOM, no renderer, no router. This means it can be used in a CLI app (we have a &lt;a href="https://github.com/chh-itt/auralis/tree/main/demos/cli-multitask" rel="noopener noreferrer"&gt;demo&lt;/a&gt;), in a Wasm browser app (we have a &lt;a href="https://github.com/chh-itt/auralis/tree/main/demos/wasm-counter" rel="noopener noreferrer"&gt;demo&lt;/a&gt;), in an egui desktop app (we have a &lt;a href="https://github.com/chh-itt/auralis/tree/main/demos/egui-demo" rel="noopener noreferrer"&gt;demo&lt;/a&gt;), or in any other scenario involving "signal change → async task reaction → automatic cleanup."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Benchmarks are invaluable for design decisions.&lt;/strong&gt; The incremental Memo subscription diff — keeping shared dependencies and only subscribing to new ones — was motivated by a benchmark showing 30x slowdown at high change rates. We found the bug, fixed it, and the performance returned to expected levels.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Was found a non-obvious bug by building a Wasm demo.&lt;/strong&gt; The &lt;code&gt;TaskScope&lt;/code&gt; clone-from-&lt;code&gt;find_scope&lt;/code&gt; was setting &lt;code&gt;cancelled = true&lt;/code&gt; on the shared inner when dropped after a task poll, silently preventing future spawns. This only manifests in long-running apps (like Wasm pages) where the scope outlives a single flush cycle. The fix — checking &lt;code&gt;Rc::strong_count == 1&lt;/code&gt; before cancelling — matches &lt;code&gt;Memo::drop&lt;/code&gt;'s behavior.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The crates are on crates.io at v0.1.6:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;auralis-signal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.1"&lt;/span&gt;
&lt;span class="py"&gt;auralis-task&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All demos are runnable — clone the repo and &lt;code&gt;cargo run&lt;/code&gt; / &lt;code&gt;trunk serve&lt;/code&gt; any of them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/chh-itt/auralis" rel="noopener noreferrer"&gt;github.com/chh-itt/auralis&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://docs.rs/auralis-signal" rel="noopener noreferrer"&gt;docs.rs/auralis-signal&lt;/a&gt; / &lt;a href="https://docs.rs/auralis-task" rel="noopener noreferrer"&gt;docs.rs/auralis-task&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If you're building something reactive in Rust and want a kernel that stays out of your way — or if you have opinions on the API — I'd love to hear from you.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
