Apply Brouter improvements (#12559)#12560
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThis PR renames the route component to ChangesBroute routing, constraints, discovery, and navigation
Estimated code review effort: 5 (Critical) | ~120 minutes Demo markup, docs, and tests
Estimated code review effort: 5 (Critical) | ~120 minutes JavaScript build and benchmarks
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant NavigationManager
participant Brouter
participant Broute
participant BrouterService
participant BitBrouterJS
NavigationManager->>Brouter: OnLocationChanging(to)
Brouter->>Broute: InvokeGuardsAsync(ctx)
alt cancel or redirect
Broute-->>Brouter: ctx.Cancel()/ctx.Redirect(url)
Brouter->>Brouter: NavigateInternal(prevented target)
else approve
Broute-->>Brouter: guard completes
end
Brouter->>Brouter: ProcessNavigationAsync(decisionAlreadyMade, navType)
Brouter->>BrouterService: ApplyNavigationEffectsAsync / SaveScrollPositionAsync
BrouterService->>BitBrouterJS: applyNavigationEffects(...) / saveScrollPosition(...)
BitBrouterJS-->>BrouterService: DOM effects applied
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai re-review |
|
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (2)
src/Brouter/Bit.Brouter/wwwroot/BitBrouter.js (1)
134-137: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueConsider bounding
scrollPositionsgrowth.Every distinct URL leaves a permanent entry, and
persistScrollPositionswrites the whole map into a single storage slot on each save. Over a long-lived session (or query-param-heavy URLs like search/pagination) this grows unbounded and can hit the Web Storage quota; the quota error is swallowed inpersistScrollPositions, so persistence silently stops. A small cap (e.g. LRU eviction of oldest keys) keeps it bounded without changing behavior for typical apps.🤖 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/wwwroot/BitBrouter.js` around lines 134 - 137, The scroll position cache in `scrollPositions` is unbounded, so each new URL can permanently add entries and eventually overflow Web Storage. Update the save path around `scrollPositions.set(...)` and `persistScrollPositions()` to enforce a small cap on stored keys, evicting the oldest entries first (LRU/oldest-first is fine) before persisting. Keep the change localized to the scroll persistence logic in `BitBrouter.js` so typical `currentScroll()` behavior remains unchanged.src/Brouter/Bit.Brouter/BrouterService.cs (1)
368-427: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDuplicated catch blocks across the four JS-interop call sites.
ApplyNavigationEffectsAsync,SaveScrollPositionAsync,DisposeAsync, andGoAsyncall repeat the same four-exception swallow list. Consider extracting a smallSafeJsCallAsynchelper to centralize this pattern and reduce duplication.🤖 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/BrouterService.cs` around lines 368 - 427, The JS interop error handling is duplicated across ApplyNavigationEffectsAsync, SaveScrollPositionAsync, DisposeAsync, and GoAsync. Extract the repeated try/catch swallow pattern into a small shared helper such as SafeJsCallAsync in BrouterService, and have each of those call sites wrap their module invocation through it so the same four exception types are handled in one place.
🤖 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/Broute.cs`:
- Around line 122-125: Update the stale comment in Broute so it matches the
current constraint resolution flow: the parser in Broute.ParseTemplate should be
described as using the DI-container-scoped Brouter.Options.Constraints plus
built-in constraints only. Remove the reference to a “process-wide registry”
since the global static registry has been removed, and keep the comment aligned
with BrouterConstraintRegistry and BrouterTemplateParser.ParseTemplate.
In `@src/Brouter/Bit.Brouter/BroutePrerenderState.cs`:
- Around line 64-84: TryRestore currently only guards the JSON deserialization
path, but the Type.GetType lookup can still throw for stale or unloadable
persisted type names and should fall back to rerunning the loader instead of
crashing. Update BroutePrerenderState.TryRestore to catch type-resolution
failures around the Type.GetType(state.TypeName, throwOnError: false) step and
return false on those failures, using the same fallback behavior already used
for a missing type or JsonException.
In `@src/Brouter/Bit.Brouter/Brouter.cs`:
- Line 279: The `_navigating` flag in `Brouter` is never driven, so the
`Navigating` pending UI in `BuildRenderTree` never appears. Update
`ProcessNavigationAsync` (especially its loader loop) to set `_navigating` to
true when navigation starts and back to false when it completes or aborts, and
ensure state changes trigger a rerender so the `Navigating` fragment is
shown/hidden correctly. Keep the logic aligned with the existing `_navigating`
field and `Navigating` parameter behavior.
In `@src/Brouter/Bit.Brouter/BrouterRouteRenderer.cs`:
- Around line 99-100: The ApplyTypedParameters call in BrouterRouteRenderer can
leave reused component instances with stale route-parameter values when a
parameter disappears from the current template. Update the parameter application
logic around ApplyTypedParameters so that route parameters not present in the
current _route.TemplateParameterNames are explicitly emitted as default/cleared
frames instead of being skipped, ensuring reused components do not retain values
from a previous route.
In `@src/Brouter/README.md`:
- Around line 254-256: Clarify the navigation precedence wording in the README
so it explicitly states that when a fragment target resolves, it scrolls and
takes focus and no further focus handling occurs. Update the precedence sentence
around the navigation behavior description to make it clear that
FocusOnNavigateSelector only applies when no fragment target was resolved, so
the rules do not imply both focus targets run on the same navigation.
---
Nitpick comments:
In `@src/Brouter/Bit.Brouter/BrouterService.cs`:
- Around line 368-427: The JS interop error handling is duplicated across
ApplyNavigationEffectsAsync, SaveScrollPositionAsync, DisposeAsync, and GoAsync.
Extract the repeated try/catch swallow pattern into a small shared helper such
as SafeJsCallAsync in BrouterService, and have each of those call sites wrap
their module invocation through it so the same four exception types are handled
in one place.
In `@src/Brouter/Bit.Brouter/wwwroot/BitBrouter.js`:
- Around line 134-137: The scroll position cache in `scrollPositions` is
unbounded, so each new URL can permanently add entries and eventually overflow
Web Storage. Update the save path around `scrollPositions.set(...)` and
`persistScrollPositions()` to enforce a small cap on stored keys, evicting the
oldest entries first (LRU/oldest-first is fine) before persisting. Keep the
change localized to the scroll persistence logic in `BitBrouter.js` so typical
`currentScroll()` behavior remains unchanged.
🪄 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: b9b04859-9f89-402b-bc03-dd74ed9d7c93
📒 Files selected for processing (37)
src/Brouter/Bit.Brouter/Broute.cssrc/Brouter/Bit.Brouter/BroutePrerenderState.cssrc/Brouter/Bit.Brouter/BrouteScanner.cssrc/Brouter/Bit.Brouter/Brouter.cssrc/Brouter/Bit.Brouter/BrouterConstraintRegistry.cssrc/Brouter/Bit.Brouter/BrouterConstraints.cssrc/Brouter/Bit.Brouter/BrouterLink.cssrc/Brouter/Bit.Brouter/BrouterNavigationContext.cssrc/Brouter/Bit.Brouter/BrouterNavigationType.cssrc/Brouter/Bit.Brouter/BrouterOptions.cssrc/Brouter/Bit.Brouter/BrouterOutlet.cssrc/Brouter/Bit.Brouter/BrouterRouteConstraint.cssrc/Brouter/Bit.Brouter/BrouterRouteRenderer.cssrc/Brouter/Bit.Brouter/BrouterScrollPositionStorage.cssrc/Brouter/Bit.Brouter/BrouterService.cssrc/Brouter/Bit.Brouter/BrouterTemplateParser.cssrc/Brouter/Bit.Brouter/BrouterTemplateSegment.cssrc/Brouter/Bit.Brouter/wwwroot/BitBrouter.jssrc/Brouter/InteralDemos/Core/AppRouter.razorsrc/Brouter/InteralDemos/Core/Pages/DiscoveredPage.razorsrc/Brouter/InteralDemos/Core/Shared/Header.razorsrc/Brouter/README.mdsrc/Brouter/Tests/Bit.Brouter.Tests/BrouterLinkTests.cssrc/Brouter/Tests/Bit.Brouter.Tests/BrouterTests.cssrc/Brouter/Tests/Bit.Brouter.Tests/ConstraintHost.razorsrc/Brouter/Tests/Bit.Brouter.Tests/ConstraintIntegrationTests.cssrc/Brouter/Tests/Bit.Brouter.Tests/ConstraintRegistryTests.cssrc/Brouter/Tests/Bit.Brouter.Tests/DiscoveredPage.razorsrc/Brouter/Tests/Bit.Brouter.Tests/DiscoveryAndPersistenceTests.cssrc/Brouter/Tests/Bit.Brouter.Tests/DiscoveryHost.razorsrc/Brouter/Tests/Bit.Brouter.Tests/GuardAndLoaderTests.cssrc/Brouter/Tests/Bit.Brouter.Tests/NavigationEffectsHost.razorsrc/Brouter/Tests/Bit.Brouter.Tests/NavigationEffectsTests.cssrc/Brouter/Tests/Bit.Brouter.Tests/NavigationTypeHost.razorsrc/Brouter/Tests/Bit.Brouter.Tests/NavigationTypeTests.cssrc/Brouter/Tests/Bit.Brouter.Tests/PreventiveGuardHost.razorsrc/Brouter/Tests/Bit.Brouter.Tests/SampleDto.cs
💤 Files with no reviewable changes (1)
- src/Brouter/Bit.Brouter/BrouterConstraints.cs
✅ Files skipped from review due to trivial changes (5)
- src/Brouter/Tests/Bit.Brouter.Tests/DiscoveredPage.razor
- src/Brouter/Tests/Bit.Brouter.Tests/PreventiveGuardHost.razor
- src/Brouter/InteralDemos/Core/Shared/Header.razor
- src/Brouter/Bit.Brouter/BrouterNavigationType.cs
- src/Brouter/Tests/Bit.Brouter.Tests/BrouterLinkTests.cs
🚧 Files skipped from review as they are similar to previous changes (1)
- src/Brouter/InteralDemos/Core/AppRouter.razor
|
@coderabbitai re-review |
|
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
src/Brouter/Tests/Bit.Brouter.Tests/GuardAndLoaderTests.cs (1)
46-58: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueTest doesn't actually assert concurrent execution.
The test only asserts render completion within 3s; per the inline comment this is meant to prove overlap indirectly (a sequential implementation would time out). Consider directly asserting on an events/timestamp collection (similar to the sequential test above) to make the concurrency guarantee explicit and resistant to a slow-but-still-sequential regression that happens to finish under 3s.
🤖 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/GuardAndLoaderTests.cs` around lines 46 - 58, The ParallelLoaders_runs_chain_loaders_concurrently test is only checking that rendering finishes, which does not explicitly prove overlap. Update the test in GuardAndLoaderTests to assert concurrent execution directly, using the same event/timestamp collection approach as the sequential loader test and referencing ParallelLoadersHost so you can verify both loaders started before either completed. Keep the final render assertion if needed, but make the concurrency guarantee explicit by checking the recorded ordering/timing rather than relying on a timeout-based proxy.src/Brouter/Bit.Brouter/BrouterService.cs (1)
115-131: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winConsider logging swallowed JS interop failures.
SafeJsCallAsyncsilently swallowsJSDisconnectedException,JSException,InvalidOperationException, andTaskCanceledExceptionacross all interop call sites (module import, scroll/focus effects,history.go, module dispose). This is reasonable for expected transient conditions (disconnected circuit, pre-render), but a genuine misconfiguration (e.g., wrong content path breaking the module import) would also be swallowed with zero diagnostics, making it hard to notice broken scroll/focus behavior in production.♻️ Optional: add debug-level logging on swallowed exceptions
- private static async ValueTask SafeJsCallAsync(Func<ValueTask> call) + private static async ValueTask SafeJsCallAsync(Func<ValueTask> call, ILogger? logger = null) { try { await call(); } - catch (JSDisconnectedException) { /* circuit disconnected mid-call */ } - catch (JSException) { /* JS interop failure (e.g. non-browser host) */ } - catch (InvalidOperationException) { /* JS interop unavailable during pre-render */ } - catch (TaskCanceledException) { /* component disposed mid-call */ } + catch (JSDisconnectedException ex) { logger?.LogDebug(ex, "Circuit disconnected mid-call."); } + catch (JSException ex) { logger?.LogDebug(ex, "JS interop failure."); } + catch (InvalidOperationException ex) { logger?.LogDebug(ex, "JS interop unavailable during pre-render."); } + catch (TaskCanceledException ex) { logger?.LogDebug(ex, "Component disposed mid-call."); } }🤖 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/BrouterService.cs` around lines 115 - 131, SafeJsCallAsync currently swallows JS interop exceptions with no diagnostics, which can hide real failures at call sites like GoAsync and the other interop uses in BrouterService. Update SafeJsCallAsync to log swallowed exceptions at debug level (or equivalent) with enough context before suppressing them, while still preserving the existing non-fatal behavior for JSDisconnectedException, JSException, InvalidOperationException, and TaskCanceledException. Keep the centralized handling in SafeJsCallAsync so all interop paths get the same logging 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/Bit.Brouter.csproj`:
- Around line 84-92: The BuildJavaScript target is not configuration-aware
because its Inputs/Outputs only track the TypeScript files and
wwwroot/bit-brouter.js, so a Debug build can incorrectly satisfy up-to-date
checks for a later Release build. Update the target in Bit.Brouter.csproj so the
incremental build key includes $(Configuration) or separate the Release
minify/move path into configuration-specific outputs, ensuring BuildJavaScript
reruns when switching between Debug and Release. Use the existing
BuildJavaScript target, the esbuild minify step, and the Move action as the
symbols to adjust.
In `@src/Brouter/Bit.Brouter/BrouterRelativeUrl.cs`:
- Around line 18-24: `IsRelative` in `BrouterRelativeUrl` does not treat
dot-prefixed refs with a مباشرة appended query or hash as relative, so `Resolve`
can send them through base-relative navigation instead of preserving the
intended target. Update `IsRelative(string url)` to recognize forms like ".?..."
and "..?..."/".#..."/"..#..." as relative alongside the existing "."/"./" and
".."/"../" cases, keeping the logic aligned with `Resolve`’s
query/hash-preservation behavior.
---
Nitpick comments:
In `@src/Brouter/Bit.Brouter/BrouterService.cs`:
- Around line 115-131: SafeJsCallAsync currently swallows JS interop exceptions
with no diagnostics, which can hide real failures at call sites like GoAsync and
the other interop uses in BrouterService. Update SafeJsCallAsync to log
swallowed exceptions at debug level (or equivalent) with enough context before
suppressing them, while still preserving the existing non-fatal behavior for
JSDisconnectedException, JSException, InvalidOperationException, and
TaskCanceledException. Keep the centralized handling in SafeJsCallAsync so all
interop paths get the same logging behavior.
In `@src/Brouter/Tests/Bit.Brouter.Tests/GuardAndLoaderTests.cs`:
- Around line 46-58: The ParallelLoaders_runs_chain_loaders_concurrently test is
only checking that rendering finishes, which does not explicitly prove overlap.
Update the test in GuardAndLoaderTests to assert concurrent execution directly,
using the same event/timestamp collection approach as the sequential loader test
and referencing ParallelLoadersHost so you can verify both loaders started
before either completed. Keep the final render assertion if needed, but make the
concurrency guarantee explicit by checking the recorded ordering/timing rather
than relying on a timeout-based proxy.
🪄 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: afea28b0-16f9-4a7e-b0b2-7c84c19ab68f
⛔ Files ignored due to path filters (1)
src/Brouter/Bit.Brouter/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (43)
.gitignoresrc/Brouter/Bit.Brouter.slnxsrc/Brouter/Bit.Brouter/Bit.Brouter.csprojsrc/Brouter/Bit.Brouter/Broute.cssrc/Brouter/Bit.Brouter/BroutePrerenderState.cssrc/Brouter/Bit.Brouter/Brouter.cssrc/Brouter/Bit.Brouter/BrouterLink.cssrc/Brouter/Bit.Brouter/BrouterNavigationContext.cssrc/Brouter/Bit.Brouter/BrouterRelativeUrl.cssrc/Brouter/Bit.Brouter/BrouterRouteData.cssrc/Brouter/Bit.Brouter/BrouterRouteRenderer.cssrc/Brouter/Bit.Brouter/BrouterService.cssrc/Brouter/Bit.Brouter/IBrouter.cssrc/Brouter/Bit.Brouter/Scripts/bit-brouter.tssrc/Brouter/Bit.Brouter/package.jsonsrc/Brouter/Bit.Brouter/tsconfig.jsonsrc/Brouter/README.mdsrc/Brouter/Tests/Bit.Brouter.Benchmarks/BenchmarkComponents.cssrc/Brouter/Tests/Bit.Brouter.Benchmarks/Bit.Brouter.Benchmarks.csprojsrc/Brouter/Tests/Bit.Brouter.Benchmarks/Harness.cssrc/Brouter/Tests/Bit.Brouter.Benchmarks/Program.cssrc/Brouter/Tests/Bit.Brouter.Benchmarks/README.mdsrc/Brouter/Tests/Bit.Brouter.Tests/AmbiguousRouteTests.cssrc/Brouter/Tests/Bit.Brouter.Tests/AmbiguousRoutesHost.razorsrc/Brouter/Tests/Bit.Brouter.Tests/BrouterLinkTests.cssrc/Brouter/Tests/Bit.Brouter.Tests/DiscoveryOverrideHost.razorsrc/Brouter/Tests/Bit.Brouter.Tests/GuardAndLoaderTests.cssrc/Brouter/Tests/Bit.Brouter.Tests/MultiReplaceLinkHost.razorsrc/Brouter/Tests/Bit.Brouter.Tests/NavigationEffectsTests.cssrc/Brouter/Tests/Bit.Brouter.Tests/NestedIndexHost.razorsrc/Brouter/Tests/Bit.Brouter.Tests/ParallelLoadersHost.razorsrc/Brouter/Tests/Bit.Brouter.Tests/RelativeLinkHost.razorsrc/Brouter/Tests/Bit.Brouter.Tests/RelativeNavigationTests.cssrc/Brouter/Tests/Bit.Brouter.Tests/RelativeRedirectHost.razorsrc/Brouter/Tests/Bit.Brouter.Tests/RenderInvalidationHost.razorsrc/Brouter/Tests/Bit.Brouter.Tests/RenderInvalidationTests.cssrc/Brouter/Tests/Bit.Brouter.Tests/ResolveUrlTests.cssrc/Brouter/Tests/Bit.Brouter.Tests/RouteDataCascadeTests.cssrc/Brouter/Tests/Bit.Brouter.Tests/SequentialLoadersHost.razorsrc/Brouter/Tests/Bit.Brouter.Tests/SwapRouteHost.razorsrc/Brouter/Tests/Bit.Brouter.Tests/TypedDataHost.razorsrc/Brouter/Tests/Bit.Brouter.Tests/TypedValueReader.razorsrc/Brouter/Tests/Bit.Brouter.Tests/ValueReader.razor
✅ Files skipped from review due to trivial changes (3)
- src/Brouter/Bit.Brouter/tsconfig.json
- src/Brouter/Bit.Brouter.slnx
- src/Brouter/Bit.Brouter/IBrouter.cs
🚧 Files skipped from review as they are similar to previous changes (3)
- src/Brouter/Bit.Brouter/BroutePrerenderState.cs
- src/Brouter/Tests/Bit.Brouter.Tests/NavigationEffectsTests.cs
- src/Brouter/Bit.Brouter/Broute.cs
|
@coderabbitai re-review |
|
✅ Action performedReview finished.
|
|
@coderabbitai re-review |
|
✅ Action performedReview finished.
|
|
@coderabbitai re-review |
|
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (2)
src/Brouter/Bit.Brouter/BrouterView.cs (1)
41-59: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueStale registration if the cascading
Routeinstance itself changes.The unregister/re-register logic only handles a changed
Name, not a changedRoute(e.g., if the component is reparented to a differentBrouteinstance across renders, which can happen with dynamic outlet content). In that case the oldRoutekeeps the stale fragment registered under_registeredNamesince only the newRoutereceivesSetNamedView. This is an edge case unlikely to occur in typical usage whereBrouterViewis statically nested inside a fixedBroute.Optional defensive fix
protected override void OnParametersSet() { if (Route is null) throw new InvalidOperationException( "A BrouterView must be declared inside a Broute (its ChildContent), whose parent layout hosts the named outlet."); if (string.IsNullOrEmpty(Name)) throw new InvalidOperationException( "BrouterView requires a non-empty Name. The route's main content already renders in the primary (unnamed) outlet via Content/Component."); - // Re-register every parameter pass: a host re-render produces a fresh fragment instance - // that the outlet must pick up, and a changed Name must vacate the old slot. - if (_registeredName is not null && string.Equals(_registeredName, Name, StringComparison.Ordinal) is false) - { - Route.SetNamedView(_registeredName, null); - } + // Re-register every parameter pass: a host re-render produces a fresh fragment instance + // that the outlet must pick up, and a changed Name or Route must vacate the old slot. + if (_registeredName is not null && (_registeredRoute != Route || string.Equals(_registeredName, Name, StringComparison.Ordinal) is false)) + { + _registeredRoute?.SetNamedView(_registeredName, null); + } Route.SetNamedView(Name, ChildContent); _registeredName = Name; + _registeredRoute = Route; }🤖 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/BrouterView.cs` around lines 41 - 59, The OnParametersSet logic in BrouterView only clears the previous registration when Name changes, so a new cascading Route instance can leave the old Route with a stale named view. Update the re-registration flow to detect when Route itself changes, unregister the previously stored _registeredName from the old Route before switching, then register ChildContent on the current Route and refresh _registeredName. Keep the fix localized to BrouterView.OnParametersSet and the _registeredName bookkeeping.src/Brouter/Tests/Bit.Brouter.Tests/RevalidateTests.cs (1)
55-83: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAssertions run before the fire-and-forget invoke completes.
Both
Revalidate_with_no_matched_chain_is_a_noop(Lines 56-67) andRoutes_without_loaders_revalidate_as_a_noop(Lines 69-83) callcut.InvokeAsync(() => brouter.RevalidateAsync().AsTask())without awaiting the returned task or wrapping the assertion incut.WaitForAssertion, unlike every other test in this class. IfRevalidateAsyncthrows or behaves asynchronously, these tests can pass trivially regardless of actual behavior, and any exception would be silently swallowed as an unobserved task fault.🧪 Proposed fix
- var brouter = Services.GetRequiredService<IBrouter>(); - cut.InvokeAsync(() => brouter.RevalidateAsync().AsTask()); - - Assert.AreEqual(0, cut.Instance.ParentLoaderRuns); - Assert.AreEqual(0, cut.Instance.ChildLoaderRuns); + var brouter = Services.GetRequiredService<IBrouter>(); + await cut.InvokeAsync(() => brouter.RevalidateAsync().AsTask()); + + Assert.AreEqual(0, cut.Instance.ParentLoaderRuns); + Assert.AreEqual(0, cut.Instance.ChildLoaderRuns);🤖 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/RevalidateTests.cs` around lines 55 - 83, The two no-op revalidation tests are asserting before `IBrouter.RevalidateAsync` finishes because `cut.InvokeAsync(...)` is fire-and-forget. Update `Revalidate_with_no_matched_chain_is_a_noop` and `Routes_without_loaders_revalidate_as_a_noop` to await the `InvokeAsync` task or verify results via `cut.WaitForAssertion`, matching the async pattern used elsewhere in `RevalidateTests`. Ensure the assertions on `ParentLoaderRuns`, `ChildLoaderRuns`, and the rendered `other` element happen only after `RevalidateAsync` completes so failures are observed.
🤖 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.Generators/BrouterRoutesGenerator.cs`:
- Around line 51-69: The route generator currently deduplicates templates in
Emit but still passes wildcard routes through, and EmitMethod does not handle
SegmentKind.Wildcard. Update BrouterRoutesGenerator so wildcard templates
emitted by RouteTemplateParser are either skipped during Emit or surfaced as a
diagnostic before generation, and make sure EmitMethod/related switch logic has
an explicit path for SegmentKind.Wildcard to avoid producing incomplete URL
builders.
- Around line 34-69: Duplicate route templates are still resolved by whichever
entry is seen first in BrouterRoutesGenerator.Emit, which silently drops
conflicting explicit names. Update the dedup logic in Emit to detect when the
same Template appears with conflicting route metadata (especially different
non-null Name values) and report a diagnostic through the generator context
instead of choosing a winner arbitrarily; keep the existing stable sort/output
path unchanged.
In `@src/Brouter/Bit.Brouter/BrouterAwait.cs`:
- Around line 41-68: The async render path in BrouterAwait’s ObserveAsync can
resume after the component has started disposing, so guard the final
InvokeAsync(StateHasChanged) call with a disposal check. Add a _disposed flag or
cancellation token to BrouterAwait, set it during disposal, and ensure
ObserveAsync exits without scheduling a render once disposal has begun; keep the
existing ReferenceEquals(_observed, task) check as part of the guard.
In `@src/Brouter/README.md`:
- Around line 320-342: Clarify the caching behavior in the README section around
preloading and loader caching: “No StaleTime anywhere → no caching” applies only
to the normal loader cache, while preloaded results are cached separately and
may still be reused during navigation via PreloadAsync and BrouterLinkPreload
with PreloadStaleTime. Update the wording in the StaleTime/preloading
explanation to distinguish the loader cache from the preload cache and avoid
implying that all caching is disabled when StaleTime is absent.
In
`@src/Brouter/Tests/Bit.Brouter.Generators.Tests/Bit.Brouter.Generators.Tests.csproj`:
- Around line 1-27: The test project is missing the shared build import/target
setup, so CI cannot resolve InstallNodejsDependencies. Update
Bit.Brouter.Generators.Tests.csproj to match the generator project’s build
integration by importing the shared Bit.Build.props/Bit.Build.targets pattern
and ensuring the InstallNodejsDependencies target is available. Use the existing
Bit.Brouter.Generators.csproj and its build-related symbols as the reference for
where the common build files and target wiring should be added.
In `@src/Brouter/Tests/Bit.Brouter.Tests/NavigateAsyncTests.cs`:
- Around line 20-129: The test methods in NavigateAsyncTests are reading the
navigation ValueTask before the cut.InvokeAsync dispatch has completed, so
navigation can still be the default value and produce a bogus outcome. Update
the affected tests to be async Task methods, await cut.InvokeAsync when
assigning navigation from BrouterNavigation.NavigateAsync, and then call Await
only after the dispatch has finished so the real ValueTask is observed.
---
Nitpick comments:
In `@src/Brouter/Bit.Brouter/BrouterView.cs`:
- Around line 41-59: The OnParametersSet logic in BrouterView only clears the
previous registration when Name changes, so a new cascading Route instance can
leave the old Route with a stale named view. Update the re-registration flow to
detect when Route itself changes, unregister the previously stored
_registeredName from the old Route before switching, then register ChildContent
on the current Route and refresh _registeredName. Keep the fix localized to
BrouterView.OnParametersSet and the _registeredName bookkeeping.
In `@src/Brouter/Tests/Bit.Brouter.Tests/RevalidateTests.cs`:
- Around line 55-83: The two no-op revalidation tests are asserting before
`IBrouter.RevalidateAsync` finishes because `cut.InvokeAsync(...)` is
fire-and-forget. Update `Revalidate_with_no_matched_chain_is_a_noop` and
`Routes_without_loaders_revalidate_as_a_noop` to await the `InvokeAsync` task or
verify results via `cut.WaitForAssertion`, matching the async pattern used
elsewhere in `RevalidateTests`. Ensure the assertions on `ParentLoaderRuns`,
`ChildLoaderRuns`, and the rendered `other` element happen only after
`RevalidateAsync` completes so failures are observed.
🪄 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: 931608d6-38f7-480f-9527-d182d4ca6dd0
📒 Files selected for processing (75)
src/Brouter/Bit.Brouter.Generators/Bit.Brouter.Generators.csprojsrc/Brouter/Bit.Brouter.Generators/BrouterRoutesGenerator.cssrc/Brouter/Bit.Brouter.Generators/IsExternalInit.cssrc/Brouter/Bit.Brouter.Generators/RazorRouteScanner.cssrc/Brouter/Bit.Brouter.Generators/RouteModel.cssrc/Brouter/Bit.Brouter.Generators/RouteTemplateParser.cssrc/Brouter/Bit.Brouter.slnxsrc/Brouter/Bit.Brouter/Broute.cssrc/Brouter/Bit.Brouter/BroutePrerenderState.cssrc/Brouter/Bit.Brouter/Brouter.cssrc/Brouter/Bit.Brouter/BrouterAwait.cssrc/Brouter/Bit.Brouter/BrouterErrorContext.cssrc/Brouter/Bit.Brouter/BrouterLink.cssrc/Brouter/Bit.Brouter/BrouterLinkPreload.cssrc/Brouter/Bit.Brouter/BrouterLoaderCache.cssrc/Brouter/Bit.Brouter/BrouterLocation.cssrc/Brouter/Bit.Brouter/BrouterNavigationContext.cssrc/Brouter/Bit.Brouter/BrouterNavigationOutcome.cssrc/Brouter/Bit.Brouter/BrouterOptions.cssrc/Brouter/Bit.Brouter/BrouterOutlet.cssrc/Brouter/Bit.Brouter/BrouterQueryBuilder.cssrc/Brouter/Bit.Brouter/BrouterRouteRenderer.cssrc/Brouter/Bit.Brouter/BrouterService.cssrc/Brouter/Bit.Brouter/BrouterStaleReloadMode.cssrc/Brouter/Bit.Brouter/BrouterView.cssrc/Brouter/Bit.Brouter/IBrouter.cssrc/Brouter/Bit.Brouter/Scripts/bit-brouter.tssrc/Brouter/InteralDemos/Core/AppRouter.razorsrc/Brouter/InteralDemos/Core/Bit.Brouter.Demos.Core.csprojsrc/Brouter/InteralDemos/Core/DemoState.cssrc/Brouter/InteralDemos/Core/Extensions/IServiceCollectionExtensions.cssrc/Brouter/InteralDemos/Core/Pages/DataPage.razorsrc/Brouter/InteralDemos/Core/Pages/DeferredPage.razorsrc/Brouter/InteralDemos/Core/Pages/EditorPage.razorsrc/Brouter/InteralDemos/Core/Pages/HistoryStatePage.razorsrc/Brouter/InteralDemos/Core/Pages/HomePage.razorsrc/Brouter/InteralDemos/Core/Pages/OutcomesPage.razorsrc/Brouter/InteralDemos/Core/Pages/StickyNotePage.razorsrc/Brouter/InteralDemos/Core/Pages/UnstablePage.razorsrc/Brouter/InteralDemos/Core/Shared/Header.razorsrc/Brouter/README.mdsrc/Brouter/Tests/Bit.Brouter.Generators.Tests/Bit.Brouter.Generators.Tests.csprojsrc/Brouter/Tests/Bit.Brouter.Generators.Tests/BrouterRoutesGeneratorTests.cssrc/Brouter/Tests/Bit.Brouter.Generators.Tests/GeneratorTestHarness.cssrc/Brouter/Tests/Bit.Brouter.Tests/CacheHost.razorsrc/Brouter/Tests/Bit.Brouter.Tests/CachedView.razorsrc/Brouter/Tests/Bit.Brouter.Tests/ChildDataView.razorsrc/Brouter/Tests/Bit.Brouter.Tests/DeferredDataTests.cssrc/Brouter/Tests/Bit.Brouter.Tests/DeferredHost.razorsrc/Brouter/Tests/Bit.Brouter.Tests/DiscoveryAndPersistenceTests.cssrc/Brouter/Tests/Bit.Brouter.Tests/ErrorContentHost.razorsrc/Brouter/Tests/Bit.Brouter.Tests/ErrorContentTests.cssrc/Brouter/Tests/Bit.Brouter.Tests/GroupRouteHost.razorsrc/Brouter/Tests/Bit.Brouter.Tests/GroupRouteTests.cssrc/Brouter/Tests/Bit.Brouter.Tests/HistoryStateHost.razorsrc/Brouter/Tests/Bit.Brouter.Tests/HistoryStateTests.cssrc/Brouter/Tests/Bit.Brouter.Tests/KeepAliveHost.razorsrc/Brouter/Tests/Bit.Brouter.Tests/KeepAliveTests.cssrc/Brouter/Tests/Bit.Brouter.Tests/LeaveGuardHost.razorsrc/Brouter/Tests/Bit.Brouter.Tests/LeaveGuardTests.cssrc/Brouter/Tests/Bit.Brouter.Tests/LoaderCacheTests.cssrc/Brouter/Tests/Bit.Brouter.Tests/NamedOutletHost.razorsrc/Brouter/Tests/Bit.Brouter.Tests/NamedOutletTests.cssrc/Brouter/Tests/Bit.Brouter.Tests/NavigateAsyncHost.razorsrc/Brouter/Tests/Bit.Brouter.Tests/NavigateAsyncTests.cssrc/Brouter/Tests/Bit.Brouter.Tests/NotFoundInteropHost.razorsrc/Brouter/Tests/Bit.Brouter.Tests/NotFoundInteropTests.cssrc/Brouter/Tests/Bit.Brouter.Tests/OnNavigateAsyncTests.cssrc/Brouter/Tests/Bit.Brouter.Tests/OnNavigateHost.razorsrc/Brouter/Tests/Bit.Brouter.Tests/QueryBuilderTests.cssrc/Brouter/Tests/Bit.Brouter.Tests/RevalidateHost.razorsrc/Brouter/Tests/Bit.Brouter.Tests/RevalidateTests.cssrc/Brouter/Tests/Bit.Brouter.Tests/SampleDto.cssrc/Brouter/Tests/Bit.Brouter.Tests/StatefulCounter.razorsrc/Brouter/Tests/Bit.Brouter.Tests/ViewTransitionTests.cs
✅ Files skipped from review due to trivial changes (4)
- src/Brouter/Bit.Brouter/BrouterLinkPreload.cs
- src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveHost.razor
- src/Brouter/InteralDemos/Core/DemoState.cs
- src/Brouter/Bit.Brouter.slnx
🚧 Files skipped from review as they are similar to previous changes (6)
- src/Brouter/Bit.Brouter/BroutePrerenderState.cs
- src/Brouter/Bit.Brouter/BrouterNavigationContext.cs
- src/Brouter/Bit.Brouter/Broute.cs
- src/Brouter/Bit.Brouter/BrouterLink.cs
- src/Brouter/Bit.Brouter/BrouterRouteRenderer.cs
- src/Brouter/Bit.Brouter/BrouterService.cs
| var rootNamespace = context.AnalyzerConfigOptionsProvider | ||
| .Select(static (provider, _) => | ||
| provider.GlobalOptions.TryGetValue("build_property.RootNamespace", out var ns) && string.IsNullOrWhiteSpace(ns) is false | ||
| ? ns | ||
| : "Bit.Brouter.Generated"); | ||
|
|
||
| context.RegisterSourceOutput(routesPerFile.Combine(rootNamespace), static (spc, input) => | ||
| { | ||
| var (perFile, ns) = input; | ||
| var source = Emit(perFile, ns); | ||
| if (source is not null) | ||
| { | ||
| spc.AddSource("BrouterRoutes.g.cs", SourceText.From(source, Encoding.UTF8)); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| private static string? Emit(ImmutableArray<EquatableArray<RouteModel>> perFile, string ns) | ||
| { | ||
| // Dedup by canonical template (case-insensitive; parameter names don't distinguish | ||
| // templates at runtime, but for generation the first declaration's names/types win - | ||
| // matching the router's "hand-declared shadows discovered" precedence closely enough). | ||
| // A named declaration is preferred over an unnamed duplicate so Names constants survive. | ||
| var byTemplate = new Dictionary<string, RouteModel>(StringComparer.OrdinalIgnoreCase); | ||
| foreach (var file in perFile) | ||
| { | ||
| foreach (var route in file.Items) | ||
| { | ||
| if (byTemplate.TryGetValue(route.Template, out var existing)) | ||
| { | ||
| if (existing.Name is null && route.Name is not null) byTemplate[route.Template] = route; | ||
| continue; | ||
| } | ||
| byTemplate[route.Template] = route; | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Repo root: '; pwd
printf '\nRelevant files:\n'
git ls-files | rg 'src/Brouter/Bit\.Brouter\.Generators/|RazorRouteScanner|RouteModel|BrouterRoutesGenerator'
printf '\nOutline generator file:\n'
ast-grep outline src/Brouter/Bit.Brouter.Generators/BrouterRoutesGenerator.cs --view expanded || true
printf '\nOutline scanner/model files if present:\n'
for f in $(git ls-files | rg 'RazorRouteScanner|RouteModel'); do
echo "== $f =="
ast-grep outline "$f" --view expanded || true
doneRepository: bitfoundation/bitplatform
Length of output: 1021
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Read the generator and nearby types in small slices with line numbers.
for f in src/Brouter/Bit.Brouter.Generators/BrouterRoutesGenerator.cs; do
echo "==== $f (1-260) ===="
nl -ba "$f" | sed -n '1,260p'
doneRepository: bitfoundation/bitplatform
Length of output: 282
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for any diagnostics or conflict handling around route names/templates.
rg -n "ReportDiagnostic|duplicate|conflict|Name is not null|Template" src/Brouter/Bit.Brouter.Generators src/Brouter -g '!**/bin/**' -g '!**/obj/**'Repository: bitfoundation/bitplatform
Length of output: 20889
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Probe whether the code itself imposes any ordering before the Collect() aggregation.
rg -n "AdditionalTextsProvider|Collect\(\)|OrderBy|ThenBy|Select\(static \(text" src/Brouter/Bit.Brouter.Generators -g '!**/bin/**' -g '!**/obj/**'Repository: bitfoundation/bitplatform
Length of output: 642
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,260p' src/Brouter/Bit.Brouter.Generators/BrouterRoutesGenerator.csRepository: bitfoundation/bitplatform
Length of output: 12224
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '== RouteModel.cs =='
sed -n '1,220p' src/Brouter/Bit.Brouter.Generators/RouteModel.cs
printf '\n%s\n' '== RazorRouteScanner.cs =='
sed -n '1,260p' src/Brouter/Bit.Brouter.Generators/RazorRouteScanner.cs
printf '\n%s\n' '== BrouterRoutesGeneratorTests.cs =='
sed -n '1,260p' src/Brouter/Tests/Bit.Brouter.Generators.Tests/BrouterRoutesGeneratorTests.csRepository: bitfoundation/bitplatform
Length of output: 14782
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "MakeUnique|methodNames|Names constants|explicit name|RouteModel\(|OrderBy\(r => r.Template|dedup" src/Brouter/Bit.Brouter.Generators/BrouterRoutesGenerator.cs
sed -n '80,220p' src/Brouter/Bit.Brouter.Generators/BrouterRoutesGenerator.csRepository: bitfoundation/bitplatform
Length of output: 7893
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "duplicate|ambiguous|name" src/Brouter/Tests/Bit.Brouter.Generators.Tests src/Brouter/Tests -g '!**/bin/**' -g '!**/obj/**'Repository: bitfoundation/bitplatform
Length of output: 16678
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '== BrouterRoutesGenerator.cs =='
sed -n '1,260p' src/Brouter/Bit.Brouter.Generators/BrouterRoutesGenerator.cs
printf '\n%s\n' '== RouteModel.cs =='
sed -n '1,220p' src/Brouter/Bit.Brouter.Generators/RouteModel.cs
printf '\n%s\n' '== RazorRouteScanner.cs =='
sed -n '1,260p' src/Brouter/Bit.Brouter.Generators/RazorRouteScanner.csRepository: bitfoundation/bitplatform
Length of output: 20601
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '== BrouterRoutesGenerator.cs =='
sed -n '1,260p' src/Brouter/Bit.Brouter.Generators/BrouterRoutesGenerator.cs
printf '\n%s\n' '== RouteModel.cs =='
sed -n '1,220p' src/Brouter/Bit.Brouter.Generators/RouteModel.cs
printf '\n%s\n' '== RazorRouteScanner.cs =='
sed -n '1,260p' src/Brouter/Bit.Brouter.Generators/RazorRouteScanner.cs
printf '\n%s\n' '== BrouterRoutesGeneratorTests.cs =='
sed -n '1,260p' src/Brouter/Tests/Bit.Brouter.Generators.Tests/BrouterRoutesGeneratorTests.csRepository: bitfoundation/bitplatform
Length of output: 26876
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '== generator name/dedup logic =='
rg -n "MakeUnique|methodNames|byTemplate|OrderBy\(r => r.Template|Names constants|ReportDiagnostic" src/Brouter/Bit.Brouter.Generators/BrouterRoutesGenerator.cs
printf '\n%s\n' '== tests mentioning duplicates/conflicts =='
rg -n "duplicate|ambiguous|conflict|name" src/Brouter/Tests/Bit.Brouter.Generators.Tests src/Brouter/Tests -g '!**/bin/**' -g '!**/obj/**'Repository: bitfoundation/bitplatform
Length of output: 17841
Handle duplicate templates deterministically
src/Brouter/Bit.Brouter.Generators/BrouterRoutesGenerator.cs:56-67 The routes are already sorted after dedup, so output order is stable; the remaining issue is that duplicate templates still pick a winner by file enumeration, and conflicting explicit Names are silently dropped. Report a diagnostic instead of keeping one arbitrarily.
🤖 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.Generators/BrouterRoutesGenerator.cs` around lines 34
- 69, Duplicate route templates are still resolved by whichever entry is seen
first in BrouterRoutesGenerator.Emit, which silently drops conflicting explicit
names. Update the dedup logic in Emit to detect when the same Template appears
with conflicting route metadata (especially different non-null Name values) and
report a diagnostic through the generator context instead of choosing a winner
arbitrarily; keep the existing stable sort/output path unchanged.
| private static string? Emit(ImmutableArray<EquatableArray<RouteModel>> perFile, string ns) | ||
| { | ||
| // Dedup by canonical template (case-insensitive; parameter names don't distinguish | ||
| // templates at runtime, but for generation the first declaration's names/types win - | ||
| // matching the router's "hand-declared shadows discovered" precedence closely enough). | ||
| // A named declaration is preferred over an unnamed duplicate so Names constants survive. | ||
| var byTemplate = new Dictionary<string, RouteModel>(StringComparer.OrdinalIgnoreCase); | ||
| foreach (var file in perFile) | ||
| { | ||
| foreach (var route in file.Items) | ||
| { | ||
| if (byTemplate.TryGetValue(route.Template, out var existing)) | ||
| { | ||
| if (existing.Name is null && route.Name is not null) byTemplate[route.Template] = route; | ||
| continue; | ||
| } | ||
| byTemplate[route.Template] = route; | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check how Wildcard segments are produced/consumed across the generator project.
fd -e cs . src/Brouter/Bit.Brouter.Generators -x rg -n -A3 -B3 'Wildcard' {}Repository: bitfoundation/bitplatform
Length of output: 594
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map the generator and scanner/parser files, then inspect the relevant slices.
for f in \
src/Brouter/Bit.Brouter.Generators/BrouterRoutesGenerator.cs \
src/Brouter/Bit.Brouter.Generators/RouteModel.cs \
src/Brouter/Bit.Brouter.Generators/RazorRouteScanner.cs \
src/Brouter/Bit.Brouter.Generators/RouteTemplateParser.cs
do
echo "===== OUTLINE: $f ====="
ast-grep outline "$f" --view expanded || true
echo
done
echo "===== GENERATOR WILDCARD / SWITCH REFERENCES ====="
rg -n -A6 -B6 'SegmentKind\.Wildcard|switch .*SegmentKind|case SegmentKind\.' src/Brouter/Bit.Brouter.Generators/BrouterRoutesGenerator.cs src/Brouter/Bit.Brouter.Generators/RouteModel.cs src/Brouter/Bit.Brouter.Generators/RazorRouteScanner.cs src/Brouter/Bit.Brouter.Generators/RouteTemplateParser.cs || true
echo "===== COLLECT / ORDER / DEDUP REFERENCES ====="
rg -n -A4 -B4 'Collect\(|OrderBy\(|ImmutableArray<EquatableArray<RouteModel>>|AdditionalTextsProvider|byTemplate|first declaration wins' src/Brouter/Bit.Brouter.Generators/BrouterRoutesGenerator.cs || trueRepository: bitfoundation/bitplatform
Length of output: 8097
Wildcard templates should be skipped or diagnosed
RouteTemplateParser emits SegmentKind.Wildcard, but EmitMethod has no case for it. */** routes will generate incomplete URL builders instead of being excluded or reported.
🤖 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.Generators/BrouterRoutesGenerator.cs` around lines 51
- 69, The route generator currently deduplicates templates in Emit but still
passes wildcard routes through, and EmitMethod does not handle
SegmentKind.Wildcard. Update BrouterRoutesGenerator so wildcard templates
emitted by RouteTemplateParser are either skipped during Emit or surfaced as a
diagnostic before generation, and make sure EmitMethod/related switch logic has
an explicit path for SegmentKind.Wildcard to avoid producing incomplete URL
builders.
| protected override void OnParametersSet() | ||
| { | ||
| if (ReferenceEquals(_observed, Task)) return; | ||
|
|
||
| _observed = Task; | ||
| if (Task is { IsCompleted: false }) | ||
| { | ||
| _ = ObserveAsync(Task); | ||
| } | ||
| } | ||
|
|
||
| private async Task ObserveAsync(Task<TValue> task) | ||
| { | ||
| try | ||
| { | ||
| await task; | ||
| } | ||
| catch | ||
| { | ||
| // The failure renders via the task's own status below; awaiting here only observes the | ||
| // exception so it never surfaces as an unobserved-task crash. | ||
| } | ||
|
|
||
| if (ReferenceEquals(_observed, task)) | ||
| { | ||
| await InvokeAsync(StateHasChanged); | ||
| } | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the target file with line numbers.
sed -n '1,220p' src/Brouter/Bit.Brouter/BrouterAwait.cs | cat -n
printf '\n---\n'
# Find disposal-related members in the component and nearby references.
rg -n --hidden -S "class BrouterAwait|IDisposable|IAsyncDisposable|Dispose\(|StateHasChanged|InvokeAsync\(" src/Brouter/Bit.Brouter
printf '\n---\n'
# Look for similar async-observation patterns elsewhere in the repo.
rg -n --hidden -S "ObserveAsync|await InvokeAsync\(StateHasChanged\)|_disposed|CancellationTokenSource" srcRepository: bitfoundation/bitplatform
Length of output: 40799
Guard the async render callback against disposal. ObserveAsync can still resume after the component is removed, so InvokeAsync(StateHasChanged) should be skipped once disposal begins. Add a _disposed flag or cancellation token and check it before scheduling the render.
🤖 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/BrouterAwait.cs` around lines 41 - 68, The async
render path in BrouterAwait’s ObserveAsync can resume after the component has
started disposing, so guard the final InvokeAsync(StateHasChanged) call with a
disposal check. Add a _disposed flag or cancellation token to BrouterAwait, set
it during disposal, and ensure ObserveAsync exits without scheduling a render
once disposal has begun; keep the existing ReferenceEquals(_observed, task)
check as part of the guard.
| With a `StaleTime` (per-route, or `o.DefaultLoaderStaleTime` globally), loader results cache per | ||
| URL (path + query): | ||
|
|
||
| - **fresh** hit (younger than `StaleTime`) → the loader is skipped entirely - Back/Forward becomes instant; | ||
| - **stale** hit → by default (`o.StaleReloadMode = Background`) the cached data renders immediately | ||
| and a background revalidation refreshes it (classic SWR); `Blocking` treats stale as a miss; | ||
| - entries die after `o.LoaderCacheGcTime` (30 min default) and the store is capped at | ||
| `o.MaxLoaderCacheEntries` (50), oldest evicted first. | ||
|
|
||
| No `StaleTime` anywhere → no caching, exactly the previous behavior. | ||
|
|
||
| ### Preloading | ||
|
|
||
| ```razor | ||
| <BrouterLink Href="/users/42" Preload="BrouterLinkPreload.Intent">Saleh</BrouterLink> | ||
| ``` | ||
|
|
||
| `Intent` runs the destination's loaders into the cache on hover/touch/focus (debounced by | ||
| `o.PreloadDelay`, 50 ms); `Viewport` fires once when the link scrolls into view; `Render` fires | ||
| immediately; `o.DefaultLinkPreload` sets an app-wide default. Programmatic: | ||
| `await brouter.PreloadAsync("/users/42")`. Preloads run **loaders only** - no guards, no rendering - | ||
| and a preloaded result younger than `o.PreloadStaleTime` (30 s) is used by the real navigation even | ||
| on routes with no `StaleTime`. Keep preloaded loaders side-effect-free (`ctx.IsPreload` is set). |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the relevant README section with line numbers
sed -n '300,360p' src/Brouter/README.md | cat -n
# Find the cache/preload terminology in the repo
rg -n --hidden --no-ignore-vcs "PreloadStaleTime|DefaultLoaderStaleTime|StaleReloadMode|LoaderCacheGcTime|MaxLoaderCacheEntries|PreloadDelay|DefaultLinkPreload|PreloadAsync|StaleTime" src/BrouterRepository: bitfoundation/bitplatform
Length of output: 15536
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the preload/cache implementation that the README describes
sed -n '2038,2058p' src/Brouter/Bit.Brouter/Brouter.cs | cat -n
printf '\n---\n'
sed -n '1,90p' src/Brouter/Bit.Brouter/BrouterLoaderCache.cs | cat -nRepository: bitfoundation/bitplatform
Length of output: 5851
Clarify preload vs normal loader caching
src/Brouter/README.md:329 — No StaleTime anywhere → no caching is only true for the normal loader cache; preloaded results are still cached separately and can be reused on navigation for PreloadStaleTime.
🤖 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/README.md` around lines 320 - 342, Clarify the caching behavior
in the README section around preloading and loader caching: “No StaleTime
anywhere → no caching” applies only to the normal loader cache, while preloaded
results are cached separately and may still be reused during navigation via
PreloadAsync and BrouterLinkPreload with PreloadStaleTime. Update the wording in
the StaleTime/preloading explanation to distinguish the loader cache from the
preload cache and avoid implying that all caching is disabled when StaleTime is
absent.
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>net10.0</TargetFramework> | ||
| <Nullable>enable</Nullable> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| <IsPackable>false</IsPackable> | ||
| <IsTestProject>true</IsTestProject> | ||
| <NoWarn>$(NoWarn);CS1591</NoWarn> | ||
| <EnableMSTestRunner>true</EnableMSTestRunner> | ||
| <GenerateDocumentationFile>false</GenerateDocumentationFile> | ||
| <SignAssembly>true</SignAssembly> | ||
| <AssemblyOriginatorKeyFile>..\..\..\AssemblyOriginatorKeyFile.snk</AssemblyOriginatorKeyFile> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="MSTest" Version="4.2.3" /> | ||
| <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <!-- Plain assembly reference: tests instantiate the generator directly and drive it with | ||
| CSharpGeneratorDriver rather than consuming it as an analyzer. --> | ||
| <ProjectReference Include="..\..\Bit.Brouter.Generators\Bit.Brouter.Generators.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift
Same CI-breaking MSB4057 failure as Bit.Brouter.Generators.csproj.
This test project also lacks the common build import and target that CI expects (InstallNodejsDependencies), and it doesn't even import a shared Bit.Build.props/Bit.Build.targets file like its sibling generator project does. See the detailed comment on Bit.Brouter.Generators.csproj for the root-cause verification script; the same fix should cover both projects.
🧰 Tools
🪛 GitHub Actions: bit platform CI - Brouter / 0_build Bit.Brouter.txt
[error] 1-1: MSBuild failed (MSB4057): The target "InstallNodejsDependencies" does not exist in the project.
🪛 GitHub Actions: bit platform CI - Brouter / build Bit.Brouter
[error] 1-1: MSB4057: The target "InstallNodejsDependencies" does not exist in the project.
🤖 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.Generators.Tests/Bit.Brouter.Generators.Tests.csproj`
around lines 1 - 27, The test project is missing the shared build import/target
setup, so CI cannot resolve InstallNodejsDependencies. Update
Bit.Brouter.Generators.Tests.csproj to match the generator project’s build
integration by importing the shared Bit.Build.props/Bit.Build.targets pattern
and ensuring the InstallNodejsDependencies target is available. Use the existing
Bit.Brouter.Generators.csproj and its build-related symbols as the reference for
where the common build files and target wiring should be added.
Source: Pipeline failures
| private BrouterNavigationOutcome Await(IRenderedComponent<NavigateAsyncHost> cut, ValueTask<BrouterNavigationOutcome> navigation) | ||
| { | ||
| var task = navigation.AsTask(); | ||
| cut.WaitForAssertion(() => Assert.IsTrue(task.IsCompleted, "navigation outcome did not resolve")); | ||
| return task.GetAwaiter().GetResult(); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void Successful_navigation_resolves_Succeeded() | ||
| { | ||
| var (cut, brouter) = RenderAtA(); | ||
|
|
||
| ValueTask<BrouterNavigationOutcome> navigation = default; | ||
| cut.InvokeAsync(() => { navigation = brouter.NavigateAsync("/b"); }); | ||
| var outcome = Await(cut, navigation); | ||
|
|
||
| Assert.AreEqual(BrouterNavigationStatus.Succeeded, outcome.Status); | ||
| Assert.IsTrue(outcome.Succeeded); | ||
| Assert.IsNotNull(cut.Find("[data-testid=b]")); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void Guard_cancel_resolves_Cancelled_and_url_stays() | ||
| { | ||
| var (cut, brouter) = RenderAtA(); | ||
| var nav = Services.GetRequiredService<FakeNavigationManager>(); | ||
|
|
||
| ValueTask<BrouterNavigationOutcome> navigation = default; | ||
| cut.InvokeAsync(() => { navigation = brouter.NavigateAsync("/blocked"); }); | ||
| var outcome = Await(cut, navigation); | ||
|
|
||
| Assert.AreEqual(BrouterNavigationStatus.Cancelled, outcome.Status); | ||
| Assert.IsTrue(nav.Uri.EndsWith("/a", StringComparison.Ordinal)); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void Guard_redirect_resolves_Redirected_with_the_target() | ||
| { | ||
| var (cut, brouter) = RenderAtA(); | ||
|
|
||
| ValueTask<BrouterNavigationOutcome> navigation = default; | ||
| cut.InvokeAsync(() => { navigation = brouter.NavigateAsync("/redirect"); }); | ||
| var outcome = Await(cut, navigation); | ||
|
|
||
| Assert.AreEqual(BrouterNavigationStatus.Redirected, outcome.Status); | ||
| Assert.AreEqual("/b", outcome.RedirectedTo); | ||
| cut.WaitForAssertion(() => cut.Find("[data-testid=b]")); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void RedirectTo_route_resolves_Redirected() | ||
| { | ||
| var (cut, brouter) = RenderAtA(); | ||
|
|
||
| ValueTask<BrouterNavigationOutcome> navigation = default; | ||
| cut.InvokeAsync(() => { navigation = brouter.NavigateAsync("/route-redirect"); }); | ||
| var outcome = Await(cut, navigation); | ||
|
|
||
| Assert.AreEqual(BrouterNavigationStatus.Redirected, outcome.Status); | ||
| Assert.AreEqual("/b", outcome.RedirectedTo); | ||
| cut.WaitForAssertion(() => cut.Find("[data-testid=b]")); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void Failing_loader_resolves_Failed_with_the_exception() | ||
| { | ||
| var (cut, brouter) = RenderAtA(); | ||
|
|
||
| ValueTask<BrouterNavigationOutcome> navigation = default; | ||
| cut.InvokeAsync(() => { navigation = brouter.NavigateAsync("/fail"); }); | ||
| var outcome = Await(cut, navigation); | ||
|
|
||
| Assert.AreEqual(BrouterNavigationStatus.Failed, outcome.Status); | ||
| Assert.IsInstanceOfType<InvalidOperationException>(outcome.Exception); | ||
| Assert.IsTrue(outcome.Exception!.Message.Contains("loader exploded")); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void Unmatched_url_resolves_NotFound() | ||
| { | ||
| var (cut, brouter) = RenderAtA(); | ||
|
|
||
| ValueTask<BrouterNavigationOutcome> navigation = default; | ||
| cut.InvokeAsync(() => { navigation = brouter.NavigateAsync("/nope/nothing/here"); }); | ||
| var outcome = Await(cut, navigation); | ||
|
|
||
| Assert.AreEqual(BrouterNavigationStatus.NotFound, outcome.Status); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void A_newer_navigation_supersedes_an_in_flight_awaiter() | ||
| { | ||
| var (cut, brouter) = RenderAtA(); | ||
|
|
||
| // First navigation parks inside the /slow loader, genuinely in-flight... | ||
| ValueTask<BrouterNavigationOutcome> first = default; | ||
| cut.InvokeAsync(() => { first = brouter.NavigateAsync("/slow"); }); | ||
|
|
||
| // ...then a second navigation starts before the first's loader finishes. | ||
| ValueTask<BrouterNavigationOutcome> second = default; | ||
| cut.InvokeAsync(() => { second = brouter.NavigateAsync("/b"); }); | ||
|
|
||
| var firstOutcome = Await(cut, first); | ||
| var secondOutcome = Await(cut, second); | ||
| cut.Instance.SlowGate.TrySetResult(); // release the parked loader so nothing leaks | ||
|
|
||
| Assert.AreEqual(BrouterNavigationStatus.Superseded, firstOutcome.Status); | ||
| Assert.AreEqual(BrouterNavigationStatus.Succeeded, secondOutcome.Status); | ||
| cut.WaitForAssertion(() => cut.Find("[data-testid=b]")); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Await the InvokeAsync dispatch before reading navigation. cut.InvokeAsync(...) is fire-and-forget here, so Await(cut, navigation) can see the default ValueTask<BrouterNavigationOutcome> and return a bogus completed result. Make the tests async Task and await the dispatch before calling Await.
🤖 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/NavigateAsyncTests.cs` around lines 20 -
129, The test methods in NavigateAsyncTests are reading the navigation ValueTask
before the cut.InvokeAsync dispatch has completed, so navigation can still be
the default value and produce a bogus outcome. Update the affected tests to be
async Task methods, await cut.InvokeAsync when assigning navigation from
BrouterNavigation.NavigateAsync, and then call Await only after the dispatch has
finished so the real ValueTask is observed.
closes #12559
beforeunloaddialog, always-on or toggled at runtime.NavigateAsynctells you if it succeeded, was cancelled, redirected, not found, failed, or superseded.ErrorContentrenders failures in place, with retry.NavigationManager.NotFound()support and real HTTP 404s under static SSR.StaleTimemakes Back/Forward instant; stale data renders now, refreshes in background.<BrouterAwait>streams slow data in after the page reveals.BrouterView.Bit.Brouter.Generators— compile-time-safe URL builders generated from your route declarations.Plus: 4 pre-existing bugs fixed, 77 tests added (205 total), a demo for every feature, full README coverage.
Summary by CodeRabbit
New Features
<Broute>route component with improved parameter binding, custom constraints, named outlets, keep-alive support, and discovered route matching.Bug Fixes