Skip to content

feat: auto-dispose effect/scope on setup failure, upgrade alien_signals to 2.3.1#42

Merged
medz merged 5 commits into
mainfrom
simplify-auto-dispose
Jun 6, 2026
Merged

feat: auto-dispose effect/scope on setup failure, upgrade alien_signals to 2.3.1#42
medz merged 5 commits into
mainfrom
simplify-auto-dispose

Conversation

@medz

@medz medz commented Jun 6, 2026

Copy link
Copy Markdown
Owner

Summary

Upgrade alien_signals to 2.3.1 and implement auto-dispose on setup failure at the oref layer.

Context

Upstream alien-signals-dart#38 removed the Dart-only auto-dispose-on-setup-failure behavior, treating it as a lifecycle policy decision for downstream code. This PR implements that policy in oref's wrapper layer and leverages new 2.3.1 features to simplify the codebase.

Changes

Dependency

  • alien_signals: ^2.2.0^2.3.1

Auto-dispose on setup failure

  • Added catch blocks in _createEffect() and _createEffectScope() that auto-dispose partially-initialized nodes when the callback throws

Simplifications (leveraging alien_signals 2.3.1)

  • _OrefEffectScope now extends EffectScopeNode (new in 2.3.1) instead of ReactiveNode, giving proper propagation support
  • call() methods simplified: removed 28 lines of manual deps iteration + switch/case dispatch, replaced with alien.stop(this) which dispatches to 2.3.1's built-in stopEffect()/ stopScope()
  • Renamed cleanup_userCleanup to avoid shadowing EffectNode.cleanup (new built-in cleanup mechanism in 2.3.1)

Tests

  • 5 new auto-dispose tests covering both Dart (context: null) and Flutter widget (BuildContext) contexts

Diff

  • -28 lines of business code (effect.dart -11, effect_scope.dart -11 + catch blocks)

Summary by CodeRabbit

  • Bug Fixes

    • Effects now auto-dispose on initialization failures, ensuring no lingering subscriptions; cleanup runs before dispose and stopping during a run is handled correctly.
    • Child effects and scopes are properly disposed when a parent scope fails to initialize; improved error handling during effect execution for reliable recovery.
  • Tests

    • Added extensive unit and widget tests covering auto-dispose, setup-failure cleanup, stop-during-run, and post-stop read behaviors.
  • Chores

    • Updated signal library dependency for stability.

…ls to 2.3.1

- Upgrade alien_signals from ^2.2.0 to ^2.3.1
- Add auto-dispose catch blocks in _createEffect/_createEffectScope
- _OrefEffectScope extends EffectScopeNode (new in 2.3.1) instead of ReactiveNode
- Rename _OrefEffect.cleanup -> _userCleanup to avoid shadowing EffectNode.cleanup
- Simplify call() methods: delegate child disposal to alien.stop(), removing manual deps iteration
- Add 5 auto-dispose tests covering Dart and Flutter widget contexts
@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@medz, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 12 minutes and 23 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 917bb5b0-d54d-4f6f-a183-23ea2553cd84

📥 Commits

Reviewing files that changed from the base of the PR and between 3584b17 and 58d63fd.

📒 Files selected for processing (3)
  • lib/src/core/effect.dart
  • lib/src/core/effect_scope.dart
  • test/core/effect_test.dart
📝 Walkthrough

Walkthrough

Effect and effect-scope setup now auto-dispose created resources when their setup callbacks throw. Internal cleanup storage was renamed, effect execution gets a try/catch to trigger disposal on throw, effect-scope base class changed to EffectScopeNode, manual dependency traversal was removed, and tests were added (including widget tests). Dependency alien_signals updated to ^2.3.1.

Changes

Auto-dispose on setup failure

Layer / File(s) Summary
Dependency upgrade
pubspec.yaml
Alien signals library updated to ^2.3.1, providing new EffectScopeNode base class and APIs for simplified effect scope management.
Effect auto-dispose on failure
lib/src/core/effect.dart
_createEffect wraps effect callback execution in try/catch to auto-dispose before rethrowing; internal cleanup field renamed to _userCleanup and wrapper invocation updated; _OrefEffect.call() removes manual dependency link traversal and relies on node stopping.
Effect scope auto-dispose on failure
lib/src/core/effect_scope.dart
effectScope wraps callback in try/catch to auto-dispose scope before rethrowing; _OrefEffectScope switches to alien.EffectScopeNode with mutable flags and drops manual dependency traversal, relying on node stopping and finalizer detachment.
Auto-dispose behavior validation
test/core/effect_test.dart
New and updated test groups verify effect and scope setup failures trigger resource disposal and leave no lingering subscriptions; includes widget-context variants covering Flutter builder execution paths.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • medz/oref#32: The cleanup refactoring and simplified _OrefEffect.call() / _OrefEffectScope.call() logic interacts with widget element disposer registration changes in that PR.

Poem

🐰 I hopped in, saw effects unwind,
When setups broke, no traces left behind.
Scopes bow out and clean their nest,
Tests confirm the burrow's rest.
Hooray — tidy runs and restful rest.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately summarizes the two main changes: auto-disposal of effects/scopes on setup failure and the upgrade to alien_signals 2.3.1. It is concise, specific, and clearly reflects the core objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch simplify-auto-dispose

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f1893151c4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/src/core/effect_scope.dart

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
test/core/effect_test.dart (3)

646-681: 💤 Low value

Consider whether explicit type annotation and cast are necessary.

The test uses signal<Object?>(null, 0) with an explicit type parameter, then later casts to WritableSignal<Object?> when calling .set(1) (line 677). In contrast, the non-widget tests (line 569) use signal(null, 0) and call .set() directly without casting (line 583).

The explicit type and cast may be required due to type inference in the widget builder context, or they might be unnecessary verbosity. Consider testing whether this simpler form works:

final source = signal(null, 0);
// ...
source.set(1);

If the cast is genuinely required, a brief comment explaining why would help future maintainers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/core/effect_test.dart` around lines 646 - 681, The test creates `source`
as signal<Object?>(null, 0) then casts to `WritableSignal<Object?>` to call
`.set(1)`; simplify by letting type inference pick the signal type (use `final
source = signal(null, 0)`) and call `source.set(1)` directly in the
failing-effect widget test (`testWidgets('failed effect setup in widget does not
leave subscription'...)`), and if the cast is actually required by the widget
builder context keep the current explicit type but add a short comment
explaining why the `WritableSignal` cast is necessary to aid future maintainers.

611-642: 💤 Low value

Consider moving this test to a separate group.

This test verifies that stopping an effect during its run prevents subsequent signal reads from creating subscriptions. While the test is correct and valuable, it doesn't test "setup failure" — it tests mid-run disposal behavior. The group name "Effect auto-dispose on setup failure" suggests all tests should verify setup-time error handling.

Consider either:

  • Moving this test to a new group like "Effect disposal during execution"
  • Renaming the group to "Effect auto-dispose behavior"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/core/effect_test.dart` around lines 611 - 642, The test named 'stopped
effect does not subscribe to signals read later in the same run' belongs to a
different behavior than "setup failure" — move this test (the test block that
creates rerun, readAfterStop, stopDuringRun and calls effect/stop) into a new
test group (e.g., "Effect disposal during execution") or rename the existing
group to a more general title like "Effect auto-dispose behavior" so the test's
intent (stopping an effect mid-run preventing later subscriptions) matches its
group; update the surrounding group(...) or group name accordingly to keep tests
organized and semantically correct.

683-721: 💤 Low value

Same type annotation and cast pattern as previous test.

This test has the same explicit type annotation and cast pattern as the previous test (lines 686, 717). See the previous comment for suggestions on potential simplification.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/core/effect_test.dart` around lines 683 - 721, The test uses an explicit
generic on signal and later casts it to WritableSignal; replace the explicit
type and cast by letting type inference produce a writable signal: change the
declaration using signal(...) (e.g. var source = signal(null, 0); or final
source = signal(null, 0);) and remove the explicit (source as
WritableSignal<Object?>) cast before calling set, using source.set(1) directly;
locate usages of signal(...) and the cast in this test to update them
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/src/core/effect.dart`:
- Around line 111-112: The onEffectCleanup callback stored in
_OrefEffect._userCleanup must be invoked during the stop/teardown path because
_OrefEffect.call() (used by stop and the setup-failure catch in _createEffect())
currently does not run _userCleanup; change _OrefEffect.call() (or the
stop/teardown branch it uses) to drain and invoke _userCleanup with the same
semantics as the drain in _wrapEffectCallback() so user-managed resources are
cleaned on stop or setup failure, or alternatively wire _userCleanup into the
existing inherited cleanup mechanism so the cleanup runs immediately when
stop/teardown executes.

---

Nitpick comments:
In `@test/core/effect_test.dart`:
- Around line 646-681: The test creates `source` as signal<Object?>(null, 0)
then casts to `WritableSignal<Object?>` to call `.set(1)`; simplify by letting
type inference pick the signal type (use `final source = signal(null, 0)`) and
call `source.set(1)` directly in the failing-effect widget test
(`testWidgets('failed effect setup in widget does not leave subscription'...)`),
and if the cast is actually required by the widget builder context keep the
current explicit type but add a short comment explaining why the
`WritableSignal` cast is necessary to aid future maintainers.
- Around line 611-642: The test named 'stopped effect does not subscribe to
signals read later in the same run' belongs to a different behavior than "setup
failure" — move this test (the test block that creates rerun, readAfterStop,
stopDuringRun and calls effect/stop) into a new test group (e.g., "Effect
disposal during execution") or rename the existing group to a more general title
like "Effect auto-dispose behavior" so the test's intent (stopping an effect
mid-run preventing later subscriptions) matches its group; update the
surrounding group(...) or group name accordingly to keep tests organized and
semantically correct.
- Around line 683-721: The test uses an explicit generic on signal and later
casts it to WritableSignal; replace the explicit type and cast by letting type
inference produce a writable signal: change the declaration using signal(...)
(e.g. var source = signal(null, 0); or final source = signal(null, 0);) and
remove the explicit (source as WritableSignal<Object?>) cast before calling set,
using source.set(1) directly; locate usages of signal(...) and the cast in this
test to update them accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 21606d44-a89a-4174-894c-cc18aa4b4149

📥 Commits

Reviewing files that changed from the base of the PR and between c279fe0 and f189315.

⛔ Files ignored due to path filters (1)
  • example/pubspec.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • lib/src/core/effect.dart
  • lib/src/core/effect_scope.dart
  • pubspec.yaml
  • test/core/effect_test.dart

Comment thread lib/src/core/effect.dart
- Remove unnecessary explicit type annotations and WritableSignal casts in widget tests
- Rename group from 'auto-dispose on setup failure' to 'auto-dispose behavior' since it also covers mid-run disposal
medz

This comment was marked as spam.

Previously _OrefEffect.call() skipped _userCleanup, so cleanup registered via
onEffectCleanup() during setup was silently dropped when setup failed and
auto-dispose fired. Drain _userCleanup before onDispose in the stop path
to match alien_signals 2.3.1's stopEffect() semantics.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 02bb160ef6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/src/core/effect.dart
Comment thread pubspec.yaml
When an effect calls its own disposer inside the callback, activeSub is
still the effect until _wrapEffectCallback returns. In alien_signals 2.3.1,
SignalNode.get() unconditionally links any non-null activeSub, so signals
read after alien.stop(this) would re-link to the disposed effect.

This didn't cause re-execution (run() guards against it), but caused
stale dependency links and unnecessary propagation on every signal change.

Fix: check if activeSub === this after stop, and clear it if so.
@medz

medz commented Jun 6, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

if (prevSub != null && !detach) {
alien.link(effect, prevSub, 0);

P1 Badge Mark active parents when linking child effects

When this wrapper creates an effect under another active effect/scope, it only calls alien.link(effect, prevSub, 0), but alien_signals 2.3.1's own surface API also sets the parent's child-effect flag. Without that flag, alien.run() does not dispose child deps before a parent effect re-runs, so child effects from the previous run remain subscribed until purgeDeps; if the parent callback writes a signal during setup, those stale children can fire or accumulate duplicate work. Please set the parent child-effect flag when linking child effects, and apply the same pattern in _createEffectScope().

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

In alien_signals 2.3.1, both effect() and effectScope() set the hasChildEffect
flag (64) on the parent when linking a child. Without this flag, run() does
not call disposeChildDepsInReverse() before the parent re-executes, so stale
child effects from a previous run stay linked and may trigger during the
parent's callback — causing duplicate work or side effects.

Reproduced by test: parent creates child on first run, then writes to the
child's dependency during re-run. Without hasChildEffect, the stale child
fires (childRuns=2). With the flag, it's disposed first (childRuns=1).
@medz medz merged commit a24e4a6 into main Jun 6, 2026
3 checks passed
@medz medz deleted the simplify-auto-dispose branch June 6, 2026 19:02
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