Skip to content

Improve route lifecycle management in Brouter (#12642)#12643

Open
msynk wants to merge 13 commits into
bitfoundation:developfrom
msynk:12642-brouter-lifecycle-management
Open

Improve route lifecycle management in Brouter (#12642)#12643
msynk wants to merge 13 commits into
bitfoundation:developfrom
msynk:12642-brouter-lifecycle-management

Conversation

@msynk

@msynk msynk commented Jul 12, 2026

Copy link
Copy Markdown
Member

closes #12642

Summary by CodeRabbit

  • New Features
    • Added route lifecycle support via IBrouterRoute / BrouterRouteBase, including activation/renavigation/deactivation payloads and keep-alive deactivation reasons.
    • Added component-level navigation locks with cancellable, awaited pre-navigation/renavigation hooks (including new lock contexts).
  • Bug Fixes
    • Improved lifecycle dispatch timing across outlets/keep-alive and made lifecycle errors non-blocking so navigation continues reliably.
  • Breaking Changes
    • Removed BrouterKeepAliveContext.
    • Removed Broute’s BindComponentParametersByName.
  • Tests
    • Expanded lifecycle, keep-alive, navigation-lock, and parameter-binding coverage.
  • Documentation
    • Updated routing/lifecycle/keep-alive and parameter-binding guidance; added view-transition and scrollbar-gutter notes.

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Brouter adds unified route lifecycle callbacks, component-level navigation locks, stable lifecycle contexts, staged arrivals, host-aware outlet dispatch, unified parameter binding, and updated demos and tests.

Changes

Route lifecycle management

Layer / File(s) Summary
Lifecycle contracts and contexts
src/Brouter/Bit.Brouter/BrouterRouteActivation.cs, src/Brouter/Bit.Brouter/BrouterRouteContext.cs, src/Brouter/Bit.Brouter/BrouterRouteBase.cs, src/Brouter/Bit.Brouter/IBrouterRoute.cs, src/Brouter/Bit.Brouter/BrouterRouteDeactivatingContext.cs, src/Brouter/Bit.Brouter/BrouterRouteRenavigatingContext.cs
Adds typed lifecycle payloads, stable route contexts, IBrouterRoute, BrouterRouteBase, and awaited navigation-lock callbacks.
Stable content contexts and rendering
src/Brouter/Bit.Brouter/BrouterRouteRenderer.cs, src/Brouter/Bit.Brouter/BrouterOutlet.cs, src/Brouter/Bit.Brouter/Broute.cs
Uses stable contexts for inline and outlet-hosted content, cascades them to components, captures route handlers, resolves outlet hosts, applies unified parameter binding, and emits teardown notifications.
Navigation staging and locks
src/Brouter/Bit.Brouter/Brouter.cs, src/Brouter/Tests/Bit.Brouter.Tests/NavigationLockTests.cs, src/Brouter/InteralDemos/Core/Pages/NavigationLockPage.razor
Stages arrivals after committed rendering, notifies departures before replacement, handles superseded navigations and error paths, and supports cancellation, redirect, and asynchronous confirmation flows.
Examples, documentation, and validation
src/Brouter/InteralDemos/Core/*, src/Brouter/README.md, src/Brouter/Tests/Bit.Brouter.Tests/*
Migrates examples and tests to route lifecycle callbacks, keep-alive reasons, interface discovery, query binding, and parameter-binding validation.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Navigation
  participant Brouter
  participant RouteContext
  participant RoutedComponent
  Navigation->>Brouter: Start navigation
  Brouter->>RouteContext: Run deactivating or renavigating locks
  RouteContext->>RoutedComponent: Request cancel, redirect, or allow
  Brouter->>RouteContext: Notify departures and stage arrivals
  Brouter->>RoutedComponent: Commit rendered content
  Brouter->>RouteContext: Flush arrivals after render
Loading

Suggested reviewers: yasmoradi, cyrus-sushiant

Poem

A rabbit hops through routes,
With callbacks tucked in sprouts.
Arrivals bloom bright,
Departures hide light,
And locks guard the way out.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.53% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the PR’s main route lifecycle improvements.
Linked Issues check ✅ Passed The changes implement route lifecycle API and behavior improvements aligned with issue #12642’s request for a clearer, more usable lifecycle model.
Out of Scope Changes check ✅ Passed The additional docs, demos, tests, and parameter-binding updates all support the lifecycle API and clarity goals, with no clear unrelated code changes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@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: 4

🧹 Nitpick comments (1)
src/Brouter/InteralDemos/Core/Pages/StickyNotePage.razor (1)

81-87: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider !activation.IsFirstActivation over is false for consistency.

activation.IsFirstActivation is false works but is less idiomatic than !activation.IsFirstActivation. The is false pattern is typically reserved for nullable bool checks or pattern-matching contexts. This is a minor readability nit.

♻️ Suggested refactor
-        if (activation.IsFirstActivation is false) _reactivations++;
+        if (!activation.IsFirstActivation) _reactivations++;
🤖 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 `@src/Brouter/InteralDemos/Core/Pages/StickyNotePage.razor` around lines 81 -
87, Update the condition in OnActivated to use the idiomatic negation
!activation.IsFirstActivation instead of the is false pattern, preserving the
existing reactivation behavior.
🤖 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 `@src/Brouter/Bit.Brouter/Brouter.cs`:
- Around line 620-632: Decouple _pendingLifecycleFlush from _navVersion by
introducing a separate lifecycle navigation generation that increments only when
navigation starts. Store that generation when staging entries, and in the flush
logic around OnAfterRenderAsync compare each entry against the live generation
inside the loop rather than capturing it once, so revalidation cannot invalidate
valid arrivals and later nodes are skipped only when superseded by a newly
started navigation.
- Around line 645-654: Abort the in-progress navigation immediately when
synchronous lifecycle callbacks supersede its token/version. Add checks directly
after each departure or arrival preparation/notification dispatch in the
affected commit paths, including NotifyChainDepartures and the referenced commit
sections, before mutating _committedChain, error state, calling SetMatched, or
queuing rendering; return through the existing stale-navigation cancellation
path rather than continuing until the late check.

In `@src/Brouter/Bit.Brouter/BrouterOutlet.cs`:
- Around line 114-117: Snapshot _kept with ToArray() before each
lifecycle-dispatch foreach, including the loops around NotifyEntryTeardown and
the highlighted locations near lines 185-191, 215-220, 237-243, and 422-423.
Ensure handlers can mutate _kept without invalidating the active enumeration.

In `@src/Brouter/Bit.Brouter/BrouterRouteBase.cs`:
- Around line 69-76: Update the StateHasChanged calls in both activation-related
paths, including the explicit interface implementation
IBrouterRoute.OnActivatedAsync and the additional occurrence noted in the
comment, to execute only when _disposed is false and IsActive is true. Preserve
the existing lifecycle callback and repaint behavior for active, undisposed
components.

---

Nitpick comments:
In `@src/Brouter/InteralDemos/Core/Pages/StickyNotePage.razor`:
- Around line 81-87: Update the condition in OnActivated to use the idiomatic
negation !activation.IsFirstActivation instead of the is false pattern,
preserving the existing reactivation behavior.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: cb2afe75-dc44-4a83-8118-28e74ea3e885

📥 Commits

Reviewing files that changed from the base of the PR and between 7529785 and ad8a1e0.

📒 Files selected for processing (21)
  • src/Brouter/Bit.Brouter/Broute.cs
  • src/Brouter/Bit.Brouter/Brouter.cs
  • src/Brouter/Bit.Brouter/BrouterKeepAliveContext.cs
  • src/Brouter/Bit.Brouter/BrouterOutlet.cs
  • src/Brouter/Bit.Brouter/BrouterRouteActivation.cs
  • src/Brouter/Bit.Brouter/BrouterRouteBase.cs
  • src/Brouter/Bit.Brouter/BrouterRouteContext.cs
  • src/Brouter/Bit.Brouter/BrouterRouteDeactivation.cs
  • src/Brouter/Bit.Brouter/BrouterRouteDeactivationReason.cs
  • src/Brouter/Bit.Brouter/BrouterRouteRenavigation.cs
  • src/Brouter/Bit.Brouter/BrouterRouteRenderer.cs
  • src/Brouter/Bit.Brouter/IBrouterRoute.cs
  • src/Brouter/InteralDemos/Core/AppRouter.razor
  • src/Brouter/InteralDemos/Core/Pages/StickyNotePage.razor
  • src/Brouter/README.md
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveLifecycle.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/LifecyclePage.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/LifecycleProbe.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/RouteLifecycleTests.cs
💤 Files with no reviewable changes (1)
  • src/Brouter/Bit.Brouter/BrouterKeepAliveContext.cs

Comment thread src/Brouter/Bit.Brouter/Brouter.cs
Comment thread src/Brouter/Bit.Brouter/Brouter.cs
Comment thread src/Brouter/Bit.Brouter/BrouterOutlet.cs Outdated
Comment thread src/Brouter/Bit.Brouter/BrouterRouteBase.cs Outdated

@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.

🧹 Nitpick comments (1)
src/Brouter/Bit.Brouter/Brouter.cs (1)

2029-2051: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider explicit test coverage for chained/superseded navigations during the pending-UI window.

The departuresNotified/notifySurvivorsAsRemaining interplay only fires the "unmount everything" departure notification once per commit that actually shows the Navigating fragment; a second navigation that supersedes the first while _navigating is still true (and has no loaders of its own) skips this branch entirely and falls through to the default NotifyChainDepartures call later. This resolves correctly today because FireArrival ultimately reads BrouterRouteContext.IsActive (already set by whichever pipeline actually deactivated the content) rather than per-pipeline bookkeeping - but it's a subtle enough invariant (relying on state rather than notification counting) that a dedicated test for "navigate A (with loader) -> navigate B (no loader) before A's loader resolves, chain partially overlaps" would be valuable to lock in the 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 `@src/Brouter/Bit.Brouter/Brouter.cs` around lines 2029 - 2051, Add a focused
test covering chained superseded navigation during the pending-UI window: start
navigation A with a loader, then navigate to loaderless B before A resolves,
with partially overlapping route chains. Assert the Navigating fragment and
departure notifications occur only as intended, and that final arrival behavior
follows BrouterRouteContext.IsActive rather than per-pipeline notification
counts. Use the existing navigation test harness and cover the
departuresNotified/NotifyChainDepartures path.
🤖 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.

Nitpick comments:
In `@src/Brouter/Bit.Brouter/Brouter.cs`:
- Around line 2029-2051: Add a focused test covering chained superseded
navigation during the pending-UI window: start navigation A with a loader, then
navigate to loaderless B before A resolves, with partially overlapping route
chains. Assert the Navigating fragment and departure notifications occur only as
intended, and that final arrival behavior follows BrouterRouteContext.IsActive
rather than per-pipeline notification counts. Use the existing navigation test
harness and cover the departuresNotified/NotifyChainDepartures path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: cb2afe75-dc44-4a83-8118-28e74ea3e885

📥 Commits

Reviewing files that changed from the base of the PR and between 7529785 and ad8a1e0.

📒 Files selected for processing (21)
  • src/Brouter/Bit.Brouter/Broute.cs
  • src/Brouter/Bit.Brouter/Brouter.cs
  • src/Brouter/Bit.Brouter/BrouterKeepAliveContext.cs
  • src/Brouter/Bit.Brouter/BrouterOutlet.cs
  • src/Brouter/Bit.Brouter/BrouterRouteActivation.cs
  • src/Brouter/Bit.Brouter/BrouterRouteBase.cs
  • src/Brouter/Bit.Brouter/BrouterRouteContext.cs
  • src/Brouter/Bit.Brouter/BrouterRouteDeactivation.cs
  • src/Brouter/Bit.Brouter/BrouterRouteDeactivationReason.cs
  • src/Brouter/Bit.Brouter/BrouterRouteRenavigation.cs
  • src/Brouter/Bit.Brouter/BrouterRouteRenderer.cs
  • src/Brouter/Bit.Brouter/IBrouterRoute.cs
  • src/Brouter/InteralDemos/Core/AppRouter.razor
  • src/Brouter/InteralDemos/Core/Pages/StickyNotePage.razor
  • src/Brouter/README.md
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveLifecycle.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/LifecyclePage.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/LifecycleProbe.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/RouteLifecycleTests.cs
💤 Files with no reviewable changes (1)
  • src/Brouter/Bit.Brouter/BrouterKeepAliveContext.cs

@msynk

msynk commented Jul 12, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full-review

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

@msynk Sure, I'll perform a full review of all the changes in this PR.

✅ Action performed

Full review finished.

@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

🤖 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 `@src/Brouter/Bit.Brouter/BrouterRouteRenderer.cs`:
- Around line 102-194: Snapshot _keptEntries with ToArray() before each
lifecycle callback enumeration in NotifyDeparture, PrepareArrival,
NotifyTeardown, and FireArrival. Iterate the snapshot while invoking
FireDeactivated or FireArrival so reentrant ClearKeepAlive mutations cannot
invalidate the enumeration, preserving the existing callback behavior.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e2736e39-b3e4-4831-9f0e-c3b044fe38e2

📥 Commits

Reviewing files that changed from the base of the PR and between 7529785 and 16ebc3a.

📒 Files selected for processing (26)
  • src/Brouter/Bit.Brouter/Broute.cs
  • src/Brouter/Bit.Brouter/Brouter.cs
  • src/Brouter/Bit.Brouter/BrouterKeepAliveContext.cs
  • src/Brouter/Bit.Brouter/BrouterOutlet.cs
  • src/Brouter/Bit.Brouter/BrouterRouteActivation.cs
  • src/Brouter/Bit.Brouter/BrouterRouteBase.cs
  • src/Brouter/Bit.Brouter/BrouterRouteContext.cs
  • src/Brouter/Bit.Brouter/BrouterRouteDeactivation.cs
  • src/Brouter/Bit.Brouter/BrouterRouteDeactivationReason.cs
  • src/Brouter/Bit.Brouter/BrouterRouteRenavigation.cs
  • src/Brouter/Bit.Brouter/BrouterRouteRenderer.cs
  • src/Brouter/Bit.Brouter/IBrouterRoute.cs
  • src/Brouter/InteralDemos/Core/AppRouter.razor
  • src/Brouter/InteralDemos/Core/DemoState.cs
  • src/Brouter/InteralDemos/Core/Pages/HomePage.razor
  • src/Brouter/InteralDemos/Core/Pages/LifecyclePage.razor
  • src/Brouter/InteralDemos/Core/Pages/LifecyclePulse.razor
  • src/Brouter/InteralDemos/Core/Pages/StickyNotePage.razor
  • src/Brouter/InteralDemos/Core/Shared/Header.razor
  • src/Brouter/README.md
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveLifecycle.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/LifecyclePage.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/LifecycleProbe.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/RouteLifecycleTests.cs
💤 Files with no reviewable changes (1)
  • src/Brouter/Bit.Brouter/BrouterKeepAliveContext.cs

Comment thread src/Brouter/Bit.Brouter/BrouterRouteRenderer.cs Outdated
@msynk

msynk commented Jul 12, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full-review

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

@msynk Sure, I'll perform a full review of all the changes in this PR again.

✅ Action performed

Full review finished.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Brouter/Bit.Brouter/Brouter.cs (1)

1310-1349: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Stale-navigation guard missing in HandleAppNotFoundAsync.

Every other NotifyChainDepartures call site in this file was hardened (per the earlier "Abort stale commits after synchronous lifecycle callbacks" review round) with a token.IsCancellationRequested || version != _navVersion check immediately afterward, before any further commit-state mutation. This call site was missed: after NotifyChainDepartures(...) at lines 1324-1328 (and again after await OnNotFound(location) at line 1334), there is no check before unconditionally overwriting _committedChain/_currentRouteData and potentially calling NavigateInternal/StateHasChanged().

Since a departure/OnNotFound callback can run synchronous code that starts a brand-new navigation on the same dispatcher (InvokeAsync runs inline when already on it), that nested navigation's freshly-committed _committedChain/_currentRouteData would be clobbered when control returns here. This method has no _navVersion/token of its own, so the exact guard used elsewhere doesn't transplant directly - it needs its own staleness check (e.g., comparing CurrentLocation against the captured location).

🐛 Proposed fix
                 NotifyChainDepartures(
                     new BrouterNavigationContext(location, location, CancellationToken.None),
                     _committedChain);
+                // A departure callback may have synchronously started a new navigation; abandon
+                // this stale continuation instead of clobbering its committed state.
+                if (string.Equals(CurrentLocation.FullUri, location.FullUri, StringComparison.Ordinal) is false) return;
                 _committedChain = [];
                 _currentRouteData = null;

                 if (OnNotFound is not null) await OnNotFound(location);

+                if (string.Equals(CurrentLocation.FullUri, location.FullUri, StringComparison.Ordinal) is false) return;
+
                 if (string.IsNullOrEmpty(NotFoundUrl) is false && IsSamePath(location.Path, NotFoundUrl) is false)
🤖 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 `@src/Brouter/Bit.Brouter/Brouter.cs` around lines 1310 - 1349, Add
stale-navigation checks in HandleAppNotFoundAsync after NotifyChainDepartures
and after awaiting OnNotFound, using the captured location versus the current
navigation state. Return immediately when a nested navigation has changed the
location, before mutating _committedChain or _currentRouteData and before
calling NavigateInternal or StateHasChanged.
🤖 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 `@src/Brouter/Bit.Brouter/Brouter.cs`:
- Around line 1042-1084: The lifecycle flush in OnAfterRenderAsync must be
scoped to the render pass that staged each arrival, not only filtered by
_lifecycleNavGeneration. Add a render token or per-render queue when staging
entries in _pendingLifecycleFlush, and have the matching commit flush only its
own entries; prevent an older overlapping OnAfterRenderAsync from draining
arrivals staged by a newer commit while preserving stale-navigation filtering.

---

Outside diff comments:
In `@src/Brouter/Bit.Brouter/Brouter.cs`:
- Around line 1310-1349: Add stale-navigation checks in HandleAppNotFoundAsync
after NotifyChainDepartures and after awaiting OnNotFound, using the captured
location versus the current navigation state. Return immediately when a nested
navigation has changed the location, before mutating _committedChain or
_currentRouteData and before calling NavigateInternal or StateHasChanged.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: fc5060bf-650b-429e-a049-04484c5ddb1a

📥 Commits

Reviewing files that changed from the base of the PR and between 7529785 and 6c53b50.

📒 Files selected for processing (28)
  • src/Brouter/Bit.Brouter/Broute.cs
  • src/Brouter/Bit.Brouter/Brouter.cs
  • src/Brouter/Bit.Brouter/BrouterKeepAliveContext.cs
  • src/Brouter/Bit.Brouter/BrouterOutlet.cs
  • src/Brouter/Bit.Brouter/BrouterRouteActivation.cs
  • src/Brouter/Bit.Brouter/BrouterRouteBase.cs
  • src/Brouter/Bit.Brouter/BrouterRouteContext.cs
  • src/Brouter/Bit.Brouter/BrouterRouteDeactivation.cs
  • src/Brouter/Bit.Brouter/BrouterRouteDeactivationReason.cs
  • src/Brouter/Bit.Brouter/BrouterRouteRenavigation.cs
  • src/Brouter/Bit.Brouter/BrouterRouteRenderer.cs
  • src/Brouter/Bit.Brouter/IBrouterRoute.cs
  • src/Brouter/InteralDemos/Core/AppRouter.razor
  • src/Brouter/InteralDemos/Core/DemoState.cs
  • src/Brouter/InteralDemos/Core/Pages/HomePage.razor
  • src/Brouter/InteralDemos/Core/Pages/LifecyclePage.razor
  • src/Brouter/InteralDemos/Core/Pages/LifecyclePulse.razor
  • src/Brouter/InteralDemos/Core/Pages/StickyNotePage.razor
  • src/Brouter/InteralDemos/Core/Shared/Header.razor
  • src/Brouter/InteralDemos/Core/Shared/Header.razor.css
  • src/Brouter/InteralDemos/Core/wwwroot/app.css
  • src/Brouter/README.md
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveLifecycle.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/LifecyclePage.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/LifecycleProbe.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/RouteLifecycleTests.cs
💤 Files with no reviewable changes (1)
  • src/Brouter/Bit.Brouter/BrouterKeepAliveContext.cs

Comment thread src/Brouter/Bit.Brouter/Brouter.cs Outdated
@msynk

msynk commented Jul 12, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full-review

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

@msynk Sure, I'll perform a full review of all the changes in this PR again.

✅ Action performed

Full review finished.

@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 (1)
src/Brouter/Bit.Brouter/Broute.cs (1)

605-612: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Snapshot Outlets.Values before invoking teardown callbacks, for consistency with the _kept fix.

ForgetChild synchronously fires deactivation callbacks (NotifyEntryTeardownFireDeactivated) for each outlet. If a handler's synchronous prefix causes another outlet under the same host to unregister (e.g. via component disposal during the same batch), this live Dictionary.Values enumeration would throw. The exact same class of hazard was just fixed for _kept elsewhere in this PR by snapshotting with .ToArray().

♻️ Proposed fix
         var outletHost = FindOutletHost();
         if (outletHost is not null)
         {
-            foreach (var outlet in outletHost.Outlets.Values)
+            foreach (var outlet in outletHost.Outlets.Values.ToArray())
             {
                 outlet.ForgetChild(this);
             }
         }
🤖 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 `@src/Brouter/Bit.Brouter/Broute.cs` around lines 605 - 612, Snapshot
outletHost.Outlets.Values with ToArray() before iterating in the teardown block,
then pass each snapshot outlet to ForgetChild(this). Preserve the existing null
check and teardown behavior while preventing callbacks from invalidating the
live dictionary enumeration.
🤖 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 `@src/Brouter/Bit.Brouter/Brouter.cs`:
- Around line 1327-1356: After NotifyChainDepartures in the not-found handling
flow, immediately check whether _lifecycleNavGeneration differs from the
captured generation and return if a new navigation started. Keep this check
before clearing _committedChain and _currentRouteData, while retaining the
existing post-OnNotFound check.

---

Nitpick comments:
In `@src/Brouter/Bit.Brouter/Broute.cs`:
- Around line 605-612: Snapshot outletHost.Outlets.Values with ToArray() before
iterating in the teardown block, then pass each snapshot outlet to
ForgetChild(this). Preserve the existing null check and teardown behavior while
preventing callbacks from invalidating the live dictionary enumeration.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3b1bb039-9396-42bb-a926-a6f22af9995d

📥 Commits

Reviewing files that changed from the base of the PR and between 7529785 and 775b7f5.

📒 Files selected for processing (28)
  • src/Brouter/Bit.Brouter/Broute.cs
  • src/Brouter/Bit.Brouter/Brouter.cs
  • src/Brouter/Bit.Brouter/BrouterKeepAliveContext.cs
  • src/Brouter/Bit.Brouter/BrouterOutlet.cs
  • src/Brouter/Bit.Brouter/BrouterRouteActivation.cs
  • src/Brouter/Bit.Brouter/BrouterRouteBase.cs
  • src/Brouter/Bit.Brouter/BrouterRouteContext.cs
  • src/Brouter/Bit.Brouter/BrouterRouteDeactivation.cs
  • src/Brouter/Bit.Brouter/BrouterRouteDeactivationReason.cs
  • src/Brouter/Bit.Brouter/BrouterRouteRenavigation.cs
  • src/Brouter/Bit.Brouter/BrouterRouteRenderer.cs
  • src/Brouter/Bit.Brouter/IBrouterRoute.cs
  • src/Brouter/InteralDemos/Core/AppRouter.razor
  • src/Brouter/InteralDemos/Core/DemoState.cs
  • src/Brouter/InteralDemos/Core/Pages/HomePage.razor
  • src/Brouter/InteralDemos/Core/Pages/LifecyclePage.razor
  • src/Brouter/InteralDemos/Core/Pages/LifecyclePulse.razor
  • src/Brouter/InteralDemos/Core/Pages/StickyNotePage.razor
  • src/Brouter/InteralDemos/Core/Shared/Header.razor
  • src/Brouter/InteralDemos/Core/Shared/Header.razor.css
  • src/Brouter/InteralDemos/Core/wwwroot/app.css
  • src/Brouter/README.md
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveLifecycle.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/LifecyclePage.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/LifecycleProbe.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/RouteLifecycleTests.cs
💤 Files with no reviewable changes (1)
  • src/Brouter/Bit.Brouter/BrouterKeepAliveContext.cs

Comment thread src/Brouter/Bit.Brouter/Brouter.cs
@msynk

msynk commented Jul 12, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full-review

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

@msynk Sure, I'll perform a full review of all the changes in this PR again.

✅ Action performed

Full review finished.

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Brouter/Bit.Brouter/BrouterRouteRenderer.cs (1)

69-90: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep-alive removals need a Disposing callback.

  • DropKeptContent clears hidden keep-alive entries and nulls _context without firing FireDeactivated(BrouterRouteDeactivationReason.Disposing, ...), so OnDeactivatedAsync cleanup is skipped when IBrouter.ClearKeepAlive() drops a route.
  • The same callback is missing when RenderKeptEntries evicts LRU entries.

Thread location/onError through the clear path and fire Disposing before removing each dropped context.

🤖 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 `@src/Brouter/Bit.Brouter/BrouterRouteRenderer.cs` around lines 69 - 90, Update
DropKeptContent and the clear path used by IBrouter.ClearKeepAlive to accept and
propagate location and onError, then invoke FireDeactivated with
BrouterRouteDeactivationReason.Disposing before removing each dropped keep-alive
context. Apply the same disposal callback when RenderKeptEntries evicts LRU
entries, while preserving the active route entry and existing singleton
behavior.
🧹 Nitpick comments (2)
src/Brouter/InteralDemos/Core/Pages/NavigationLockPage.razor (1)

53-67: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add basic accessibility attributes to the confirmation dialog.

The modal overlay lacks role="dialog", aria-modal="true", and focus management — keyboard users can Tab past the overlay to background elements. This is a demo, but the dialog is the central interaction of this page.

♿ Suggested accessibility improvements
 `@if` (_prompt is not null)
 {
-    <div style="position: fixed; inset: 0; background: rgba(0,0,0,.45); display: grid; place-items: center; z-index: 1000;">
-        <div class="bb-card" style="max-width: 26rem; margin: 0;">
+    <div role="dialog" aria-modal="true" aria-labelledby="lock-prompt-title"
+         style="position: fixed; inset: 0; background: rgba(0,0,0,.45); display: grid; place-items: center; z-index: 1000;">
+        <div class="bb-card" style="max-width: 26rem; margin: 0;" `@onkeydown`="@(e => { if (e.Key == "Escape") AnswerPrompt(stay: true); })">
             <h3 class="bb-card-title" id="lock-prompt-title"><span class="bb-dot bb-dot-warning"></span> Unsaved changes</h3>
🤖 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 `@src/Brouter/InteralDemos/Core/Pages/NavigationLockPage.razor` around lines 53
- 67, Update the confirmation modal rendered by the _prompt block in
NavigationLockPage to include role="dialog" and aria-modal="true", and implement
focus management so keyboard focus is moved into the dialog when it opens and
cannot tab to background content while it remains active.
src/Brouter/Tests/Bit.Brouter.Tests/RouteLifecycleTests.cs (1)

17-23: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate RenderAt helper across test classes.

Identical to KeepAliveTests.cs's RenderAt(string url) and structurally the same as NavigationLockTests.cs's version (differing only by host type). Worth consolidating into a generic helper on the shared BunitTestContext base.

♻️ Suggested consolidation
// In BunitTestContext (shared base)
protected (IRenderedComponent<THost> Cut, IBrouter Brouter) RenderAt<THost>(string url) where THost : IComponent
{
    var nav = Services.GetRequiredService<FakeNavigationManager>();
    nav.NavigateTo(url);
    var cut = RenderComponent<THost>();
    return (cut, Services.GetRequiredService<IBrouter>());
}

Then each test class calls RenderAt<KeepAliveHost>(url) / RenderAt<NavigationLockHost>(url), removing the three near-identical private copies.

🤖 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 `@src/Brouter/Tests/Bit.Brouter.Tests/RouteLifecycleTests.cs` around lines 17 -
23, Consolidate the duplicated RenderAt helpers from the test classes into a
protected generic RenderAt<THost> method on the shared BunitTestContext,
constrained to IComponent and preserving navigation, rendering, and IBrouter
resolution. Remove the private helpers from KeepAliveTests, NavigationLockTests,
and the current test class, and update callers to specify KeepAliveHost or
NavigationLockHost explicitly.
🤖 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 `@src/Brouter/Bit.Brouter/BrouterOutlet.cs`:
- Around line 108-126: Fix the cleanup predicate in BrouterOutlet.ForgetChild so
_kept.RemoveAll compares each ChildEntry’s Route property with the route
parameter via ReferenceEquals, matching the earlier teardown checks. Ensure
retained entries belonging to the disposed route are actually removed while
preserving entries for other routes.

In `@src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveLifecycle.razor`:
- Around line 1-8: Rename the lifecycle markup label `_activations` in the `<div
data-testid="lifecycle">` output to a non-overlapping label, and update the
corresponding assertions in KeepAliveTests and RouteLifecycleTests to use that
label. Preserve the existing counter values and all other lifecycle labels
unchanged.

---

Outside diff comments:
In `@src/Brouter/Bit.Brouter/BrouterRouteRenderer.cs`:
- Around line 69-90: Update DropKeptContent and the clear path used by
IBrouter.ClearKeepAlive to accept and propagate location and onError, then
invoke FireDeactivated with BrouterRouteDeactivationReason.Disposing before
removing each dropped keep-alive context. Apply the same disposal callback when
RenderKeptEntries evicts LRU entries, while preserving the active route entry
and existing singleton behavior.

---

Nitpick comments:
In `@src/Brouter/InteralDemos/Core/Pages/NavigationLockPage.razor`:
- Around line 53-67: Update the confirmation modal rendered by the _prompt block
in NavigationLockPage to include role="dialog" and aria-modal="true", and
implement focus management so keyboard focus is moved into the dialog when it
opens and cannot tab to background content while it remains active.

In `@src/Brouter/Tests/Bit.Brouter.Tests/RouteLifecycleTests.cs`:
- Around line 17-23: Consolidate the duplicated RenderAt helpers from the test
classes into a protected generic RenderAt<THost> method on the shared
BunitTestContext, constrained to IComponent and preserving navigation,
rendering, and IBrouter resolution. Remove the private helpers from
KeepAliveTests, NavigationLockTests, and the current test class, and update
callers to specify KeepAliveHost or NavigationLockHost explicitly.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0961fd26-7f75-4a42-8299-fa9bec3d3694

📥 Commits

Reviewing files that changed from the base of the PR and between 7529785 and de2210a.

📒 Files selected for processing (35)
  • src/Brouter/Bit.Brouter/Broute.cs
  • src/Brouter/Bit.Brouter/Brouter.cs
  • src/Brouter/Bit.Brouter/BrouterKeepAliveContext.cs
  • src/Brouter/Bit.Brouter/BrouterOutlet.cs
  • src/Brouter/Bit.Brouter/BrouterRouteActivation.cs
  • src/Brouter/Bit.Brouter/BrouterRouteBase.cs
  • src/Brouter/Bit.Brouter/BrouterRouteContext.cs
  • src/Brouter/Bit.Brouter/BrouterRouteDeactivatingContext.cs
  • src/Brouter/Bit.Brouter/BrouterRouteDeactivation.cs
  • src/Brouter/Bit.Brouter/BrouterRouteDeactivationReason.cs
  • src/Brouter/Bit.Brouter/BrouterRouteRenavigatingContext.cs
  • src/Brouter/Bit.Brouter/BrouterRouteRenavigation.cs
  • src/Brouter/Bit.Brouter/BrouterRouteRenderer.cs
  • src/Brouter/Bit.Brouter/IBrouterRoute.cs
  • src/Brouter/InteralDemos/Core/AppRouter.razor
  • src/Brouter/InteralDemos/Core/DemoState.cs
  • src/Brouter/InteralDemos/Core/Pages/HomePage.razor
  • src/Brouter/InteralDemos/Core/Pages/LifecyclePage.razor
  • src/Brouter/InteralDemos/Core/Pages/LifecyclePulse.razor
  • src/Brouter/InteralDemos/Core/Pages/NavigationLockPage.razor
  • src/Brouter/InteralDemos/Core/Pages/StickyNotePage.razor
  • src/Brouter/InteralDemos/Core/Shared/Header.razor
  • src/Brouter/InteralDemos/Core/Shared/Header.razor.css
  • src/Brouter/InteralDemos/Core/wwwroot/app.css
  • src/Brouter/README.md
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveLifecycle.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/LifecyclePage.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/LifecycleProbe.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/NavigationLockHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/NavigationLockProbe.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/NavigationLockState.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/NavigationLockTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/RouteLifecycleTests.cs
💤 Files with no reviewable changes (1)
  • src/Brouter/Bit.Brouter/BrouterKeepAliveContext.cs

Comment thread src/Brouter/Bit.Brouter/BrouterOutlet.cs
Comment thread src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveLifecycle.razor Outdated
@msynk

msynk commented Jul 13, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full-review

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

@msynk Sure, I'll perform a full review of all the changes in this PR again.

✅ Action performed

Full review finished.

@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: 6

🤖 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 `@src/Brouter/Bit.Brouter/Brouter.cs`:
- Around line 2541-2544: Update the reason selection in the route deactivation
flow around FireDeactivatingAsync so a KeepAlive route whose outlet host is
leaving is reported as BrouterRouteDeactivationReason.Disposing rather than
Hidden. Use the existing hosting-layout/outlet context to distinguish retained
routes from kept children destroyed with their host, while preserving Hidden for
routes that remain alive.

In `@src/Brouter/Bit.Brouter/BrouterOutlet.cs`:
- Around line 160-172: Update the named-outlet rendering branch around
RenderChild so named views are wrapped with the same entry context via
WrapRouteContext as primary outlet children. Preserve the existing named-outlet
rendering behavior while ensuring BrouterRouteBase descendants receive lifecycle
and navigation-lock callbacks, and add a regression test covering named-outlet
lock registration.
- Around line 197-203: Update the transient-entry branch in the BrouterOutlet
navigation logic to preserve the current entry when willRemainMatched indicates
its route remains matched, including parameter/query changes. Only fire
Disposing and clear _current when the transient route is no longer matched;
retained entries must continue through the renavigation path so component state
is preserved.

In `@src/Brouter/Bit.Brouter/BrouterRouteRenderer.cs`:
- Around line 513-524: Update the route rendering flow around
ApplyTypedParameters and context.AutoRegisterDelegate so replacing direct
component rendering with error, content, or Found output removes the previously
registered page instance from the stable context. Ensure every
non-direct-component branch unregisters the matching capture before rendering
replacement output, and add a regression test covering error-content navigation
to verify stale lifecycle registrations cannot block later navigation.

In `@src/Brouter/InteralDemos/Core/Pages/NavigationLockPage.razor`:
- Around line 116-124: Update the navigation lock flow around the token.Register
callback and stay handling so token cancellation does not clear _draft. Track
whether the prompt completed through an explicit user decision, invoke cancel
only for an explicit stay choice, and assign _draft = _saved only when the user
explicitly chooses to leave; preserve the dirty state for superseding
navigation.

In `@src/Brouter/Tests/Bit.Brouter.Tests/NavigationLockTests.cs`:
- Around line 8-14: Update the navigation-lock test scenario around the Prompt
and Navigate calls to leave the first custom prompt unresolved, then start a
second navigation while it is pending. Make NavigationLockProbe observe
context.CancellationToken, assert the second navigation completes through
supersession, and verify the original prompt is not resolved.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c20b0cd7-2582-4919-a538-283b326648e5

📥 Commits

Reviewing files that changed from the base of the PR and between 7529785 and d7093d5.

📒 Files selected for processing (36)
  • src/Brouter/Bit.Brouter/Broute.cs
  • src/Brouter/Bit.Brouter/Brouter.cs
  • src/Brouter/Bit.Brouter/BrouterKeepAliveContext.cs
  • src/Brouter/Bit.Brouter/BrouterOutlet.cs
  • src/Brouter/Bit.Brouter/BrouterRouteActivation.cs
  • src/Brouter/Bit.Brouter/BrouterRouteBase.cs
  • src/Brouter/Bit.Brouter/BrouterRouteContext.cs
  • src/Brouter/Bit.Brouter/BrouterRouteDeactivatingContext.cs
  • src/Brouter/Bit.Brouter/BrouterRouteDeactivation.cs
  • src/Brouter/Bit.Brouter/BrouterRouteDeactivationReason.cs
  • src/Brouter/Bit.Brouter/BrouterRouteRenavigatingContext.cs
  • src/Brouter/Bit.Brouter/BrouterRouteRenavigation.cs
  • src/Brouter/Bit.Brouter/BrouterRouteRenderer.cs
  • src/Brouter/Bit.Brouter/IBrouterRoute.cs
  • src/Brouter/InteralDemos/Core/AppRouter.razor
  • src/Brouter/InteralDemos/Core/DemoState.cs
  • src/Brouter/InteralDemos/Core/Pages/HomePage.razor
  • src/Brouter/InteralDemos/Core/Pages/LifecyclePage.razor
  • src/Brouter/InteralDemos/Core/Pages/LifecyclePulse.razor
  • src/Brouter/InteralDemos/Core/Pages/NavigationLockPage.razor
  • src/Brouter/InteralDemos/Core/Pages/StickyNotePage.razor
  • src/Brouter/InteralDemos/Core/Shared/Header.razor
  • src/Brouter/InteralDemos/Core/Shared/Header.razor.css
  • src/Brouter/InteralDemos/Core/wwwroot/app.css
  • src/Brouter/README.md
  • src/Brouter/Tests/Bit.Brouter.Tests/BunitTestContext.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveLifecycle.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/LifecyclePage.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/LifecycleProbe.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/NavigationLockHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/NavigationLockProbe.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/NavigationLockState.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/NavigationLockTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/RouteLifecycleTests.cs
💤 Files with no reviewable changes (1)
  • src/Brouter/Bit.Brouter/BrouterKeepAliveContext.cs

Comment thread src/Brouter/Bit.Brouter/Brouter.cs Outdated
Comment thread src/Brouter/Bit.Brouter/BrouterOutlet.cs Outdated
Comment thread src/Brouter/Bit.Brouter/BrouterOutlet.cs Outdated
Comment thread src/Brouter/Bit.Brouter/BrouterRouteRenderer.cs
Comment thread src/Brouter/InteralDemos/Core/Pages/NavigationLockPage.razor Outdated
Comment thread src/Brouter/Tests/Bit.Brouter.Tests/NavigationLockTests.cs
@msynk

msynk commented Jul 13, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full-review

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

@msynk Sure, I'll perform a full review of all the changes in this PR again.

✅ Action performed

Full review finished.

@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.

🧹 Nitpick comments (1)
src/Brouter/Bit.Brouter/BrouterRouteDeactivatingContext.cs (1)

15-71: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a shared base for the two navigation-lock context types.

BrouterRouteDeactivatingContext and BrouterRouteRenavigatingContext duplicate From/To/NavigationType/CancellationToken/IsCancelled/Cancel/Redirect/HasDecision verbatim; only Reason differs. Extracting a common base keeps the two in lockstep as the lock API evolves.

♻️ Proposed shared base
+public abstract class BrouterRouteNavigationLockContext
+{
+    protected readonly BrouterNavigationContext Navigation;
+    protected BrouterRouteNavigationLockContext(BrouterNavigationContext navigation) => Navigation = navigation;
+
+    public BrouterLocation From => Navigation.From;
+    public BrouterLocation To => Navigation.To;
+    public BrouterNavigationType NavigationType => Navigation.NavigationType;
+    public CancellationToken CancellationToken => Navigation.CancellationToken;
+    public bool IsCancelled => Navigation.IsCancelled;
+    public void Cancel() => Navigation.Cancel();
+    public void Redirect(string url) => Navigation.Redirect(url);
+    internal bool HasDecision => Navigation.IsCancelled || Navigation.RedirectUrl is not null;
+}

-public sealed class BrouterRouteDeactivatingContext
+public sealed class BrouterRouteDeactivatingContext : BrouterRouteNavigationLockContext
 {
-    private readonly BrouterNavigationContext _navigation;
-
     internal BrouterRouteDeactivatingContext(BrouterNavigationContext navigation, BrouterRouteDeactivationReason reason)
+        : base(navigation)
     {
-        _navigation = navigation;
         Reason = reason;
     }
-    // ...delegated members removed, inherited from base
     public BrouterRouteDeactivationReason Reason { get; }
 }
🤖 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 `@src/Brouter/Bit.Brouter/BrouterRouteDeactivatingContext.cs` around lines 15 -
71, Extract the duplicated navigation-lock members into a shared base context
used by both BrouterRouteDeactivatingContext and
BrouterRouteRenavigatingContext. Move the common navigation state and
operations—From, To, NavigationType, CancellationToken, IsCancelled, Cancel,
Redirect, and HasDecision—into the base, while keeping the deactivation-specific
Reason property in BrouterRouteDeactivatingContext and preserving existing
constructors and behavior.
🤖 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.

Nitpick comments:
In `@src/Brouter/Bit.Brouter/BrouterRouteDeactivatingContext.cs`:
- Around line 15-71: Extract the duplicated navigation-lock members into a
shared base context used by both BrouterRouteDeactivatingContext and
BrouterRouteRenavigatingContext. Move the common navigation state and
operations—From, To, NavigationType, CancellationToken, IsCancelled, Cancel,
Redirect, and HasDecision—into the base, while keeping the deactivation-specific
Reason property in BrouterRouteDeactivatingContext and preserving existing
constructors and behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 31eaa4b3-aa83-4790-84ca-fed193276978

📥 Commits

Reviewing files that changed from the base of the PR and between 7529785 and 2106f3e.

📒 Files selected for processing (51)
  • src/Brouter/Bit.Brouter/Broute.cs
  • src/Brouter/Bit.Brouter/Brouter.cs
  • src/Brouter/Bit.Brouter/BrouterKeepAliveContext.cs
  • src/Brouter/Bit.Brouter/BrouterLocation.cs
  • src/Brouter/Bit.Brouter/BrouterOutlet.cs
  • src/Brouter/Bit.Brouter/BrouterParameterAttribute.cs
  • src/Brouter/Bit.Brouter/BrouterQueryAttribute.cs
  • src/Brouter/Bit.Brouter/BrouterRouteActivation.cs
  • src/Brouter/Bit.Brouter/BrouterRouteBase.cs
  • src/Brouter/Bit.Brouter/BrouterRouteContext.cs
  • src/Brouter/Bit.Brouter/BrouterRouteDeactivatingContext.cs
  • src/Brouter/Bit.Brouter/BrouterRouteDeactivation.cs
  • src/Brouter/Bit.Brouter/BrouterRouteDeactivationReason.cs
  • src/Brouter/Bit.Brouter/BrouterRouteRenavigatingContext.cs
  • src/Brouter/Bit.Brouter/BrouterRouteRenavigation.cs
  • src/Brouter/Bit.Brouter/BrouterRouteRenderer.cs
  • src/Brouter/Bit.Brouter/IBrouterRoute.cs
  • src/Brouter/InteralDemos/Core/AppRouter.razor
  • src/Brouter/InteralDemos/Core/DemoState.cs
  • src/Brouter/InteralDemos/Core/Pages/GalleryItemPage.razor
  • src/Brouter/InteralDemos/Core/Pages/HomePage.razor
  • src/Brouter/InteralDemos/Core/Pages/LifecyclePage.razor
  • src/Brouter/InteralDemos/Core/Pages/LifecyclePulse.razor
  • src/Brouter/InteralDemos/Core/Pages/NavigationLockPage.razor
  • src/Brouter/InteralDemos/Core/Pages/NotebookPage.razor
  • src/Brouter/InteralDemos/Core/Pages/PostPage.razor
  • src/Brouter/InteralDemos/Core/Pages/ProfilePage.razor
  • src/Brouter/InteralDemos/Core/Pages/StickyNotePage.razor
  • src/Brouter/InteralDemos/Core/Shared/Header.razor
  • src/Brouter/InteralDemos/Core/Shared/Header.razor.css
  • src/Brouter/InteralDemos/Core/wwwroot/app.css
  • src/Brouter/README.md
  • src/Brouter/Tests/Bit.Brouter.Tests/BindConsumer.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/BindHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/BunitTestContext.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/ErrorContentHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/ErrorContentTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/ErrorLockPage.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveLifecycle.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/LifecyclePage.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/LifecycleProbe.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/NavigationLockHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/NavigationLockProbe.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/NavigationLockState.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/NavigationLockTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/ParameterBindingTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/QueryBindingTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/QueryConsumer.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/RouteLifecycleTests.cs
💤 Files with no reviewable changes (1)
  • src/Brouter/Bit.Brouter/BrouterKeepAliveContext.cs

@msynk

msynk commented Jul 13, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full-review

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

@msynk Sure, I'll perform a full review of all the changes in this PR again.

✅ Action performed

Full review finished.

@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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Brouter/Bit.Brouter/BrouterOutlet.cs (1)

128-147: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

ClearKeepAlive drops entries without firing their Disposing deactivation.

_kept.RemoveAll(k => ReferenceEquals(k, active) is false) (line 135) discards every non-active retained entry, but unlike ForgetChild and Dispose - which both call NotifyEntryTeardown on entries before removing/disposing them - this method never fires Disposing for the entries it drops. Consumers relying on OnDeactivated/lifecycle handlers to clean up (timers, subscriptions, etc.) on a kept route never get notified when IBrouter.ClearKeepAlive() unmounts their subtree, breaking the "every teardown path fires Disposing" contract this PR otherwise establishes.

🐛 Proposed fix
     internal void ClearKeepAlive()
     {
         var active = _current is not null && _current.Route.Matched ? _current : null;
+        // Snapshot: teardown handlers run synchronously and can mutate _kept.
+        foreach (var k in _kept.ToArray())
+        {
+            if (ReferenceEquals(k, active) is false) NotifyEntryTeardown(k);
+        }
         var removed = _kept.RemoveAll(k => ReferenceEquals(k, active) is false);
🤖 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 `@src/Brouter/Bit.Brouter/BrouterOutlet.cs` around lines 128 - 147, Update
ClearKeepAlive to call NotifyEntryTeardown for every non-active entry before
removing it from _kept, matching the teardown behavior of ForgetChild and
Dispose. Preserve the active matched entry, clear _current when it is no longer
active, and retain the existing rerender behavior.
🤖 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 `@src/Brouter/Bit.Brouter/Brouter.cs`:
- Around line 2526-2548: Update the deactivation loop in the route lifecycle
dispatch to determine the reason for each collected lifecycle context rather
than once from the route node. Carry or derive retention metadata for every
context, preserving Hidden only for contexts whose outlet content remains
retained and sending Disposing to non-retained named views before calling
FireDeactivatingAsync.
- Around line 2629-2631: Update Brouter.cs at lines 2629-2631 to notify the
boundary node’s departure with an explicit forced Disposing reason. Update
BrouterRouteRenderer.cs at lines 117-126 to accept that reason, clear _context
when the active page is replaced, and preserve existing reason inference for
other departures.

In `@src/Brouter/Bit.Brouter/BrouterOutlet.cs`:
- Around line 176-207: Move the willRemainMatched early return in
NotifyDeparture so it applies before the keep-alive and transient/named-outlet
branches. Preserve the existing deactivation and cleanup behavior when
willRemainMatched is false, while skipping FireDeactivated and _current removal
for every route that remains matched.

In `@src/Brouter/InteralDemos/Core/Pages/NavigationLockPage.razor`:
- Around line 82-83: Bind cancellation callbacks and cleanup in the navigation
prompt flow to the specific _prompt instance they create, rather than the
mutable field alone. Capture the prompt TaskCompletionSource and use it in
cancellation, decision, and finally/cleanup paths; only clear or complete
_prompt when the current field still references that captured instance. Preserve
prompt B when prompt A is replaced before A finishes unwinding.

---

Outside diff comments:
In `@src/Brouter/Bit.Brouter/BrouterOutlet.cs`:
- Around line 128-147: Update ClearKeepAlive to call NotifyEntryTeardown for
every non-active entry before removing it from _kept, matching the teardown
behavior of ForgetChild and Dispose. Preserve the active matched entry, clear
_current when it is no longer active, and retain the existing rerender behavior.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d7369b10-e694-4bc6-8791-4d3217ee61e9

📥 Commits

Reviewing files that changed from the base of the PR and between 7529785 and a10c958.

📒 Files selected for processing (52)
  • src/Brouter/Bit.Brouter/Broute.cs
  • src/Brouter/Bit.Brouter/Brouter.cs
  • src/Brouter/Bit.Brouter/BrouterKeepAliveContext.cs
  • src/Brouter/Bit.Brouter/BrouterLocation.cs
  • src/Brouter/Bit.Brouter/BrouterOutlet.cs
  • src/Brouter/Bit.Brouter/BrouterParameterAttribute.cs
  • src/Brouter/Bit.Brouter/BrouterQueryAttribute.cs
  • src/Brouter/Bit.Brouter/BrouterRouteActivation.cs
  • src/Brouter/Bit.Brouter/BrouterRouteBase.cs
  • src/Brouter/Bit.Brouter/BrouterRouteContext.cs
  • src/Brouter/Bit.Brouter/BrouterRouteDeactivatingContext.cs
  • src/Brouter/Bit.Brouter/BrouterRouteDeactivation.cs
  • src/Brouter/Bit.Brouter/BrouterRouteDeactivationReason.cs
  • src/Brouter/Bit.Brouter/BrouterRouteNavigatingContext.cs
  • src/Brouter/Bit.Brouter/BrouterRouteRenavigatingContext.cs
  • src/Brouter/Bit.Brouter/BrouterRouteRenavigation.cs
  • src/Brouter/Bit.Brouter/BrouterRouteRenderer.cs
  • src/Brouter/Bit.Brouter/IBrouterRoute.cs
  • src/Brouter/InteralDemos/Core/AppRouter.razor
  • src/Brouter/InteralDemos/Core/DemoState.cs
  • src/Brouter/InteralDemos/Core/Pages/GalleryItemPage.razor
  • src/Brouter/InteralDemos/Core/Pages/HomePage.razor
  • src/Brouter/InteralDemos/Core/Pages/LifecyclePage.razor
  • src/Brouter/InteralDemos/Core/Pages/LifecyclePulse.razor
  • src/Brouter/InteralDemos/Core/Pages/NavigationLockPage.razor
  • src/Brouter/InteralDemos/Core/Pages/NotebookPage.razor
  • src/Brouter/InteralDemos/Core/Pages/PostPage.razor
  • src/Brouter/InteralDemos/Core/Pages/ProfilePage.razor
  • src/Brouter/InteralDemos/Core/Pages/StickyNotePage.razor
  • src/Brouter/InteralDemos/Core/Shared/Header.razor
  • src/Brouter/InteralDemos/Core/Shared/Header.razor.css
  • src/Brouter/InteralDemos/Core/wwwroot/app.css
  • src/Brouter/README.md
  • src/Brouter/Tests/Bit.Brouter.Tests/BindConsumer.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/BindHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/BunitTestContext.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/ErrorContentHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/ErrorContentTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/ErrorLockPage.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveLifecycle.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/LifecyclePage.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/LifecycleProbe.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/NavigationLockHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/NavigationLockProbe.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/NavigationLockState.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/NavigationLockTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/ParameterBindingTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/QueryBindingTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/QueryConsumer.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/RouteLifecycleTests.cs
💤 Files with no reviewable changes (1)
  • src/Brouter/Bit.Brouter/BrouterKeepAliveContext.cs

Comment thread src/Brouter/Bit.Brouter/Brouter.cs Outdated
Comment thread src/Brouter/Bit.Brouter/Brouter.cs Outdated
Comment thread src/Brouter/Bit.Brouter/BrouterOutlet.cs
Comment thread src/Brouter/InteralDemos/Core/Pages/NavigationLockPage.razor Outdated
@msynk

msynk commented Jul 13, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full-review

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

@msynk Sure, I'll perform a full review of all the changes in this PR again.

✅ Action performed

Full review finished.

@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: 2

🧹 Nitpick comments (1)
src/Brouter/InteralDemos/Core/Pages/LifecyclePulse.razor (1)

26-42: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Consider capturing the context reference at registration time.

Dispose uses the current RouteContext cascading parameter value to unregister, but if the cascade were ever cleared or swapped before disposal, the unregister would silently no-op (or target the wrong context). Capturing the reference at registration time would make the dispose path robust regardless of cascade lifecycle:

🛡️ Optional defensive refactor
 private bool _registered;
+private BrouterRouteContext? _registeredContext;
 private int _activations;
 private int _renavigations;

 protected override void OnParametersSet()
 {
     if (_registered is false && RouteContext is not null)
     {
+        _registeredContext = RouteContext;
         RouteContext.Register(this);
         _registered = true;
     }
 }

 // ... in Dispose:
 public void Dispose()
 {
-    if (_registered) RouteContext?.Unregister(this);
+    if (_registered) _registeredContext?.Unregister(this);
 }

The comment states the cascade is a stable, fixed instance, so this is purely defensive and can be deferred.

🤖 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 `@src/Brouter/InteralDemos/Core/Pages/LifecyclePulse.razor` around lines 26 -
42, Optionally store the RouteContext instance used by Register in a dedicated
field when registration succeeds, then update Dispose to unregister through that
captured reference instead of the current cascading RouteContext value. Preserve
the existing one-time registration behavior and null handling.
🤖 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 `@src/Brouter/Bit.Brouter/BrouterRouteContext.cs`:
- Line 25: Update BrouterRouteContext.Register and Unregister to locate handlers
by object identity using ReferenceEquals rather than List.Contains/Remove value
equality. Apply the same reference-based lookup when cleaning up
_autoRegistered, ensuring registration and removal affect only the exact route
instance supplied.

In `@src/Brouter/InteralDemos/Core/Pages/StickyNotePage.razor`:
- Around line 70-76: Update the Timer callback in StickyNotePage so the disposed
check, _visibleSeconds increment, and StateHasChanged call all execute inside a
single InvokeAsync dispatcher callback. Remove the off-dispatcher state mutation
and preserve the existing disposal guard and repaint behavior.

---

Nitpick comments:
In `@src/Brouter/InteralDemos/Core/Pages/LifecyclePulse.razor`:
- Around line 26-42: Optionally store the RouteContext instance used by Register
in a dedicated field when registration succeeds, then update Dispose to
unregister through that captured reference instead of the current cascading
RouteContext value. Preserve the existing one-time registration behavior and
null handling.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 30e03f35-459e-4b5c-a824-2255c04daa9e

📥 Commits

Reviewing files that changed from the base of the PR and between 7529785 and ea849be.

📒 Files selected for processing (52)
  • src/Brouter/Bit.Brouter/Broute.cs
  • src/Brouter/Bit.Brouter/Brouter.cs
  • src/Brouter/Bit.Brouter/BrouterKeepAliveContext.cs
  • src/Brouter/Bit.Brouter/BrouterLocation.cs
  • src/Brouter/Bit.Brouter/BrouterOutlet.cs
  • src/Brouter/Bit.Brouter/BrouterParameterAttribute.cs
  • src/Brouter/Bit.Brouter/BrouterQueryAttribute.cs
  • src/Brouter/Bit.Brouter/BrouterRouteActivation.cs
  • src/Brouter/Bit.Brouter/BrouterRouteBase.cs
  • src/Brouter/Bit.Brouter/BrouterRouteContext.cs
  • src/Brouter/Bit.Brouter/BrouterRouteDeactivatingContext.cs
  • src/Brouter/Bit.Brouter/BrouterRouteDeactivation.cs
  • src/Brouter/Bit.Brouter/BrouterRouteDeactivationReason.cs
  • src/Brouter/Bit.Brouter/BrouterRouteNavigatingContext.cs
  • src/Brouter/Bit.Brouter/BrouterRouteRenavigatingContext.cs
  • src/Brouter/Bit.Brouter/BrouterRouteRenavigation.cs
  • src/Brouter/Bit.Brouter/BrouterRouteRenderer.cs
  • src/Brouter/Bit.Brouter/IBrouterRoute.cs
  • src/Brouter/InteralDemos/Core/AppRouter.razor
  • src/Brouter/InteralDemos/Core/DemoState.cs
  • src/Brouter/InteralDemos/Core/Pages/GalleryItemPage.razor
  • src/Brouter/InteralDemos/Core/Pages/HomePage.razor
  • src/Brouter/InteralDemos/Core/Pages/LifecyclePage.razor
  • src/Brouter/InteralDemos/Core/Pages/LifecyclePulse.razor
  • src/Brouter/InteralDemos/Core/Pages/NavigationLockPage.razor
  • src/Brouter/InteralDemos/Core/Pages/NotebookPage.razor
  • src/Brouter/InteralDemos/Core/Pages/PostPage.razor
  • src/Brouter/InteralDemos/Core/Pages/ProfilePage.razor
  • src/Brouter/InteralDemos/Core/Pages/StickyNotePage.razor
  • src/Brouter/InteralDemos/Core/Shared/Header.razor
  • src/Brouter/InteralDemos/Core/Shared/Header.razor.css
  • src/Brouter/InteralDemos/Core/wwwroot/app.css
  • src/Brouter/README.md
  • src/Brouter/Tests/Bit.Brouter.Tests/BindConsumer.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/BindHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/BunitTestContext.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/ErrorContentHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/ErrorContentTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/ErrorLockPage.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveLifecycle.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/LifecyclePage.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/LifecycleProbe.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/NavigationLockHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/NavigationLockProbe.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/NavigationLockState.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/NavigationLockTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/ParameterBindingTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/QueryBindingTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/QueryConsumer.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/RouteLifecycleTests.cs
💤 Files with no reviewable changes (1)
  • src/Brouter/Bit.Brouter/BrouterKeepAliveContext.cs

/// </remarks>
public sealed class BrouterRouteContext
{
private readonly List<IBrouterRoute> _handlers = [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C3 'class .*IBrouterRoute|record .*IBrouterRoute|override\s+bool\s+Equals' src/Brouter

Repository: bitfoundation/bitplatform

Length of output: 2088


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the context implementation and related route types.
sed -n '1,220p' src/Brouter/Bit.Brouter/BrouterRouteContext.cs
printf '\n---\n'
sed -n '1,220p' src/Brouter/Bit.Brouter/BrouterRouteBase.cs
printf '\n---\n'
rg -n -C3 'interface IBrouterRoute|class .*IBrouterRoute|record .*IBrouterRoute|override\s+bool\s+Equals|GetHashCode\(' src/Brouter/Bit.Brouter src/Brouter/Bit.Brouter.Generators

Repository: bitfoundation/bitplatform

Length of output: 21590


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map all IBrouterRoute implementers and any equality overrides in the Brouter area.
rg -n -C2 ':\s*IBrouterRoute\b|override\s+bool\s+Equals|record\s+\w+.*IBrouterRoute|record\s+\w+' src/Brouter
printf '\n--- IBrouterRoute ---\n'
sed -n '1,220p' src/Brouter/Bit.Brouter/IBrouterRoute.cs

Repository: bitfoundation/bitplatform

Length of output: 14868


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find every in-repo implementation/use of IBrouterRoute and any custom equality on those types.
rg -n -C2 'IBrouterRoute' src/Brouter --glob '*.cs'
printf '\n--- equality overrides near route types ---\n'
rg -n -C2 'override\s+bool\s+Equals|override\s+int\s+GetHashCode|record\s+(class|struct)?' src/Brouter --glob '*.cs'

Repository: bitfoundation/bitplatform

Length of output: 33117


Use reference identity for lifecycle handlers src/Brouter/Bit.Brouter/BrouterRouteContext.cs:25,65-73,85-87

Register/Unregister currently go through List<T>.Contains/Remove, so a value-equality IBrouterRoute can suppress or unregister a different instance. Switch these paths, including _autoRegistered cleanup, to ReferenceEquals-based lookup.

🤖 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 `@src/Brouter/Bit.Brouter/BrouterRouteContext.cs` at line 25, Update
BrouterRouteContext.Register and Unregister to locate handlers by object
identity using ReferenceEquals rather than List.Contains/Remove value equality.
Apply the same reference-based lookup when cleaning up _autoRegistered, ensuring
registration and removal affect only the exact route instance supplied.

Comment on lines 70 to 76
_timer = new System.Threading.Timer(_ =>
{
// Timer.Dispose does not wait for in-flight callbacks, so guard against
// touching the component after disposal has started.
if (_disposed) return;
_visibleSeconds++;
_ = InvokeAsync(StateHasChanged);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Move the timer state update onto the renderer dispatcher.

System.Threading.Timer mutates _visibleSeconds off-dispatcher and may race with rendering or an in-flight deactivation. Dispatch the state check, increment, and repaint together.

Proposed fix
             _timer = new System.Threading.Timer(_ =>
             {
-                // Timer.Dispose does not wait for in-flight callbacks, so guard against
-                // touching the component after disposal has started.
-                if (_disposed) return;
-                _visibleSeconds++;
-                _ = InvokeAsync(StateHasChanged);
+                _ = InvokeAsync(() =>
+                {
+                    if (_disposed || IsActive is false) return;
+                    _visibleSeconds++;
+                    StateHasChanged();
+                });
             }, null, dueTime: System.Threading.Timeout.Infinite, period: System.Threading.Timeout.Infinite);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_timer = new System.Threading.Timer(_ =>
{
// Timer.Dispose does not wait for in-flight callbacks, so guard against
// touching the component after disposal has started.
if (_disposed) return;
_visibleSeconds++;
_ = InvokeAsync(StateHasChanged);
_timer = new System.Threading.Timer(_ =>
{
_ = InvokeAsync(() =>
{
if (_disposed || IsActive is false) return;
_visibleSeconds++;
StateHasChanged();
});
}, null, dueTime: System.Threading.Timeout.Infinite, period: System.Threading.Timeout.Infinite);
🤖 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 `@src/Brouter/InteralDemos/Core/Pages/StickyNotePage.razor` around lines 70 -
76, Update the Timer callback in StickyNotePage so the disposed check,
_visibleSeconds increment, and StateHasChanged call all execute inside a single
InvokeAsync dispatcher callback. Remove the off-dispatcher state mutation and
preserve the existing disposal guard and repaint behavior.

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.

The lifecycle management of routes in Brouter need improvements

1 participant