Skip to content

Async Derived Cells#8

Merged
adebola-io merged 18 commits intomainfrom
derived-async
Jan 31, 2026
Merged

Async Derived Cells#8
adebola-io merged 18 commits intomainfrom
derived-async

Conversation

@adebola-io
Copy link
Owner

@adebola-io adebola-io commented Jan 31, 2026

This pull request significantly refactors the library's approach to asynchronous state management. It replaces the imperative Cell.async utility with a reactive Cell.derivedAsync model, introduces Cell.createComposite for synchronizing multiple reactive sources, and removes utility functions for flattening cells. Additionally, it fixes issues regarding nested batch operations.

Breaking Changes

  • Removed Cell.async: The AsyncRequestAtoms type and the Cell.async static method have been removed. Asynchronous operations should now be modeled using Cell.derivedAsync.
  • Removed Flattening Utilities: Cell.flatten, Cell.flattenArray, and Cell.flattenObject have been removed from the public API.

New Features

Cell.derivedAsync

Introduced AsyncDerivedCell, accessible via Cell.derivedAsync. This allows for the creation of derived cells that compute their values asynchronously while maintaining reactive dependency tracking.

  • Signature: Accepts a callback (get, signal) => Promise<T>.
    • get: A function used to read values from other cells, registering them as dependencies.
    • signal: An AbortSignal that is triggered when dependencies change, allowing for the cancellation of stale in-flight requests.
  • State Cells: The instance exposes pending (Cell<boolean>) and error (Cell<Error | null>) properties to track the state of the computation.
  • Behavior:
    • Automatically re-runs the computation when dependencies change.
    • Prevents race conditions by aborting previous runs and only committing the result of the latest run.
    • Supports peek() to read the current promise/value without registering a dependency.
    • Supports revalidate() to manually trigger a re-computation.

Cell.createComposite

Introduced a utility to manage a group of cells (synchronous or asynchronous) as a single synchronized unit.

  • Barrier Synchronization: The returned values object ensures that if any input cell is pending, all output accessors wait until all inputs have settled.
  • Unified State: Provides unified pending and error cells that aggregate the state of all input cells.

Internal Changes and Improvements

  • Nested Batching: Fixed Cell.batch logic. Nested batch calls now correctly merge their BATCHED_EFFECTS and UPDATE_BUFFER into the parent batch context rather than executing prematurely or being lost.
  • Update Cycle: Modified triggerUpdate to handle AsyncDerivedCell. The update loop now yields control for async cells, allowing them to manage propagation via internal Promise chains rather than synchronous recursion.
  • Context Disposal: Updated LocalContext to properly dispose of AsyncDerivedCell instances, ensuring AbortController signals are fired when a context is destroyed.
  • Deep Equality: AsyncDerivedCell implements deep equality checks on resolved values to prevent unnecessary downstream updates if the resolved value has not changed.

Testing

  • Added extensive test suites for Cell.derivedAsync covering:
    • Basic lifecycle and state transitions.
    • Race condition handling and AbortSignal usage.
    • Error propagation and recovery.
    • Chaining of multiple async cells.
    • Diamond dependency resolution.
  • Added tests for Cell.createComposite covering synchronization and error aggregation.
  • Added regression tests for nested Cell.batch operations.
  • Removed obsolete tests related to Cell.async and flattening.

Summary by CodeRabbit

  • New Features

    • First-class async-derived cells with loading/error tracking, cancellation control, manual revalidation, and a public async API
    • Composite cells to join multiple cells into a unified container with shared pending/error state
  • Documentation

    • Major rewrite of API docs: restructured sections, expanded examples for async-derived patterns, custom equality, composition, and diagnostics
  • Chores

    • CI test step updated to run tests with the new test runner invocation

✏️ Tip: You can customize this high-level summary in your review settings.

Track an AbortSignal alongside each upstream promise in
AsyncDerivedCell. This ensures that if a parent discards a promise,
children do not remain stuck waiting for it in get().
Prevent calling update() on the initial resolution of an
AsyncDerivedCell. Ensure that get() properly waits for any pending
computation before returning the current value. Refactor internal value
change detection to use a controlled promise for more predictable
resolution.
Covers various scenarios including:
- Basic state transitions and pending status
- Race conditions and rapid update handling
- Deep equality suppression for results
- Error handling and stale-while-revalidate behavior
- AbortSignal propagation and AbortError handling
- Chaining multiple async cells
- Listener behavior and batching support
Clone the effects array during updates to ensure all listeners,
including those with `once: true`, are called correctly.

Improve AsyncDerivedCell consistency by refining notification
propagation and using `peek()` in `get()` to avoid unnecessary
dependency tracking. Added extensive tests for complex async dependency
graphs.
Merge nested batched effects and update buffers into the parent context
to prevent them from being lost when finishing a nested batch operation.
Implement explicit disposal to ensure downstream cells are released when
an ancestor is destroyed. This prevents deadlocks where children wait
indefinitely for disposed upstream promises.

This change refactors upstream tracking to use a Set and adds internal
AbortController management to cancel pending computations.
Manually trigger a recomputation of async cells and abort any in-flight
computation.
Use a run ID to track computation cycles and maintain a set of
consumed dependencies. This allows skipping restarts for pending
downstream cells that have not yet accessed the updated parent cell.
Includes test cases for combining sync and async cells, error
propagation,
barrier synchronization, and pending state tracking. Also updates the
method's documentation for clarity.
@coderabbitai
Copy link

coderabbitai bot commented Jan 31, 2026

📝 Walkthrough

Walkthrough

Adds an AsyncDerivedCell implementation with pending/error tracking, cancellation and disposal hooks; introduces Cell.derivedAsync() and Cell.createComposite() factories; integrates async-derived lifecycle with LocalContext; restructures README async/equality docs; and changes the CI test step command.

Changes

Cohort / File(s) Summary
CI Workflow
\.github/workflows/pkg-pr.yml
Changed test execution step from bun test to bun run test --run.
Documentation
README.md
Large rework of API sections: renumbering/reordering, replaces prior Async Operations with custom equality and detailed async-derived guidance, adds composition/chaining examples and diagnostics.
Core async implementation
library/classes.js
Introduced AsyncDerivedCell class and symbol DisposeAsyncCell; added Cell.derivedAsync() and Cell.createComposite() factories; added pending/error state cells, get/peek/revalidate APIs; wired async disposal into LocalContext; updated triggerUpdate/batching and removed legacy async helpers; added JSDoc typedefs (ComputedFn, Composite).

Sequence Diagram

sequenceDiagram
    participant App as Application
    participant Cell as AsyncDerivedCell
    participant Compute as Async Callback
    participant State as Pending/Error Cells
    participant Dep as Dependent Cells
    participant Context as LocalContext

    App->>Cell: revalidate()
    Cell->>Cell: if already pending? (skip)
    alt not pending
        Cell->>State: set pending = true
        Cell->>Compute: invoke callback(AbortSignal)
        Compute->>Compute: async work
        alt success
            Compute-->>Cell: result
            Cell->>State: set error = null
            Cell->>Dep: notify dependents with new value
        else error or abort
            Compute-->>Cell: throw Error / AbortError
            Cell->>State: set error = Error
            Cell->>Dep: notify dependents of error state
        end
        Cell->>State: set pending = false
    end

    App->>Context: destroy()
    Context->>Cell: call [DisposeAsyncCell]()
    Cell->>Compute: abort pending work
    Cell->>Cell: cleanup internal resources
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰
I hopped through signals, pending bright and airy,
Async leaves rustle—errors handled, not scary.
Compose my clover, cancel with a cheer,
Cells hum together—cleanups draw near.
Hop on, new features, springtime in the code!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Async Derived Cells' directly and concisely summarizes the main feature addition in the pull request.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch derived-async

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@library/classes.js`:
- Around line 1279-1289: The method `#notifyUpstreamOnly` currently uses return
inside the loop which prematurely exits and prevents remaining children from
being processed; change that early return to continue so the loop skips the
current child but continues iterating, keeping the checks on child instanceof
AsyncDerivedCell, child.#upstream.has(promise) -> continue, and ensuring you
still call child.#upstream.add(promise), promise.finally(() =>
child.#upstream.delete(promise)), and child.#notifyUpstreamOnly(promise) for
other children.
- Around line 168-180: Inside the AsyncDerivedCell handling block, the loop over
cell.derivations incorrectly checks "cell instanceof AsyncDerivedCell" (always
true) so no computed derivations are pushed; change that in the for-loop to
check the derived item instead: replace the bogus condition with a check on
computedCell using "computedCell instanceof AsyncDerivedCell" so async derived
cells are skipped but synchronous computed cells are pushed to UPDATE_BUFFER and
marked via computedCell[IsScheduled].
- Around line 1243-1272: In `#notify`, don't exit the entire method when a child
already has the promise; replace the early "return" at the
child.#upstream.has(promise) check with a "continue" so the loop proceeds to
other children in this.derivations (ensuring diamond-shaped dependency graphs
all get notified); keep the existing logic that adds the promise to
child.#upstream and calls child.#notifyUpstreamOnly(promise) only for children
that didn't already have it.

- Correct variable check in triggerUpdate
- Ensure state resolution for outdated runs
- Continue notifying other children instead of returning early
@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 31, 2026

Open in StackBlitz

npm i https://pkg.pr.new/adebola-io/cells/@adbl/cells@8

commit: 53242e3

@adebola-io adebola-io merged commit b9da871 into main Jan 31, 2026
3 checks passed
@adebola-io adebola-io deleted the derived-async branch January 31, 2026 18:08
@coderabbitai coderabbitai bot mentioned this pull request Feb 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant