Conversation
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.
📝 WalkthroughWalkthroughAdds an AsyncDerivedCell implementation with pending/error tracking, cancellation and disposal hooks; introduces Changes
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
commit: |
This pull request significantly refactors the library's approach to asynchronous state management. It replaces the imperative
Cell.asyncutility with a reactiveCell.derivedAsyncmodel, introducesCell.createCompositefor synchronizing multiple reactive sources, and removes utility functions for flattening cells. Additionally, it fixes issues regarding nested batch operations.Breaking Changes
Cell.async: TheAsyncRequestAtomstype and theCell.asyncstatic method have been removed. Asynchronous operations should now be modeled usingCell.derivedAsync.Cell.flatten,Cell.flattenArray, andCell.flattenObjecthave been removed from the public API.New Features
Cell.derivedAsyncIntroduced
AsyncDerivedCell, accessible viaCell.derivedAsync. This allows for the creation of derived cells that compute their values asynchronously while maintaining reactive dependency tracking.(get, signal) => Promise<T>.get: A function used to read values from other cells, registering them as dependencies.signal: AnAbortSignalthat is triggered when dependencies change, allowing for the cancellation of stale in-flight requests.pending(Cell<boolean>) anderror(Cell<Error | null>) properties to track the state of the computation.peek()to read the current promise/value without registering a dependency.revalidate()to manually trigger a re-computation.Cell.createCompositeIntroduced a utility to manage a group of cells (synchronous or asynchronous) as a single synchronized unit.
valuesobject ensures that if any input cell is pending, all output accessors wait until all inputs have settled.pendinganderrorcells that aggregate the state of all input cells.Internal Changes and Improvements
Cell.batchlogic. Nested batch calls now correctly merge theirBATCHED_EFFECTSandUPDATE_BUFFERinto the parent batch context rather than executing prematurely or being lost.triggerUpdateto handleAsyncDerivedCell. The update loop now yields control for async cells, allowing them to manage propagation via internal Promise chains rather than synchronous recursion.LocalContextto properly dispose ofAsyncDerivedCellinstances, ensuringAbortControllersignals are fired when a context is destroyed.AsyncDerivedCellimplements deep equality checks on resolved values to prevent unnecessary downstream updates if the resolved value has not changed.Testing
Cell.derivedAsynccovering:AbortSignalusage.Cell.createCompositecovering synchronization and error aggregation.Cell.batchoperations.Cell.asyncand flattening.Summary by CodeRabbit
New Features
Documentation
Chores
✏️ Tip: You can customize this high-level summary in your review settings.