Skip to content

feat(ConnectionSnapshot): add ConnectionSnapshot response model - #1255

Merged
ArgoZhang merged 9 commits into
masterfrom
dev-middleware
Oct 27, 2026
Merged

ArgoZhang merged 9 commits into
masterfrom
dev-middleware

Conversation

@ArgoZhang

@ArgoZhang ArgoZhang commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Link issues

fixes #1256

Summary By Copilot

Regression?

  • Yes
  • No

Risk

  • High
  • Medium
  • Low

Verification

  • Manual (required)
  • Automated

Packaging changes reviewed?

  • Yes
  • No
  • N/A

☑️ Self Check before Merge

⚠️ Please check all items below before review. ⚠️

  • Doc is updated/provided or not needed
  • Demo is updated/provided or not needed
  • Merge the latest code from the main branch

Summary by Sourcery

Add a typed connection snapshot response for authorized online connection queries.

New Features:

  • Add a connection snapshot response model for retrieving the current online connection count and details.

Enhancements:

  • Centralize connection-service access and authorization checks for snapshot retrieval and heartbeat reporting.

@sourcery-ai

sourcery-ai Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Adds a new SignalR client extension for WASM that shares a lifecycle-managed HubConnection across generic message dispatch and connection-monitoring services, with automatic reconnect, subscription restoration, heartbeat updates, snapshot polling, and DI registration.

Sequence diagram for SignalR dispatch and subscription lifecycle

sequenceDiagram
    participant App
    participant Dispatch as DefaultDispatchService
    participant Connection as SignalRConnection
    participant Hub as SignalR Hub
    participant Subscriber as Local callback

    App->>Dispatch: SubscribeAsync(callback)
    Dispatch->>Connection: SubscribeAsync(entryType, ReceiveAsync)
    Connection->>Hub: Subscribe(entryType)
    Hub-->>Connection: subscription result
    Connection-->>Dispatch: subscription ready
    App->>Dispatch: DispatchAsync(entry)
    Dispatch->>Connection: DispatchAsync(entryType, entry)
    Connection->>Hub: Dispatch(entryType, entry)
    Hub-->>Connection: Receive(entryType, entry)
    Connection->>Dispatch: ReceiveAsync(json)
    Dispatch->>Subscriber: callback(entry)
    App->>Dispatch: UnSubscribeAsync(callback)
    Dispatch->>Connection: UnsubscribeAsync(entryType)
    Connection->>Hub: Unsubscribe(entryType)
Loading

Sequence diagram for connection heartbeat and snapshot polling

sequenceDiagram
    participant App
    participant Monitor as DefaultConnectionService
    participant Connection as SignalRConnection
    participant Hub as SignalR Hub

    App->>Monitor: AddOrUpdateAsync(client)
    Monitor->>Connection: InvokeAsync(Heartbeat, client)
    Connection->>Hub: Heartbeat(client)
    Hub-->>Connection: success or disabled
    loop Every 3 seconds
        Monitor->>Connection: InvokeAsync(GetConnectionSnapshot, null)
        Connection->>Hub: GetConnectionSnapshot()
        Hub-->>Connection: ConnectionSnapshot
        Connection-->>Monitor: snapshot
    end
    App->>Monitor: DisposeAsync()
    Monitor->>Connection: ReleaseAsync()
    Connection->>Connection: StopAsync when unused
Loading

State diagram for shared SignalR connection lifecycle

stateDiagram-v2
    [*] --> Disconnected
    Disconnected --> Connected: EnsureConnectedAsync / StartAsync
    Connected --> Reconnecting: SignalR transport loss
    Reconnecting --> Connected: RestoreSubscriptionsAsync
    Connected --> Disconnected: UnsubscribeAsync or ReleaseAsync
    Disconnected --> [*]: DisposeAsync
    Reconnecting --> Disconnected: reconnect failure
Loading

File-Level Changes

Change Details Files
Introduces a new WASM-facing SignalR client package and dependency-injection registration for shared Hub connectivity.
  • Adds the client project and solution registration.
  • Registers a shared SignalR connection, generic dispatch services, and connection service.
  • Resolves the configured Hub path through NavigationManager and validates it on supported target frameworks.
BootstrapBlazor.Extensions.slnx
src/extensions/BootstrapBlazor.SignalR.Client/BootstrapBlazor.SignalR.Client.csproj
src/extensions/BootstrapBlazor.SignalR.Client/ServiceCollectionExtensions.cs
Implements a shared SignalR connection manager with lifecycle reference counting, serialized operations, automatic reconnect, and subscription restoration.
  • Creates and disposes a HubConnection with automatic retry and a Receive handler.
  • Coordinates connect, invoke, dispatch, subscribe, unsubscribe, and stop operations through a semaphore.
  • Tracks typed subscriptions and restores transiently failed subscriptions after reconnect.
  • Classifies transient transport failures and applies bounded operation/reconnect retries.
src/extensions/BootstrapBlazor.SignalR.Client/SignalRConnection.cs
Adds client-side dispatch services that multiplex local callbacks over one server subscription per message type.
  • Dispatches entries to the server and locally notifies subscribers after successful sends.
  • Serializes subscribe/unsubscribe transitions and rolls back failed initial subscriptions.
  • Retries transient subscription failures while distinguishing rejected subscriptions.
  • Deserializes received payloads, invokes callbacks in order, isolates callback failures, and cleans up subscriptions during disposal.
src/extensions/BootstrapBlazor.SignalR.Client/DefaultDispatchService.cs
Adds a client connection service for heartbeat reporting and periodic connection snapshots.
  • Retains the shared connection and exposes the latest snapshot count and connection collection.
  • Sends heartbeat updates through the Hub and logs when server-side heartbeats are disabled.
  • Polls snapshots every three seconds, clears/stops polling for disabled or unauthorized queries, and logs transient failures.
  • Cancels polling and releases the shared connection reference during asynchronous disposal.
src/extensions/BootstrapBlazor.SignalR.Client/DefaultConnectionService.cs

Assessment against linked issues

Issue Objective Addressed Explanation
#1254 Provide SignalR-backed dispatch services for WebAssembly clients, including sending dispatch entries and receiving them through local subscriptions. ✅
#1254 Provide a SignalR-backed connection service for WebAssembly clients, including heartbeat reporting and access to a periodically refreshed server connection snapshot. ✅
#1254 Expose client-side dependency-injection registration for these services and share a Hub connection between them. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@bb-auto bb-auto Bot added the enhancement New feature or request label Oct 27, 2026
@bb-auto bb-auto Bot added this to the v10.0.0 milestone Oct 27, 2026
bb-auto[bot]
bb-auto Bot previously approved these changes Oct 27, 2026

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

Auto approved by bb-auto

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

Hey - I've found 8 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/extensions/BootstrapBlazor.SignalR.Client/DefaultConnectionService.cs" line_range="92" />
<code_context>
+            {
+                try
+                {
+                    var snapshot = await _connection.InvokeAsync<ConnectionSnapshot?>("GetConnectionSnapshot", null, cancellationToken);
+                    if (snapshot == null)
+                    {
</code_context>
<issue_to_address>
**Connection snapshots reach every Hub client**

When the host enables connection snapshots, leaves the snapshot authorization policy unset, and allows callers who should not see connection metadata to connect to the Hub, `PollAsync` invokes `GetConnectionSnapshot` and caches the returned client metadata. The server skips its policy check when no snapshot policy is configured, so every caller allowed to connect to that Hub can read the snapshot, including callers without a separate authorization to view it.

Require a dedicated authorization policy before returning snapshots, or ensure the Hub endpoint’s authorization is sufficiently restrictive whenever snapshots are enabled.
</issue_to_address>

### Comment 2
<location path="src/extensions/BootstrapBlazor.SignalR.Client/DefaultDispatchService.cs" line_range="96-102" />
<code_context>
+                        }
+                    }
+                }
+                catch
+                {
+                    // 订阅未就绪时撤销本次本地注册,使后续订阅者仍能走 first 分支重新尝试。
+                    // Roll back this registration if subscribing fails so the next subscriber can retry through the first branch.
+                    lock (_locker)
+                    {
+                        _callbacks.Remove(subscription);
+                    }
+                    throw;
</code_context>
<issue_to_address>
**Server group subscriptions leak**

When the subscribe request is canceled or fails after the server adds the connection to the group, `SubscribeAsync` removes the local callback but leaves `_requiresUnsubscribe` set and never calls `connection.UnsubscribeAsync`, so the server keeps sending messages that the client discards.

On subscribe failure, attempt `connection.UnsubscribeAsync` to clean up the server-side group subscription before rethrowing.
</issue_to_address>

### Comment 3
<location path="src/extensions/BootstrapBlazor.SignalR.Client/SignalRConnection.cs" line_range="64" />
<code_context>
+    {
+        _logger = logger;
+        _connection = new HubConnectionBuilder()
+            .WithUrl(navigation.ToAbsoluteUri(hubPath))
+            .WithAutomaticReconnect(new ConnectionRetryPolicy())
+            .Build();
</code_context>
<issue_to_address>
**Bearer-token clients cannot connect**

When the Hub requires bearer authentication and the WebAssembly app does not use same-origin cookies, `WithUrl` has no access-token provider or other authentication configuration, so the handshake lacks a bearer token and the Hub rejects the connection, leaving dispatch and related calls unusable.

Configure the Hub connection with an access-token provider or other bearer-token authentication options.
</issue_to_address>

### Comment 4
<location path="src/extensions/BootstrapBlazor.SignalR.Client/SignalRConnection.cs" line_range="204" />
<code_context>
+        try
+        {
+            await EnsureConnectedAsync(token);
+            await _connection.InvokeAsync("Dispatch", entryType, entry, token);
+        }
+        finally
</code_context>
<issue_to_address>
**Dispatch calls can hang**

When a Hub dispatch or server-side publish callback stops responding while the connection remains open, `SignalRConnection.DispatchAsync` invokes the Hub without a timeout, and the default cancellation token is not canceled. If the server-side publish callback or transport stops responding without disconnecting, the dispatch task stays pending and the caller never gets a result.

Apply a bounded timeout to dispatch invocations, while still allowing callers to provide cancellation.
</issue_to_address>

### Comment 5
<location path="src/extensions/BootstrapBlazor.SignalR.Client/DefaultDispatchService.cs" line_range="37-39" />
<code_context>
+    /// <param name="entry"><para lang="zh">消息分发项</para><para lang="en">The dispatch entry</para></param>
+    /// <param name="cancellationToken"><para lang="zh">取消令牌</para><para lang="en">The cancellation token</para></param>
+    /// <returns><para lang="zh">表示异步操作的任务</para><para lang="en">A task representing the asynchronous operation</para></returns>
+    public async Task DispatchAsync(DispatchEntry<TEntry> entry, CancellationToken cancellationToken = default)
+    {
+        await connection.DispatchAsync(_entryType, entry, cancellationToken);
+        await DispatchCoreAsync(entry, cancellationToken);
+    }
</code_context>
<issue_to_address>
**Retried dispatches publish twice**

When the server processes a dispatch but the response is lost and the caller retries after seeing a failure, `DispatchAsync` has no dispatch identifier or deduplication check, so if the server processes `Dispatch` but its response is lost, a caller retry publishes the same entry again to other clients and server-side subscribers.

Add a stable dispatch ID and deduplicate it server-side before invoking subscribers or broadcasting.
</issue_to_address>

### Comment 6
<location path="src/extensions/BootstrapBlazor.SignalR.Client/DefaultDispatchService.cs" line_range="39-40" />
<code_context>
+    /// <returns><para lang="zh">表示异步操作的任务</para><para lang="en">A task representing the asynchronous operation</para></returns>
+    public async Task DispatchAsync(DispatchEntry<TEntry> entry, CancellationToken cancellationToken = default)
+    {
+        await connection.DispatchAsync(_entryType, entry, cancellationToken);
+        await DispatchCoreAsync(entry, cancellationToken);
+    }
+
</code_context>
<issue_to_address>
**Sender misses committed dispatch**

When the caller cancels after the server processes the dispatch but before local notification finishes, `connection.DispatchAsync` can complete the server-side dispatch before cancellation is observed. The call then throws or `DispatchCoreAsync` exits on the canceled token, so other recipients receive the entry while this client's local subscribers do not.

Define post-commit cancellation semantics and ensure a committed dispatch is still delivered to local subscribers, or use an idempotent dispatch protocol that can safely reconcile the outcome.
</issue_to_address>

### Comment 7
<location path="src/extensions/BootstrapBlazor.SignalR.Client/DefaultDispatchService.cs" line_range="37-40" />
<code_context>
+    /// <param name="entry"><para lang="zh">消息分发项</para><para lang="en">The dispatch entry</para></param>
+    /// <param name="cancellationToken"><para lang="zh">取消令牌</para><para lang="en">The cancellation token</para></param>
+    /// <returns><para lang="zh">表示异步操作的任务</para><para lang="en">A task representing the asynchronous operation</para></returns>
+    public async Task DispatchAsync(DispatchEntry<TEntry> entry, CancellationToken cancellationToken = default)
+    {
+        await connection.DispatchAsync(_entryType, entry, cancellationToken);
+        await DispatchCoreAsync(entry, cancellationToken);
+    }
+
</code_context>
<issue_to_address>
**Local dispatches arrive out of order**

When two callers dispatch concurrently and the first caller is delayed between the server response and its local callback notification, `SignalRConnection.DispatchAsync` releases its gate when the server invocation completes, before `DispatchCoreAsync` notifies local subscribers. A second caller can therefore complete its server invocation and notify local subscribers before the first caller does, so local consumers observe entries in an order different from the server’s accepted order.

Serialize the complete remote-send and local-notification sequence per dispatch service, or assign ordering information and make local delivery preserve server acceptance order.
</issue_to_address>

### Comment 8
<location path="src/extensions/BootstrapBlazor.SignalR.Client/SignalRConnection.cs" line_range="203-204" />
<code_context>
+        await _gate.WaitAsync(token);
+        try
+        {
+            await EnsureConnectedAsync(token);
+            await _connection.InvokeAsync("Dispatch", entryType, entry, token);
+        }
+        finally
</code_context>
<issue_to_address>
**Idle Hub connections stay open**

When a caller dispatches without subscriptions or a connection service retaining the Hub, `EnsureConnectedAsync` starts the Hub, but `DispatchAsync` releases no usage reference and does not stop the connection, leaving the WebSocket open indefinitely.

Track dispatch-only connection use and stop the Hub after dispatch when no other consumer retains it.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨

bb-auto[bot]
bb-auto Bot previously approved these changes Oct 27, 2026

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

Auto approved by bb-auto

bb-auto[bot]
bb-auto Bot previously approved these changes Oct 27, 2026

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

Auto approved by bb-auto

@ArgoZhang ArgoZhang changed the title feat(SignalR): add Dispatch/Connection services for wasm mode feat(ConnectionSnapshot): add ConnectionSnapshot response model Oct 27, 2026
bb-auto[bot]
bb-auto Bot previously approved these changes Oct 27, 2026

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

Auto approved by bb-auto

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

Auto approved by bb-auto

@ArgoZhang
ArgoZhang merged commit 8da8241 into master Oct 27, 2026
3 checks passed
@ArgoZhang
ArgoZhang deleted the dev-middleware branch October 27, 2026 05:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(ConnectionSnapshot): add ConnectionSnapshot response model

1 participant