Skip to content

feat: Add stream client to dotnet sdk - #156

Open
aholmis wants to merge 8 commits into
mainfrom
POWER-5102-stream-sdk
Open

feat: Add stream client to dotnet sdk#156
aholmis wants to merge 8 commits into
mainfrom
POWER-5102-stream-sdk

Conversation

@aholmis

@aholmis aholmis commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

POWER-5102

Add C# .NET client to receive streaming events from Heimdall Cloud Stream Api

@aholmis aholmis changed the title POWER-5102 Add stream client to sdk feat: Add stream client to sdk Aug 28, 2026
@aholmis aholmis changed the title feat: Add stream client to sdk feat: Add stream client to dotnet sdk Aug 28, 2026

Copilot AI 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.

Pull request overview

Adds a new .NET streaming client for consuming Heimdall Cloud Stream API Server-Sent Events, along with supporting auth/header infrastructure, tests, and documentation/examples to help SDK consumers adopt streaming.

Changes:

  • Introduces HeimdallStreamClient / IHeimdallStreamClient to receive SSE events with automatic reconnect + backoff and token refresh.
  • Refactors shared auth/client header handling into AccessTokenHeaderRefresher, and centralizes endpoint constants in HeimdallApiEndpoints.
  • Adds unit tests for parsing/auth/reconnect behavior, plus README + example console app.

Reviewed changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
dotnet/tests/unit/HeimdallPower.Api.Client.UnitTests/WhenStreaming/WhenReconnecting.cs Unit tests for reconnect and cancellation behavior.
dotnet/tests/unit/HeimdallPower.Api.Client.UnitTests/WhenStreaming/WhenParsingEvents.cs Unit tests for SSE parsing, skipping heartbeats/unknown events, and recovery from malformed payloads.
dotnet/tests/unit/HeimdallPower.Api.Client.UnitTests/WhenStreaming/WhenAuthenticating.cs Unit tests for header attachment, token reuse, and forced refresh on 401.
dotnet/tests/unit/HeimdallPower.Api.Client.UnitTests/WhenStreaming/Fakes/SseTestData.cs SSE test-body and canned HTTP response builders.
dotnet/tests/unit/HeimdallPower.Api.Client.UnitTests/WhenStreaming/Fakes/NeverEndingSseStream.cs Test stream for cancellation behavior during long-lived reads.
dotnet/tests/unit/HeimdallPower.Api.Client.UnitTests/WhenStreaming/Fakes/CountingAccessTokenProvider.cs Token provider fake that counts acquisitions for assertions.
dotnet/tests/unit/HeimdallPower.Api.Client.UnitTests/WhenComputingRetryDelay/WhenComputingRetryDelay.cs Unit tests for exponential backoff + jitter math.
dotnet/README.md Adds streaming documentation and DI registration guidance.
dotnet/HeimdallPower.Api.Client/HeimdallPower.Api.Client/Stream/StreamConnectionRetryPolicy.cs Backoff + jitter policy used by stream reconnect logic.
dotnet/HeimdallPower.Api.Client/HeimdallPower.Api.Client/Stream/IHeimdallStreamClient.cs Public interface for receiving stream events.
dotnet/HeimdallPower.Api.Client/HeimdallPower.Api.Client/Stream/HeimdallStreamJsonSerializerOptions.cs Shared JSON serializer options for stream payloads (snake_case, enums).
dotnet/HeimdallPower.Api.Client/HeimdallPower.Api.Client/Stream/HeimdallStreamClient.cs Core SSE client implementation (connect, parse, reconnect, token refresh).
dotnet/HeimdallPower.Api.Client/HeimdallPower.Api.Client/Stream/HeimdallEventEnvelope.cs Event envelope model with typed accessor for DLR events.
dotnet/HeimdallPower.Api.Client/HeimdallPower.Api.Client/Stream/CapacityMonitoring/Lines/HeimdallDlrEvent.cs DLR payload model and metric name constant.
dotnet/HeimdallPower.Api.Client/HeimdallPower.Api.Client/HeimdallApiHttpClient.cs Refactors REST client to use shared header/token refresher.
dotnet/HeimdallPower.Api.Client/HeimdallPower.Api.Client/HeimdallApiEndpoints.cs Centralizes API/stream URLs + AAD/B2C authority/scope constants.
dotnet/HeimdallPower.Api.Client/HeimdallPower.Api.Client/HeimdallApiClient.cs Switches to shared endpoint constants.
dotnet/HeimdallPower.Api.Client/HeimdallPower.Api.Client/ClientHeaders.cs Centralizes x-client-* header construction.
dotnet/HeimdallPower.Api.Client/HeimdallPower.Api.Client/AccessTokenHeaderRefresher.cs New shared component to keep auth/client headers fresh with concurrency control.
dotnet/HeimdallPower.Api.Client/HeimdallPower.Api.Client.Extensions/HeimdallStreamClientOptions.cs DI options for stream client credentials/proxy/metadata.
dotnet/HeimdallPower.Api.Client/HeimdallPower.Api.Client.Extensions/HeimdallStreamClientExtensions.cs Adds AddHeimdallPowerStreamClient DI registration.
dotnet/HeimdallPower.Api.Client.slnx Adds the new stream example project to the solution.
dotnet/examples/Stream.Client.Examples/Stream.Client.Examples.csproj New example project for stream usage.
dotnet/examples/Stream.Client.Examples/Program.cs Example showing how to consume stream events and stop via Ctrl+C.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread dotnet/examples/Stream.Client.Examples/Program.cs Outdated
@aholmis
aholmis marked this pull request as ready for review August 30, 2026 08:55
@aholmis
aholmis requested a review from a team as a code owner August 30, 2026 08:55
@aholmis
aholmis requested a lite review from Copilot August 30, 2026 09:03

Copilot AI 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.

Pull request overview

Copilot reviewed 27 out of 27 changed files in this pull request and generated 2 comments.

Suppressed comments (1)

dotnet/HeimdallPower.Api.Client/HeimdallPower.Api.Client/AccessTokenHeaderRefresher.cs:28

  • SemaphoreSlim.WaitAsync(TimeSpan, CancellationToken) returns bool (timeout vs lock-acquired). The current code ignores the return value, so a timeout could lead to executing the critical section without having entered the semaphore and then calling Release(), which can throw SemaphoreFullException and break token refresh under contention.
        await _tokenLock.WaitAsync(TimeSpan.FromSeconds(30), cancellationToken);

Comment thread dotnet/README.md Outdated

Copilot AI 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.

🟡 Changes recommended

Retry timing, graceful cancellation, and token-refresh synchronization contain correctness issues.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

dotnet/HeimdallPower.Api.Client/HeimdallPower.Api.Client/Stream/StreamConnectionRetryPolicy.cs:48

  • Jitter is applied after capping, so this can return up to 120% of MaxDelay. That contradicts the public option's guarantee that MaxDelay is the maximum delay between attempts. Apply the cap again after jitter (the new tests should also assert delay <= MaxDelay, not MaxDelay * 1.2).

dotnet/HeimdallPower.Api.Client/HeimdallPower.Api.Client/AccessTokenHeaderRefresher.cs:28

  • SemaphoreSlim.WaitAsync(TimeSpan, ...) returns false on timeout rather than throwing. Ignoring that result lets a timed-out caller enter the critical section without owning the semaphore and then call Release(), allowing concurrent token/header mutation and potentially causing SemaphoreFullException when the real owner releases. Wait with cancellation only, or check the Boolean and fail on timeout.
        await _tokenLock.WaitAsync(TimeSpan.FromSeconds(30), cancellationToken);
  • Files reviewed: 27/27 changed files
  • Comments generated: 4
  • Review effort level: Balanced

/// <summary>
/// Controls which physical quantity is returned by a rating endpoint.
/// </summary>
public enum Quantity
{
failedAttempts++;
infoLogger?.Invoke($"Stream error: unauthorized. Refreshing token and reconnecting... (attempt #{failedAttempts})");
await _tokenRefresher.ForceRefreshAsync(token);
{
if (_retryPolicy.ShouldRetry(failedAttempts))
{
await Task.Delay(_retryPolicy.GetDelay(failedAttempts), token);

Assert.Single(events);
Assert.Contains(traceMessages, m => m.Contains("Heartbeat", StringComparison.OrdinalIgnoreCase));
Assert.Contains(logMessages, m => !m.Contains("Heartbeat", StringComparison.OrdinalIgnoreCase));
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.

2 participants