Skip to content

feat(LinqMixins): add fused BlendUnique operator (merge + distinct-until-changed)#18

Merged
glennawatson merged 4 commits into
mainfrom
feat/blend-unique-fused-sink
Jun 2, 2026
Merged

feat(LinqMixins): add fused BlendUnique operator (merge + distinct-until-changed)#18
glennawatson merged 4 commits into
mainfrom
feat/blend-unique-fused-sink

Conversation

@glennawatson
Copy link
Copy Markdown
Contributor

What

Adds LinqMixins.BlendUnique<T> — a single fused sink that concurrently merges a fixed set of sources and applies distinct-until-changed, in one subscription hop:

public static IObservable<T> BlendUnique<T>(params IObservable<T>[] sources);
public static IObservable<T> BlendUnique<T>(IObservable<T>[] sources, IEqualityComparer<T>? comparer);

Semantics: forwards a merged value only when it differs from the previously forwarded one; forwards the first source error and suppresses later notifications; completes once every source has completed. An optional comparer controls duplicate suppression (defaults to EqualityComparer<T>.Default).

Why

sources.Blend().Unique() is two sinks — a Blend coordinator plus a Unique observer — so it costs an extra subscription hop and allocation on a hot path. Activation/lifecycle code (merge several boolean event streams, then distinct) wants this as one allocation-light pass. BlendUnique folds the merge and the distinct into a single sink.

Tests

New BlendUniqueTests (5 cases): merge + consecutive-duplicate suppression, empty-source completion, custom comparer, first-source-error propagation, null-argument validation. Full ReactiveUI.Primitives.Tests suite green on net9.0; core builds on net8.0/net9.0/net462. API approval snapshot updated for the two new public methods.

…til-changed)

Folds a concurrent merge of a fixed set of sources and distinct-until-changed
into a single sink, avoiding the extra subscription hop and allocation of
sources.Blend().Unique(). Forwards the first source error and completes once
every source has completed; an optional comparer suppresses duplicates.
Copy link
Copy Markdown

Copilot AI left a comment

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 fused operator LinqMixins.BlendUnique<T> to merge a fixed set of IObservable<T> sources while applying distinct-until-changed semantics in a single sink, reducing the extra hop/allocation of sources.Blend().Unique().

Changes:

  • Added LinqMixins.BlendUnique<T> overloads and a dedicated fused sink implementation.
  • Added BlendUniqueTests covering core behavior (merge + adjacent duplicate suppression, empty sources, comparer, error, null validation).
  • Updated the net9.0 API approval snapshot to include the new public APIs.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
src/ReactiveUI.Primitives/SignalOperatorMixins.BlendUnique.cs Implements the fused merge + distinct-until-changed operator and its sink.
src/tests/ReactiveUI.Primitives.Tests/BlendUniqueTests.cs Adds unit tests for BlendUnique behavior and argument validation.
src/tests/ReactiveUI.Primitives.Tests/ApiApprovalTests.Primitives.DotNet9_0.verified.txt Records the new public API surface for net9.0 approval testing.

Comment thread src/ReactiveUI.Primitives/SignalOperatorMixins.BlendUnique.cs
Comment thread src/ReactiveUI.Primitives/SignalOperatorMixins.BlendUnique.cs
Comment thread src/tests/ReactiveUI.Primitives.Tests/BlendUniqueTests.cs
…ce completion, add net8/net10 API snapshots

- BlendUnique now throws ArgumentNullException for a null source element at
  call time (matching the repo's params-factory convention) instead of erroring
  at subscription; drops the now-redundant in-sink null check.
- Empty-source path completes directly instead of decrementing the active
  counter below zero.
- Add a null-source-element test case.
- Record the new BlendUnique API surface in the net8.0 and net10.0 approval
  snapshots (only net9.0 was updated).
@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 2, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.62%. Comparing base (d8c6eda) to head (4e326b3).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #18      +/-   ##
==========================================
+ Coverage   91.53%   91.62%   +0.08%     
==========================================
  Files         398      400       +2     
  Lines       15481    15613     +132     
  Branches     2231     2259      +28     
==========================================
+ Hits        14171    14305     +134     
+ Misses        990      987       -3     
- Partials      320      321       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Adds DisposeUnsubscribesFromSources and SuppressesNotificationsAfterTerminalError
(value/completion/error all suppressed once a source has errored, via a third
source) and drops the unreachable done-check in the empty-source path, bringing
BlendUnique to full line coverage.
The scheduler-routed WaitFor* tests schedule the subscribe on a sequencer and
then block the calling thread on done.Wait(timeout). Routing through
TaskPoolSequencer means the scheduled subscribe needs a thread-pool thread while
the test thread is blocked; under parallel test load on Windows CI's few cores
the pool starves and the waits time out spuriously. Route these tests through
ImmediateSequencer so the subscribe runs inline before the blocking wait — the
non-null-scheduler code path is still exercised, deterministically and without a
thread-pool dependency.
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Jun 2, 2026

@glennawatson glennawatson merged commit 5a7a671 into main Jun 2, 2026
13 of 14 checks passed
@glennawatson glennawatson deleted the feat/blend-unique-fused-sink branch June 2, 2026 16:29
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