From d463f978e2e73ac50bba90d2b6ae95de82130f8c Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Thu, 4 Jun 2026 19:41:33 +0100 Subject: [PATCH 01/15] Refactor names to blend with Primitives naming Apply widespread renames and API clarifications across ReactiveUI.Primitives.Async. Key changes: replace anonymous/empty disposable types with clearer Delegate/Noop names; unify disposed sentinels to DisposedSlotMarker and rename DisposalHelper.IsDisposed -> HasDisposed; rename AsyncGate -> AsyncSerialGate and LockAsync/Releaser -> EnterAsync/Lease (with related wake/ wait method renames); rename CombineLatestSubscriptionBase -> CombineLatestCoordinatorBase and adjust lifecycle names (e.g. CompleteAsync -> FinishAsync, OnErrorResume -> RelaySourceErrorAsync); update various Observer/Signal wrapper names (Anonymous* -> Delegate*, Wrapped -> Forwarding*), task/witness base class references, and other identifier updates. Tests and operator/signal files updated accordingly. The changes are primarily renames and refactors to improve clarity and consistency without intended behavioral changes. --- .../AsyncContext.cs | 6 +- .../Disposables/DisposableAsync.cs | 14 +- .../Disposables/DisposableAsyncSlot.cs | 14 +- .../SingleAssignmentDisposableAsync.cs | 22 +- .../SingleReplaceableDisposableAsync.cs | 14 +- .../{AsyncGate.cs => AsyncSerialGate.cs} | 52 ++-- ...ase.cs => CombineLatestCoordinatorBase.cs} | 14 +- .../Internals/CombineLatestIndexedObserver.cs | 2 +- .../Internals/CombineLatestLifecycle.cs | 20 +- ...serverAsync.cs => DelegateAsyncWitness.cs} | 8 +- ...sSignalAsync.cs => DelegateSignalAsync.cs} | 2 +- .../Internals/DisposalHelper.cs | 2 +- ...rverAsync.cs => ForwardingAsyncWitness.cs} | 4 +- .../Internals/MulticastSignalAsync.cs | 8 +- .../Internals/SingleElementObserver.cs | 12 +- .../Internals/TakeUntilLifecycle.cs | 14 +- .../Internals/TakeUntilSourceObserver.cs | 6 +- ...scription.cs => TaskSignalSubscription.cs} | 28 +-- ...ion{T}.cs => TaskSignalSubscription{T}.cs} | 24 +- ...erAsyncBase.cs => TaskWitnessAsyncBase.cs} | 14 +- .../Mixins/AsyncContextMixins.cs | 4 +- .../Observables/Create.cs | 16 +- .../Observables/Return.cs | 8 +- .../ObserverAsync.cs | 34 +-- .../Operators/AggregateAsync.cs | 12 +- .../Operators/AnyAllAsync.cs | 28 +-- .../Operators/Cast.cs | 2 +- .../Operators/Catch.cs | 6 +- .../Operators/CombineLatest10.cs | 10 +- .../Operators/CombineLatest11.cs | 10 +- .../Operators/CombineLatest12.cs | 10 +- .../Operators/CombineLatest13.cs | 10 +- .../Operators/CombineLatest14.cs | 10 +- .../Operators/CombineLatest15.cs | 10 +- .../Operators/CombineLatest16.cs | 10 +- .../Operators/CombineLatest2.cs | 10 +- .../Operators/CombineLatest3.cs | 10 +- .../Operators/CombineLatest4.cs | 10 +- .../Operators/CombineLatest5.cs | 10 +- .../Operators/CombineLatest6.cs | 10 +- .../Operators/CombineLatest7.cs | 10 +- .../Operators/CombineLatest8.cs | 10 +- .../Operators/CombineLatest9.cs | 10 +- .../Operators/CombineLatestEnumerable.cs | 30 +-- .../Operators/ConcatEnumerableSignal{T}.cs | 40 +-- .../Operators/ConcatSignalSourcesSignal{T}.cs | 64 ++--- .../Operators/ContainsAsync.cs | 14 +- .../Operators/CountAsync.cs | 12 +- .../Operators/Distinct.cs | 4 +- .../Operators/DistinctUntilChanged.cs | 4 +- .../Operators/Do.cs | 8 +- .../Operators/FirstAsync.cs | 18 +- .../Operators/FirstOrDefaultAsync.cs | 18 +- .../Operators/ForEachAsync.cs | 24 +- .../Operators/GroupBy.cs | 12 +- .../Operators/LastAsync.cs | 20 +- .../Operators/LastOrDefaultAsync.cs | 16 +- .../Operators/LongCountAsync.cs | 12 +- .../Operators/Merge.cs | 160 ++++++------ .../Operators/OfType.cs | 2 +- .../Operators/ParityHelpers.FilterFusions.cs | 18 +- .../ParityHelpers.OperatorFusions.cs | 20 +- .../Operators/ParityHelpers.cs | 8 +- .../Operators/Prepend.cs | 2 +- .../Operators/RefCount.cs | 12 +- .../Operators/Scan.cs | 4 +- .../Operators/Select.cs | 4 +- .../Operators/SingleAsync.cs | 2 +- .../Operators/SingleOrDefaultAsync.cs | 2 +- .../Operators/Skip.cs | 2 +- .../Operators/SkipWhile.cs | 4 +- .../Operators/SubscribeAsync.cs | 8 +- .../Operators/SwitchSignal.cs | 68 ++--- .../Operators/Take.cs | 6 +- .../Operators/TakeUntil.cs | 106 ++++---- .../Operators/TakeWhile.cs | 4 +- .../Operators/Throttle.cs | 2 +- .../Operators/Timeout.cs | 4 +- .../Operators/ToAsyncEnumerable.cs | 4 +- .../Operators/ToDictionaryAsync.cs | 16 +- .../Operators/ToListAsync.cs | 12 +- .../Operators/WaitCompletionAsync.cs | 12 +- .../Operators/Where.cs | 4 +- .../Operators/{ObserveOn.cs => WitnessOn.cs} | 32 +-- ...AsyncSignal.cs => WitnessOnAsyncSignal.cs} | 20 +- .../Operators/Wrap.cs | 2 +- .../Operators/Yield.cs | 2 +- .../Base/BaseReplayLatestSignalAsync.cs | 12 +- .../BaseStatelessReplayLatestSignalAsync.cs | 12 +- .../UnhandledExceptionHandler.cs | 8 +- .../SignalOperatorParityMixins.cs | 34 +++ .../Signals/Signal{Collect}.cs | 225 +++++++++++++++++ .../Signals/Signal{Create}.cs | 70 ++++++ .../Signals/Signal{EmitIfQuiet}.cs | 234 ++++++++++++++++++ .../{Signal{Return}.cs => Signal{Emit}.cs} | 0 .../Signals/Signal{Factories}.cs | 56 +++++ .../{Signal{Throw}.cs => Signal{Fail}.cs} | 0 .../{Signal{Empty}.cs => Signal{None}.cs} | 0 .../{Signal{Catch}.cs => Signal{Recover}.cs} | 0 .../{Signal{Never}.cs => Signal{Silent}.cs} | 0 .../AsyncPrimitiveContractTests.cs | 2 +- ...ncGateTests.cs => AsyncSerialGateTests.cs} | 30 +-- .../CombineLatestEnumerableInternalsTests.cs | 2 +- ...mbineLatestOperatorTests.EnumerableRest.cs | 2 +- .../CombiningOperatorTests.Concat.cs | 28 +-- .../CombiningOperatorTests.Merge.cs | 42 ++-- ...ngOperatorTests.MergeEnumerableDisposal.cs | 34 +-- ...biningOperatorTests.MergeSignalDisposal.cs | 42 ++-- .../CombiningOperatorTests.Multicast.cs | 4 +- .../CombiningOperatorTests.OnDispose.cs | 82 +++--- .../CombiningOperatorTests.Switch.cs | 2 +- .../CombiningOperatorTests.cs | 2 +- .../ConcurrentSignalBaseTests.cs | 26 +- .../DisposableAsyncSlotTests.cs | 4 +- .../DisposableTests.cs | 22 +- .../FactorySignalTests.cs | 2 +- ...s => CombineLatestCoordinatorBaseTests.cs} | 24 +- .../CombineLatestIndexedObserverTests.cs | 2 +- .../ObserveOnAsyncSignalTests.cs | 44 ++-- ...keUntilOperatorTests.CompletionDelegate.cs | 20 +- ...akeUntilOperatorTests.DisposalAndErrors.cs | 50 ++-- .../TakeUntilOperatorTests.cs | 30 +-- .../TerminalOperatorTests.cs | 2 +- .../TransformationOperatorTests.cs | 6 +- ...alTests.Primitives.DotNet10_0.verified.txt | 12 +- ...valTests.Primitives.DotNet8_0.verified.txt | 12 +- ...valTests.Primitives.DotNet9_0.verified.txt | 12 +- .../PublicApiBehaviorTests.cs | 2 +- 128 files changed, 1610 insertions(+), 961 deletions(-) rename src/ReactiveUI.Primitives.Async/Internals/{AsyncGate.cs => AsyncSerialGate.cs} (74%) rename src/ReactiveUI.Primitives.Async/Internals/{CombineLatestSubscriptionBase.cs => CombineLatestCoordinatorBase.cs} (86%) rename src/ReactiveUI.Primitives.Async/Internals/{AnonymousObserverAsync.cs => DelegateAsyncWitness.cs} (86%) rename src/ReactiveUI.Primitives.Async/Internals/{AnonymousSignalAsync.cs => DelegateSignalAsync.cs} (95%) rename src/ReactiveUI.Primitives.Async/Internals/{WrappedObserverAsync.cs => ForwardingAsyncWitness.cs} (85%) rename src/ReactiveUI.Primitives.Async/Internals/{CancelableTaskSubscription.cs => TaskSignalSubscription.cs} (50%) rename src/ReactiveUI.Primitives.Async/Internals/{CancelableTaskSubscription{T}.cs => TaskSignalSubscription{T}.cs} (83%) rename src/ReactiveUI.Primitives.Async/Internals/{TaskObserverAsyncBase.cs => TaskWitnessAsyncBase.cs} (84%) rename src/ReactiveUI.Primitives.Async/Operators/{ObserveOn.cs => WitnessOn.cs} (87%) rename src/ReactiveUI.Primitives.Async/Operators/{ObserveOnAsyncSignal.cs => WitnessOnAsyncSignal.cs} (83%) create mode 100644 src/ReactiveUI.Primitives/Signals/Signal{Collect}.cs create mode 100644 src/ReactiveUI.Primitives/Signals/Signal{EmitIfQuiet}.cs rename src/ReactiveUI.Primitives/Signals/{Signal{Return}.cs => Signal{Emit}.cs} (100%) rename src/ReactiveUI.Primitives/Signals/{Signal{Throw}.cs => Signal{Fail}.cs} (100%) rename src/ReactiveUI.Primitives/Signals/{Signal{Empty}.cs => Signal{None}.cs} (100%) rename src/ReactiveUI.Primitives/Signals/{Signal{Catch}.cs => Signal{Recover}.cs} (100%) rename src/ReactiveUI.Primitives/Signals/{Signal{Never}.cs => Signal{Silent}.cs} (100%) rename src/tests/ReactiveUI.Primitives.Async.Tests/{AsyncGateTests.cs => AsyncSerialGateTests.cs} (84%) rename src/tests/ReactiveUI.Primitives.Async.Tests/Internals/{CombineLatestSubscriptionBaseTests.cs => CombineLatestCoordinatorBaseTests.cs} (90%) diff --git a/src/ReactiveUI.Primitives.Async/AsyncContext.cs b/src/ReactiveUI.Primitives.Async/AsyncContext.cs index 73dba9f..1a8ac9c 100644 --- a/src/ReactiveUI.Primitives.Async/AsyncContext.cs +++ b/src/ReactiveUI.Primitives.Async/AsyncContext.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. // ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. @@ -58,7 +58,7 @@ private AsyncContext() /// Gets a value indicating whether the current context uses the default task scheduler and no synchronization /// context. /// - internal bool IsDefaultContext => SynchronizationContext is null && + internal bool UsesDefaultSequencer => SynchronizationContext is null && Sequencer is null && (TaskScheduler is null || TaskScheduler == TaskScheduler.Default); @@ -250,7 +250,7 @@ private sealed class ContinuationWorkItem(Action continuation) : IWorkItem "Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Kept as an internal adapter for generator and test smoke scenarios that need TaskScheduler-shaped sequencer execution.")] - internal sealed class SchedulerTaskScheduler(ISequencer scheduler) : TaskScheduler + internal sealed class SequencerTaskScheduler(ISequencer scheduler) : TaskScheduler { /// /// Gets the sequencer used by this task-scheduler adapter. diff --git a/src/ReactiveUI.Primitives.Async/Disposables/DisposableAsync.cs b/src/ReactiveUI.Primitives.Async/Disposables/DisposableAsync.cs index bf62276..b310555 100644 --- a/src/ReactiveUI.Primitives.Async/Disposables/DisposableAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Disposables/DisposableAsync.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. // ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. @@ -19,7 +19,7 @@ public static class DisposableAsync /// /// Use this property when an is required but no disposal logic is /// necessary. This can be useful as a default or placeholder implementation. - public static IAsyncDisposable Empty { get; } = new EmptyAsyncDisposable(); + public static IAsyncDisposable Empty { get; } = new NoopAsyncDisposable(); /// /// Creates a new asynchronous disposable object that invokes the specified delegate when disposed asynchronously. @@ -31,7 +31,7 @@ public static IAsyncDisposable Create(Func disposeAsync) { ArgumentExceptionHelper.ThrowIfNull(disposeAsync); - return new AnonymousAsyncDisposable(disposeAsync); + return new DelegateAsyncDisposable(disposeAsync); } /// @@ -48,13 +48,13 @@ public static IAsyncDisposable Create(TState state, Func(state, disposeAsync); + return new DelegateAsyncDisposable(state, disposeAsync); } /// /// An asynchronous disposable that invokes a delegate when disposed. /// - internal sealed class AnonymousAsyncDisposable(Func disposeAsync) : IAsyncDisposable + internal sealed class DelegateAsyncDisposable(Func disposeAsync) : IAsyncDisposable { /// /// A flag indicating whether has already been called (0 = not disposed, 1 = disposed). @@ -71,7 +71,7 @@ internal sealed class AnonymousAsyncDisposable(Func disposeAsync) : I /// name="TState"/>. /// /// The type of the state passed to the dispose delegate. - internal sealed class AnonymousAsyncDisposable(TState state, Func disposeAsync) : IAsyncDisposable + internal sealed class DelegateAsyncDisposable(TState state, Func disposeAsync) : IAsyncDisposable { /// /// A flag indicating whether has already been called (0 = not disposed, 1 = disposed). @@ -85,7 +85,7 @@ internal sealed class AnonymousAsyncDisposable(TState state, Func /// An asynchronous disposable that performs no action when disposed. /// - internal sealed class EmptyAsyncDisposable : IAsyncDisposable + internal sealed class NoopAsyncDisposable : IAsyncDisposable { /// public ValueTask DisposeAsync() => default; diff --git a/src/ReactiveUI.Primitives.Async/Disposables/DisposableAsyncSlot.cs b/src/ReactiveUI.Primitives.Async/Disposables/DisposableAsyncSlot.cs index c73d543..b7f4cd1 100644 --- a/src/ReactiveUI.Primitives.Async/Disposables/DisposableAsyncSlot.cs +++ b/src/ReactiveUI.Primitives.Async/Disposables/DisposableAsyncSlot.cs @@ -32,7 +32,7 @@ public static ValueTask SwapAsync(ref IAsyncDisposable? slot, IAsyncDisposable? var current = Volatile.Read(ref slot); while (true) { - if (ReferenceEquals(current, DisposedSentinel.Instance)) + if (ReferenceEquals(current, DisposedSlotMarker.Instance)) { return value?.DisposeAsync() ?? default; } @@ -64,7 +64,7 @@ public static ValueTask AssignAsync(ref IAsyncDisposable? slot, IAsyncDisposable return default; } - if (ReferenceEquals(current, DisposedSentinel.Instance)) + if (ReferenceEquals(current, DisposedSlotMarker.Instance)) { return value?.DisposeAsync() ?? default; } @@ -80,8 +80,8 @@ public static ValueTask AssignAsync(ref IAsyncDisposable? slot, IAsyncDisposable [DebuggerStepThrough] public static ValueTask DisposeAsync(ref IAsyncDisposable? slot) { - var current = Interlocked.Exchange(ref slot, DisposedSentinel.Instance); - if (current is null || ReferenceEquals(current, DisposedSentinel.Instance)) + var current = Interlocked.Exchange(ref slot, DisposedSlotMarker.Instance); + if (current is null || ReferenceEquals(current, DisposedSlotMarker.Instance)) { return default; } @@ -93,15 +93,15 @@ public static ValueTask DisposeAsync(ref IAsyncDisposable? slot) /// The slot field to inspect. /// if the slot currently holds the disposed sentinel. public static bool IsDisposed(IAsyncDisposable? slot) => - ReferenceEquals(slot, DisposedSentinel.Instance); + ReferenceEquals(slot, DisposedSlotMarker.Instance); /// Shared sentinel marking a disposed slot. Distinct from the per-class sentinels in /// and so the /// slot helpers can be used independently of (and alongside) those wrapper classes. - internal sealed class DisposedSentinel : IAsyncDisposable + internal sealed class DisposedSlotMarker : IAsyncDisposable { /// Singleton sentinel instance. - public static readonly DisposedSentinel Instance = new(); + public static readonly DisposedSlotMarker Instance = new(); /// ValueTask IAsyncDisposable.DisposeAsync() => default; diff --git a/src/ReactiveUI.Primitives.Async/Disposables/SingleAssignmentDisposableAsync.cs b/src/ReactiveUI.Primitives.Async/Disposables/SingleAssignmentDisposableAsync.cs index caed3f6..d28cbd0 100644 --- a/src/ReactiveUI.Primitives.Async/Disposables/SingleAssignmentDisposableAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Disposables/SingleAssignmentDisposableAsync.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. // ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. @@ -24,7 +24,7 @@ public sealed class SingleAssignmentDisposableAsync : IAsyncDisposable /// /// Gets a value indicating whether the object has been disposed. /// - public bool IsDisposed => ReferenceEquals(Volatile.Read(ref _current), DisposedSentinel.Instance); + public bool IsDisposed => ReferenceEquals(Volatile.Read(ref _current), DisposedSlotMarker.Instance); /// /// Gets the current asynchronous disposable resource, or an empty disposable if the resource has already been @@ -35,7 +35,7 @@ public sealed class SingleAssignmentDisposableAsync : IAsyncDisposable public IAsyncDisposable? GetDisposable() { var field = Volatile.Read(ref _current); - if (ReferenceEquals(field, DisposedSentinel.Instance)) + if (ReferenceEquals(field, DisposedSlotMarker.Instance)) { return DisposableAsync.Empty; } @@ -50,7 +50,7 @@ public sealed class SingleAssignmentDisposableAsync : IAsyncDisposable /// The new instance to set as the current resource, or to /// clear the current resource. /// A that represents the asynchronous operation. - public ValueTask SetDisposableAsync(IAsyncDisposable? value) => SetDisposableAsync(ref _current, value); + public ValueTask SetDisposableAsync(IAsyncDisposable? value) => AssignDisposableAsync(ref _current, value); /// /// Asynchronously releases the unmanaged resources used by the object. @@ -69,7 +69,7 @@ public sealed class SingleAssignmentDisposableAsync : IAsyncDisposable /// The instance to assign to the field, or null to leave the field unset. /// A that represents the asynchronous dispose operation if the field was already disposed; /// otherwise, a default . - internal static ValueTask SetDisposableAsync(ref IAsyncDisposable? field, IAsyncDisposable? value) + internal static ValueTask AssignDisposableAsync(ref IAsyncDisposable? field, IAsyncDisposable? value) { var current = Interlocked.CompareExchange(ref field, value, null); if (current == null) @@ -78,7 +78,7 @@ internal static ValueTask SetDisposableAsync(ref IAsyncDisposable? field, IAsync return default; } - if (ReferenceEquals(current, DisposedSentinel.Instance)) + if (ReferenceEquals(current, DisposedSlotMarker.Instance)) { if (value is not null) { @@ -104,8 +104,8 @@ internal static ValueTask SetDisposableAsync(ref IAsyncDisposable? field, IAsync [DebuggerStepThrough] internal static ValueTask DisposeAsync(ref IAsyncDisposable? field) { - var current = Interlocked.Exchange(ref field, DisposedSentinel.Instance); - if (ReferenceEquals(current, DisposedSentinel.Instance) || current is null) + var current = Interlocked.Exchange(ref field, DisposedSlotMarker.Instance); + if (ReferenceEquals(current, DisposedSlotMarker.Instance) || current is null) { return default; } @@ -123,12 +123,12 @@ internal static InvalidOperationException CreateAlreadyAssignedException() => /// /// A sentinel object used to indicate that the has been disposed. /// - internal sealed class DisposedSentinel : IAsyncDisposable + internal sealed class DisposedSlotMarker : IAsyncDisposable { /// - /// Gets the singleton instance of . + /// Gets the singleton instance of . /// - public static readonly DisposedSentinel Instance = new(); + public static readonly DisposedSlotMarker Instance = new(); /// ValueTask IAsyncDisposable.DisposeAsync() => default; diff --git a/src/ReactiveUI.Primitives.Async/Disposables/SingleReplaceableDisposableAsync.cs b/src/ReactiveUI.Primitives.Async/Disposables/SingleReplaceableDisposableAsync.cs index 07e9243..1136294 100644 --- a/src/ReactiveUI.Primitives.Async/Disposables/SingleReplaceableDisposableAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Disposables/SingleReplaceableDisposableAsync.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. // ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. @@ -37,7 +37,7 @@ public ValueTask SetDisposableAsync(IAsyncDisposable? value) var field = Volatile.Read(ref _current); while (true) { - if (ReferenceEquals(field, DisposedSentinel.Instance)) + if (ReferenceEquals(field, DisposedSlotMarker.Instance)) { if (value is not null) { @@ -71,8 +71,8 @@ public ValueTask SetDisposableAsync(IAsyncDisposable? value) /// have been released. public ValueTask DisposeAsync() { - var field = Interlocked.Exchange(ref _current, DisposedSentinel.Instance); - if (!ReferenceEquals(field, DisposedSentinel.Instance) && field is not null) + var field = Interlocked.Exchange(ref _current, DisposedSlotMarker.Instance); + if (!ReferenceEquals(field, DisposedSlotMarker.Instance) && field is not null) { // Dispose the current resource asynchronously. var disposeTask = field.DisposeAsync(); @@ -90,12 +90,12 @@ public ValueTask DisposeAsync() /// /// A sentinel object used to indicate that the has been disposed. /// - internal sealed class DisposedSentinel : IAsyncDisposable + internal sealed class DisposedSlotMarker : IAsyncDisposable { /// - /// Gets the singleton instance of . + /// Gets the singleton instance of . /// - public static readonly DisposedSentinel Instance = new(); + public static readonly DisposedSlotMarker Instance = new(); /// public ValueTask DisposeAsync() => default; diff --git a/src/ReactiveUI.Primitives.Async/Internals/AsyncGate.cs b/src/ReactiveUI.Primitives.Async/Internals/AsyncSerialGate.cs similarity index 74% rename from src/ReactiveUI.Primitives.Async/Internals/AsyncGate.cs rename to src/ReactiveUI.Primitives.Async/Internals/AsyncSerialGate.cs index 0d72beb..b0e1370 100644 --- a/src/ReactiveUI.Primitives.Async/Internals/AsyncGate.cs +++ b/src/ReactiveUI.Primitives.Async/Internals/AsyncSerialGate.cs @@ -16,10 +16,10 @@ namespace ReactiveUI.Primitives.Async.Internals; /// /// /// Same-thread reentry is granted via an owner-thread-id check, with a non-shared recursion -/// counter; nested releases just decrement it. Cross-thread reentry inside a single lock -/// acquisition would deadlock — no in-tree caller exercises that pattern. +/// counter; nested exits just decrement it. Cross-thread reentry inside a single gate entry +/// would deadlock — no in-tree caller exercises that pattern. /// -internal sealed class AsyncGate : IDisposable +internal sealed class AsyncSerialGate : IDisposable { /// /// Signal-only semaphore used to wake one waiter when the gate is released. Initial count is @@ -35,13 +35,13 @@ internal sealed class AsyncGate : IDisposable private int _ownerThreadId; /// - /// Number of nested calls beyond the initial acquisition. Read / written + /// Number of nested calls beyond the initial acquisition. Read / written /// only by the owning thread, so unguarded mutation is safe. /// private int _recursionDepth; /// - /// Count of awaiters parked on the slow path. Read by to decide whether + /// Count of awaiters parked on the slow path. Read by to decide whether /// to signal the semaphore; incremented / decremented around each . /// private int _waiters; @@ -53,16 +53,16 @@ internal sealed class AsyncGate : IDisposable /// Gets the number of awaiters currently parked on the slow path. Exposed for /// deterministic contention tests so they can spin-wait until a contender has entered - /// before tripping the release. + /// before tripping the release. internal int WaitersCount => Volatile.Read(ref _waiters); /// - /// Asynchronously acquires the gate, returning a that releases it on disposal. + /// Asynchronously acquires the gate, returning a that releases it on disposal. /// /// The cancellation token. - /// A that completes when the gate has been acquired. + /// A that completes when the gate has been acquired. [DebuggerStepThrough] - public ValueTask LockAsync(CancellationToken cancellationToken = default) + public ValueTask EnterAsync(CancellationToken cancellationToken = default) { var currentThreadId = Environment.CurrentManagedThreadId; @@ -70,16 +70,16 @@ public ValueTask LockAsync(CancellationToken cancellationToken = defau if (Volatile.Read(ref _ownerThreadId) == currentThreadId) { _recursionDepth++; - return new(new Releaser(this)); + return new(new Lease(this)); } // Fast uncontended acquire: pure CAS, no semaphore touch. if (Interlocked.CompareExchange(ref _ownerThreadId, currentThreadId, 0) == 0) { - return new(new Releaser(this)); + return new(new Lease(this)); } - return WaitForReleaseAsync(cancellationToken); + return WaitForEntryAsync(cancellationToken); } /// @@ -95,10 +95,10 @@ public void Dispose() } /// - /// Releases the gate. Decrements the recursion depth on a nested release, or clears the owner + /// Exits the gate. Decrements the recursion depth on a nested exit, or clears the owner /// and signals one waiter (if any) on the outermost release. /// - internal void Release() + internal void Exit() { if (_recursionDepth > 0) { @@ -107,7 +107,7 @@ internal void Release() } Volatile.Write(ref _ownerThreadId, 0); - SignalIfWaiting(); + WakeNextWaiter(); } /// @@ -115,7 +115,7 @@ internal void Release() /// read / race lands harmlessly in /// the semaphore count and is consumed by the next waiter that arrives. /// - private void SignalIfWaiting() + private void WakeNextWaiter() { if (Volatile.Read(ref _waiters) == 0) { @@ -129,8 +129,8 @@ private void SignalIfWaiting() /// Slow path: park as a waiter and retry the acquire CAS after each semaphore signal. /// /// Cancellation token observed while waiting. - /// A for the acquired gate. - private async ValueTask WaitForReleaseAsync(CancellationToken cancellationToken) + /// A for the acquired gate. + private async ValueTask WaitForEntryAsync(CancellationToken cancellationToken) { Interlocked.Increment(ref _waiters); try @@ -154,22 +154,22 @@ private async ValueTask WaitForReleaseAsync(CancellationToken cancella } /// - /// Releases a previously acquired when disposed. + /// Releases a previously acquired when disposed. /// - public readonly record struct Releaser : IDisposable + public readonly record struct Lease : IDisposable { /// - /// The parent whose lock is released when this releaser is disposed. + /// The parent whose lock is released when this lease is disposed. /// - private readonly AsyncGate _parent; + private readonly AsyncSerialGate _parent; /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the struct. /// - /// The that owns this releaser. - public Releaser(AsyncGate parent) => _parent = parent; + /// The that owns this lease. + public Lease(AsyncSerialGate parent) => _parent = parent; /// - public void Dispose() => _parent.Release(); + public void Dispose() => _parent.Exit(); } } diff --git a/src/ReactiveUI.Primitives.Async/Internals/CombineLatestSubscriptionBase.cs b/src/ReactiveUI.Primitives.Async/Internals/CombineLatestCoordinatorBase.cs similarity index 86% rename from src/ReactiveUI.Primitives.Async/Internals/CombineLatestSubscriptionBase.cs rename to src/ReactiveUI.Primitives.Async/Internals/CombineLatestCoordinatorBase.cs index ac80816..8f717e3 100644 --- a/src/ReactiveUI.Primitives.Async/Internals/CombineLatestSubscriptionBase.cs +++ b/src/ReactiveUI.Primitives.Async/Internals/CombineLatestCoordinatorBase.cs @@ -6,20 +6,20 @@ namespace ReactiveUI.Primitives.Async.Internals; /// /// Shared scaffolding for the arity-specific CombineLatestN subscription types. Each -/// per-arity CombineLatestSubscription derives from this class so the otherwise-identical +/// per-arity CombineLatestCoordinator derives from this class so the otherwise-identical /// wiring (gate / dispose CTS / external link), /// the values-lock, the source-subscribe loop, the error-resume forwarder, and /// live here once instead of repeated 15× across CombineLatest2..16. /// /// The downstream element type. -internal abstract class CombineLatestSubscriptionBase : IAsyncDisposable +internal abstract class CombineLatestCoordinatorBase : IAsyncDisposable { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The downstream observer. /// The number of upstream sources (e.g. 2 for arity-2). - protected CombineLatestSubscriptionBase(IObserverAsync observer, int sourceCount) + protected CombineLatestCoordinatorBase(IObserverAsync observer, int sourceCount) { Lifecycle = new(observer, sourceCount); } @@ -52,13 +52,13 @@ public async ValueTask SubscribeSourcesAsync(CancellationToken cancellationToken public ValueTask DisposeAsync() => Lifecycle.DisposeAsync(); /// - /// Forwards an upstream error to the downstream observer; thin shim with the + /// Relays an upstream error to the downstream observer; thin shim with the /// (error, ct) signature that expects. /// /// The error to forward. /// Ignored — the lifecycle uses its own dispose token. /// A ValueTask representing the asynchronous forward. - internal ValueTask OnErrorResume(Exception error, CancellationToken cancellationToken) + internal ValueTask RelaySourceErrorAsync(Exception error, CancellationToken cancellationToken) { _ = cancellationToken; return Lifecycle.OnErrorResumeAsync(error); @@ -75,7 +75,7 @@ internal ValueTask OnErrorResume(Exception error, CancellationToken cancellation /// /// Subscribes to a single source by 0-based index. Implemented per-arity by the derived - /// CombineLatestSubscription with a typed switch dispatch over the bundled sources. + /// CombineLatestCoordinator with a typed switch dispatch over the bundled sources. /// /// 0-based source index. /// A token to cancel the subscription. diff --git a/src/ReactiveUI.Primitives.Async/Internals/CombineLatestIndexedObserver.cs b/src/ReactiveUI.Primitives.Async/Internals/CombineLatestIndexedObserver.cs index c23940b..786034d 100644 --- a/src/ReactiveUI.Primitives.Async/Internals/CombineLatestIndexedObserver.cs +++ b/src/ReactiveUI.Primitives.Async/Internals/CombineLatestIndexedObserver.cs @@ -20,7 +20,7 @@ namespace ReactiveUI.Primitives.Async.Internals; /// The completion bitmask bit owned by this source (1 << index). /// Stores the freshly-emitted value into the parent's typed _valN slot. internal sealed class CombineLatestIndexedObserver( - CombineLatestSubscriptionBase parent, + CombineLatestCoordinatorBase parent, int sourceBit, Action recordValue) : ObserverAsync { diff --git a/src/ReactiveUI.Primitives.Async/Internals/CombineLatestLifecycle.cs b/src/ReactiveUI.Primitives.Async/Internals/CombineLatestLifecycle.cs index 50523a6..6f9d63b 100644 --- a/src/ReactiveUI.Primitives.Async/Internals/CombineLatestLifecycle.cs +++ b/src/ReactiveUI.Primitives.Async/Internals/CombineLatestLifecycle.cs @@ -6,7 +6,7 @@ namespace ReactiveUI.Primitives.Async.Internals; /// /// Shared subscription lifecycle for the arity-specific CombineLatestN operators (2..16) and -/// the enumerable variant. Each per-arity CombineLatestSubscription composes one instance of +/// the enumerable variant. Each per-arity CombineLatestCoordinator composes one instance of /// this class (has-a, not is-a) and forwards lifecycle / error / gating work into it, so the /// previously-duplicated infrastructure (gate, dispose CTS, external-link registration, observer /// fan-out, completion-bitmask handling) lives in one place. @@ -15,7 +15,7 @@ namespace ReactiveUI.Primitives.Async.Internals; internal sealed class CombineLatestLifecycle : IAsyncDisposable { /// Serializes downstream notifications so OnNext / OnError / OnCompleted never overlap. - private readonly AsyncGate _gate = new(); + private readonly AsyncSerialGate _gate = new(); /// Cancellation source for subscription disposal; cancelled exactly once. private readonly CancellationTokenSource _disposeCts = new(); @@ -59,7 +59,7 @@ public CombineLatestLifecycle(IObserverAsync observer, int sourceCount) public IAsyncDisposable?[] Subscriptions { get; } /// Gets a value indicating whether disposal has been signalled. - public bool IsDisposed => DisposalHelper.IsDisposed(_disposed); + public bool HasDisposed => DisposalHelper.HasDisposed(_disposed); /// /// Links the original subscribe-time cancellation token into this subscription's dispose chain so @@ -92,9 +92,9 @@ public void LinkExternalCancellation(CancellationToken external) /// A ValueTask representing the asynchronous forward. public async ValueTask OnErrorResumeAsync(Exception error) { - using (await _gate.LockAsync(DisposeToken).ConfigureAwait(false)) + using (await _gate.EnterAsync(DisposeToken).ConfigureAwait(false)) { - if (IsDisposed) + if (HasDisposed) { return; } @@ -110,9 +110,9 @@ public async ValueTask OnErrorResumeAsync(Exception error) /// A ValueTask representing the asynchronous emit. public async ValueTask EmitDownstreamAsync(TResult value) { - using (await _gate.LockAsync(DisposeToken).ConfigureAwait(false)) + using (await _gate.EnterAsync(DisposeToken).ConfigureAwait(false)) { - if (IsDisposed) + if (HasDisposed) { return; } @@ -133,7 +133,7 @@ public ValueTask OnSourceCompletedAsync(Result result, int doneBit) { if (result.IsFailure) { - return CompleteAsync(result); + return FinishAsync(result); } int updated; @@ -148,14 +148,14 @@ public ValueTask OnSourceCompletedAsync(Result result, int doneBit) /// Disposes the lifecycle without signalling a terminal notification. /// A ValueTask representing the asynchronous teardown. - public ValueTask DisposeAsync() => CompleteAsync(null); + public ValueTask DisposeAsync() => FinishAsync(null); /// /// Completes the combined sequence and disposes every source subscription. /// /// The completion result, or when disposing without signaling. /// A ValueTask representing the asynchronous teardown. - public async ValueTask CompleteAsync(Result? result) + public async ValueTask FinishAsync(Result? result) { if (DisposalHelper.TrySetDisposed(ref _disposed)) { diff --git a/src/ReactiveUI.Primitives.Async/Internals/AnonymousObserverAsync.cs b/src/ReactiveUI.Primitives.Async/Internals/DelegateAsyncWitness.cs similarity index 86% rename from src/ReactiveUI.Primitives.Async/Internals/AnonymousObserverAsync.cs rename to src/ReactiveUI.Primitives.Async/Internals/DelegateAsyncWitness.cs index b6c8f89..ad88bd3 100644 --- a/src/ReactiveUI.Primitives.Async/Internals/AnonymousObserverAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Internals/DelegateAsyncWitness.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. // ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. @@ -11,7 +11,7 @@ namespace ReactiveUI.Primitives.Async.Internals; /// The asynchronous function invoked for each element. /// An optional asynchronous function invoked when a resumable error occurs. /// An optional asynchronous function invoked when the sequence completes. -internal sealed class AnonymousObserverAsync( +internal sealed class DelegateAsyncWitness( Func onNextAsync, Func? onErrorResumeAsync = null, Func? onCompletedAsync = null) : ObserverAsync @@ -25,7 +25,7 @@ protected override ValueTask OnErrorResumeAsyncCore(Exception error, Cancellatio { if (onErrorResumeAsync is null) { - UnhandledExceptionHandler.OnUnhandledException(error); + UnhandledExceptionHandler.ReportUnhandledException(error); return default; } @@ -40,7 +40,7 @@ protected override ValueTask OnCompletedAsyncCore(Result result) var exception = result.Exception; if (exception is not null) { - UnhandledExceptionHandler.OnUnhandledException(exception); + UnhandledExceptionHandler.ReportUnhandledException(exception); } return default; diff --git a/src/ReactiveUI.Primitives.Async/Internals/AnonymousSignalAsync.cs b/src/ReactiveUI.Primitives.Async/Internals/DelegateSignalAsync.cs similarity index 95% rename from src/ReactiveUI.Primitives.Async/Internals/AnonymousSignalAsync.cs rename to src/ReactiveUI.Primitives.Async/Internals/DelegateSignalAsync.cs index 861e56e..6caf79e 100644 --- a/src/ReactiveUI.Primitives.Async/Internals/AnonymousSignalAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Internals/DelegateSignalAsync.cs @@ -9,7 +9,7 @@ namespace ReactiveUI.Primitives.Async.Internals; /// /// The type of the elements in the observable sequence. /// The asynchronous function invoked when an observer subscribes. -internal sealed class AnonymousSignalAsync( +internal sealed class DelegateSignalAsync( Func, CancellationToken, ValueTask> subscribeAsync) : SignalAsync { /// diff --git a/src/ReactiveUI.Primitives.Async/Internals/DisposalHelper.cs b/src/ReactiveUI.Primitives.Async/Internals/DisposalHelper.cs index fc051f3..d23b7f1 100644 --- a/src/ReactiveUI.Primitives.Async/Internals/DisposalHelper.cs +++ b/src/ReactiveUI.Primitives.Async/Internals/DisposalHelper.cs @@ -19,7 +19,7 @@ internal static class DisposalHelper /// The disposed flag value. /// if disposed; otherwise . [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool IsDisposed(int disposed) => disposed == 1; + internal static bool HasDisposed(int disposed) => disposed == 1; /// /// Atomically sets the disposed flag and returns whether it was already set. diff --git a/src/ReactiveUI.Primitives.Async/Internals/WrappedObserverAsync.cs b/src/ReactiveUI.Primitives.Async/Internals/ForwardingAsyncWitness.cs similarity index 85% rename from src/ReactiveUI.Primitives.Async/Internals/WrappedObserverAsync.cs rename to src/ReactiveUI.Primitives.Async/Internals/ForwardingAsyncWitness.cs index 77bde0a..5ef95c1 100644 --- a/src/ReactiveUI.Primitives.Async/Internals/WrappedObserverAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Internals/ForwardingAsyncWitness.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. // ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. @@ -9,7 +9,7 @@ namespace ReactiveUI.Primitives.Async.Internals; /// /// The type of elements received by the observer. /// The inner observer to delegate notifications to. -internal sealed class WrappedObserverAsync(IObserverAsync observer) : ObserverAsync +internal sealed class ForwardingAsyncWitness(IObserverAsync observer) : ObserverAsync { /// protected override ValueTask OnNextAsyncCore(T value, CancellationToken cancellationToken) => diff --git a/src/ReactiveUI.Primitives.Async/Internals/MulticastSignalAsync.cs b/src/ReactiveUI.Primitives.Async/Internals/MulticastSignalAsync.cs index 0823a70..0d10118 100644 --- a/src/ReactiveUI.Primitives.Async/Internals/MulticastSignalAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Internals/MulticastSignalAsync.cs @@ -26,7 +26,7 @@ internal sealed class MulticastSignalAsync(IObservableAsync source, ISigna /// /// The asynchronous gate used to synchronize connection and disconnection operations. /// - private readonly AsyncGate _gate = new(); + private readonly AsyncSerialGate _gate = new(); /// /// The current connection subscription, or if not connected. @@ -71,7 +71,7 @@ public override async ValueTask ConnectAsync(CancellationToken try { - using (await _gate.LockAsync(token).ConfigureAwait(false)) + using (await _gate.EnterAsync(token).ConfigureAwait(false)) { if (_connection != null) { @@ -85,7 +85,7 @@ await connection.SetDisposableAsync(await source.SubscribeAsync( token).ConfigureAwait(false)).ConfigureAwait(false); return DisposableAsync.Create(async () => { - using (await _gate.LockAsync(DisposedCancellationToken).ConfigureAwait(false)) + using (await _gate.EnterAsync(DisposedCancellationToken).ConfigureAwait(false)) { if (connection is null) { @@ -144,7 +144,7 @@ protected override ValueTask SubscribeAsyncCore( // linked-CTS allocation that the downstream's TryEnter would otherwise produce. The wrap // itself benefits from SignalAsyncObserver forwarding CancellationToken.None to the // signal (the wrap's TryEnter sees None and fast-paths), so no wrap-side link is needed. - var wrap = new WrappedObserverAsync(observer); + var wrap = new ForwardingAsyncWitness(observer); if (observer is ObserverAsync downstream) { downstream.LinkUpstreamCancellation(wrap.InternalDisposedToken); diff --git a/src/ReactiveUI.Primitives.Async/Internals/SingleElementObserver.cs b/src/ReactiveUI.Primitives.Async/Internals/SingleElementObserver.cs index 8dc75f8..57ecb14 100644 --- a/src/ReactiveUI.Primitives.Async/Internals/SingleElementObserver.cs +++ b/src/ReactiveUI.Primitives.Async/Internals/SingleElementObserver.cs @@ -23,7 +23,7 @@ internal sealed class SingleElementObserver( Func? predicate, bool requireExactlyOne, T? defaultValue, - CancellationToken cancellationToken) : TaskObserverAsyncBase(cancellationToken) + CancellationToken cancellationToken) : TaskWitnessAsyncBase(cancellationToken) { /// A value indicating whether a matching element has been found. private bool _hasValue; @@ -44,7 +44,7 @@ protected override async ValueTask OnNextAsyncCore(T value, CancellationToken ca var message = predicate is null ? "Sequence contains more than one element." : "Sequence contains more than one matching element."; - await TrySetException(new InvalidOperationException(message)).ConfigureAwait(false); + await SetExceptionAndDisposeAsync(new InvalidOperationException(message)).ConfigureAwait(false); return; } @@ -54,14 +54,14 @@ protected override async ValueTask OnNextAsyncCore(T value, CancellationToken ca /// protected override ValueTask OnErrorResumeAsyncCore(Exception error, CancellationToken cancellationToken) => - TrySetException(error); + SetExceptionAndDisposeAsync(error); /// protected override ValueTask OnCompletedAsyncCore(Result result) { if (!result.IsSuccess) { - return TrySetException(result.Exception); + return SetExceptionAndDisposeAsync(result.Exception); } if (!_hasValue && requireExactlyOne) @@ -69,9 +69,9 @@ protected override ValueTask OnCompletedAsyncCore(Result result) var message = predicate is null ? "Sequence contains no elements." : "Sequence contains no matching elements."; - return TrySetException(new InvalidOperationException(message)); + return SetExceptionAndDisposeAsync(new InvalidOperationException(message)); } - return TrySetCompleted(_value); + return SetResultAndDisposeAsync(_value); } } diff --git a/src/ReactiveUI.Primitives.Async/Internals/TakeUntilLifecycle.cs b/src/ReactiveUI.Primitives.Async/Internals/TakeUntilLifecycle.cs index f305e46..2c874b1 100644 --- a/src/ReactiveUI.Primitives.Async/Internals/TakeUntilLifecycle.cs +++ b/src/ReactiveUI.Primitives.Async/Internals/TakeUntilLifecycle.cs @@ -19,7 +19,7 @@ internal sealed class TakeUntilLifecycle : IAsyncDisposable private readonly CancellationTokenSource _cts = new(); /// Serializes downstream notifications so OnNext / OnError / OnCompleted never overlap. - private readonly AsyncGate _gate = new(); + private readonly AsyncSerialGate _gate = new(); /// The downstream observer that receives values, errors, and the terminal completion. private readonly IObserverAsync _observer; @@ -67,9 +67,9 @@ public void LinkExternalCancellation(CancellationToken external) /// Forwards a value to the downstream observer under the serialization gate. /// The value to forward. /// A ValueTask representing the asynchronous forward. - public async ValueTask ForwardOnNextAsync(T value) + public async ValueTask RelayNextAsync(T value) { - using (await _gate.LockAsync(DisposeToken).ConfigureAwait(false)) + using (await _gate.EnterAsync(DisposeToken).ConfigureAwait(false)) { await _observer.OnNextAsync(value, DisposeToken).ConfigureAwait(false); } @@ -78,9 +78,9 @@ public async ValueTask ForwardOnNextAsync(T value) /// Forwards a non-terminal error to the downstream observer under the serialization gate. /// The error to forward. /// A ValueTask representing the asynchronous forward. - public async ValueTask ForwardOnErrorResumeAsync(Exception error) + public async ValueTask RelayErrorAsync(Exception error) { - using (await _gate.LockAsync(DisposeToken).ConfigureAwait(false)) + using (await _gate.EnterAsync(DisposeToken).ConfigureAwait(false)) { await _observer.OnErrorResumeAsync(error, DisposeToken).ConfigureAwait(false); } @@ -89,9 +89,9 @@ public async ValueTask ForwardOnErrorResumeAsync(Exception error) /// Forwards the completion signal to the downstream observer under the serialization gate. /// The completion result. /// A ValueTask representing the asynchronous forward. - public async ValueTask ForwardOnCompletedAsync(Result result) + public async ValueTask RelayCompletionAsync(Result result) { - using (await _gate.LockAsync().ConfigureAwait(false)) + using (await _gate.EnterAsync().ConfigureAwait(false)) { await _observer.OnCompletedAsync(result).ConfigureAwait(false); } diff --git a/src/ReactiveUI.Primitives.Async/Internals/TakeUntilSourceObserver.cs b/src/ReactiveUI.Primitives.Async/Internals/TakeUntilSourceObserver.cs index 72a2a19..7754db6 100644 --- a/src/ReactiveUI.Primitives.Async/Internals/TakeUntilSourceObserver.cs +++ b/src/ReactiveUI.Primitives.Async/Internals/TakeUntilSourceObserver.cs @@ -18,17 +18,17 @@ internal sealed class TakeUntilSourceObserver(TakeUntilLifecycle lifecycle protected override ValueTask OnNextAsyncCore(T value, CancellationToken cancellationToken) { _ = cancellationToken; - return lifecycle.ForwardOnNextAsync(value); + return lifecycle.RelayNextAsync(value); } /// protected override ValueTask OnErrorResumeAsyncCore(Exception error, CancellationToken cancellationToken) { _ = cancellationToken; - return lifecycle.ForwardOnErrorResumeAsync(error); + return lifecycle.RelayErrorAsync(error); } /// protected override ValueTask OnCompletedAsyncCore(Result result) => - lifecycle.ForwardOnCompletedAsync(result); + lifecycle.RelayCompletionAsync(result); } diff --git a/src/ReactiveUI.Primitives.Async/Internals/CancelableTaskSubscription.cs b/src/ReactiveUI.Primitives.Async/Internals/TaskSignalSubscription.cs similarity index 50% rename from src/ReactiveUI.Primitives.Async/Internals/CancelableTaskSubscription.cs rename to src/ReactiveUI.Primitives.Async/Internals/TaskSignalSubscription.cs index 1b4d1af..ce57362 100644 --- a/src/ReactiveUI.Primitives.Async/Internals/CancelableTaskSubscription.cs +++ b/src/ReactiveUI.Primitives.Async/Internals/TaskSignalSubscription.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. // ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. @@ -7,21 +7,21 @@ namespace ReactiveUI.Primitives.Async.Internals; /// /// Provides factory methods for creating and starting cancelable task-based subscriptions. /// -internal static class CancelableTaskSubscription +internal static class TaskSignalSubscription { /// /// Creates and immediately starts a new cancelable task subscription. /// /// The type of the elements observed by the subscription. - /// The asynchronous function that defines the subscription logic. + /// The asynchronous function that defines the subscription logic. /// The observer that receives notifications. - /// A running instance. - public static CancelableTaskSubscription CreateAndStart( - Func, CancellationToken, ValueTask> runAsyncCore, + /// A running instance. + public static TaskSignalSubscription StartNew( + Func, CancellationToken, ValueTask> executeAsyncCore, IObserverAsync observer) { - var ret = new AnonymousCancelableTaskSubscription(runAsyncCore, observer); - ret.Run(); + var ret = new AnonymousTaskSignalSubscription(executeAsyncCore, observer); + ret.Start(); return ret; } @@ -29,14 +29,14 @@ public static CancelableTaskSubscription CreateAndStart( /// A cancelable task subscription that delegates its core logic to a user-supplied function. /// /// The type of the elements observed by the subscription. - /// The asynchronous function that defines the subscription logic. + /// The asynchronous function that defines the subscription logic. /// The observer that receives notifications. - internal sealed class AnonymousCancelableTaskSubscription( - Func, CancellationToken, ValueTask> runAsyncCore, - IObserverAsync observer) : CancelableTaskSubscription(observer) + internal sealed class AnonymousTaskSignalSubscription( + Func, CancellationToken, ValueTask> executeAsyncCore, + IObserverAsync observer) : TaskSignalSubscription(observer) { /// - protected override ValueTask RunAsyncCore(IObserverAsync observer, CancellationToken cancellationToken) => - runAsyncCore(observer, cancellationToken); + protected override ValueTask ExecuteAsyncCore(IObserverAsync observer, CancellationToken cancellationToken) => + executeAsyncCore(observer, cancellationToken); } } diff --git a/src/ReactiveUI.Primitives.Async/Internals/CancelableTaskSubscription{T}.cs b/src/ReactiveUI.Primitives.Async/Internals/TaskSignalSubscription{T}.cs similarity index 83% rename from src/ReactiveUI.Primitives.Async/Internals/CancelableTaskSubscription{T}.cs rename to src/ReactiveUI.Primitives.Async/Internals/TaskSignalSubscription{T}.cs index 8b5e9b8..961a0ae 100644 --- a/src/ReactiveUI.Primitives.Async/Internals/CancelableTaskSubscription{T}.cs +++ b/src/ReactiveUI.Primitives.Async/Internals/TaskSignalSubscription{T}.cs @@ -11,10 +11,10 @@ namespace ReactiveUI.Primitives.Async.Internals; /// This type provides a base for implementing cancellable, asynchronously disposable /// subscriptions that coordinate observer notifications and resource cleanup. Disposal cancels any ongoing /// operations and ensures that all resources are released before completion. Derived classes should implement the -/// core execution logic in RunCoreAsync. +/// core execution logic in . /// The type of the elements observed by the subscription. /// The observer that receives notifications for the subscription. Cannot be null. -internal abstract class CancelableTaskSubscription(IObserverAsync observer) : IAsyncDisposable +internal abstract class TaskSignalSubscription(IObserverAsync observer) : IAsyncDisposable { /// /// The task completion source used to signal when the subscription's asynchronous operation has finished. @@ -26,13 +26,13 @@ internal abstract class CancelableTaskSubscription(IObserverAsync observer /// private readonly CancellationTokenSource _cts = new(); - /// Managed-thread ID of the thread currently inside , or + /// Managed-thread ID of the thread currently inside , or /// 0 when no run is in flight. Replaces an -based /// reentry flag — the AsyncLocal cloned - /// on every set, costing ~80 B per Run. Thread-ID detection is exact for the + /// on every set, costing ~80 B per execution. Thread-ID detection is exact for the /// synchronous-reentry deadlock case (Dispose called from within the same call stack - /// as RunAsync); asynchronous reentry after a thread hop may return from Dispose - /// slightly before RunAsync's finally fires, but cancellation has already been + /// as ); asynchronous reentry after a thread hop may return from Dispose + /// slightly before 's finally fires, but cancellation has already been /// signalled so no observer notifications race the dispose. private int _runningThreadId; @@ -46,9 +46,9 @@ internal abstract class CancelableTaskSubscription(IObserverAsync observer /// /// This method initiates the asynchronous operation and does not wait for its completion. To /// monitor progress or handle completion, use the asynchronous counterpart directly. The - /// returned by is converted to a + /// returned by is converted to a /// before being discarded so the fire-and-forget pattern stays compatible with CA2012. - public void Run() => _ = RunAsync(_cts.Token).AsTask(); + public void Start() => _ = ExecuteAsync(_cts.Token).AsTask(); /// /// Asynchronously releases the resources used by the object and cancels any ongoing operations. @@ -88,7 +88,7 @@ internal static async ValueTask CompleteWithFailureAsync(IObserverAsync obser } catch (Exception exception) { - UnhandledExceptionHandler.OnUnhandledException(exception); + UnhandledExceptionHandler.ReportUnhandledException(exception); } } @@ -97,12 +97,12 @@ internal static async ValueTask CompleteWithFailureAsync(IObserverAsync obser /// /// A cancellation token that can be used to cancel the operation. /// A representing the asynchronous operation. - internal async ValueTask RunAsync(CancellationToken cancellationToken) + internal async ValueTask ExecuteAsync(CancellationToken cancellationToken) { Volatile.Write(ref _runningThreadId, Environment.CurrentManagedThreadId); try { - await RunAsyncCore(observer, cancellationToken).ConfigureAwait(false); + await ExecuteAsyncCore(observer, cancellationToken).ConfigureAwait(false); } catch (Exception e) { @@ -121,5 +121,5 @@ internal async ValueTask RunAsync(CancellationToken cancellationToken) /// The observer that receives notifications. /// A cancellation token that can be used to cancel the operation. /// A representing the asynchronous operation. - protected abstract ValueTask RunAsyncCore(IObserverAsync observer, CancellationToken cancellationToken); + protected abstract ValueTask ExecuteAsyncCore(IObserverAsync observer, CancellationToken cancellationToken); } diff --git a/src/ReactiveUI.Primitives.Async/Internals/TaskObserverAsyncBase.cs b/src/ReactiveUI.Primitives.Async/Internals/TaskWitnessAsyncBase.cs similarity index 84% rename from src/ReactiveUI.Primitives.Async/Internals/TaskObserverAsyncBase.cs rename to src/ReactiveUI.Primitives.Async/Internals/TaskWitnessAsyncBase.cs index 2dd81bf..d764e41 100644 --- a/src/ReactiveUI.Primitives.Async/Internals/TaskObserverAsyncBase.cs +++ b/src/ReactiveUI.Primitives.Async/Internals/TaskWitnessAsyncBase.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. // ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. @@ -12,7 +12,7 @@ namespace ReactiveUI.Primitives.Async.Internals; /// The type of elements received from the observable sequence. /// The type of the result value produced by this observer. /// A cancellation token used to cancel the waiting operation. -internal abstract class TaskObserverAsyncBase(CancellationToken cancellationToken) : ObserverAsync +internal abstract class TaskWitnessAsyncBase(CancellationToken cancellationToken) : ObserverAsync { /// /// The task completion source used to produce the observer's single result value. @@ -28,7 +28,7 @@ internal abstract class TaskObserverAsyncBase(CancellationToken c /// Asynchronously waits for the observer to produce its result value. /// /// A task representing the asynchronous operation, containing the result value. - public async ValueTask WaitValueAsync() + public async ValueTask AwaitResultAsync() { try { @@ -36,7 +36,7 @@ public async ValueTask WaitValueAsync() await using var ct = _cancellationToken.Register( static x => { - var @this = (TaskObserverAsyncBase)x!; + var @this = (TaskWitnessAsyncBase)x!; @this._tcs.TrySetException(new OperationCanceledException(@this._cancellationToken)); }, this); @@ -44,7 +44,7 @@ public async ValueTask WaitValueAsync() using var ct = _cancellationToken.Register( static x => { - var @this = (TaskObserverAsyncBase)x!; + var @this = (TaskWitnessAsyncBase)x!; @this._tcs.TrySetException(new OperationCanceledException(@this._cancellationToken)); }, this); @@ -64,7 +64,7 @@ public async ValueTask WaitValueAsync() /// The result value to set. /// A task representing the asynchronous operation. [DebuggerStepThrough] - protected async ValueTask TrySetCompleted(TTaskValue value) + protected async ValueTask SetResultAndDisposeAsync(TTaskValue value) { try { @@ -81,7 +81,7 @@ protected async ValueTask TrySetCompleted(TTaskValue value) /// /// The exception that caused the fault. /// A task representing the asynchronous operation. - protected async ValueTask TrySetException(Exception e) + protected async ValueTask SetExceptionAndDisposeAsync(Exception e) { try { diff --git a/src/ReactiveUI.Primitives.Async/Mixins/AsyncContextMixins.cs b/src/ReactiveUI.Primitives.Async/Mixins/AsyncContextMixins.cs index 9e4070d..c6617f2 100644 --- a/src/ReactiveUI.Primitives.Async/Mixins/AsyncContextMixins.cs +++ b/src/ReactiveUI.Primitives.Async/Mixins/AsyncContextMixins.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. // ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. @@ -31,7 +31,7 @@ public static bool IsSameAsCurrentAsyncContext(this AsyncContext @this) if (@this.Sequencer is not null) { - return TaskScheduler.Current is AsyncContext.SchedulerTaskScheduler adapter && + return TaskScheduler.Current is AsyncContext.SequencerTaskScheduler adapter && ReferenceEquals(adapter.Sequencer, @this.Sequencer); } diff --git a/src/ReactiveUI.Primitives.Async/Observables/Create.cs b/src/ReactiveUI.Primitives.Async/Observables/Create.cs index 3975eac..a793344 100644 --- a/src/ReactiveUI.Primitives.Async/Observables/Create.cs +++ b/src/ReactiveUI.Primitives.Async/Observables/Create.cs @@ -33,7 +33,7 @@ public static IObservableAsync Create( Func, CancellationToken, ValueTask> subscribeAsync) => subscribeAsync is null ? throw new ArgumentNullException(nameof(subscribeAsync)) - : new AnonymousSignalAsync(subscribeAsync); + : new DelegateSignalAsync(subscribeAsync); /// /// Creates a new observable sequence that runs the specified asynchronous job as a background task. @@ -44,7 +44,7 @@ subscribeAsync is null /// An SignalAsync{T} that represents the observable sequence produced by the background job. public static IObservableAsync CreateAsBackgroundJob( Func, CancellationToken, ValueTask> job) => - CreateAsBackgroundJob(job, false, null); + CreateBackgroundJobSignal(job, false, null); /// /// Creates a new observable sequence that runs the specified asynchronous job as a background task. @@ -58,7 +58,7 @@ public static IObservableAsync CreateAsBackgroundJob( public static IObservableAsync CreateAsBackgroundJob( Func, CancellationToken, ValueTask> job, bool startSynchronously) => - CreateAsBackgroundJob(job, startSynchronously, null); + CreateBackgroundJobSignal(job, startSynchronously, null); /// /// Creates a new observable sequence that runs the specified asynchronous job as a background task using the @@ -72,7 +72,7 @@ public static IObservableAsync CreateAsBackgroundJob( public static IObservableAsync CreateAsBackgroundJob( Func, CancellationToken, ValueTask> job, TaskScheduler taskScheduler) => - CreateAsBackgroundJob(job, false, taskScheduler); + CreateBackgroundJobSignal(job, false, taskScheduler); /// /// Creates a new observable sequence that runs the specified asynchronous job as a background task, @@ -83,7 +83,7 @@ public static IObservableAsync CreateAsBackgroundJob( /// true to start the job synchronously; otherwise, false. /// An optional task scheduler for scheduling the job, or to use the default. /// An observable that emits values produced by the background job. - private static IObservableAsync CreateAsBackgroundJob( + private static IObservableAsync CreateBackgroundJobSignal( Func, CancellationToken, ValueTask> job, bool startSynchronously, TaskScheduler? taskScheduler) @@ -92,12 +92,12 @@ private static IObservableAsync CreateAsBackgroundJob( if (startSynchronously) { - return Create((observer, _) => new(CancelableTaskSubscription.CreateAndStart(job, observer))); + return Create((observer, _) => new(TaskSignalSubscription.StartNew(job, observer))); } if (taskScheduler is null) { - return Create((observer, _) => new(CancelableTaskSubscription.CreateAndStart( + return Create((observer, _) => new(TaskSignalSubscription.StartNew( async (obs, token) => { await Task.Yield(); @@ -106,7 +106,7 @@ private static IObservableAsync CreateAsBackgroundJob( observer))); } - return Create((observer, _) => new(CancelableTaskSubscription.CreateAndStart( + return Create((observer, _) => new(TaskSignalSubscription.StartNew( async (obs, ct) => await Task.Factory.StartNew( () => job(obs, ct).AsTask(), ct, diff --git a/src/ReactiveUI.Primitives.Async/Observables/Return.cs b/src/ReactiveUI.Primitives.Async/Observables/Return.cs index 6e1d1df..619baf6 100644 --- a/src/ReactiveUI.Primitives.Async/Observables/Return.cs +++ b/src/ReactiveUI.Primitives.Async/Observables/Return.cs @@ -27,7 +27,7 @@ public static partial class SignalAsync /// /// Single-value observable that captures the emitted value as a field and routes through a typed - /// . Same deferred-emit semantic as the previous + /// . Same deferred-emit semantic as the previous /// CreateAsBackgroundJob path, but without the per-call Func closure allocation. /// /// The element type emitted. @@ -40,17 +40,17 @@ protected override ValueTask SubscribeAsyncCore( CancellationToken cancellationToken) { var subscription = new ReturnSubscription(observer, value); - subscription.Run(); + subscription.Start(); return new(subscription); } /// Per-subscription task body that emits the captured value and signals completion. /// The downstream observer. /// The captured value. - private sealed class ReturnSubscription(IObserverAsync observer, T value) : CancelableTaskSubscription(observer) + private sealed class ReturnSubscription(IObserverAsync observer, T value) : TaskSignalSubscription(observer) { /// - protected override async ValueTask RunAsyncCore(IObserverAsync downstream, CancellationToken cancellationToken) + protected override async ValueTask ExecuteAsyncCore(IObserverAsync downstream, CancellationToken cancellationToken) { await downstream.OnNextAsync(value, cancellationToken).ConfigureAwait(false); await downstream.OnCompletedAsync(Result.Success).ConfigureAwait(false); diff --git a/src/ReactiveUI.Primitives.Async/ObserverAsync.cs b/src/ReactiveUI.Primitives.Async/ObserverAsync.cs index 58e9867..8ad82f5 100644 --- a/src/ReactiveUI.Primitives.Async/ObserverAsync.cs +++ b/src/ReactiveUI.Primitives.Async/ObserverAsync.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. // ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. @@ -79,7 +79,7 @@ protected ObserverAsync() /// /// Gets a value indicating whether this observer has been disposed. /// - internal bool IsDisposed => Volatile.Read(ref _disposed) != 0; + internal bool HasDisposed => Volatile.Read(ref _disposed) != 0; /// /// Gets the cancellation token that fires when this observer disposes. Exposed for sibling operators @@ -142,11 +142,11 @@ public ValueTask OnErrorResumeAsync(Exception error, CancellationToken cancellat return default; } - // OnErrorResumeAsync_Private is an async ValueTask method — any sync or async exception + // RouteObserverErrorAsync is an async ValueTask method — any sync or async exception // it raises is captured into the returned ValueTask and surfaces through the await in // OnErrorResumeAsyncSlow. A try/catch around the invocation expression itself would be // dead code in modern C# async semantics. - var core = OnErrorResumeAsync_Private(error, scope.Token); + var core = RouteObserverErrorAsync(error, scope.Token); if (core.IsCompletedSuccessfully) { @@ -181,7 +181,7 @@ public ValueTask OnCompletedAsync(Result result) } catch (Exception e) { - UnhandledExceptionHandler.OnUnhandledException(e); + UnhandledExceptionHandler.ReportUnhandledException(e); scope.Dispose(); return CompleteOrChainDispose(); } @@ -216,8 +216,8 @@ public async ValueTask DisposeAsync() /// /// The source subscription to track, or to clear it. /// A representing the asynchronous operation. - internal ValueTask SetSourceSubscriptionAsync(IAsyncDisposable? value) => - SingleAssignmentDisposableAsync.SetDisposableAsync(ref _sourceSubscription, value); + internal ValueTask AssignSourceSubscriptionAsync(IAsyncDisposable? value) => + SingleAssignmentDisposableAsync.AssignDisposableAsync(ref _sourceSubscription, value); /// /// Internal wrapper around so sibling operators @@ -257,7 +257,7 @@ internal bool TryEnterOnSomethingCall(CancellationToken cancellationToken, out L // are legal — only cross-thread overlap fires the exception. if (oldCount > 0 && oldThreadId != currentThreadId) { - UnhandledExceptionHandler.OnUnhandledException(new ConcurrentObserverCallsException()); + UnhandledExceptionHandler.ReportUnhandledException(new ConcurrentObserverCallsException()); scope = default; return false; } @@ -321,13 +321,13 @@ internal bool ExitOnSomethingCall() /// The exception that triggered error handling. /// A cancellation token for the operation. /// A task representing the asynchronous operation. - internal async ValueTask OnErrorResumeAsync_Private(Exception error, CancellationToken cancellationToken) + internal async ValueTask RouteObserverErrorAsync(Exception error, CancellationToken cancellationToken) { try { if (cancellationToken.IsCancellationRequested) { - UnhandledExceptionHandler.OnUnhandledException(error); + UnhandledExceptionHandler.ReportUnhandledException(error); return; } @@ -335,11 +335,11 @@ internal async ValueTask OnErrorResumeAsync_Private(Exception error, Cancellatio } catch (OperationCanceledException) { - UnhandledExceptionHandler.OnUnhandledException(error); + UnhandledExceptionHandler.ReportUnhandledException(error); } catch (Exception e) { - UnhandledExceptionHandler.OnUnhandledException(e); + UnhandledExceptionHandler.ReportUnhandledException(e); } } @@ -493,7 +493,7 @@ private async ValueTask CompleteDisposeAfterCancelAsync(Task? allOnSomethingCall } catch (Exception e) { - UnhandledExceptionHandler.OnUnhandledException(e); + UnhandledExceptionHandler.ReportUnhandledException(e); } } @@ -525,7 +525,7 @@ private async ValueTask OnNextAsyncSlow(ValueTask core, LinkedTokenScope scope) } catch (Exception e) { - await OnErrorResumeAsync_Private(e, scope.Token).ConfigureAwait(false); + await RouteObserverErrorAsync(e, scope.Token).ConfigureAwait(false); } finally { @@ -536,7 +536,7 @@ private async ValueTask OnNextAsyncSlow(ValueTask core, LinkedTokenScope scope) /// /// Async continuation for when threw synchronously. - /// Routes the error through off the fast path so the + /// Routes the error through off the fast path so the /// caller-visible stays state-machine free in the common case. /// /// The exception thrown by the core. @@ -546,7 +546,7 @@ private async ValueTask OnNextAsyncSlowAfterSyncThrow(Exception error, LinkedTok { try { - await OnErrorResumeAsync_Private(error, scope.Token).ConfigureAwait(false); + await RouteObserverErrorAsync(error, scope.Token).ConfigureAwait(false); } finally { @@ -590,7 +590,7 @@ private async ValueTask OnCompletedAsyncSlow(ValueTask core, LinkedTokenScope sc } catch (Exception e) { - UnhandledExceptionHandler.OnUnhandledException(e); + UnhandledExceptionHandler.ReportUnhandledException(e); } finally { diff --git a/src/ReactiveUI.Primitives.Async/Operators/AggregateAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/AggregateAsync.cs index 2e24def..5bbcf70 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/AggregateAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/AggregateAsync.cs @@ -55,9 +55,9 @@ public static async ValueTask AggregateAsync( ArgumentExceptionHelper.ThrowIfNull(accumulator); cancellationToken.ThrowIfCancellationRequested(); - var observer = new AggregateAsyncObserver(seed, accumulator, cancellationToken); + var observer = new AggregateTaskWitness(seed, accumulator, cancellationToken); await using var subscription = await @this.SubscribeAsync(observer, cancellationToken).ConfigureAwait(false); - return await observer.WaitValueAsync().ConfigureAwait(false); + return await observer.AwaitResultAsync().ConfigureAwait(false); } /// @@ -160,10 +160,10 @@ public static async ValueTask AggregateAsync( /// The initial accumulator value. /// The asynchronous accumulator function. /// A cancellation token for the operation. - internal sealed class AggregateAsyncObserver( + internal sealed class AggregateTaskWitness( TAcc seed, Func> accumulator, - CancellationToken cancellationToken) : TaskObserverAsyncBase(cancellationToken) + CancellationToken cancellationToken) : TaskWitnessAsyncBase(cancellationToken) { /// /// The current accumulated value. @@ -176,10 +176,10 @@ protected override async ValueTask OnNextAsyncCore(T value, CancellationToken ca /// protected override ValueTask OnErrorResumeAsyncCore(Exception error, CancellationToken cancellationToken) => - TrySetException(error); + SetExceptionAndDisposeAsync(error); /// protected override ValueTask OnCompletedAsyncCore(Result result) => - result.IsSuccess ? TrySetCompleted(_acc) : TrySetException(result.Exception); + result.IsSuccess ? SetResultAndDisposeAsync(_acc) : SetExceptionAndDisposeAsync(result.Exception); } } diff --git a/src/ReactiveUI.Primitives.Async/Operators/AnyAllAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/AnyAllAsync.cs index 13e101f..9d4a117 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/AnyAllAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/AnyAllAsync.cs @@ -29,9 +29,9 @@ public static partial class SignalAsync public static async ValueTask AnyAsync(this IObservableAsync @this, Func? predicate, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - var observer = new AnyAsyncObserver(predicate, cancellationToken); + var observer = new AnyTaskWitness(predicate, cancellationToken); await using var subscription = await @this.SubscribeAsync(observer, cancellationToken).ConfigureAwait(false); - return await observer.WaitValueAsync().ConfigureAwait(false); + return await observer.AwaitResultAsync().ConfigureAwait(false); } /// @@ -84,42 +84,42 @@ public static async ValueTask AllAsync(this IObservableAsync @this, ArgumentExceptionHelper.ThrowIfNull(predicate); cancellationToken.ThrowIfCancellationRequested(); - var observer = new AllAsyncObserver(predicate, cancellationToken); + var observer = new AllTaskWitness(predicate, cancellationToken); await using var subscription = await @this.SubscribeAsync(observer, cancellationToken).ConfigureAwait(false); - return await observer.WaitValueAsync().ConfigureAwait(false); + return await observer.AwaitResultAsync().ConfigureAwait(false); } /// /// An observer that determines whether any element in the sequence satisfies a predicate. /// /// The type of elements in the sequence. - internal sealed class AnyAsyncObserver(Func? predicate, CancellationToken cancellationToken) - : TaskObserverAsyncBase(cancellationToken) + internal sealed class AnyTaskWitness(Func? predicate, CancellationToken cancellationToken) + : TaskWitnessAsyncBase(cancellationToken) { /// protected override async ValueTask OnNextAsyncCore(T value, CancellationToken cancellationToken) { if (predicate is null || predicate(value)) { - await TrySetCompleted(true).ConfigureAwait(false); + await SetResultAndDisposeAsync(true).ConfigureAwait(false); } } /// protected override ValueTask OnErrorResumeAsyncCore(Exception error, CancellationToken cancellationToken) => - TrySetException(error); + SetExceptionAndDisposeAsync(error); /// protected override ValueTask OnCompletedAsyncCore(Result result) => - !result.IsSuccess ? TrySetException(result.Exception) : TrySetCompleted(false); + !result.IsSuccess ? SetExceptionAndDisposeAsync(result.Exception) : SetResultAndDisposeAsync(false); } /// /// An observer that determines whether all elements in the sequence satisfy a predicate. /// /// The type of elements in the sequence. - internal sealed class AllAsyncObserver(Func predicate, CancellationToken cancellationToken) - : TaskObserverAsyncBase(cancellationToken) + internal sealed class AllTaskWitness(Func predicate, CancellationToken cancellationToken) + : TaskWitnessAsyncBase(cancellationToken) { /// /// The predicate function used to test each element in the sequence. @@ -131,16 +131,16 @@ protected override async ValueTask OnNextAsyncCore(T value, CancellationToken ca { if (!_predicate(value)) { - await TrySetCompleted(false).ConfigureAwait(false); + await SetResultAndDisposeAsync(false).ConfigureAwait(false); } } /// protected override ValueTask OnErrorResumeAsyncCore(Exception error, CancellationToken cancellationToken) => - TrySetException(error); + SetExceptionAndDisposeAsync(error); /// protected override ValueTask OnCompletedAsyncCore(Result result) => - !result.IsSuccess ? TrySetException(result.Exception) : TrySetCompleted(true); + !result.IsSuccess ? SetExceptionAndDisposeAsync(result.Exception) : SetResultAndDisposeAsync(true); } } diff --git a/src/ReactiveUI.Primitives.Async/Operators/Cast.cs b/src/ReactiveUI.Primitives.Async/Operators/Cast.cs index a8ab090..d4ed076 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/Cast.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/Cast.cs @@ -57,7 +57,7 @@ protected override async ValueTask SubscribeAsyncCore( } var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } diff --git a/src/ReactiveUI.Primitives.Async/Operators/Catch.cs b/src/ReactiveUI.Primitives.Async/Operators/Catch.cs index 6cf7f7f..5c1ed96 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/Catch.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/Catch.cs @@ -77,7 +77,7 @@ public static IObservableAsync Catch( public static IObservableAsync CatchAndIgnoreErrorResume(this IObservableAsync source, Func> handler) => source.Catch(handler, static (error, _) => { - UnhandledExceptionHandler.OnUnhandledException(error); + UnhandledExceptionHandler.ReportUnhandledException(error); return default; }); @@ -111,7 +111,7 @@ protected override async ValueTask SubscribeAsyncCore( } var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } @@ -177,7 +177,7 @@ protected override async ValueTask DisposeAsyncCore() } catch (Exception e) { - UnhandledExceptionHandler.OnUnhandledException(e); + UnhandledExceptionHandler.ReportUnhandledException(e); } await base.DisposeAsyncCore().ConfigureAwait(false); diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest10.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest10.cs index 7512c2c..9e12936 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest10.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest10.cs @@ -116,7 +116,7 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var subscription = new CombineLatestSubscription(observer, sources, selector); + var subscription = new CombineLatestCoordinator(observer, sources, selector); subscription.Lifecycle.LinkExternalCancellation(cancellationToken); return SubscriptionHelper.SubscribeAndDisposeOnFailureAsync( subscription, @@ -127,10 +127,10 @@ protected override ValueTask SubscribeAsyncCore( /// Per-arity subscription holding the typed Optional slots, the pre-built indexed /// observers, the SubscribeAtAsync switch, and the selector invocation. Shared scaffolding /// (gate, lifecycle, ValuesLock, OnErrorResume, SubscribeSourcesAsync, DisposeAsync) lives - /// in ; the per-source OnNext / OnError / + /// in ; the per-source OnNext / OnError / /// OnCompleted forwarding lives in . /// - internal sealed class CombineLatestSubscription : CombineLatestSubscriptionBase + internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase { /// Bit owned by source 1 inside the lifecycle's completion bitmask. private const int Source1Bit = 1 << 0; @@ -252,12 +252,12 @@ internal readonly record struct Values( T10 V10); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The downstream observer. /// The bundled source observables. /// The selector that projects the latest values. - public CombineLatestSubscription( + public CombineLatestCoordinator( IObserverAsync observer, Sources sources, Func selector) diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest11.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest11.cs index f18ee15..378ac56 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest11.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest11.cs @@ -122,7 +122,7 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var subscription = new CombineLatestSubscription(observer, sources, selector); + var subscription = new CombineLatestCoordinator(observer, sources, selector); subscription.Lifecycle.LinkExternalCancellation(cancellationToken); return SubscriptionHelper.SubscribeAndDisposeOnFailureAsync( subscription, @@ -133,10 +133,10 @@ protected override ValueTask SubscribeAsyncCore( /// Per-arity subscription holding the typed Optional slots, the pre-built indexed /// observers, the SubscribeAtAsync switch, and the selector invocation. Shared scaffolding /// (gate, lifecycle, ValuesLock, OnErrorResume, SubscribeSourcesAsync, DisposeAsync) lives - /// in ; the per-source OnNext / OnError / + /// in ; the per-source OnNext / OnError / /// OnCompleted forwarding lives in . /// - internal sealed class CombineLatestSubscription : CombineLatestSubscriptionBase + internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase { /// Bit owned by source 1 inside the lifecycle's completion bitmask. private const int Source1Bit = 1 << 0; @@ -269,12 +269,12 @@ internal readonly record struct Values( T11 V11); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The downstream observer. /// The bundled source observables. /// The selector that projects the latest values. - public CombineLatestSubscription( + public CombineLatestCoordinator( IObserverAsync observer, Sources sources, Func selector) diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest12.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest12.cs index 21434fd..04cf41a 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest12.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest12.cs @@ -128,7 +128,7 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var subscription = new CombineLatestSubscription(observer, sources, selector); + var subscription = new CombineLatestCoordinator(observer, sources, selector); subscription.Lifecycle.LinkExternalCancellation(cancellationToken); return SubscriptionHelper.SubscribeAndDisposeOnFailureAsync( subscription, @@ -139,10 +139,10 @@ protected override ValueTask SubscribeAsyncCore( /// Per-arity subscription holding the typed Optional slots, the pre-built indexed /// observers, the SubscribeAtAsync switch, and the selector invocation. Shared scaffolding /// (gate, lifecycle, ValuesLock, OnErrorResume, SubscribeSourcesAsync, DisposeAsync) lives - /// in ; the per-source OnNext / OnError / + /// in ; the per-source OnNext / OnError / /// OnCompleted forwarding lives in . /// - internal sealed class CombineLatestSubscription : CombineLatestSubscriptionBase + internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase { /// Bit owned by source 1 inside the lifecycle's completion bitmask. private const int Source1Bit = 1 << 0; @@ -286,12 +286,12 @@ internal readonly record struct Values( T12 V12); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The downstream observer. /// The bundled source observables. /// The selector that projects the latest values. - public CombineLatestSubscription( + public CombineLatestCoordinator( IObserverAsync observer, Sources sources, Func selector) diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest13.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest13.cs index 58ed1c6..9158aab 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest13.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest13.cs @@ -134,7 +134,7 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var subscription = new CombineLatestSubscription(observer, sources, selector); + var subscription = new CombineLatestCoordinator(observer, sources, selector); subscription.Lifecycle.LinkExternalCancellation(cancellationToken); return SubscriptionHelper.SubscribeAndDisposeOnFailureAsync( subscription, @@ -145,10 +145,10 @@ protected override ValueTask SubscribeAsyncCore( /// Per-arity subscription holding the typed Optional slots, the pre-built indexed /// observers, the SubscribeAtAsync switch, and the selector invocation. Shared scaffolding /// (gate, lifecycle, ValuesLock, OnErrorResume, SubscribeSourcesAsync, DisposeAsync) lives - /// in ; the per-source OnNext / OnError / + /// in ; the per-source OnNext / OnError / /// OnCompleted forwarding lives in . /// - internal sealed class CombineLatestSubscription : CombineLatestSubscriptionBase + internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase { /// Bit owned by source 1 inside the lifecycle's completion bitmask. private const int Source1Bit = 1 << 0; @@ -303,12 +303,12 @@ internal readonly record struct Values( T13 V13); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The downstream observer. /// The bundled source observables. /// The selector that projects the latest values. - public CombineLatestSubscription( + public CombineLatestCoordinator( IObserverAsync observer, Sources sources, Func selector) diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest14.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest14.cs index a24a4e3..948b89d 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest14.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest14.cs @@ -140,7 +140,7 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var subscription = new CombineLatestSubscription(observer, sources, selector); + var subscription = new CombineLatestCoordinator(observer, sources, selector); subscription.Lifecycle.LinkExternalCancellation(cancellationToken); return SubscriptionHelper.SubscribeAndDisposeOnFailureAsync( subscription, @@ -151,10 +151,10 @@ protected override ValueTask SubscribeAsyncCore( /// Per-arity subscription holding the typed Optional slots, the pre-built indexed /// observers, the SubscribeAtAsync switch, and the selector invocation. Shared scaffolding /// (gate, lifecycle, ValuesLock, OnErrorResume, SubscribeSourcesAsync, DisposeAsync) lives - /// in ; the per-source OnNext / OnError / + /// in ; the per-source OnNext / OnError / /// OnCompleted forwarding lives in . /// - internal sealed class CombineLatestSubscription : CombineLatestSubscriptionBase + internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase { /// Bit owned by source 1 inside the lifecycle's completion bitmask. private const int Source1Bit = 1 << 0; @@ -320,12 +320,12 @@ internal readonly record struct Values( T14 V14); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The downstream observer. /// The bundled source observables. /// The selector that projects the latest values. - public CombineLatestSubscription( + public CombineLatestCoordinator( IObserverAsync observer, Sources sources, Func selector) diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest15.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest15.cs index f831ed1..35c7490 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest15.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest15.cs @@ -146,7 +146,7 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var subscription = new CombineLatestSubscription(observer, sources, selector); + var subscription = new CombineLatestCoordinator(observer, sources, selector); subscription.Lifecycle.LinkExternalCancellation(cancellationToken); return SubscriptionHelper.SubscribeAndDisposeOnFailureAsync( subscription, @@ -157,10 +157,10 @@ protected override ValueTask SubscribeAsyncCore( /// Per-arity subscription holding the typed Optional slots, the pre-built indexed /// observers, the SubscribeAtAsync switch, and the selector invocation. Shared scaffolding /// (gate, lifecycle, ValuesLock, OnErrorResume, SubscribeSourcesAsync, DisposeAsync) lives - /// in ; the per-source OnNext / OnError / + /// in ; the per-source OnNext / OnError / /// OnCompleted forwarding lives in . /// - internal sealed class CombineLatestSubscription : CombineLatestSubscriptionBase + internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase { /// Bit owned by source 1 inside the lifecycle's completion bitmask. private const int Source1Bit = 1 << 0; @@ -337,12 +337,12 @@ internal readonly record struct Values( T15 V15); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The downstream observer. /// The bundled source observables. /// The selector that projects the latest values. - public CombineLatestSubscription( + public CombineLatestCoordinator( IObserverAsync observer, Sources sources, Func selector) diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest16.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest16.cs index ba0c5e5..5b01add 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest16.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest16.cs @@ -152,7 +152,7 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var subscription = new CombineLatestSubscription(observer, sources, selector); + var subscription = new CombineLatestCoordinator(observer, sources, selector); subscription.Lifecycle.LinkExternalCancellation(cancellationToken); return SubscriptionHelper.SubscribeAndDisposeOnFailureAsync( subscription, @@ -163,10 +163,10 @@ protected override ValueTask SubscribeAsyncCore( /// Per-arity subscription holding the typed Optional slots, the pre-built indexed /// observers, the SubscribeAtAsync switch, and the selector invocation. Shared scaffolding /// (gate, lifecycle, ValuesLock, OnErrorResume, SubscribeSourcesAsync, DisposeAsync) lives - /// in ; the per-source OnNext / OnError / + /// in ; the per-source OnNext / OnError / /// OnCompleted forwarding lives in . /// - internal sealed class CombineLatestSubscription : CombineLatestSubscriptionBase + internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase { /// Bit owned by source 1 inside the lifecycle's completion bitmask. private const int Source1Bit = 1 << 0; @@ -354,12 +354,12 @@ internal readonly record struct Values( T16 V16); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The downstream observer. /// The bundled source observables. /// The selector that projects the latest values. - public CombineLatestSubscription( + public CombineLatestCoordinator( IObserverAsync observer, Sources sources, Func selector) diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest2.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest2.cs index e0a7a6d..4878c00 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest2.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest2.cs @@ -68,7 +68,7 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var subscription = new CombineLatestSubscription(observer, sources, selector); + var subscription = new CombineLatestCoordinator(observer, sources, selector); subscription.Lifecycle.LinkExternalCancellation(cancellationToken); return SubscriptionHelper.SubscribeAndDisposeOnFailureAsync( subscription, @@ -79,10 +79,10 @@ protected override ValueTask SubscribeAsyncCore( /// Per-arity subscription holding the typed Optional slots, the pre-built indexed /// observers, the SubscribeAtAsync switch, and the selector invocation. Shared scaffolding /// (gate, lifecycle, ValuesLock, OnErrorResume, SubscribeSourcesAsync, DisposeAsync) lives - /// in ; the per-source OnNext / OnError / + /// in ; the per-source OnNext / OnError / /// OnCompleted forwarding lives in . /// - internal sealed class CombineLatestSubscription : CombineLatestSubscriptionBase + internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase { /// Bit owned by source 1 inside the lifecycle's completion bitmask. private const int Source1Bit = 1 << 0; @@ -116,12 +116,12 @@ internal readonly record struct Values( T2 V2); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The downstream observer. /// The bundled source observables. /// The selector that projects the latest values. - public CombineLatestSubscription( + public CombineLatestCoordinator( IObserverAsync observer, Sources sources, Func selector) diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest3.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest3.cs index 8a2757a..44305d1 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest3.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest3.cs @@ -74,7 +74,7 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var subscription = new CombineLatestSubscription(observer, sources, selector); + var subscription = new CombineLatestCoordinator(observer, sources, selector); subscription.Lifecycle.LinkExternalCancellation(cancellationToken); return SubscriptionHelper.SubscribeAndDisposeOnFailureAsync( subscription, @@ -85,10 +85,10 @@ protected override ValueTask SubscribeAsyncCore( /// Per-arity subscription holding the typed Optional slots, the pre-built indexed /// observers, the SubscribeAtAsync switch, and the selector invocation. Shared scaffolding /// (gate, lifecycle, ValuesLock, OnErrorResume, SubscribeSourcesAsync, DisposeAsync) lives - /// in ; the per-source OnNext / OnError / + /// in ; the per-source OnNext / OnError / /// OnCompleted forwarding lives in . /// - internal sealed class CombineLatestSubscription : CombineLatestSubscriptionBase + internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase { /// Bit owned by source 1 inside the lifecycle's completion bitmask. private const int Source1Bit = 1 << 0; @@ -133,12 +133,12 @@ internal readonly record struct Values( T3 V3); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The downstream observer. /// The bundled source observables. /// The selector that projects the latest values. - public CombineLatestSubscription( + public CombineLatestCoordinator( IObserverAsync observer, Sources sources, Func selector) diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest4.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest4.cs index f736185..cf08969 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest4.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest4.cs @@ -80,7 +80,7 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var subscription = new CombineLatestSubscription(observer, sources, selector); + var subscription = new CombineLatestCoordinator(observer, sources, selector); subscription.Lifecycle.LinkExternalCancellation(cancellationToken); return SubscriptionHelper.SubscribeAndDisposeOnFailureAsync( subscription, @@ -91,10 +91,10 @@ protected override ValueTask SubscribeAsyncCore( /// Per-arity subscription holding the typed Optional slots, the pre-built indexed /// observers, the SubscribeAtAsync switch, and the selector invocation. Shared scaffolding /// (gate, lifecycle, ValuesLock, OnErrorResume, SubscribeSourcesAsync, DisposeAsync) lives - /// in ; the per-source OnNext / OnError / + /// in ; the per-source OnNext / OnError / /// OnCompleted forwarding lives in . /// - internal sealed class CombineLatestSubscription : CombineLatestSubscriptionBase + internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase { /// Bit owned by source 1 inside the lifecycle's completion bitmask. private const int Source1Bit = 1 << 0; @@ -150,12 +150,12 @@ internal readonly record struct Values( T4 V4); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The downstream observer. /// The bundled source observables. /// The selector that projects the latest values. - public CombineLatestSubscription( + public CombineLatestCoordinator( IObserverAsync observer, Sources sources, Func selector) diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest5.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest5.cs index cd70110..95208f4 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest5.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest5.cs @@ -86,7 +86,7 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var subscription = new CombineLatestSubscription(observer, sources, selector); + var subscription = new CombineLatestCoordinator(observer, sources, selector); subscription.Lifecycle.LinkExternalCancellation(cancellationToken); return SubscriptionHelper.SubscribeAndDisposeOnFailureAsync( subscription, @@ -97,10 +97,10 @@ protected override ValueTask SubscribeAsyncCore( /// Per-arity subscription holding the typed Optional slots, the pre-built indexed /// observers, the SubscribeAtAsync switch, and the selector invocation. Shared scaffolding /// (gate, lifecycle, ValuesLock, OnErrorResume, SubscribeSourcesAsync, DisposeAsync) lives - /// in ; the per-source OnNext / OnError / + /// in ; the per-source OnNext / OnError / /// OnCompleted forwarding lives in . /// - internal sealed class CombineLatestSubscription : CombineLatestSubscriptionBase + internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase { /// Bit owned by source 1 inside the lifecycle's completion bitmask. private const int Source1Bit = 1 << 0; @@ -167,12 +167,12 @@ internal readonly record struct Values( T5 V5); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The downstream observer. /// The bundled source observables. /// The selector that projects the latest values. - public CombineLatestSubscription( + public CombineLatestCoordinator( IObserverAsync observer, Sources sources, Func selector) diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest6.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest6.cs index 7e47634..243b5b7 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest6.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest6.cs @@ -92,7 +92,7 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var subscription = new CombineLatestSubscription(observer, sources, selector); + var subscription = new CombineLatestCoordinator(observer, sources, selector); subscription.Lifecycle.LinkExternalCancellation(cancellationToken); return SubscriptionHelper.SubscribeAndDisposeOnFailureAsync( subscription, @@ -103,10 +103,10 @@ protected override ValueTask SubscribeAsyncCore( /// Per-arity subscription holding the typed Optional slots, the pre-built indexed /// observers, the SubscribeAtAsync switch, and the selector invocation. Shared scaffolding /// (gate, lifecycle, ValuesLock, OnErrorResume, SubscribeSourcesAsync, DisposeAsync) lives - /// in ; the per-source OnNext / OnError / + /// in ; the per-source OnNext / OnError / /// OnCompleted forwarding lives in . /// - internal sealed class CombineLatestSubscription : CombineLatestSubscriptionBase + internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase { /// Bit owned by source 1 inside the lifecycle's completion bitmask. private const int Source1Bit = 1 << 0; @@ -184,12 +184,12 @@ internal readonly record struct Values( T6 V6); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The downstream observer. /// The bundled source observables. /// The selector that projects the latest values. - public CombineLatestSubscription( + public CombineLatestCoordinator( IObserverAsync observer, Sources sources, Func selector) diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest7.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest7.cs index 715a560..999ef02 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest7.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest7.cs @@ -98,7 +98,7 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var subscription = new CombineLatestSubscription(observer, sources, selector); + var subscription = new CombineLatestCoordinator(observer, sources, selector); subscription.Lifecycle.LinkExternalCancellation(cancellationToken); return SubscriptionHelper.SubscribeAndDisposeOnFailureAsync( subscription, @@ -109,10 +109,10 @@ protected override ValueTask SubscribeAsyncCore( /// Per-arity subscription holding the typed Optional slots, the pre-built indexed /// observers, the SubscribeAtAsync switch, and the selector invocation. Shared scaffolding /// (gate, lifecycle, ValuesLock, OnErrorResume, SubscribeSourcesAsync, DisposeAsync) lives - /// in ; the per-source OnNext / OnError / + /// in ; the per-source OnNext / OnError / /// OnCompleted forwarding lives in . /// - internal sealed class CombineLatestSubscription : CombineLatestSubscriptionBase + internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase { /// Bit owned by source 1 inside the lifecycle's completion bitmask. private const int Source1Bit = 1 << 0; @@ -201,12 +201,12 @@ internal readonly record struct Values( T7 V7); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The downstream observer. /// The bundled source observables. /// The selector that projects the latest values. - public CombineLatestSubscription( + public CombineLatestCoordinator( IObserverAsync observer, Sources sources, Func selector) diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest8.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest8.cs index e14d36e..bc9f236 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest8.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest8.cs @@ -104,7 +104,7 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var subscription = new CombineLatestSubscription(observer, sources, selector); + var subscription = new CombineLatestCoordinator(observer, sources, selector); subscription.Lifecycle.LinkExternalCancellation(cancellationToken); return SubscriptionHelper.SubscribeAndDisposeOnFailureAsync( subscription, @@ -115,10 +115,10 @@ protected override ValueTask SubscribeAsyncCore( /// Per-arity subscription holding the typed Optional slots, the pre-built indexed /// observers, the SubscribeAtAsync switch, and the selector invocation. Shared scaffolding /// (gate, lifecycle, ValuesLock, OnErrorResume, SubscribeSourcesAsync, DisposeAsync) lives - /// in ; the per-source OnNext / OnError / + /// in ; the per-source OnNext / OnError / /// OnCompleted forwarding lives in . /// - internal sealed class CombineLatestSubscription : CombineLatestSubscriptionBase + internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase { /// Bit owned by source 1 inside the lifecycle's completion bitmask. private const int Source1Bit = 1 << 0; @@ -218,12 +218,12 @@ internal readonly record struct Values( T8 V8); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The downstream observer. /// The bundled source observables. /// The selector that projects the latest values. - public CombineLatestSubscription( + public CombineLatestCoordinator( IObserverAsync observer, Sources sources, Func selector) diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest9.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest9.cs index e675c0e..76a23ec 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest9.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest9.cs @@ -110,7 +110,7 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var subscription = new CombineLatestSubscription(observer, sources, selector); + var subscription = new CombineLatestCoordinator(observer, sources, selector); subscription.Lifecycle.LinkExternalCancellation(cancellationToken); return SubscriptionHelper.SubscribeAndDisposeOnFailureAsync( subscription, @@ -121,10 +121,10 @@ protected override ValueTask SubscribeAsyncCore( /// Per-arity subscription holding the typed Optional slots, the pre-built indexed /// observers, the SubscribeAtAsync switch, and the selector invocation. Shared scaffolding /// (gate, lifecycle, ValuesLock, OnErrorResume, SubscribeSourcesAsync, DisposeAsync) lives - /// in ; the per-source OnNext / OnError / + /// in ; the per-source OnNext / OnError / /// OnCompleted forwarding lives in . /// - internal sealed class CombineLatestSubscription : CombineLatestSubscriptionBase + internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase { /// Bit owned by source 1 inside the lifecycle's completion bitmask. private const int Source1Bit = 1 << 0; @@ -235,12 +235,12 @@ internal readonly record struct Values( T9 V9); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The downstream observer. /// The bundled source observables. /// The selector that projects the latest values. - public CombineLatestSubscription( + public CombineLatestCoordinator( IObserverAsync observer, Sources sources, Func selector) diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatestEnumerable.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatestEnumerable.cs index a8251f7..0ece9f5 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatestEnumerable.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatestEnumerable.cs @@ -91,7 +91,7 @@ protected override async ValueTask SubscribeAsyncCore( return DisposableAsync.Empty; } - var subscription = new Subscription(_sources, observer, resultSelector); + var subscription = new EnumerableCombineLatestCoordinator(_sources, observer, resultSelector); return await SubscriptionHelper.SubscribeAndDisposeOnFailureAsync( subscription, () => subscription.SubscribeSourcesAsync(cancellationToken)).ConfigureAwait(false); @@ -101,9 +101,9 @@ protected override async ValueTask SubscribeAsyncCore( /// Per-source observer that forwards to the parent subscription with its own index. Replaces the three /// captured-index lambdas the previous shape allocated per source. /// - /// The parent subscription. + /// The parent coordinator. /// The source index. - internal sealed class IndexedObserver(Subscription parent, int index) : IObserverAsync + internal sealed class IndexedObserver(EnumerableCombineLatestCoordinator parent, int index) : IObserverAsync { /// public ValueTask OnNextAsync(TSource value, CancellationToken cancellationToken) => @@ -127,13 +127,13 @@ public ValueTask OnCompletedAsync(Result result) => /// The source sequences. /// The observer. /// The result selector. - internal sealed class Subscription( + internal sealed class EnumerableCombineLatestCoordinator( IObservableAsync[] sources, IObserverAsync observer, Func, TResult> resultSelector) : IAsyncDisposable { /// Synchronization gate. - private readonly AsyncGate _gate = new(); + private readonly AsyncSerialGate _gate = new(); /// Cancellation source for disposal. private readonly CancellationTokenSource _disposeCts = new(); @@ -197,7 +197,7 @@ public async ValueTask SubscribeSourcesAsync(CancellationToken cancellationToken } /// - public ValueTask DisposeAsync() => CompleteAsync(null); + public ValueTask DisposeAsync() => FinishAsync(null); /// /// Handles OnNext from a source. @@ -208,9 +208,9 @@ public async ValueTask SubscribeSourcesAsync(CancellationToken cancellationToken /// A value task representing the operation. internal async ValueTask OnNextAsync(int index, TSource indexValue, CancellationToken cancellationToken) { - using (await _gate.LockAsync(cancellationToken).ConfigureAwait(false)) + using (await _gate.EnterAsync(cancellationToken).ConfigureAwait(false)) { - if (DisposalHelper.IsDisposed(_disposed)) + if (DisposalHelper.HasDisposed(_disposed)) { return; } @@ -242,7 +242,7 @@ internal async ValueTask OnNextAsync(int index, TSource indexValue, Cancellation } catch (Exception ex) { - await CompleteAsync(Result.Failure(ex)).ConfigureAwait(false); + await FinishAsync(Result.Failure(ex)).ConfigureAwait(false); return; } @@ -258,9 +258,9 @@ internal async ValueTask OnNextAsync(int index, TSource indexValue, Cancellation /// A value task representing the operation. internal async ValueTask OnErrorResumeAsync(Exception error, CancellationToken cancellationToken) { - using (await _gate.LockAsync(cancellationToken).ConfigureAwait(false)) + using (await _gate.EnterAsync(cancellationToken).ConfigureAwait(false)) { - if (DisposalHelper.IsDisposed(_disposed)) + if (DisposalHelper.HasDisposed(_disposed)) { return; } @@ -279,7 +279,7 @@ internal ValueTask OnCompletedAsync(int index, Result result) { if (result.IsFailure) { - return CompleteAsync(result); + return FinishAsync(result); } bool shouldComplete; @@ -295,17 +295,17 @@ internal ValueTask OnCompletedAsync(int index, Result result) shouldComplete = !_values[index].HasValue || _completedCount == _sources.Length; } - return shouldComplete ? CompleteAsync(Result.Success) : default; + return shouldComplete ? FinishAsync(Result.Success) : default; } /// /// Completes the subscription. The gate / dispose CTS are always released in the finally /// block so a misbehaving downstream's OnCompletedAsync can't leak the SemaphoreSlim inside - /// AsyncGate or the dispose CTS's wait handles. + /// AsyncSerialGate or the dispose CTS's wait handles. /// /// The result. /// A value task representing the operation. - internal async ValueTask CompleteAsync(Result? result) + internal async ValueTask FinishAsync(Result? result) { if (DisposalHelper.TrySetDisposed(ref _disposed)) { diff --git a/src/ReactiveUI.Primitives.Async/Operators/ConcatEnumerableSignal{T}.cs b/src/ReactiveUI.Primitives.Async/Operators/ConcatEnumerableSignal{T}.cs index 10cb913..2cdaa44 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/ConcatEnumerableSignal{T}.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/ConcatEnumerableSignal{T}.cs @@ -26,7 +26,7 @@ internal sealed class ConcatEnumerableSignal(IEnumerable> private readonly IEnumerable> _signals = signals; /// - /// Subscribes the specified observer by creating a that iterates + /// Subscribes the specified observer by creating a that iterates /// through the enumerable of observables sequentially. /// /// The observer to receive elements from the concatenated sequences. @@ -36,17 +36,17 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var subscription = new ConcatEnumerableSubscription(this, observer); + var subscription = new ConcatSequenceCoordinator(this, observer); return SubscriptionHelper.SubscribeAndDisposeOnFailureAsync( subscription, - subscription.SubscribeNextAsync); + subscription.SubscribeNextSignalAsync); } /// /// Manages sequential iteration through the enumerable of observables, subscribing to each /// inner observable only after the previous one completes. /// - internal sealed class ConcatEnumerableSubscription : IAsyncDisposable + internal sealed class ConcatSequenceCoordinator : IAsyncDisposable { /// /// Enumerator that iterates through the collection of observable sequences to concatenate. @@ -79,11 +79,11 @@ internal sealed class ConcatEnumerableSubscription : IAsyncDisposable private int _disposed; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The parent observable that provides the enumerable of observables. /// The downstream observer to forward elements to. - public ConcatEnumerableSubscription(ConcatEnumerableSignal parent, IObserverAsync observer) + public ConcatSequenceCoordinator(ConcatEnumerableSignal parent, IObserverAsync observer) { _observer = observer; _enumerator = parent._signals.GetEnumerator(); @@ -95,7 +95,7 @@ public ConcatEnumerableSubscription(ConcatEnumerableSignal parent, IObserverA /// or completes if no more observables are available. /// /// A task representing the asynchronous operation. - public async ValueTask SubscribeNextAsync() + public async ValueTask SubscribeNextSignalAsync() { try { @@ -103,29 +103,29 @@ public async ValueTask SubscribeNextAsync() { var current = _enumerator.Current; var subscription = await current.SubscribeAsync( - OnInnerNextAsync, - OnInnerErrorResumeAsync, - result => result.IsFailure ? CompleteAsync(result) : SubscribeNextAsync(), + RelayInnerValueAsync, + RelayInnerErrorAsync, + result => result.IsFailure ? FinishAsync(result) : SubscribeNextSignalAsync(), _disposedCancellationToken).ConfigureAwait(false); await _innerDisposable.SetDisposableAsync(subscription).ConfigureAwait(false); } else { - await CompleteAsync(Result.Success).ConfigureAwait(false); + await FinishAsync(Result.Success).ConfigureAwait(false); } } catch (Exception e) { - await CompleteAsync(Result.Failure(e)).ConfigureAwait(false); + await FinishAsync(Result.Failure(e)).ConfigureAwait(false); } } /// - public ValueTask DisposeAsync() => CompleteAsync(null); + public ValueTask DisposeAsync() => FinishAsync(null); /// - /// Handles a second call to when already disposed, + /// Handles a second call to when already disposed, /// routing any failure exception to the unhandled exception handler. /// /// The completion result from the second call. @@ -136,7 +136,7 @@ internal static void HandleAlreadyDisposed(Result? result) return; } - UnhandledExceptionHandler.OnUnhandledException(exception); + UnhandledExceptionHandler.ReportUnhandledException(exception); } /// @@ -145,9 +145,9 @@ internal static void HandleAlreadyDisposed(Result? result) /// The error to forward. /// A token to cancel the operation. /// A task representing the asynchronous operation. - internal ValueTask OnInnerErrorResumeAsync(Exception exception, CancellationToken cancellationToken) + internal ValueTask RelayInnerErrorAsync(Exception exception, CancellationToken cancellationToken) { - // The inner subscription is rooted in _disposedCancellationToken (see SubscribeNextAsync), + // The inner subscription is rooted in _disposedCancellationToken (see SubscribeNextSignalAsync), // so its disposal already cascades into the inner observer's own cancellation. Forwarding // _disposedCancellationToken directly preserves the cancellation semantics that a linked // CTS would have provided, without the per-emission Linked2CancellationTokenSource alloc @@ -160,9 +160,9 @@ internal ValueTask OnInnerErrorResumeAsync(Exception exception, CancellationToke /// Forwards an element from the current inner sequence to the downstream observer. /// /// The element to forward. - /// A token to cancel the operation. Ignored — see . + /// A token to cancel the operation. Ignored — see . /// A task representing the asynchronous operation. - internal ValueTask OnInnerNextAsync(T value, CancellationToken cancellationToken) + internal ValueTask RelayInnerValueAsync(T value, CancellationToken cancellationToken) { _ = cancellationToken; return _observer.OnNextAsync(value, _disposedCancellationToken); @@ -174,7 +174,7 @@ internal ValueTask OnInnerNextAsync(T value, CancellationToken cancellationToken /// /// The completion result to forward, or if disposing without signaling completion. /// A task representing the asynchronous operation. - internal async ValueTask CompleteAsync(Result? result) + internal async ValueTask FinishAsync(Result? result) { if (DisposalHelper.TrySetDisposed(ref _disposed)) { diff --git a/src/ReactiveUI.Primitives.Async/Operators/ConcatSignalSourcesSignal{T}.cs b/src/ReactiveUI.Primitives.Async/Operators/ConcatSignalSourcesSignal{T}.cs index f491df7..1b6d95e 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/ConcatSignalSourcesSignal{T}.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/ConcatSignalSourcesSignal{T}.cs @@ -17,7 +17,7 @@ namespace ReactiveUI.Primitives.Async; internal sealed class ConcatSignalSourcesSignal(IObservableAsync> source) : SignalAsync { /// - /// Subscribes the specified observer by creating a that manages + /// Subscribes the specified observer by creating a that manages /// sequential subscription to inner observables. /// /// The observer to receive elements from the concatenated sequences. @@ -27,7 +27,7 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var subscription = new ConcatSubscription(observer); + var subscription = new ConcatCoordinator(observer); return SubscriptionHelper.SubscribeAndDisposeOnFailureAsync( subscription, () => subscription.SubscribeAsync(source, cancellationToken)); @@ -37,7 +37,7 @@ protected override ValueTask SubscribeAsyncCore( /// Manages the lifetime of the outer subscription and buffers inner observables, /// subscribing to each one sequentially as the previous completes. /// - internal sealed class ConcatSubscription : IAsyncDisposable + internal sealed class ConcatCoordinator : IAsyncDisposable { /// /// Concurrent queue that buffers inner observables waiting to be subscribed to. @@ -72,7 +72,7 @@ internal sealed class ConcatSubscription : IAsyncDisposable /// /// Async gate that serializes observer callbacks to ensure thread-safe emission. /// - private readonly AsyncGate _observerOnSomethingGate = new(); + private readonly AsyncSerialGate _observerOnSomethingGate = new(); /// /// Indicates whether the outer observable sequence has completed. @@ -85,10 +85,10 @@ internal sealed class ConcatSubscription : IAsyncDisposable private int _disposed; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The downstream observer to forward elements to. - public ConcatSubscription(IObserverAsync observer) + public ConcatCoordinator(IObserverAsync observer) { _observer = observer; _disposedCancellationToken = _disposeCts.Token; @@ -104,7 +104,7 @@ public async ValueTask SubscribeAsync( IObservableAsync> source, CancellationToken subscriptionToken) { - var outerSubscription = await source.SubscribeAsync(new ConcatOuterObserver(this), subscriptionToken).ConfigureAwait(false); + var outerSubscription = await source.SubscribeAsync(new ConcatOuterWitness(this), subscriptionToken).ConfigureAwait(false); await _outerDisposable.SetDisposableAsync(outerSubscription).ConfigureAwait(false); } @@ -114,7 +114,7 @@ public async ValueTask SubscribeAsync( /// /// The inner observable to enqueue. /// A task representing the asynchronous operation. - public ValueTask OnNextOuterAsync(IObservableAsync inner) + public ValueTask AcceptOuterValueAsync(IObservableAsync inner) { var shouldSubscribe = false; lock (_buffer) @@ -131,7 +131,7 @@ public ValueTask OnNextOuterAsync(IObservableAsync inner) return default; } - return SubscribeToInnerLoop(inner); + return SubscribeCurrentInnerAsync(inner); } /// @@ -140,7 +140,7 @@ public ValueTask OnNextOuterAsync(IObservableAsync inner) /// /// The completion result from the outer sequence. /// A task representing the asynchronous completion operation. - public ValueTask OnCompletedOuterAsync(Result result) + public ValueTask AcceptOuterCompletionAsync(Result result) { var shouldComplete = false; Result? completeResult = null; @@ -154,7 +154,7 @@ public ValueTask OnCompletedOuterAsync(Result result) } } - return shouldComplete ? CompleteAsync(completeResult) : default; + return shouldComplete ? FinishAsync(completeResult) : default; } /// @@ -163,11 +163,11 @@ public ValueTask OnCompletedOuterAsync(Result result) /// /// The completion result from the inner sequence. /// A task representing the asynchronous completion operation. - public ValueTask OnCompletedInnerAsync(Result result) + public ValueTask AcceptInnerCompletionAsync(Result result) { if (result.IsFailure) { - return CompleteAsync(result); + return FinishAsync(result); } IObservableAsync? nextInner; @@ -181,17 +181,17 @@ public ValueTask OnCompletedInnerAsync(Result result) if (nextInner is null) { - return outerCompleted ? CompleteAsync(Result.Success) : default; + return outerCompleted ? FinishAsync(Result.Success) : default; } - return SubscribeToInnerLoop(nextInner); + return SubscribeCurrentInnerAsync(nextInner); } /// - public ValueTask DisposeAsync() => CompleteAsync(null); + public ValueTask DisposeAsync() => FinishAsync(null); /// - /// Handles a second call to when already disposed, + /// Handles a second call to when already disposed, /// routing any failure exception to the unhandled exception handler. /// /// The completion result from the second call. @@ -202,7 +202,7 @@ internal static void HandleAlreadyDisposed(Result? result) return; } - UnhandledExceptionHandler.OnUnhandledException(exception); + UnhandledExceptionHandler.ReportUnhandledException(exception); } /// @@ -210,17 +210,17 @@ internal static void HandleAlreadyDisposed(Result? result) /// /// The inner observable to subscribe to. /// A task representing the asynchronous operation. - internal async ValueTask SubscribeToInnerLoop(IObservableAsync currentInner) + internal async ValueTask SubscribeCurrentInnerAsync(IObservableAsync currentInner) { try { var innerSubscription = - await currentInner.SubscribeAsync(new ConcatInnerObserver(this), _disposedCancellationToken).ConfigureAwait(false); + await currentInner.SubscribeAsync(new ConcatInnerWitness(this), _disposedCancellationToken).ConfigureAwait(false); await _innerSubscription.SetDisposableAsync(innerSubscription).ConfigureAwait(false); } catch (Exception e) { - await CompleteAsync(Result.Failure(e)).ConfigureAwait(false); + await FinishAsync(Result.Failure(e)).ConfigureAwait(false); } } @@ -230,7 +230,7 @@ internal async ValueTask SubscribeToInnerLoop(IObservableAsync currentInner) /// /// The completion result to forward, or if disposing without signaling completion. /// A task representing the asynchronous operation. - internal async ValueTask CompleteAsync(Result? result) + internal async ValueTask FinishAsync(Result? result) { if (DisposalHelper.TrySetDisposed(ref _disposed)) { @@ -251,10 +251,10 @@ internal async ValueTask CompleteAsync(Result? result) } /// - /// Observer for the outer observable sequence that delegates to the parent . + /// Observer for the outer observable sequence that delegates to the parent . /// /// The parent concat subscription. - internal sealed class ConcatOuterObserver(ConcatSubscription subscription) : ObserverAsync> + internal sealed class ConcatOuterWitness(ConcatCoordinator subscription) : ObserverAsync> { /// /// Forwards a new inner observable to the parent subscription for buffering and sequential subscription. @@ -263,7 +263,7 @@ internal sealed class ConcatOuterObserver(ConcatSubscription subscription) : Obs /// A token to cancel the operation. /// A task representing the asynchronous operation. protected override ValueTask OnNextAsyncCore(IObservableAsync value, CancellationToken cancellationToken) - => subscription.OnNextOuterAsync(value); + => subscription.AcceptOuterValueAsync(value); /// /// Forwards a non-fatal error from the outer sequence to the downstream observer. @@ -281,7 +281,7 @@ protected override async ValueTask OnErrorResumeAsyncCore( // provided, without the per-emission Linked2CancellationTokenSource alloc. _ = cancellationToken; var token = subscription._disposedCancellationToken; - using (await subscription._observerOnSomethingGate.LockAsync(token).ConfigureAwait(false)) + using (await subscription._observerOnSomethingGate.EnterAsync(token).ConfigureAwait(false)) { await subscription._observer.OnErrorResumeAsync(error, token).ConfigureAwait(false); } @@ -293,14 +293,14 @@ protected override async ValueTask OnErrorResumeAsyncCore( /// The completion result. /// A task representing the asynchronous operation. protected override ValueTask OnCompletedAsyncCore(Result result) - => subscription.OnCompletedOuterAsync(result); + => subscription.AcceptOuterCompletionAsync(result); } /// - /// Observer for the currently active inner observable sequence that delegates to the parent . + /// Observer for the currently active inner observable sequence that delegates to the parent . /// /// The parent concat subscription. - internal sealed class ConcatInnerObserver(ConcatSubscription subscription) : ObserverAsync + internal sealed class ConcatInnerWitness(ConcatCoordinator subscription) : ObserverAsync { /// /// Forwards an element from the inner sequence to the downstream observer. @@ -312,7 +312,7 @@ protected override async ValueTask OnNextAsyncCore(T value, CancellationToken ca { _ = cancellationToken; var token = subscription._disposedCancellationToken; - using (await subscription._observerOnSomethingGate.LockAsync(token).ConfigureAwait(false)) + using (await subscription._observerOnSomethingGate.EnterAsync(token).ConfigureAwait(false)) { await subscription._observer.OnNextAsync(value, token).ConfigureAwait(false); } @@ -330,7 +330,7 @@ protected override async ValueTask OnErrorResumeAsyncCore( { _ = cancellationToken; var token = subscription._disposedCancellationToken; - using (await subscription._observerOnSomethingGate.LockAsync(token).ConfigureAwait(false)) + using (await subscription._observerOnSomethingGate.EnterAsync(token).ConfigureAwait(false)) { await subscription._observer.OnErrorResumeAsync(error, token).ConfigureAwait(false); } @@ -342,7 +342,7 @@ protected override async ValueTask OnErrorResumeAsyncCore( /// The completion result. /// A task representing the asynchronous operation. protected override ValueTask OnCompletedAsyncCore(Result result) - => subscription.OnCompletedInnerAsync(result); + => subscription.AcceptInnerCompletionAsync(result); } } } diff --git a/src/ReactiveUI.Primitives.Async/Operators/ContainsAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/ContainsAsync.cs index 02710f1..eb59bf5 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/ContainsAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/ContainsAsync.cs @@ -46,9 +46,9 @@ public static async ValueTask ContainsAsync( { cancellationToken.ThrowIfCancellationRequested(); var cmp = comparer ?? EqualityComparer.Default; - var observer = new ContainsAsyncObserver(value, cmp, cancellationToken); + var observer = new ContainsTaskWitness(value, cmp, cancellationToken); await using var subscription = await @this.SubscribeAsync(observer, cancellationToken).ConfigureAwait(false); - return await observer.WaitValueAsync().ConfigureAwait(false); + return await observer.AwaitResultAsync().ConfigureAwait(false); } /// @@ -81,26 +81,26 @@ public static ValueTask ContainsAsync(this IObservableAsync @this, T /// The value to search for. /// The equality comparer to use for comparison. /// A cancellation token for the operation. - internal sealed class ContainsAsyncObserver( + internal sealed class ContainsTaskWitness( T value, IEqualityComparer comparer, - CancellationToken cancellationToken) : TaskObserverAsyncBase(cancellationToken) + CancellationToken cancellationToken) : TaskWitnessAsyncBase(cancellationToken) { /// protected override async ValueTask OnNextAsyncCore(T value1, CancellationToken cancellationToken) { if (comparer.Equals(value, value1)) { - await TrySetCompleted(true).ConfigureAwait(false); + await SetResultAndDisposeAsync(true).ConfigureAwait(false); } } /// protected override ValueTask OnErrorResumeAsyncCore(Exception error, CancellationToken cancellationToken) - => TrySetException(error); + => SetExceptionAndDisposeAsync(error); /// protected override ValueTask OnCompletedAsyncCore(Result result) - => result.IsSuccess ? TrySetCompleted(false) : TrySetException(result.Exception); + => result.IsSuccess ? SetResultAndDisposeAsync(false) : SetExceptionAndDisposeAsync(result.Exception); } } diff --git a/src/ReactiveUI.Primitives.Async/Operators/CountAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/CountAsync.cs index 988ca71..6b57f07 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CountAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CountAsync.cs @@ -37,9 +37,9 @@ public static ValueTask CountAsync(this IObservableAsync @this, Func< public static async ValueTask CountAsync(this IObservableAsync @this, Func? predicate, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - var observer = new CountAsyncObserver(predicate, cancellationToken); + var observer = new CountTaskWitness(predicate, cancellationToken); await using var subscription = await @this.SubscribeAsync(observer, cancellationToken).ConfigureAwait(false); - return await observer.WaitValueAsync().ConfigureAwait(false); + return await observer.AwaitResultAsync().ConfigureAwait(false); } /// @@ -69,8 +69,8 @@ public static ValueTask CountAsync(this IObservableAsync @this, Cance /// The type of elements in the source sequence. /// An optional predicate to filter elements. If null, all elements are counted. /// A cancellation token for the operation. - internal sealed class CountAsyncObserver(Func? predicate, CancellationToken cancellationToken) - : TaskObserverAsyncBase(cancellationToken) + internal sealed class CountTaskWitness(Func? predicate, CancellationToken cancellationToken) + : TaskWitnessAsyncBase(cancellationToken) { /// /// The running count of elements that satisfy the predicate. @@ -92,10 +92,10 @@ protected override ValueTask OnNextAsyncCore(T value, CancellationToken cancella /// protected override ValueTask OnErrorResumeAsyncCore(Exception error, CancellationToken cancellationToken) => - TrySetException(error); + SetExceptionAndDisposeAsync(error); /// protected override ValueTask OnCompletedAsyncCore(Result result) => - !result.IsSuccess ? TrySetException(result.Exception) : TrySetCompleted(_count); + !result.IsSuccess ? SetExceptionAndDisposeAsync(result.Exception) : SetResultAndDisposeAsync(_count); } } diff --git a/src/ReactiveUI.Primitives.Async/Operators/Distinct.cs b/src/ReactiveUI.Primitives.Async/Operators/Distinct.cs index 28913d4..e2b6ee1 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/Distinct.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/Distinct.cs @@ -108,7 +108,7 @@ protected override async ValueTask SubscribeAsyncCore( } var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } @@ -162,7 +162,7 @@ protected override async ValueTask SubscribeAsyncCore( } var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } diff --git a/src/ReactiveUI.Primitives.Async/Operators/DistinctUntilChanged.cs b/src/ReactiveUI.Primitives.Async/Operators/DistinctUntilChanged.cs index 59d045e..9f30d9b 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/DistinctUntilChanged.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/DistinctUntilChanged.cs @@ -114,7 +114,7 @@ protected override async ValueTask SubscribeAsyncCore( } var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } @@ -183,7 +183,7 @@ protected override async ValueTask SubscribeAsyncCore( } var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } diff --git a/src/ReactiveUI.Primitives.Async/Operators/Do.cs b/src/ReactiveUI.Primitives.Async/Operators/Do.cs index bd1dd52..b54b44f 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/Do.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/Do.cs @@ -96,14 +96,14 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var doObserver = new DoAsyncObserver(observer, onNext, onErrorResume, onCompleted); + var doObserver = new AsyncSideEffectWitness(observer, onNext, onErrorResume, onCompleted); return source.SubscribeAsync(doObserver, cancellationToken); } /// /// An observer that invokes asynchronous side-effect callbacks before forwarding notifications. /// - internal sealed class DoAsyncObserver( + internal sealed class AsyncSideEffectWitness( IObserverAsync observer, Func? onNext, Func? onErrorResume, @@ -161,14 +161,14 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var doObserver = new DoSyncObserver(observer, onNext, onErrorResume, onCompleted); + var doObserver = new SyncSideEffectWitness(observer, onNext, onErrorResume, onCompleted); return source.SubscribeAsync(doObserver, cancellationToken); } /// /// An observer that invokes synchronous side-effect callbacks before forwarding notifications. /// - internal sealed class DoSyncObserver( + internal sealed class SyncSideEffectWitness( IObserverAsync observer, Action? onNext, Action? onErrorResume, diff --git a/src/ReactiveUI.Primitives.Async/Operators/FirstAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/FirstAsync.cs index 4196652..b36b27f 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/FirstAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/FirstAsync.cs @@ -39,9 +39,9 @@ public static ValueTask FirstAsync(this IObservableAsync @this, Func FirstAsync(this IObservableAsync @this, Func predicate, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - var observer = new FirstAsyncObserver(predicate, cancellationToken); + var observer = new FirstTaskWitness(predicate, cancellationToken); await using var subscription = await @this.SubscribeAsync(observer, cancellationToken).ConfigureAwait(false); - return await observer.WaitValueAsync().ConfigureAwait(false); + return await observer.AwaitResultAsync().ConfigureAwait(false); } /// @@ -69,9 +69,9 @@ public static ValueTask FirstAsync(this IObservableAsync @this) public static async ValueTask FirstAsync(this IObservableAsync @this, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - var observer = new FirstAsyncObserver(null, cancellationToken); + var observer = new FirstTaskWitness(null, cancellationToken); await using var subscription = await @this.SubscribeAsync(observer, cancellationToken).ConfigureAwait(false); - return await observer.WaitValueAsync().ConfigureAwait(false); + return await observer.AwaitResultAsync().ConfigureAwait(false); } /// @@ -80,21 +80,21 @@ public static async ValueTask FirstAsync(this IObservableAsync @this, C /// The type of elements in the source sequence. /// An optional predicate to filter elements. /// A cancellation token for the operation. - internal sealed class FirstAsyncObserver(Func? predicate, CancellationToken cancellationToken) - : TaskObserverAsyncBase(cancellationToken) + internal sealed class FirstTaskWitness(Func? predicate, CancellationToken cancellationToken) + : TaskWitnessAsyncBase(cancellationToken) { /// protected override async ValueTask OnNextAsyncCore(T value, CancellationToken cancellationToken) { if (predicate is null || predicate(value)) { - await TrySetCompleted(value).ConfigureAwait(false); + await SetResultAndDisposeAsync(value).ConfigureAwait(false); } } /// protected override ValueTask OnErrorResumeAsyncCore(Exception error, CancellationToken cancellationToken) => - TrySetException(error); + SetExceptionAndDisposeAsync(error); /// protected override ValueTask OnCompletedAsyncCore(Result result) @@ -112,7 +112,7 @@ protected override ValueTask OnCompletedAsyncCore(Result result) exception = result.Exception; } - return TrySetException(exception); + return SetExceptionAndDisposeAsync(exception); } } } diff --git a/src/ReactiveUI.Primitives.Async/Operators/FirstOrDefaultAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/FirstOrDefaultAsync.cs index f28149e..e7c311f 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/FirstOrDefaultAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/FirstOrDefaultAsync.cs @@ -51,9 +51,9 @@ public static partial class SignalAsync CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - var observer = new FirstOrDefaultObserver(predicate, defaultValue, cancellationToken); + var observer = new FirstOrDefaultTaskWitness(predicate, defaultValue, cancellationToken); await using var subscription = await @this.SubscribeAsync(observer, cancellationToken).ConfigureAwait(false); - return await observer.WaitValueAsync().ConfigureAwait(false); + return await observer.AwaitResultAsync().ConfigureAwait(false); } /// @@ -104,9 +104,9 @@ public static partial class SignalAsync public static async ValueTask FirstOrDefaultAsync(this IObservableAsync @this, T? defaultValue, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - var observer = new FirstOrDefaultObserver(null, defaultValue, cancellationToken); + var observer = new FirstOrDefaultTaskWitness(null, defaultValue, cancellationToken); await using var subscription = await @this.SubscribeAsync(observer, cancellationToken).ConfigureAwait(false); - return await observer.WaitValueAsync().ConfigureAwait(false); + return await observer.AwaitResultAsync().ConfigureAwait(false); } /// @@ -116,26 +116,26 @@ public static partial class SignalAsync /// An optional predicate to filter elements. /// The default value to return if no element matches. /// A cancellation token for the operation. - internal sealed class FirstOrDefaultObserver( + internal sealed class FirstOrDefaultTaskWitness( Func? predicate, T? defaultValue, - CancellationToken cancellationToken) : TaskObserverAsyncBase(cancellationToken) + CancellationToken cancellationToken) : TaskWitnessAsyncBase(cancellationToken) { /// protected override async ValueTask OnNextAsyncCore(T value, CancellationToken cancellationToken) { if (predicate is null || predicate(value)) { - await TrySetCompleted(value).ConfigureAwait(false); + await SetResultAndDisposeAsync(value).ConfigureAwait(false); } } /// protected override ValueTask OnErrorResumeAsyncCore(Exception error, CancellationToken cancellationToken) => - TrySetException(error); + SetExceptionAndDisposeAsync(error); /// protected override ValueTask OnCompletedAsyncCore(Result result) => - result.IsSuccess ? TrySetCompleted(defaultValue!) : TrySetException(result.Exception); + result.IsSuccess ? SetResultAndDisposeAsync(defaultValue!) : SetExceptionAndDisposeAsync(result.Exception); } } diff --git a/src/ReactiveUI.Primitives.Async/Operators/ForEachAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/ForEachAsync.cs index 61f4cd3..f1a66d5 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/ForEachAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/ForEachAsync.cs @@ -57,9 +57,9 @@ public static async ValueTask ForEachAsync( } cancellationToken.ThrowIfCancellationRequested(); - var observer = new ForEachObserver(onNextAsync, cancellationToken); + var observer = new ForEachAsyncTaskWitness(onNextAsync, cancellationToken); await @this.SubscribeAsync(observer, cancellationToken).ConfigureAwait(false); - await observer.WaitValueAsync().ConfigureAwait(false); + await observer.AwaitResultAsync().ConfigureAwait(false); } /// @@ -89,18 +89,18 @@ public static async ValueTask ForEachAsync(this IObservableAsync @this, Ac ArgumentExceptionHelper.ThrowIfNull(onNext); cancellationToken.ThrowIfCancellationRequested(); - var observer = new ForEachObserverSync(onNext, cancellationToken); + var observer = new ForEachSyncTaskWitness(onNext, cancellationToken); await @this.SubscribeAsync(observer, cancellationToken).ConfigureAwait(false); - await observer.WaitValueAsync().ConfigureAwait(false); + await observer.AwaitResultAsync().ConfigureAwait(false); } /// /// An observer that invokes an asynchronous callback for each element and signals completion via a task. /// /// The type of elements in the sequence. - internal sealed class ForEachObserver( + internal sealed class ForEachAsyncTaskWitness( Func onNextAsync, - CancellationToken cancellationToken) : TaskObserverAsyncBase(cancellationToken) + CancellationToken cancellationToken) : TaskWitnessAsyncBase(cancellationToken) { /// protected override ValueTask OnNextAsyncCore(T value, CancellationToken cancellationToken) => @@ -108,19 +108,19 @@ protected override ValueTask OnNextAsyncCore(T value, CancellationToken cancella /// protected override ValueTask OnErrorResumeAsyncCore(Exception error, CancellationToken cancellationToken) => - TrySetException(error); + SetExceptionAndDisposeAsync(error); /// protected override ValueTask OnCompletedAsyncCore(Result result) => - result.IsSuccess ? TrySetCompleted(true) : TrySetException(result.Exception); + result.IsSuccess ? SetResultAndDisposeAsync(true) : SetExceptionAndDisposeAsync(result.Exception); } /// /// An observer that invokes a synchronous callback for each element and signals completion via a task. /// /// The type of elements in the sequence. - internal sealed class ForEachObserverSync(Action onNext, CancellationToken cancellationToken) - : TaskObserverAsyncBase(cancellationToken) + internal sealed class ForEachSyncTaskWitness(Action onNext, CancellationToken cancellationToken) + : TaskWitnessAsyncBase(cancellationToken) { /// /// The synchronous callback invoked for each element in the sequence. @@ -136,10 +136,10 @@ protected override ValueTask OnNextAsyncCore(T value, CancellationToken cancella /// protected override ValueTask OnErrorResumeAsyncCore(Exception error, CancellationToken cancellationToken) => - TrySetException(error); + SetExceptionAndDisposeAsync(error); /// protected override ValueTask OnCompletedAsyncCore(Result result) => - result.IsSuccess ? TrySetCompleted(true) : TrySetException(result.Exception); + result.IsSuccess ? SetResultAndDisposeAsync(true) : SetExceptionAndDisposeAsync(result.Exception); } } diff --git a/src/ReactiveUI.Primitives.Async/Operators/GroupBy.cs b/src/ReactiveUI.Primitives.Async/Operators/GroupBy.cs index a9e4299..ba53cf3 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/GroupBy.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/GroupBy.cs @@ -102,7 +102,7 @@ internal sealed class GroupByAsyncSignal( private readonly Func> _groupSignalSelector = groupSignalSelector; /// - /// Subscribes the specified observer by creating a that tracks groups by key. + /// Subscribes the specified observer by creating a that tracks groups by key. /// /// The observer to receive grouped observable sequences. /// A token to cancel the subscription. @@ -111,7 +111,7 @@ protected override async ValueTask SubscribeAsyncCore( IObserverAsync> observer, CancellationToken cancellationToken) { - var subscription = new Subscription(this, observer); + var subscription = new GroupingCoordinator(this, observer); try { return await subscription.SubscribeSourcesAsync(cancellationToken).ConfigureAwait(false); @@ -128,7 +128,7 @@ protected override async ValueTask SubscribeAsyncCore( /// /// The parent GroupBy observable that provides the key selector and signal factory. /// The downstream observer to receive grouped observables. - internal sealed class Subscription( + internal sealed class GroupingCoordinator( GroupByAsyncSignal parent, IObserverAsync> observer) : ObserverAsync { @@ -210,10 +210,10 @@ protected override async ValueTask DisposeAsyncCore() /// /// Represents a single grouped async observable identified by its key. /// - /// The parent subscription that manages group disposables. + /// The parent coordinator that manages group disposables. /// The key that identifies this group. /// The observable sequence of values for this group. - internal sealed class Signal(Subscription parent, TKey key, IObservableAsync signalValues) + internal sealed class Signal(GroupingCoordinator parent, TKey key, IObservableAsync signalValues) : GroupedAsyncSignal { /// @@ -236,7 +236,7 @@ protected override async ValueTask SubscribeAsyncCore( // that token); the downstream observer (if an ObserverAsync) observes the // wrap's dispose token. Together they collapse the per-emission // CancellationTokenSource.CreateLinkedTokenSource allocations to zero. - var wrap = new WrappedObserverAsync(observer); + var wrap = new ForwardingAsyncWitness(observer); wrap.LinkUpstreamCancellation(parent.InternalDisposedToken); if (observer is ObserverAsync downstream) { diff --git a/src/ReactiveUI.Primitives.Async/Operators/LastAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/LastAsync.cs index 0b24891..b5c3cb1 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/LastAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/LastAsync.cs @@ -39,9 +39,9 @@ public static ValueTask LastAsync(this IObservableAsync @this, Func LastAsync(this IObservableAsync @this, Func predicate, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - var observer = new LastAsyncObserver(predicate, cancellationToken); + var observer = new LastTaskWitness(predicate, cancellationToken); await using var subscription = await @this.SubscribeAsync(observer, cancellationToken).ConfigureAwait(false); - return await observer.WaitValueAsync().ConfigureAwait(false); + return await observer.AwaitResultAsync().ConfigureAwait(false); } /// @@ -71,9 +71,9 @@ public static ValueTask LastAsync(this IObservableAsync @this) public static async ValueTask LastAsync(this IObservableAsync @this, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - var observer = new LastAsyncObserver(null, cancellationToken); + var observer = new LastTaskWitness(null, cancellationToken); await using var subscription = await @this.SubscribeAsync(observer, cancellationToken).ConfigureAwait(false); - return await observer.WaitValueAsync().ConfigureAwait(false); + return await observer.AwaitResultAsync().ConfigureAwait(false); } /// @@ -82,8 +82,8 @@ public static async ValueTask LastAsync(this IObservableAsync @this, Ca /// The type of elements in the source sequence. /// An optional predicate to filter elements. /// A cancellation token for the operation. - internal sealed class LastAsyncObserver(Func? predicate, CancellationToken cancellationToken) - : TaskObserverAsyncBase(cancellationToken) + internal sealed class LastTaskWitness(Func? predicate, CancellationToken cancellationToken) + : TaskWitnessAsyncBase(cancellationToken) { /// /// A value indicating whether any matching element has been observed. @@ -111,25 +111,25 @@ protected override ValueTask OnNextAsyncCore(T value, CancellationToken cancella /// protected override ValueTask OnErrorResumeAsyncCore(Exception error, CancellationToken cancellationToken) => - TrySetException(error); + SetExceptionAndDisposeAsync(error); /// protected override ValueTask OnCompletedAsyncCore(Result result) { if (!result.IsSuccess) { - return TrySetException(result.Exception); + return SetExceptionAndDisposeAsync(result.Exception); } if (_hasValue) { - return TrySetCompleted(_last!); + return SetResultAndDisposeAsync(_last!); } var message = predicate is null ? "Sequence contains no elements." : "Sequence contains no matching elements."; - return TrySetException(new InvalidOperationException(message)); + return SetExceptionAndDisposeAsync(new InvalidOperationException(message)); } } } diff --git a/src/ReactiveUI.Primitives.Async/Operators/LastOrDefaultAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/LastOrDefaultAsync.cs index 6055743..611ceb3 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/LastOrDefaultAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/LastOrDefaultAsync.cs @@ -50,9 +50,9 @@ public static partial class SignalAsync CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - var observer = new LastOrDefaultObserver(predicate, defaultValue, cancellationToken); + var observer = new LastOrDefaultTaskWitness(predicate, defaultValue, cancellationToken); await using var subscription = await @this.SubscribeAsync(observer, cancellationToken).ConfigureAwait(false); - return await observer.WaitValueAsync().ConfigureAwait(false); + return await observer.AwaitResultAsync().ConfigureAwait(false); } /// @@ -103,9 +103,9 @@ public static partial class SignalAsync public static async ValueTask LastOrDefaultAsync(this IObservableAsync @this, T? defaultValue, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - var observer = new LastOrDefaultObserver(null, defaultValue, cancellationToken); + var observer = new LastOrDefaultTaskWitness(null, defaultValue, cancellationToken); await using var subscription = await @this.SubscribeAsync(observer, cancellationToken).ConfigureAwait(false); - return await observer.WaitValueAsync().ConfigureAwait(false); + return await observer.AwaitResultAsync().ConfigureAwait(false); } /// @@ -115,10 +115,10 @@ public static partial class SignalAsync /// An optional predicate to filter elements. /// The default value to return if no element matches. /// A cancellation token for the operation. - internal sealed class LastOrDefaultObserver( + internal sealed class LastOrDefaultTaskWitness( Func? predicate, T? defaultValue, - CancellationToken cancellationToken) : TaskObserverAsyncBase(cancellationToken) + CancellationToken cancellationToken) : TaskWitnessAsyncBase(cancellationToken) { /// /// The most recently observed matching element, or the default value if no match has been found. @@ -140,10 +140,10 @@ protected override ValueTask OnNextAsyncCore(T value, CancellationToken cancella /// protected override ValueTask OnErrorResumeAsyncCore(Exception error, CancellationToken cancellationToken) => - TrySetException(error); + SetExceptionAndDisposeAsync(error); /// protected override ValueTask OnCompletedAsyncCore(Result result) => - result.IsSuccess ? TrySetCompleted(_last!) : TrySetException(result.Exception); + result.IsSuccess ? SetResultAndDisposeAsync(_last!) : SetExceptionAndDisposeAsync(result.Exception); } } diff --git a/src/ReactiveUI.Primitives.Async/Operators/LongCountAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/LongCountAsync.cs index b6f8732..e70ff50 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/LongCountAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/LongCountAsync.cs @@ -40,9 +40,9 @@ public static async ValueTask LongCountAsync( CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - var observer = new LongCountAsyncObserver(predicate, cancellationToken); + var observer = new LongCountTaskWitness(predicate, cancellationToken); await using var subscription = await @this.SubscribeAsync(observer, cancellationToken).ConfigureAwait(false); - return await observer.WaitValueAsync().ConfigureAwait(false); + return await observer.AwaitResultAsync().ConfigureAwait(false); } /// @@ -72,8 +72,8 @@ public static ValueTask LongCountAsync(this IObservableAsync @this, /// The type of elements in the source sequence. /// An optional predicate to filter elements. If null, all elements are counted. /// A cancellation token for the operation. - internal sealed class LongCountAsyncObserver(Func? predicate, CancellationToken cancellationToken) - : TaskObserverAsyncBase(cancellationToken) + internal sealed class LongCountTaskWitness(Func? predicate, CancellationToken cancellationToken) + : TaskWitnessAsyncBase(cancellationToken) { /// /// The running count of elements that satisfy the predicate. @@ -95,10 +95,10 @@ protected override ValueTask OnNextAsyncCore(T value, CancellationToken cancella /// protected override ValueTask OnErrorResumeAsyncCore(Exception error, CancellationToken cancellationToken) => - TrySetException(error); + SetExceptionAndDisposeAsync(error); /// protected override ValueTask OnCompletedAsyncCore(Result result) => - !result.IsSuccess ? TrySetException(result.Exception) : TrySetCompleted(_count); + !result.IsSuccess ? SetExceptionAndDisposeAsync(result.Exception) : SetResultAndDisposeAsync(_count); } } diff --git a/src/ReactiveUI.Primitives.Async/Operators/Merge.cs b/src/ReactiveUI.Primitives.Async/Operators/Merge.cs index 98a7596..35c23cd 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/Merge.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/Merge.cs @@ -88,7 +88,7 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var subscription = new MergeSubscription(observer); + var subscription = new MergeCoordinator(observer); subscription.LinkExternalCancellation(cancellationToken); return SubscriptionHelper.SubscribeAndDisposeOnFailureAsync( subscription, @@ -109,7 +109,7 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var subscription = new MergeSubscriptionWithMaxConcurrency(observer, maxConcurrent); + var subscription = new BoundedMergeCoordinator(observer, maxConcurrent); subscription.LinkExternalCancellation(cancellationToken); return SubscriptionHelper.SubscribeAndDisposeOnFailureAsync( subscription, @@ -121,7 +121,7 @@ protected override ValueTask SubscribeAsyncCore( /// Manages subscriptions for merged observable sequences, forwarding items from all inner sources to a single observer. /// /// The type of the elements in the merged sequence. - internal class MergeSubscription : IAsyncDisposable + internal class MergeCoordinator : IAsyncDisposable { /// The cancellation token source backing . private readonly CancellationTokenSource _disposeCts = new(); @@ -136,7 +136,7 @@ internal class MergeSubscription : IAsyncDisposable private readonly MultipleDisposableAsync _innerDisposables = new(); /// Serializes observer notifications to prevent concurrent calls. - private readonly AsyncGate _onSomethingGate = new(); + private readonly AsyncSerialGate _onSomethingGate = new(); /// The downstream observer that receives merged items. private readonly IObserverAsync _observer; @@ -154,10 +154,10 @@ internal class MergeSubscription : IAsyncDisposable private int _disposed; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The downstream observer to forward merged items to. - public MergeSubscription(IObserverAsync observer) => _observer = observer; + public MergeCoordinator(IObserverAsync observer) => _observer = observer; /// /// Subscribes to the outer observable and begins merging inner observable sequences. @@ -171,8 +171,8 @@ public async ValueTask SubscribeSourcesAsync( { _ = cancellationToken; var outerSubscription = await @this.SubscribeAsync( - (x, _) => SubscribeInnerAsync(x), - ForwardOnErrorResume, + (x, _) => SubscribeBranchAsync(x), + RelayErrorAsync, result => { bool shouldComplete; @@ -182,7 +182,7 @@ public async ValueTask SubscribeSourcesAsync( shouldComplete = _innerActiveCount == 0 || result.IsFailure; } - return shouldComplete ? CompleteAsync(result) : default; + return shouldComplete ? FinishAsync(result) : default; }, DisposedCancellationToken).ConfigureAwait(false); @@ -193,7 +193,7 @@ public async ValueTask SubscribeSourcesAsync( /// Asynchronously releases resources used by this subscription. /// /// A task representing the asynchronous dispose operation. - public ValueTask DisposeAsync() => CompleteAsync(null); + public ValueTask DisposeAsync() => FinishAsync(null); /// /// Links the original subscribe-time cancellation token into this subscription's dispose chain so @@ -226,9 +226,9 @@ internal void LinkExternalCancellation(CancellationToken external) /// /// The value to forward. /// A task representing the asynchronous forward operation. - internal ValueTask ForwardOnNextLocked(T value) + internal ValueTask RelayNextIfActiveAsync(T value) { - if (DisposalHelper.IsDisposed(_disposed)) + if (DisposalHelper.HasDisposed(_disposed)) { return default; } @@ -243,9 +243,9 @@ internal ValueTask ForwardOnNextLocked(T value) /// /// The error to forward. /// A task representing the asynchronous forward operation. - internal ValueTask ForwardOnErrorResumeLocked(Exception exception) + internal ValueTask RelayErrorIfActiveAsync(Exception exception) { - if (DisposalHelper.IsDisposed(_disposed)) + if (DisposalHelper.HasDisposed(_disposed)) { return default; } @@ -259,17 +259,17 @@ internal ValueTask ForwardOnErrorResumeLocked(Exception exception) /// The value to forward. /// A token to cancel the operation. /// A task representing the asynchronous forward operation. - protected internal async ValueTask ForwardOnNext(T value, CancellationToken cancellationToken) + protected internal async ValueTask RelayNextAsync(T value, CancellationToken cancellationToken) { _ = cancellationToken; - if (DisposalHelper.IsDisposed(_disposed)) + if (DisposalHelper.HasDisposed(_disposed)) { return; } - using (await _onSomethingGate.LockAsync(DisposedCancellationToken).ConfigureAwait(false)) + using (await _onSomethingGate.EnterAsync(DisposedCancellationToken).ConfigureAwait(false)) { - await ForwardOnNextLocked(value).ConfigureAwait(false); + await RelayNextIfActiveAsync(value).ConfigureAwait(false); } } @@ -279,19 +279,19 @@ protected internal async ValueTask ForwardOnNext(T value, CancellationToken canc /// The error to forward. /// A token to cancel the operation. /// A task representing the asynchronous forward operation. - protected internal async ValueTask ForwardOnErrorResume( + protected internal async ValueTask RelayErrorAsync( Exception exception, CancellationToken cancellationToken) { _ = cancellationToken; - if (DisposalHelper.IsDisposed(_disposed)) + if (DisposalHelper.HasDisposed(_disposed)) { return; } - using (await _onSomethingGate.LockAsync(DisposedCancellationToken).ConfigureAwait(false)) + using (await _onSomethingGate.EnterAsync(DisposedCancellationToken).ConfigureAwait(false)) { - await ForwardOnErrorResumeLocked(exception).ConfigureAwait(false); + await RelayErrorIfActiveAsync(exception).ConfigureAwait(false); } } @@ -300,16 +300,16 @@ protected internal async ValueTask ForwardOnErrorResume( /// /// The inner observable to subscribe to. /// A task representing the asynchronous subscribe operation. - protected internal virtual async ValueTask SubscribeInnerAsync(IObservableAsync inner) + protected internal virtual async ValueTask SubscribeBranchAsync(IObservableAsync inner) { try { - var innerObserver = CreateInnerObserver(); + var innerObserver = CreateBranchObserver(); await innerObserver.SubscribeSourcesAsync(inner, DisposedCancellationToken).ConfigureAwait(false); } catch (Exception e) { - await CompleteAsync(Result.Failure(e)).ConfigureAwait(false); + await FinishAsync(Result.Failure(e)).ConfigureAwait(false); } } @@ -317,14 +317,14 @@ protected internal virtual async ValueTask SubscribeInnerAsync(IObservableAsync< /// Creates a new inner observer for subscribing to an inner observable sequence. /// /// A new inner async observer instance. - protected internal virtual InnerAsyncObserver CreateInnerObserver() => new(this); + protected internal virtual MergeBranchObserver CreateBranchObserver() => new(this); /// /// Completes the merged sequence, disposes all subscriptions, and optionally signals the downstream observer. /// /// The completion result to forward, or null if disposing without signaling completion. /// A task representing the asynchronous completion operation. - protected internal async ValueTask CompleteAsync(Result? result) + protected internal async ValueTask FinishAsync(Result? result) { if (DisposalHelper.TrySetDisposed(ref _disposed)) { @@ -351,7 +351,7 @@ protected internal async ValueTask CompleteAsync(Result? result) /// /// Observer that forwards items from an inner observable to the parent merge subscription. /// - internal class InnerAsyncObserver(MergeSubscription parent) : ObserverAsync + internal class MergeBranchObserver(MergeCoordinator parent) : ObserverAsync { /// /// Subscribes this observer to an inner observable sequence. @@ -372,11 +372,11 @@ public async ValueTask SubscribeSourcesAsync(IObservableAsync inner, Cancella /// protected override ValueTask OnNextAsyncCore(T value, CancellationToken cancellationToken) => - parent.ForwardOnNext(value, cancellationToken); + parent.RelayNextAsync(value, cancellationToken); /// protected override ValueTask OnErrorResumeAsyncCore(Exception error, CancellationToken cancellationToken) => - parent.ForwardOnErrorResume(error, cancellationToken); + parent.RelayErrorAsync(error, cancellationToken); /// protected override ValueTask OnCompletedAsyncCore(Result result) @@ -388,13 +388,13 @@ protected override ValueTask OnCompletedAsyncCore(Result result) shouldComplete = result.IsFailure || (count == 0 && parent._outerCompleted); } - return shouldComplete ? parent.CompleteAsync(result) : default; + return shouldComplete ? parent.FinishAsync(result) : default; } /// protected override async ValueTask DisposeAsyncCore() { - await OnDisposeAsync().ConfigureAwait(false); + await CleanupBranchAsync().ConfigureAwait(false); await parent._innerDisposables.Remove(this).ConfigureAwait(false); await base.DisposeAsyncCore().ConfigureAwait(false); } @@ -403,25 +403,25 @@ protected override async ValueTask DisposeAsyncCore() /// Called during disposal to perform subclass-specific cleanup such as releasing semaphore slots. /// /// A task representing the asynchronous cleanup operation. - protected virtual ValueTask OnDisposeAsync() => default; + protected virtual ValueTask CleanupBranchAsync() => default; } } /// - /// Extends to limit the number of concurrently subscribed inner observables. + /// Extends to limit the number of concurrently subscribed inner observables. /// /// The type of the elements in the merged sequence. - internal sealed class MergeSubscriptionWithMaxConcurrency(IObserverAsync observer, int maxConcurrent) - : MergeSubscription(observer) + internal sealed class BoundedMergeCoordinator(IObserverAsync observer, int maxConcurrent) + : MergeCoordinator(observer) { /// Limits the number of concurrently subscribed inner observables. private readonly SemaphoreSlim _semaphore = new(maxConcurrent, maxConcurrent); /// - protected internal override async ValueTask SubscribeInnerAsync(IObservableAsync inner) + protected internal override async ValueTask SubscribeBranchAsync(IObservableAsync inner) { await _semaphore.WaitAsync(DisposedCancellationToken).ConfigureAwait(false); - var innerObserver = (InnerAsyncObserverWithSemaphore)CreateInnerObserver(); + var innerObserver = (MergeBranchObserverWithPermit)CreateBranchObserver(); var subscribed = false; try { @@ -430,13 +430,13 @@ protected internal override async ValueTask SubscribeInnerAsync(IObservableAsync } catch (Exception e) { - await CompleteAsync(Result.Failure(e)).ConfigureAwait(false); + await FinishAsync(Result.Failure(e)).ConfigureAwait(false); } finally { // On success the observer owns its semaphore slot and releases it on its own disposal - // (auto-dispose after OnCompletedAsync, or via parent CompleteAsync). On failure we dispose - // the observer here so its idempotent OnDisposeAsync returns the slot exactly once, + // (auto-dispose after OnCompletedAsync, or via parent FinishAsync). On failure we dispose + // the observer here so its idempotent CleanupBranchAsync returns the slot exactly once, // regardless of whether the observer also gets disposed again through _innerDisposables. if (!subscribed) { @@ -446,20 +446,20 @@ protected internal override async ValueTask SubscribeInnerAsync(IObservableAsync } /// - protected internal override InnerAsyncObserver CreateInnerObserver() => - new InnerAsyncObserverWithSemaphore(this); + protected internal override MergeBranchObserver CreateBranchObserver() => + new MergeBranchObserverWithPermit(this); /// /// Inner observer that releases a semaphore slot on disposal. /// - internal sealed class InnerAsyncObserverWithSemaphore(MergeSubscriptionWithMaxConcurrency parent) - : InnerAsyncObserver(parent) + internal sealed class MergeBranchObserverWithPermit(BoundedMergeCoordinator parent) + : MergeBranchObserver(parent) { /// Tracks whether the semaphore slot has already been released for this observer. /// /// can be invoked more than once for the same observer /// (auto-dispose after OnCompletedAsync, then again from CompositeDisposableAsync.Remove - /// and from the parent's CompleteAsync path). Without this guard, + /// and from the parent's FinishAsync path). Without this guard, /// would be called multiple times per observer, exceeding maxCount and throwing /// — which interrupts the parent's completion chain and leaves /// downstream observers waiting forever. @@ -467,7 +467,7 @@ internal sealed class InnerAsyncObserverWithSemaphore(MergeSubscriptionWithMaxCo private int _released; /// - protected override ValueTask OnDisposeAsync() + protected override ValueTask CleanupBranchAsync() { if (Interlocked.Exchange(ref _released, 1) != 0) { @@ -491,16 +491,16 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var subscription = new MergeEnumerableSubscription(observer, sources); + var subscription = new MergeSequenceCoordinator(observer, sources); subscription.LinkExternalCancellation(cancellationToken); - subscription.StartAsync(); + subscription.BeginSubscribing(); return new(subscription); } /// /// Manages subscriptions to all sources in the enumerable and forwards their items to a single observer. /// - internal sealed class MergeEnumerableSubscription : IAsyncDisposable + internal sealed class MergeSequenceCoordinator : IAsyncDisposable { /// The collection of source observables to merge. private readonly IEnumerable> _sources; @@ -515,7 +515,7 @@ internal sealed class MergeEnumerableSubscription : IAsyncDisposable private readonly CancellationToken _disposedCancellationToken; /// Serializes observer notifications to prevent concurrent calls. - private readonly AsyncGate _onSomethingGate = new(); + private readonly AsyncSerialGate _onSomethingGate = new(); /// Signals when the initial subscription loop has finished. private readonly TaskCompletionSource _subscriptionFinished = @@ -537,11 +537,11 @@ internal sealed class MergeEnumerableSubscription : IAsyncDisposable private int _disposed; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The downstream observer to forward merged items to. /// The enumerable of observable sources to merge. - public MergeEnumerableSubscription(IObserverAsync observer, IEnumerable> sources) + public MergeSequenceCoordinator(IObserverAsync observer, IEnumerable> sources) { _observer = observer; _sources = sources; @@ -555,7 +555,7 @@ public MergeEnumerableSubscription(IObserverAsync observer, IEnumerable FireAndForgetHelper.Run(async () => + public void BeginSubscribing() => FireAndForgetHelper.Run(async () => { _reentrant.Value = true; try @@ -569,7 +569,7 @@ public void StartAsync() => FireAndForgetHelper.Run(async () => { Interlocked.Increment(ref _active); - var innerObserver = new InnerAsyncObserver(this); + var innerObserver = new MergeBranchObserver(this); await _innerDisposables.AddAsync(innerObserver).ConfigureAwait(false); try { @@ -581,7 +581,7 @@ public void StartAsync() => FireAndForgetHelper.Run(async () => } catch (Exception ex) { - await CompleteAsync(Result.Failure(ex)).ConfigureAwait(false); + await FinishAsync(Result.Failure(ex)).ConfigureAwait(false); return; } } @@ -589,12 +589,12 @@ public void StartAsync() => FireAndForgetHelper.Run(async () => // Remove sentinel: if all inner sources completed during the loop, this triggers final completion. if (Interlocked.Decrement(ref _active) == 0) { - await CompleteAsync(Result.Success).ConfigureAwait(false); + await FinishAsync(Result.Success).ConfigureAwait(false); } } catch (Exception e) { - await CompleteAsync(Result.Failure(e)).ConfigureAwait(false); + await FinishAsync(Result.Failure(e)).ConfigureAwait(false); } finally { @@ -606,7 +606,7 @@ public void StartAsync() => FireAndForgetHelper.Run(async () => /// Asynchronously releases resources used by this subscription. /// /// A task representing the asynchronous dispose operation. - public ValueTask DisposeAsync() => CompleteAsync(null); + public ValueTask DisposeAsync() => FinishAsync(null); /// /// Routes an exception from a post-disposal completion result to the unhandled exception handler. @@ -621,7 +621,7 @@ internal static void RoutePostDisposalException(Result? result) return; } - UnhandledExceptionHandler.OnUnhandledException(ex); + UnhandledExceptionHandler.ReportUnhandledException(ex); } /// @@ -654,17 +654,17 @@ internal void LinkExternalCancellation(CancellationToken external) /// The value to forward. /// A token to cancel the operation. /// A task representing the asynchronous forward operation. - internal async ValueTask OnNextAsync(T value, CancellationToken token) + internal async ValueTask RelayNextAsync(T value, CancellationToken token) { _ = token; - if (DisposalHelper.IsDisposed(_disposed)) + if (DisposalHelper.HasDisposed(_disposed)) { return; } - using (await _onSomethingGate.LockAsync(_disposedCancellationToken).ConfigureAwait(false)) + using (await _onSomethingGate.EnterAsync(_disposedCancellationToken).ConfigureAwait(false)) { - await OnNextAsyncLocked(value).ConfigureAwait(false); + await RelayNextIfActiveAsync(value).ConfigureAwait(false); } } @@ -675,9 +675,9 @@ internal async ValueTask OnNextAsync(T value, CancellationToken token) /// /// The value to forward. /// A task representing the asynchronous forward operation. - internal ValueTask OnNextAsyncLocked(T value) + internal ValueTask RelayNextIfActiveAsync(T value) { - if (DisposalHelper.IsDisposed(_disposed)) + if (DisposalHelper.HasDisposed(_disposed)) { return default; } @@ -691,17 +691,17 @@ internal ValueTask OnNextAsyncLocked(T value) /// The error to forward. /// A token to cancel the operation. /// A task representing the asynchronous forward operation. - internal async ValueTask OnErrorResumeAsync(Exception ex, CancellationToken token) + internal async ValueTask RelayErrorAsync(Exception ex, CancellationToken token) { _ = token; - if (DisposalHelper.IsDisposed(_disposed)) + if (DisposalHelper.HasDisposed(_disposed)) { return; } - using (await _onSomethingGate.LockAsync(_disposedCancellationToken).ConfigureAwait(false)) + using (await _onSomethingGate.EnterAsync(_disposedCancellationToken).ConfigureAwait(false)) { - await OnErrorResumeAsyncLocked(ex).ConfigureAwait(false); + await RelayErrorIfActiveAsync(ex).ConfigureAwait(false); } } @@ -712,9 +712,9 @@ internal async ValueTask OnErrorResumeAsync(Exception ex, CancellationToken toke /// /// The error to forward. /// A task representing the asynchronous forward operation. - internal ValueTask OnErrorResumeAsyncLocked(Exception ex) + internal ValueTask RelayErrorIfActiveAsync(Exception ex) { - if (DisposalHelper.IsDisposed(_disposed)) + if (DisposalHelper.HasDisposed(_disposed)) { return default; } @@ -727,11 +727,11 @@ internal ValueTask OnErrorResumeAsyncLocked(Exception ex) /// /// The completion result from the inner source. /// A task representing the asynchronous completion operation. - internal ValueTask OnCompletedAsync(Result result) + internal ValueTask AcceptBranchCompletionAsync(Result result) { if (result.IsFailure) { - return CompleteAsync(result); + return FinishAsync(result); } if (Interlocked.Decrement(ref _active) != 0) @@ -739,7 +739,7 @@ internal ValueTask OnCompletedAsync(Result result) return default; } - return CompleteAsync(Result.Success); + return FinishAsync(Result.Success); } /// @@ -747,7 +747,7 @@ internal ValueTask OnCompletedAsync(Result result) /// /// The completion result to forward, or if disposing without signaling completion. /// A task representing the asynchronous completion operation. - internal async ValueTask CompleteAsync(Result? result) + internal async ValueTask FinishAsync(Result? result) { if (DisposalHelper.TrySetDisposed(ref _disposed)) { @@ -779,21 +779,21 @@ internal async ValueTask CompleteAsync(Result? result) /// /// Observer that forwards items from an inner source to the parent enumerable merge subscription. /// - internal sealed class InnerAsyncObserver(MergeEnumerableSubscription parent) : ObserverAsync + internal sealed class MergeBranchObserver(MergeSequenceCoordinator parent) : ObserverAsync { /// protected override ValueTask OnNextAsyncCore(T value, CancellationToken cancellationToken) - => parent.OnNextAsync(value, cancellationToken); + => parent.RelayNextAsync(value, cancellationToken); /// protected override ValueTask OnErrorResumeAsyncCore( Exception error, CancellationToken cancellationToken) - => parent.OnErrorResumeAsync(error, cancellationToken); + => parent.RelayErrorAsync(error, cancellationToken); /// protected override ValueTask OnCompletedAsyncCore(Result result) - => parent.OnCompletedAsync(result); + => parent.AcceptBranchCompletionAsync(result); /// protected override async ValueTask DisposeAsyncCore() diff --git a/src/ReactiveUI.Primitives.Async/Operators/OfType.cs b/src/ReactiveUI.Primitives.Async/Operators/OfType.cs index 1165b35..663b42b 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/OfType.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/OfType.cs @@ -58,7 +58,7 @@ protected override async ValueTask SubscribeAsyncCore( } var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } diff --git a/src/ReactiveUI.Primitives.Async/Operators/ParityHelpers.FilterFusions.cs b/src/ReactiveUI.Primitives.Async/Operators/ParityHelpers.FilterFusions.cs index a1e2586..8e99fe1 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/ParityHelpers.FilterFusions.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/ParityHelpers.FilterFusions.cs @@ -38,7 +38,7 @@ protected override async ValueTask SubscribeAsyncCore( } var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } @@ -104,7 +104,7 @@ protected override async ValueTask SubscribeAsyncCore( } var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } @@ -170,7 +170,7 @@ protected override async ValueTask SubscribeAsyncCore( } var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } @@ -229,7 +229,7 @@ protected override async ValueTask SubscribeAsyncCore( await observer.OnNextAsync(defaultValue, cancellationToken).ConfigureAwait(false); var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } @@ -295,7 +295,7 @@ protected override async ValueTask SubscribeAsyncCore( } var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } @@ -355,7 +355,7 @@ protected override async ValueTask SubscribeAsyncCore( } var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } @@ -399,7 +399,7 @@ protected override async ValueTask SubscribeAsyncCore( } var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } @@ -443,7 +443,7 @@ protected override async ValueTask SubscribeAsyncCore( } var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } @@ -487,7 +487,7 @@ protected override async ValueTask SubscribeAsyncCore( } var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } diff --git a/src/ReactiveUI.Primitives.Async/Operators/ParityHelpers.OperatorFusions.cs b/src/ReactiveUI.Primitives.Async/Operators/ParityHelpers.OperatorFusions.cs index 1c7fd24..97f300a 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/ParityHelpers.OperatorFusions.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/ParityHelpers.OperatorFusions.cs @@ -47,7 +47,7 @@ protected override async ValueTask SubscribeAsyncCore( await observer.OnNextAsync(initial, cancellationToken).ConfigureAwait(false); var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } @@ -110,7 +110,7 @@ protected override async ValueTask SubscribeAsyncCore( await observer.OnNextAsync(initial, cancellationToken).ConfigureAwait(false); var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } @@ -189,7 +189,7 @@ protected override async ValueTask SubscribeAsyncCore( } var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } @@ -309,7 +309,7 @@ protected override ValueTask DisposeAsyncCore() /// Waits the debounce window, then forwards the value if /// approves it. The single catch routes everything - /// through , which + /// through , which /// already filters out internally — /// so a separate OCE-only catch would just duplicate the same silent-drop behavior. /// The candidate value. @@ -331,7 +331,7 @@ private async Task FireAfterDelayAsync(T value, long id, CancellationToken cance } catch (Exception e) { - UnhandledExceptionHandler.OnUnhandledException(e); + UnhandledExceptionHandler.ReportUnhandledException(e); } } } @@ -362,7 +362,7 @@ protected override async ValueTask SubscribeAsyncCore( } var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } @@ -751,7 +751,7 @@ protected override async ValueTask SubscribeAsyncCore( } var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } @@ -849,7 +849,7 @@ protected override ValueTask DisposeAsyncCore() /// Waits the debounce window, then forwards the value if /// confirms the emission was not superseded. /// The single catch routes everything through - /// , which already + /// , which already /// filters out internally. /// The candidate value. /// The id stamped when this delay was started. @@ -870,7 +870,7 @@ private async Task DelayAndEmitAsync(T value, long id, CancellationToken cancell } catch (Exception e) { - UnhandledExceptionHandler.OnUnhandledException(e); + UnhandledExceptionHandler.ReportUnhandledException(e); } } } @@ -900,7 +900,7 @@ protected override async ValueTask SubscribeAsyncCore( } var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } diff --git a/src/ReactiveUI.Primitives.Async/Operators/ParityHelpers.cs b/src/ReactiveUI.Primitives.Async/Operators/ParityHelpers.cs index 5bcb341..aed9c27 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/ParityHelpers.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/ParityHelpers.cs @@ -207,7 +207,7 @@ public IObservableAsync ObserveOnSafe(AsyncContext? asyncContext, bool forceY { ArgumentExceptionHelper.ThrowIfNull(source); - return asyncContext is null ? source : source.ObserveOn(asyncContext, forceYielding); + return asyncContext is null ? source : source.WitnessOn(asyncContext, forceYielding); } /// @@ -227,7 +227,7 @@ public IObservableAsync ObserveOnSafe(TaskScheduler? taskScheduler, bool forc { ArgumentExceptionHelper.ThrowIfNull(source); - return taskScheduler is null ? source : source.ObserveOn(taskScheduler, forceYielding); + return taskScheduler is null ? source : source.WitnessOn(taskScheduler, forceYielding); } /// @@ -250,7 +250,7 @@ public IObservableAsync ObserveOnIf(bool condition, AsyncContext asyncContext ArgumentExceptionHelper.ThrowIfNull(source); ArgumentExceptionHelper.ThrowIfNull(asyncContext); - return condition ? source.ObserveOn(asyncContext, forceYielding) : source; + return condition ? source.WitnessOn(asyncContext, forceYielding) : source; } /// @@ -273,7 +273,7 @@ public IObservableAsync ObserveOnIf(bool condition, TaskScheduler taskSchedul ArgumentExceptionHelper.ThrowIfNull(source); ArgumentExceptionHelper.ThrowIfNull(taskScheduler); - return condition ? source.ObserveOn(taskScheduler, forceYielding) : source; + return condition ? source.WitnessOn(taskScheduler, forceYielding) : source; } /// diff --git a/src/ReactiveUI.Primitives.Async/Operators/Prepend.cs b/src/ReactiveUI.Primitives.Async/Operators/Prepend.cs index 51356a3..db9f40d 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/Prepend.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/Prepend.cs @@ -124,7 +124,7 @@ private static async ValueTask ReportFailureAsync(IObserverAsync observer, } catch (Exception escalated) { - UnhandledExceptionHandler.OnUnhandledException(escalated); + UnhandledExceptionHandler.ReportUnhandledException(escalated); } } diff --git a/src/ReactiveUI.Primitives.Async/Operators/RefCount.cs b/src/ReactiveUI.Primitives.Async/Operators/RefCount.cs index 32e24d5..cc7b95e 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/RefCount.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/RefCount.cs @@ -42,7 +42,7 @@ internal sealed class RefCountSignal(ConnectableSignalAsync source) : Sign /// /// The asynchronous gate used to serialize subscribe and dispose operations. /// - private readonly AsyncGate _gate = new(); + private readonly AsyncSerialGate _gate = new(); /// /// The current number of active subscribers. @@ -100,14 +100,14 @@ protected override async ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - using (await _gate.LockAsync(cancellationToken).ConfigureAwait(false)) + using (await _gate.EnterAsync(cancellationToken).ConfigureAwait(false)) { // incr refCount before Subscribe(completed source decrement refCxount in Subscribe) ++_refCount; var needConnect = _refCount == 1; - var coObserver = new RefCountObsever(this, observer); + var coObserver = new RefCountWitness(this, observer); var subcription = await source.SubscribeAsync(coObserver, cancellationToken).ConfigureAwait(false); - if (needConnect && !coObserver.IsDisposed) + if (needConnect && !coObserver.HasDisposed) { SingleAssignmentDisposableAsync connection = new(); _connection = connection; @@ -124,7 +124,7 @@ protected override async ValueTask SubscribeAsyncCore( /// /// The parent ref-count observable. /// The downstream observer to forward notifications to. - internal sealed class RefCountObsever(RefCountSignal parent, IObserverAsync observer) + internal sealed class RefCountWitness(RefCountSignal parent, IObserverAsync observer) : ObserverAsync { /// @@ -159,7 +159,7 @@ protected override ValueTask OnErrorResumeAsyncCore(Exception error, Cancellatio [DebuggerStepThrough] protected override async ValueTask DisposeAsyncCore() { - using (await parent._gate.LockAsync().ConfigureAwait(false)) + using (await parent._gate.EnterAsync().ConfigureAwait(false)) { if (--parent._refCount == 0) { diff --git a/src/ReactiveUI.Primitives.Async/Operators/Scan.cs b/src/ReactiveUI.Primitives.Async/Operators/Scan.cs index a0d6e80..277cca6 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/Scan.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/Scan.cs @@ -86,7 +86,7 @@ protected override async ValueTask SubscribeAsyncCore( } var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } @@ -168,7 +168,7 @@ protected override async ValueTask SubscribeAsyncCore( } var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } diff --git a/src/ReactiveUI.Primitives.Async/Operators/Select.cs b/src/ReactiveUI.Primitives.Async/Operators/Select.cs index b048da0..cd8a5ca 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/Select.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/Select.cs @@ -76,7 +76,7 @@ protected override async ValueTask SubscribeAsyncCore( } var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } @@ -134,7 +134,7 @@ protected override async ValueTask SubscribeAsyncCore( } var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } diff --git a/src/ReactiveUI.Primitives.Async/Operators/SingleAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/SingleAsync.cs index 94538af..9705f07 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/SingleAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/SingleAsync.cs @@ -90,7 +90,7 @@ private static async ValueTask SingleCoreAsync( cancellationToken.ThrowIfCancellationRequested(); var observer = new SingleElementObserver(predicate, requireExactlyOne: true, defaultValue: default, cancellationToken); await using var subscription = await source.SubscribeAsync(observer, cancellationToken).ConfigureAwait(false); - var result = await observer.WaitValueAsync().ConfigureAwait(false); + var result = await observer.AwaitResultAsync().ConfigureAwait(false); return result!; } } diff --git a/src/ReactiveUI.Primitives.Async/Operators/SingleOrDefaultAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/SingleOrDefaultAsync.cs index ec8a004..ff31c91 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/SingleOrDefaultAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/SingleOrDefaultAsync.cs @@ -132,6 +132,6 @@ public static partial class SignalAsync cancellationToken.ThrowIfCancellationRequested(); var observer = new SingleElementObserver(predicate, requireExactlyOne: false, defaultValue, cancellationToken); await using var subscription = await source.SubscribeAsync(observer, cancellationToken).ConfigureAwait(false); - return await observer.WaitValueAsync().ConfigureAwait(false); + return await observer.AwaitResultAsync().ConfigureAwait(false); } } diff --git a/src/ReactiveUI.Primitives.Async/Operators/Skip.cs b/src/ReactiveUI.Primitives.Async/Operators/Skip.cs index dd9502a..8ee31f1 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/Skip.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/Skip.cs @@ -61,7 +61,7 @@ protected override async ValueTask SubscribeAsyncCore( } var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } diff --git a/src/ReactiveUI.Primitives.Async/Operators/SkipWhile.cs b/src/ReactiveUI.Primitives.Async/Operators/SkipWhile.cs index a2792cb..3765e91 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/SkipWhile.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/SkipWhile.cs @@ -73,7 +73,7 @@ protected override async ValueTask SubscribeAsyncCore( } var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } @@ -139,7 +139,7 @@ protected override async ValueTask SubscribeAsyncCore( } var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } diff --git a/src/ReactiveUI.Primitives.Async/Operators/SubscribeAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/SubscribeAsync.cs index d73c48e..19ea4fa 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/SubscribeAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/SubscribeAsync.cs @@ -48,7 +48,7 @@ public static ValueTask SubscribeAsync( ArgumentExceptionHelper.ThrowIfNull(source); ArgumentExceptionHelper.ThrowIfNull(onNextAsync); - var observer = new AnonymousObserverAsync(onNextAsync, onErrorResumeAsync, onCompletedAsync); + var observer = new DelegateAsyncWitness(onNextAsync, onErrorResumeAsync, onCompletedAsync); return source.SubscribeAsync(observer, cancellationToken); } @@ -105,7 +105,7 @@ public static ValueTask SubscribeAsync( { ArgumentExceptionHelper.ThrowIfNull(onNext); - var observer = new AnonymousObserverAsync((x, _) => + var observer = new DelegateAsyncWitness((x, _) => { onNext(x); return default; @@ -166,7 +166,7 @@ static ValueTask OnCompletedAsync(Result x, Action onCompleted) return ValueTask.CompletedTask; } - var observer = new AnonymousObserverAsync( + var observer = new DelegateAsyncWitness( (x, _) => { onNext(x); @@ -220,7 +220,7 @@ public static ValueTask SubscribeAsync( ArgumentExceptionHelper.ThrowIfNull(source); ArgumentExceptionHelper.ThrowIfNull(onNextAsync); - var observer = new AnonymousObserverAsync(onNextAsync); + var observer = new DelegateAsyncWitness(onNextAsync); return source.SubscribeAsync(observer, cancellationToken); } } diff --git a/src/ReactiveUI.Primitives.Async/Operators/SwitchSignal.cs b/src/ReactiveUI.Primitives.Async/Operators/SwitchSignal.cs index 8cc0119..9fa8cfd 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/SwitchSignal.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/SwitchSignal.cs @@ -16,7 +16,7 @@ namespace ReactiveUI.Primitives.Async; internal sealed class SwitchSignal(IObservableAsync> source) : SignalAsync { /// - /// Subscribes the specified observer by creating a that manages + /// Subscribes the specified observer by creating a that manages /// the outer and inner observable lifetimes. /// /// The observer to receive elements from the most recent inner sequence. @@ -26,7 +26,7 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var subscription = new SwitchSubscription(observer); + var subscription = new SwitchCoordinator(observer); subscription.LinkExternalCancellation(cancellationToken); return SubscriptionHelper.SubscribeAndDisposeOnFailureAsync( subscription, @@ -37,7 +37,7 @@ protected override ValueTask SubscribeAsyncCore( /// Manages the lifetime of the outer subscription and the currently active inner subscription, /// switching to new inner sequences as they arrive. /// - internal sealed class SwitchSubscription : IAsyncDisposable + internal sealed class SwitchCoordinator : IAsyncDisposable { /// /// The downstream observer to forward elements to. @@ -67,7 +67,7 @@ internal sealed class SwitchSubscription : IAsyncDisposable /// /// Async gate that serializes observer callbacks to ensure thread-safe emission. /// - private readonly AsyncGate _observerOnSomethingGate = new(); + private readonly AsyncSerialGate _observerOnSomethingGate = new(); /// Registration that propagates the original subscribe-token cancellation into . private CancellationTokenRegistration _externalLinkRegistration; @@ -88,10 +88,10 @@ internal sealed class SwitchSubscription : IAsyncDisposable private bool _disposed; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The downstream observer to forward elements to. - public SwitchSubscription(IObserverAsync observer) + public SwitchCoordinator(IObserverAsync observer) { _observer = observer; _disposeCancellationToken = _disposeCts.Token; @@ -107,7 +107,7 @@ public async ValueTask SubscribeAsync( IObservableAsync> source, CancellationToken subscriptionToken) { - var outerSubscription = await source.SubscribeAsync(new SwitchOuterObserver(this), subscriptionToken).ConfigureAwait(false); + var outerSubscription = await source.SubscribeAsync(new SwitchOuterWitness(this), subscriptionToken).ConfigureAwait(false); await _outerDisposable.SetDisposableAsync(outerSubscription).ConfigureAwait(false); } @@ -117,7 +117,7 @@ public async ValueTask SubscribeAsync( /// /// The new inner observable to switch to. /// A task representing the asynchronous switch operation. - public ValueTask OnNextOuterAsync(IObservableAsync inner) + public ValueTask AcceptOuterValueAsync(IObservableAsync inner) { IAsyncDisposable? previousSubscription; lock (_gate) @@ -126,7 +126,7 @@ public ValueTask OnNextOuterAsync(IObservableAsync inner) _currentInnerSubscription = null; } - return SubscribeToInnerAfterDisposingPrevious(inner, previousSubscription); + return SubscribeReplacementInnerAsync(inner, previousSubscription); } /// @@ -135,11 +135,11 @@ public ValueTask OnNextOuterAsync(IObservableAsync inner) /// /// The completion result from the outer sequence. /// A task representing the asynchronous completion operation. - public ValueTask OnCompletedOuterAsync(Result result) + public ValueTask AcceptOuterCompletionAsync(Result result) { if (result.IsFailure) { - return CompleteAsync(result); + return FinishAsync(result); } bool shouldComplete; @@ -149,7 +149,7 @@ public ValueTask OnCompletedOuterAsync(Result result) shouldComplete = _currentInnerSubscription is null; } - return shouldComplete ? CompleteAsync(Result.Success) : default; + return shouldComplete ? FinishAsync(Result.Success) : default; } /// @@ -158,7 +158,7 @@ public ValueTask OnCompletedOuterAsync(Result result) /// /// The completion result from the inner sequence. /// A task representing the asynchronous completion operation. - public ValueTask OnCompletedInnerAsync(Result result) + public ValueTask AcceptInnerCompletionAsync(Result result) { Result? actualResult = null; lock (_gate) @@ -174,7 +174,7 @@ public ValueTask OnCompletedInnerAsync(Result result) } } - return actualResult is not null ? CompleteAsync(actualResult) : default; + return actualResult is not null ? FinishAsync(actualResult) : default; } /// @@ -183,10 +183,10 @@ public ValueTask OnCompletedInnerAsync(Result result) /// The element to forward. /// A token to cancel the operation. /// A task representing the asynchronous forward operation. - public async ValueTask OnNextInnerAsync(T value, CancellationToken cancellationToken) + public async ValueTask AcceptInnerValueAsync(T value, CancellationToken cancellationToken) { _ = cancellationToken; - using (await _observerOnSomethingGate.LockAsync(_disposeCancellationToken).ConfigureAwait(false)) + using (await _observerOnSomethingGate.EnterAsync(_disposeCancellationToken).ConfigureAwait(false)) { await _observer.OnNextAsync(value, _disposeCancellationToken).ConfigureAwait(false); } @@ -198,17 +198,17 @@ public async ValueTask OnNextInnerAsync(T value, CancellationToken cancellationT /// The error to forward. /// A token to cancel the operation. /// A task representing the asynchronous error forwarding operation. - public async ValueTask OnErrorInnerAsync(Exception error, CancellationToken cancellationToken) + public async ValueTask AcceptInnerErrorAsync(Exception error, CancellationToken cancellationToken) { _ = cancellationToken; - using (await _observerOnSomethingGate.LockAsync(_disposeCancellationToken).ConfigureAwait(false)) + using (await _observerOnSomethingGate.EnterAsync(_disposeCancellationToken).ConfigureAwait(false)) { await _observer.OnErrorResumeAsync(error, _disposeCancellationToken).ConfigureAwait(false); } } /// - public ValueTask DisposeAsync() => CompleteAsync(null); + public ValueTask DisposeAsync() => FinishAsync(null); /// /// Links the original subscribe-time cancellation token into this subscription's dispose chain so @@ -240,7 +240,7 @@ internal void LinkExternalCancellation(CancellationToken external) /// The new inner observable to subscribe to. /// The previous inner subscription to dispose, or if none. /// A task representing the asynchronous operation. - internal async ValueTask SubscribeToInnerAfterDisposingPrevious( + internal async ValueTask SubscribeReplacementInnerAsync( IObservableAsync inner, IAsyncDisposable? previousSubscription) { @@ -254,12 +254,12 @@ internal async ValueTask SubscribeToInnerAfterDisposingPrevious( } catch (Exception e) { - await CompleteAsync(Result.Failure(e)).ConfigureAwait(false); + await FinishAsync(Result.Failure(e)).ConfigureAwait(false); return; } } - var innerObserver = new SwitchInnerObserver(this); + var innerObserver = new SwitchInnerWitness(this); var innerSubscription = await inner.SubscribeAsync(innerObserver, _disposeCancellationToken).ConfigureAwait(false); var shouldDispose = false; lock (_gate) @@ -281,7 +281,7 @@ internal async ValueTask SubscribeToInnerAfterDisposingPrevious( } catch (Exception e) { - await CompleteAsync(Result.Failure(e)).ConfigureAwait(false); + await FinishAsync(Result.Failure(e)).ConfigureAwait(false); } } @@ -291,7 +291,7 @@ internal async ValueTask SubscribeToInnerAfterDisposingPrevious( /// /// The completion result to forward, or if disposing without signaling completion. /// A task representing the asynchronous operation. - internal async ValueTask CompleteAsync(Result? result) + internal async ValueTask FinishAsync(Result? result) { IAsyncDisposable? toDispose; lock (_gate) @@ -329,10 +329,10 @@ internal async ValueTask CompleteAsync(Result? result) } /// - /// Observer for the outer observable sequence that delegates to the parent . + /// Observer for the outer observable sequence that delegates to the parent . /// /// The parent switch subscription. - internal sealed class SwitchOuterObserver(SwitchSubscription subscription) : ObserverAsync> + internal sealed class SwitchOuterWitness(SwitchCoordinator subscription) : ObserverAsync> { /// /// Forwards a new inner observable to the parent subscription for switching. @@ -341,7 +341,7 @@ internal sealed class SwitchOuterObserver(SwitchSubscription subscription) : Obs /// A token to cancel the operation. /// A task representing the asynchronous operation. protected override ValueTask OnNextAsyncCore(IObservableAsync value, CancellationToken cancellationToken) - => subscription.OnNextOuterAsync(value); + => subscription.AcceptOuterValueAsync(value); /// /// Forwards a non-fatal error from the outer sequence to the downstream observer. @@ -354,7 +354,7 @@ protected override async ValueTask OnErrorResumeAsyncCore( CancellationToken cancellationToken) { _ = cancellationToken; - using (await subscription._observerOnSomethingGate.LockAsync(subscription._disposeCancellationToken).ConfigureAwait(false)) + using (await subscription._observerOnSomethingGate.EnterAsync(subscription._disposeCancellationToken).ConfigureAwait(false)) { await subscription._observer.OnErrorResumeAsync(error, subscription._disposeCancellationToken).ConfigureAwait(false); } @@ -366,14 +366,14 @@ protected override async ValueTask OnErrorResumeAsyncCore( /// The completion result. /// A task representing the asynchronous operation. protected override ValueTask OnCompletedAsyncCore(Result result) - => subscription.OnCompletedOuterAsync(result); + => subscription.AcceptOuterCompletionAsync(result); } /// - /// Observer for the currently active inner observable sequence that delegates to the parent . + /// Observer for the currently active inner observable sequence that delegates to the parent . /// /// The parent switch subscription. - internal sealed class SwitchInnerObserver(SwitchSubscription subscription) : ObserverAsync + internal sealed class SwitchInnerWitness(SwitchCoordinator subscription) : ObserverAsync { /// /// Forwards an element from the inner sequence to the downstream observer. @@ -382,7 +382,7 @@ internal sealed class SwitchInnerObserver(SwitchSubscription subscription) : Obs /// A token to cancel the operation. /// A task representing the asynchronous operation. protected override ValueTask OnNextAsyncCore(T value, CancellationToken cancellationToken) - => subscription.OnNextInnerAsync(value, cancellationToken); + => subscription.AcceptInnerValueAsync(value, cancellationToken); /// /// Forwards a non-fatal error from the inner sequence to the downstream observer. @@ -391,7 +391,7 @@ protected override ValueTask OnNextAsyncCore(T value, CancellationToken cancella /// A token to cancel the operation. /// A task representing the asynchronous operation. protected override ValueTask OnErrorResumeAsyncCore(Exception error, CancellationToken cancellationToken) - => subscription.OnErrorInnerAsync(error, cancellationToken); + => subscription.AcceptInnerErrorAsync(error, cancellationToken); /// /// Handles the inner sequence completing. @@ -399,7 +399,7 @@ protected override ValueTask OnErrorResumeAsyncCore(Exception error, Cancellatio /// The completion result. /// A task representing the asynchronous operation. protected override ValueTask OnCompletedAsyncCore(Result result) - => subscription.OnCompletedInnerAsync(result); + => subscription.AcceptInnerCompletionAsync(result); } } } diff --git a/src/ReactiveUI.Primitives.Async/Operators/Take.cs b/src/ReactiveUI.Primitives.Async/Operators/Take.cs index afa871f..ab0b58b 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/Take.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/Take.cs @@ -84,7 +84,7 @@ protected override async ValueTask SubscribeAsyncCore( } var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } @@ -111,7 +111,7 @@ protected override ValueTask OnNextAsyncCore(T value, CancellationToken cancella _remaining--; if (_remaining == 0) { - return ForwardThenCompleteAsync(value, cancellationToken); + return ForwardThenFinishAsync(value, cancellationToken); } return downstream.OnNextAsync(value, cancellationToken); @@ -129,7 +129,7 @@ protected override ValueTask OnCompletedAsyncCore(Result result) => /// The final value. /// The cancellation token. /// A task that completes after both the value and the completion are forwarded. - private async ValueTask ForwardThenCompleteAsync(T value, CancellationToken cancellationToken) + private async ValueTask ForwardThenFinishAsync(T value, CancellationToken cancellationToken) { await downstream.OnNextAsync(value, cancellationToken).ConfigureAwait(false); await downstream.OnCompletedAsync(Result.Success).ConfigureAwait(false); diff --git a/src/ReactiveUI.Primitives.Async/Operators/TakeUntil.cs b/src/ReactiveUI.Primitives.Async/Operators/TakeUntil.cs index 9dc2fbd..254fbcb 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/TakeUntil.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/TakeUntil.cs @@ -127,7 +127,7 @@ public IObservableAsync TakeUntil( { ArgumentExceptionHelper.ThrowIfNull(source); - var inner = new TakeUntilTask(source, task, options ?? TakeUntilOptions.Default); + var inner = new TaskStopSignal(source, task, options ?? TakeUntilOptions.Default); return cancellationToken.CanBeCanceled ? inner.TakeUntil(cancellationToken) : inner; } @@ -141,7 +141,7 @@ public IObservableAsync TakeUntil( /// An observable sequence that completes when the provided cancellation token is canceled or when the source /// sequence completes. public IObservableAsync TakeUntil(CancellationToken cancellationToken) => - new TakeUntilCancellationToken(source, cancellationToken); + new CancellationStopSignal(source, cancellationToken); /// /// Returns a sequence that emits elements from the source until the specified predicate returns true for an @@ -169,7 +169,7 @@ public IObservableAsync TakeUntil(Func predicate, CancellationToken { ArgumentExceptionHelper.ThrowIfNull(predicate); - var inner = new TakeUntilPredicate(source, predicate); + var inner = new PredicateStopSignal(source, predicate); return cancellationToken.CanBeCanceled ? inner.TakeUntil(cancellationToken) : inner; } @@ -199,7 +199,7 @@ public IObservableAsync TakeUntil( { ArgumentExceptionHelper.ThrowIfNull(asyncPredicate); - var inner = new TakeUntilAsyncPredicate(source, asyncPredicate); + var inner = new AsyncPredicateStopSignal(source, asyncPredicate); return cancellationToken.CanBeCanceled ? inner.TakeUntil(cancellationToken) : inner; } @@ -256,7 +256,7 @@ public IObservableAsync TakeUntil( { ArgumentExceptionHelper.ThrowIfNull(stopSignal); - var inner = new TakeUntilFromRawSignal(source, stopSignal, options ?? TakeUntilOptions.Default); + var inner = new DelegateStopSignal(source, stopSignal, options ?? TakeUntilOptions.Default); return cancellationToken.CanBeCanceled ? inner.TakeUntil(cancellationToken) : inner; } } @@ -265,7 +265,7 @@ public IObservableAsync TakeUntil( /// Async observable that emits items from the source until the specified predicate returns true. /// /// The type of the elements in the source sequence. - internal sealed class TakeUntilPredicate(IObservableAsync source, Func predicate) + internal sealed class PredicateStopSignal(IObservableAsync source, Func predicate) : SignalAsync { /// The predicate that signals when to stop emitting items. @@ -279,7 +279,7 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var subscription = new TakeUntilPredicateSubscription(this, observer); + var subscription = new PredicateStopCoordinator(this, observer); return SubscriptionHelper.SubscribeAndDisposeOnFailureAsync( subscription, () => subscription.SubscribeSourcesAsync(cancellationToken)); @@ -288,7 +288,7 @@ protected override ValueTask SubscribeAsyncCore( /// /// Observer that forwards items from the source until the predicate returns true. /// - internal sealed class TakeUntilPredicateSubscription(TakeUntilPredicate parent, IObserverAsync observer) + internal sealed class PredicateStopCoordinator(PredicateStopSignal parent, IObserverAsync observer) : ObserverAsync { /// The inner subscription handle. @@ -337,7 +337,7 @@ protected override async ValueTask DisposeAsyncCore() /// Async observable that emits items from the source until the specified cancellation token is canceled. /// /// The type of the elements in the source sequence. - internal sealed class TakeUntilCancellationToken(IObservableAsync source, CancellationToken cancellationToken) + internal sealed class CancellationStopSignal(IObservableAsync source, CancellationToken cancellationToken) : SignalAsync { /// The source observable sequence. @@ -351,7 +351,7 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var subscription = new Subscription(this, observer); + var subscription = new CancellationStopCoordinator(this, observer); subscription.LinkExternalCancellation(cancellationToken); return SubscriptionHelper.SubscribeAndDisposeOnFailureAsync( subscription, @@ -363,10 +363,10 @@ protected override ValueTask SubscribeAsyncCore( /// Composes for the shared gate / dispose-CTS / external- /// link / gated-forwarding plumbing so this class only carries the operator-specific state. /// - internal sealed class Subscription : IAsyncDisposable + internal sealed class CancellationStopCoordinator : IAsyncDisposable { /// The parent observable that owns this subscription. - private readonly TakeUntilCancellationToken _parent; + private readonly CancellationStopSignal _parent; /// Shared subscription lifecycle (gate / dispose CTS / external link / forwarders). private readonly TakeUntilLifecycle _lifecycle; @@ -378,11 +378,11 @@ internal sealed class Subscription : IAsyncDisposable private CancellationTokenRegistration? _tokenRegistration; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The parent observable that owns this subscription. /// The downstream observer to forward items to. - public Subscription(TakeUntilCancellationToken parent, IObserverAsync observer) + public CancellationStopCoordinator(CancellationStopSignal parent, IObserverAsync observer) { _parent = parent; _lifecycle = new(observer); @@ -395,7 +395,7 @@ public Subscription(TakeUntilCancellationToken parent, IObserverAsync obse /// A task representing the asynchronous subscribe operation. public async ValueTask SubscribeSourcesAsync(CancellationToken cancellationToken) { - _tokenRegistration = _parent._cancellationToken.Register(OnTokenCanceled); + _tokenRegistration = _parent._cancellationToken.Register(CompleteFromCancellation); _subscription = await _parent._source.SubscribeAsync(new TakeUntilSourceObserver(_lifecycle), cancellationToken).ConfigureAwait(false); } @@ -430,10 +430,10 @@ internal void LinkExternalCancellation(CancellationToken external) => /// /// Callback invoked when the external cancellation token is canceled; forwards completion to the observer. /// - internal void OnTokenCanceled() => FireAndForgetHelper.Run(async () => + internal void CompleteFromCancellation() => FireAndForgetHelper.Run(async () => { await Task.Yield(); - await _lifecycle.ForwardOnCompletedAsync(Result.Success).ConfigureAwait(false); + await _lifecycle.RelayCompletionAsync(Result.Success).ConfigureAwait(false); }); } } @@ -442,7 +442,7 @@ internal void OnTokenCanceled() => FireAndForgetHelper.Run(async () => /// Async observable that emits items from the source until a raw completion signal fires. /// /// The type of the elements in the source sequence. - internal sealed class TakeUntilFromRawSignal( + internal sealed class DelegateStopSignal( IObservableAsync source, CompletionSignalDelegate stopSignal, TakeUntilOptions options) : SignalAsync @@ -461,7 +461,7 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var subscription = new Subscription(this, observer); + var subscription = new DelegateStopCoordinator(this, observer); subscription.LinkExternalCancellation(cancellationToken); return SubscriptionHelper.SubscribeAndDisposeOnFailureAsync( subscription, @@ -471,10 +471,10 @@ protected override ValueTask SubscribeAsyncCore( /// /// Manages the subscription lifetime and completes when the raw stop signal fires. /// - internal sealed class Subscription : IAsyncDisposable + internal sealed class DelegateStopCoordinator : IAsyncDisposable { /// The parent observable that owns this subscription. - private readonly TakeUntilFromRawSignal _parent; + private readonly DelegateStopSignal _parent; /// Shared subscription lifecycle (gate / dispose CTS / external link / forwarders). private readonly TakeUntilLifecycle _lifecycle; @@ -483,11 +483,11 @@ internal sealed class Subscription : IAsyncDisposable private IAsyncDisposable? _subscription; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The parent observable that owns this subscription. /// The downstream observer to forward items to. - public Subscription(TakeUntilFromRawSignal parent, IObserverAsync observer) + public DelegateStopCoordinator(DelegateStopSignal parent, IObserverAsync observer) { _parent = parent; _lifecycle = new(observer); @@ -500,7 +500,7 @@ public Subscription(TakeUntilFromRawSignal parent, IObserverAsync observer /// A task representing the asynchronous subscribe operation. public async ValueTask SubscribeSourcesAsync(CancellationToken cancellationToken) { - WaitAndComplete(); + AwaitStopThenComplete(); _subscription = await _parent._source.SubscribeAsync(new TakeUntilSourceObserver(_lifecycle), cancellationToken).ConfigureAwait(false); } @@ -526,7 +526,7 @@ internal void LinkExternalCancellation(CancellationToken external) => /// /// Waits for the stop signal to fire, then forwards completion or error to the downstream observer. /// - internal void WaitAndComplete() => FireAndForgetHelper.Run(async () => + internal void AwaitStopThenComplete() => FireAndForgetHelper.Run(async () => { var tcs = new TaskCompletionSource(); @@ -556,7 +556,7 @@ void Stop(Result result) // Ignored } - await _lifecycle.ForwardOnCompletedAsync(Result.Success).ConfigureAwait(false); + await _lifecycle.RelayCompletionAsync(Result.Success).ConfigureAwait(false); } catch (Exception e) { @@ -571,11 +571,11 @@ void Stop(Result result) if (_parent._options.SourceFailsWhenOtherFails) { - await _lifecycle.ForwardOnCompletedAsync(Result.Failure(e)).ConfigureAwait(false); + await _lifecycle.RelayCompletionAsync(Result.Failure(e)).ConfigureAwait(false); } else { - await _lifecycle.ForwardOnErrorResumeAsync(e).ConfigureAwait(false); + await _lifecycle.RelayErrorAsync(e).ConfigureAwait(false); } } }); @@ -586,7 +586,7 @@ void Stop(Result result) /// Async observable that emits items from the source until the specified task completes. /// /// The type of the elements in the source sequence. - internal sealed class TakeUntilTask(IObservableAsync source, Task task, TakeUntilOptions options) + internal sealed class TaskStopSignal(IObservableAsync source, Task task, TakeUntilOptions options) : SignalAsync { /// The source observable sequence. @@ -603,7 +603,7 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var subscription = new Subscription(this, observer); + var subscription = new TaskStopCoordinator(this, observer); subscription.LinkExternalCancellation(cancellationToken); return SubscriptionHelper.SubscribeAndDisposeOnFailureAsync( subscription, @@ -614,10 +614,10 @@ protected override ValueTask SubscribeAsyncCore( /// Manages the subscription lifetime and completes when the task finishes. Composes /// for the shared plumbing. /// - internal sealed class Subscription : IAsyncDisposable + internal sealed class TaskStopCoordinator : IAsyncDisposable { /// The parent observable that owns this subscription. - private readonly TakeUntilTask _parent; + private readonly TaskStopSignal _parent; /// Shared subscription lifecycle (gate / dispose CTS / external link / forwarders). private readonly TakeUntilLifecycle _lifecycle; @@ -626,11 +626,11 @@ internal sealed class Subscription : IAsyncDisposable private IAsyncDisposable? _subscription; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The parent observable that owns this subscription. /// The downstream observer to forward items to. - public Subscription(TakeUntilTask parent, IObserverAsync observer) + public TaskStopCoordinator(TaskStopSignal parent, IObserverAsync observer) { _parent = parent; _lifecycle = new(observer); @@ -644,7 +644,7 @@ public Subscription(TakeUntilTask parent, IObserverAsync observer) public async ValueTask SubscribeSourcesAsync(CancellationToken cancellationToken) { var task = _parent._task; - WaitAndComplete(task); + AwaitStopThenComplete(task); _subscription = await _parent._source.SubscribeAsync(new TakeUntilSourceObserver(_lifecycle), cancellationToken).ConfigureAwait(false); } @@ -671,22 +671,22 @@ internal void LinkExternalCancellation(CancellationToken external) => /// Waits for the task to complete, then forwards completion or error to the downstream observer. /// /// The task to await. - internal void WaitAndComplete(Task task) => FireAndForgetHelper.Run(async () => + internal void AwaitStopThenComplete(Task task) => FireAndForgetHelper.Run(async () => { try { await task.WaitAsync(System.Threading.Timeout.InfiniteTimeSpan, _lifecycle.DisposeToken).ConfigureAwait(false); - await _lifecycle.ForwardOnCompletedAsync(Result.Success).ConfigureAwait(false); + await _lifecycle.RelayCompletionAsync(Result.Success).ConfigureAwait(false); } catch (Exception e) { if (_parent._options.SourceFailsWhenOtherFails) { - await _lifecycle.ForwardOnCompletedAsync(Result.Failure(e)).ConfigureAwait(false); + await _lifecycle.RelayCompletionAsync(Result.Failure(e)).ConfigureAwait(false); } else { - await _lifecycle.ForwardOnErrorResumeAsync(e).ConfigureAwait(false); + await _lifecycle.RelayErrorAsync(e).ConfigureAwait(false); } } }); @@ -717,7 +717,7 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var subscription = new Subscription(this, observer); + var subscription = new AsyncStopCoordinator(this, observer); subscription.LinkExternalCancellation(cancellationToken); return SubscriptionHelper.SubscribeAndDisposeOnFailureAsync( subscription, @@ -728,7 +728,7 @@ protected override ValueTask SubscribeAsyncCore( /// Manages subscriptions to both the source and signal observables, completing when the signal /// fires. Composes for the shared plumbing. /// - internal sealed class Subscription : IAsyncDisposable + internal sealed class AsyncStopCoordinator : IAsyncDisposable { /// The parent observable that owns this subscription. private readonly TakeUntilAsyncSignal _parent; @@ -743,11 +743,11 @@ internal sealed class Subscription : IAsyncDisposable private readonly SingleAssignmentDisposableAsync _otherDisposable = new(); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The parent observable that owns this subscription. /// The downstream observer to forward items to. - public Subscription(TakeUntilAsyncSignal parent, IObserverAsync observer) + public AsyncStopCoordinator(TakeUntilAsyncSignal parent, IObserverAsync observer) { _parent = parent; _lifecycle = new(observer); @@ -760,7 +760,7 @@ public Subscription(TakeUntilAsyncSignal parent, IObserverAsync ob /// This subscription as an async disposable. public async ValueTask SubscribeSourcesAsync(CancellationToken cancellationToken) { - var otherSubscription = await _parent._other.SubscribeAsync(new OtherObserver(this), cancellationToken).ConfigureAwait(false); + var otherSubscription = await _parent._other.SubscribeAsync(new StopSignalWitness(this), cancellationToken).ConfigureAwait(false); await _otherDisposable.SetDisposableAsync(otherSubscription).ConfigureAwait(false); var sourceSubscription = @@ -789,14 +789,14 @@ internal void LinkExternalCancellation(CancellationToken external) => /// /// Observer for the signal observable that triggers completion of the source subscription. /// - internal sealed class OtherObserver(Subscription parent) : ObserverAsync + internal sealed class StopSignalWitness(AsyncStopCoordinator parent) : ObserverAsync { /// protected override async ValueTask OnNextAsyncCore(TOther value, CancellationToken cancellationToken) { _ = value; _ = cancellationToken; - await parent._lifecycle.ForwardOnCompletedAsync(Result.Success).ConfigureAwait(false); + await parent._lifecycle.RelayCompletionAsync(Result.Success).ConfigureAwait(false); await DisposeAsync().ConfigureAwait(false); } @@ -804,7 +804,7 @@ protected override async ValueTask OnNextAsyncCore(TOther value, CancellationTok protected override ValueTask OnErrorResumeAsyncCore(Exception error, CancellationToken cancellationToken) { _ = cancellationToken; - return parent._lifecycle.ForwardOnErrorResumeAsync(error); + return parent._lifecycle.RelayErrorAsync(error); } /// @@ -817,10 +817,10 @@ protected override ValueTask OnCompletedAsyncCore(Result result) if (parent._parent._options.SourceFailsWhenOtherFails) { - return parent._lifecycle.ForwardOnCompletedAsync(result); + return parent._lifecycle.RelayCompletionAsync(result); } - return parent._lifecycle.ForwardOnCompletedAsync(Result.Success); + return parent._lifecycle.RelayCompletionAsync(Result.Success); } } } @@ -830,7 +830,7 @@ protected override ValueTask OnCompletedAsyncCore(Result result) /// Async observable that emits items from the source until the specified asynchronous predicate returns true. /// /// The type of the elements in the source sequence. - internal sealed class TakeUntilAsyncPredicate( + internal sealed class AsyncPredicateStopSignal( IObservableAsync source, Func> asyncPredicate) : SignalAsync { @@ -845,7 +845,7 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var subscription = new TakeUntilAsyncPredicateSubscription(this, observer); + var subscription = new AsyncPredicateStopCoordinator(this, observer); return SubscriptionHelper.SubscribeAndDisposeOnFailureAsync( subscription, () => subscription.SubscribeSourcesAsync(cancellationToken)); @@ -854,8 +854,8 @@ protected override ValueTask SubscribeAsyncCore( /// /// Observer that forwards items from the source until the async predicate returns true. /// - internal sealed class TakeUntilAsyncPredicateSubscription( - TakeUntilAsyncPredicate parent, + internal sealed class AsyncPredicateStopCoordinator( + AsyncPredicateStopSignal parent, IObserverAsync observer) : ObserverAsync { /// The inner subscription handle. diff --git a/src/ReactiveUI.Primitives.Async/Operators/TakeWhile.cs b/src/ReactiveUI.Primitives.Async/Operators/TakeWhile.cs index 6790e22..71b6467 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/TakeWhile.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/TakeWhile.cs @@ -72,7 +72,7 @@ protected override async ValueTask SubscribeAsyncCore( } var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } @@ -139,7 +139,7 @@ protected override async ValueTask SubscribeAsyncCore( } var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } diff --git a/src/ReactiveUI.Primitives.Async/Operators/Throttle.cs b/src/ReactiveUI.Primitives.Async/Operators/Throttle.cs index 882c016..d7cd842 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/Throttle.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/Throttle.cs @@ -155,7 +155,7 @@ internal async Task FireAfterDelayAsync(T value, long id, CancellationToken canc { // UnhandledExceptionHandler filters OperationCanceledException internally so // a separate OCE-only catch would just duplicate the silent-drop behavior. - UnhandledExceptionHandler.OnUnhandledException(e); + UnhandledExceptionHandler.ReportUnhandledException(e); } } diff --git a/src/ReactiveUI.Primitives.Async/Operators/Timeout.cs b/src/ReactiveUI.Primitives.Async/Operators/Timeout.cs index 8ffbf84..561bf73 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/Timeout.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/Timeout.cs @@ -179,7 +179,7 @@ internal void StartTimer(CancellationToken cancellationToken) // exception handler rather than tearing down the subscription. Without a timer // the operator degrades to a pass-through; downstream callers continue to // receive emissions without a timeout signal. - UnhandledExceptionHandler.OnUnhandledException(e); + UnhandledExceptionHandler.ReportUnhandledException(e); } } @@ -279,7 +279,7 @@ private async Task FireTimeoutAsync() } catch (Exception e) { - UnhandledExceptionHandler.OnUnhandledException(e); + UnhandledExceptionHandler.ReportUnhandledException(e); } } diff --git a/src/ReactiveUI.Primitives.Async/Operators/ToAsyncEnumerable.cs b/src/ReactiveUI.Primitives.Async/Operators/ToAsyncEnumerable.cs index b7e55e1..ae594d0 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/ToAsyncEnumerable.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/ToAsyncEnumerable.cs @@ -62,9 +62,9 @@ public static IAsyncEnumerable ToAsyncEnumerable( ArgumentExceptionHelper.ThrowIfNull(@this); ArgumentExceptionHelper.ThrowIfNull(channelFactory); - return Impl(@this, channelFactory, onErrorResume); + return ReadObservableValuesAsync(@this, channelFactory, onErrorResume); - static async IAsyncEnumerable Impl( + static async IAsyncEnumerable ReadObservableValuesAsync( IObservableAsync @this, Func> channelFactory, Func? onErrorResume, diff --git a/src/ReactiveUI.Primitives.Async/Operators/ToDictionaryAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/ToDictionaryAsync.cs index 2629bf6..35a2263 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/ToDictionaryAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/ToDictionaryAsync.cs @@ -40,9 +40,9 @@ public static async ValueTask> ToDictionaryAsync( ArgumentExceptionHelper.ThrowIfNull(keySelector); cancellationToken.ThrowIfCancellationRequested(); - var observer = new ToDictionaryAsyncObserver(keySelector, x => x, comparer, cancellationToken); + var observer = new ToDictionaryTaskWitness(keySelector, x => x, comparer, cancellationToken); await using var subscription = await @this.SubscribeAsync(observer, cancellationToken).ConfigureAwait(false); - return await observer.WaitValueAsync().ConfigureAwait(false); + return await observer.AwaitResultAsync().ConfigureAwait(false); } /// @@ -91,13 +91,13 @@ public static async ValueTask> ToDictionaryAsync( + new ToDictionaryTaskWitness( keySelector, elementSelector, comparer, cancellationToken); await using var subscription = await @this.SubscribeAsync(observer, cancellationToken).ConfigureAwait(false); - return await observer.WaitValueAsync().ConfigureAwait(false); + return await observer.AwaitResultAsync().ConfigureAwait(false); } /// @@ -130,12 +130,12 @@ public static ValueTask> ToDictionaryAsyncA function to extract a value from each element. /// An optional equality comparer for keys. /// A cancellation token for the operation. - internal sealed class ToDictionaryAsyncObserver( + internal sealed class ToDictionaryTaskWitness( Func keySelector, Func elementSelector, IEqualityComparer? comparer, CancellationToken cancellationToken) - : TaskObserverAsyncBase>(cancellationToken) + : TaskWitnessAsyncBase>(cancellationToken) where TKey : notnull { /// @@ -153,10 +153,10 @@ protected override ValueTask OnNextAsyncCore(TSource value, CancellationToken ca /// protected override ValueTask OnErrorResumeAsyncCore(Exception error, CancellationToken cancellationToken) - => TrySetException(error); + => SetExceptionAndDisposeAsync(error); /// protected override ValueTask OnCompletedAsyncCore(Result result) - => !result.IsSuccess ? TrySetException(result.Exception) : TrySetCompleted(_map); + => !result.IsSuccess ? SetExceptionAndDisposeAsync(result.Exception) : SetResultAndDisposeAsync(_map); } } diff --git a/src/ReactiveUI.Primitives.Async/Operators/ToListAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/ToListAsync.cs index d290cf7..9c1d5b8 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/ToListAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/ToListAsync.cs @@ -35,9 +35,9 @@ public static ValueTask> ToListAsync(this IObservableAsync @this) public static async ValueTask> ToListAsync(this IObservableAsync @this, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - var observer = new ToListAsyncObserver(cancellationToken); + var observer = new ToListTaskWitness(cancellationToken); await using var subscription = await @this.SubscribeAsync(observer, cancellationToken).ConfigureAwait(false); - return await observer.WaitValueAsync().ConfigureAwait(false); + return await observer.AwaitResultAsync().ConfigureAwait(false); } /// @@ -45,8 +45,8 @@ public static async ValueTask> ToListAsync(this IObservableAsync @ /// /// The type of elements in the source sequence. /// A cancellation token for the operation. - internal sealed class ToListAsyncObserver(CancellationToken cancellationToken) - : TaskObserverAsyncBase>(cancellationToken) + internal sealed class ToListTaskWitness(CancellationToken cancellationToken) + : TaskWitnessAsyncBase>(cancellationToken) { /// /// The list that accumulates all elements received from the source sequence. @@ -62,10 +62,10 @@ protected override ValueTask OnNextAsyncCore(T value, CancellationToken cancella /// protected override ValueTask OnErrorResumeAsyncCore(Exception error, CancellationToken cancellationToken) => - TrySetException(error); + SetExceptionAndDisposeAsync(error); /// protected override ValueTask OnCompletedAsyncCore(Result result) => - !result.IsSuccess ? TrySetException(result.Exception) : TrySetCompleted(_items); + !result.IsSuccess ? SetExceptionAndDisposeAsync(result.Exception) : SetResultAndDisposeAsync(_items); } } diff --git a/src/ReactiveUI.Primitives.Async/Operators/WaitCompletionAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/WaitCompletionAsync.cs index 2e5b0dc..c7e4337 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/WaitCompletionAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/WaitCompletionAsync.cs @@ -39,9 +39,9 @@ public static async ValueTask WaitCompletionAsync( ArgumentExceptionHelper.ThrowIfNull(@this); cancellationToken.ThrowIfCancellationRequested(); - var observer = new WaitCompletionAsyncObserver(cancellationToken); + var observer = new CompletionTaskWitness(cancellationToken); await using var subscription = await @this.SubscribeAsync(observer, cancellationToken).ConfigureAwait(false); - await observer.WaitValueAsync().ConfigureAwait(false); + await observer.AwaitResultAsync().ConfigureAwait(false); } /// @@ -49,18 +49,18 @@ public static async ValueTask WaitCompletionAsync( /// /// The type of elements in the source sequence. /// A cancellation token for the operation. - internal sealed class WaitCompletionAsyncObserver(CancellationToken cancellationToken) - : TaskObserverAsyncBase(cancellationToken) + internal sealed class CompletionTaskWitness(CancellationToken cancellationToken) + : TaskWitnessAsyncBase(cancellationToken) { /// protected override ValueTask OnNextAsyncCore(T value, CancellationToken cancellationToken) => default; /// protected override ValueTask OnErrorResumeAsyncCore(Exception error, CancellationToken cancellationToken) => - TrySetException(error); + SetExceptionAndDisposeAsync(error); /// protected override ValueTask OnCompletedAsyncCore(Result result) => - !result.IsSuccess ? TrySetException(result.Exception) : TrySetCompleted(null); + !result.IsSuccess ? SetExceptionAndDisposeAsync(result.Exception) : SetResultAndDisposeAsync(null); } } diff --git a/src/ReactiveUI.Primitives.Async/Operators/Where.cs b/src/ReactiveUI.Primitives.Async/Operators/Where.cs index 3c16f2e..86c889e 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/Where.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/Where.cs @@ -74,7 +74,7 @@ protected override async ValueTask SubscribeAsyncCore( } var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } @@ -133,7 +133,7 @@ protected override async ValueTask SubscribeAsyncCore( } var subscription = await source.SubscribeAsync(sink, cancellationToken).ConfigureAwait(false); - await sink.SetSourceSubscriptionAsync(subscription).ConfigureAwait(false); + await sink.AssignSourceSubscriptionAsync(subscription).ConfigureAwait(false); return sink; } diff --git a/src/ReactiveUI.Primitives.Async/Operators/ObserveOn.cs b/src/ReactiveUI.Primitives.Async/Operators/WitnessOn.cs similarity index 87% rename from src/ReactiveUI.Primitives.Async/Operators/ObserveOn.cs rename to src/ReactiveUI.Primitives.Async/Operators/WitnessOn.cs index 0a76be3..234e550 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/ObserveOn.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/WitnessOn.cs @@ -23,8 +23,8 @@ public static partial class SignalAsync /// When true, forces an asynchronous yield before invoking each callback, even if already on the target /// context. /// An observable sequence whose observer callbacks execute on the specified context. - public static IObservableAsync ObserveOn(this IObservableAsync @this, AsyncContext asyncContext, bool forceYielding) => - new ObserveOnAsyncSignal(@this, asyncContext, forceYielding); + public static IObservableAsync WitnessOn(this IObservableAsync @this, AsyncContext asyncContext, bool forceYielding) => + new WitnessOnAsyncSignal(@this, asyncContext, forceYielding); /// /// Wraps the source observable so that observer callbacks are invoked on the specified async context. @@ -33,8 +33,8 @@ public static IObservableAsync ObserveOn(this IObservableAsync @this, A /// The source observable sequence. /// The async context on which observer callbacks should be invoked. /// An observable sequence whose observer callbacks execute on the specified context. - public static IObservableAsync ObserveOn(this IObservableAsync @this, AsyncContext asyncContext) => - @this.ObserveOn(asyncContext, false); + public static IObservableAsync WitnessOn(this IObservableAsync @this, AsyncContext asyncContext) => + @this.WitnessOn(asyncContext, false); /// /// Wraps the source observable so that observer callbacks are invoked on the specified synchronization context. @@ -44,10 +44,10 @@ public static IObservableAsync ObserveOn(this IObservableAsync @this, A /// The synchronization context on which observer callbacks should be posted. /// When true, forces an asynchronous yield before invoking each callback. /// An observable sequence whose observer callbacks execute on the specified synchronization context. - public static IObservableAsync ObserveOn(this IObservableAsync @this, SynchronizationContext synchronizationContext, bool forceYielding) + public static IObservableAsync WitnessOn(this IObservableAsync @this, SynchronizationContext synchronizationContext, bool forceYielding) { var asyncContext = AsyncContext.From(synchronizationContext); - return new ObserveOnAsyncSignal(@this, asyncContext, forceYielding); + return new WitnessOnAsyncSignal(@this, asyncContext, forceYielding); } /// @@ -57,8 +57,8 @@ public static IObservableAsync ObserveOn(this IObservableAsync @this, S /// The source observable sequence. /// The synchronization context on which observer callbacks should be posted. /// An observable sequence whose observer callbacks execute on the specified synchronization context. - public static IObservableAsync ObserveOn(this IObservableAsync @this, SynchronizationContext synchronizationContext) => - @this.ObserveOn(synchronizationContext, false); + public static IObservableAsync WitnessOn(this IObservableAsync @this, SynchronizationContext synchronizationContext) => + @this.WitnessOn(synchronizationContext, false); /// /// Wraps the source observable so that observer callbacks are invoked using the specified task scheduler. @@ -68,10 +68,10 @@ public static IObservableAsync ObserveOn(this IObservableAsync @this, S /// The task scheduler on which observer callbacks should be scheduled. /// When true, forces an asynchronous yield before invoking each callback. /// An observable sequence whose observer callbacks execute on the specified task scheduler. - public static IObservableAsync ObserveOn(this IObservableAsync @this, TaskScheduler taskScheduler, bool forceYielding) + public static IObservableAsync WitnessOn(this IObservableAsync @this, TaskScheduler taskScheduler, bool forceYielding) { var asyncContext = AsyncContext.From(taskScheduler); - return new ObserveOnAsyncSignal(@this, asyncContext, forceYielding); + return new WitnessOnAsyncSignal(@this, asyncContext, forceYielding); } /// @@ -81,8 +81,8 @@ public static IObservableAsync ObserveOn(this IObservableAsync @this, T /// The source observable sequence. /// The task scheduler on which observer callbacks should be scheduled. /// An observable sequence whose observer callbacks execute on the specified task scheduler. - public static IObservableAsync ObserveOn(this IObservableAsync @this, TaskScheduler taskScheduler) => - @this.ObserveOn(taskScheduler, false); + public static IObservableAsync WitnessOn(this IObservableAsync @this, TaskScheduler taskScheduler) => + @this.WitnessOn(taskScheduler, false); /// /// Configures the observable sequence to notify observers on the specified scheduler. @@ -95,10 +95,10 @@ public static IObservableAsync ObserveOn(this IObservableAsync @this, T /// The scheduler on which to observe and deliver notifications to observers. Cannot be null. /// true to force yielding to the scheduler even if already on the target context; otherwise, false. /// An observable sequence whose notifications are delivered on the specified scheduler. - public static IObservableAsync ObserveOn(this IObservableAsync @this, ISequencer scheduler, bool forceYielding) + public static IObservableAsync WitnessOn(this IObservableAsync @this, ISequencer scheduler, bool forceYielding) { var asyncContext = AsyncContext.From(scheduler); - return new ObserveOnAsyncSignal(@this, asyncContext, forceYielding); + return new WitnessOnAsyncSignal(@this, asyncContext, forceYielding); } /// @@ -108,6 +108,6 @@ public static IObservableAsync ObserveOn(this IObservableAsync @this, I /// The source observable sequence. /// The scheduler on which to observe and deliver notifications to observers. Cannot be null. /// An observable sequence whose notifications are delivered on the specified scheduler. - public static IObservableAsync ObserveOn(this IObservableAsync @this, ISequencer scheduler) => - @this.ObserveOn(scheduler, false); + public static IObservableAsync WitnessOn(this IObservableAsync @this, ISequencer scheduler) => + @this.WitnessOn(scheduler, false); } diff --git a/src/ReactiveUI.Primitives.Async/Operators/ObserveOnAsyncSignal.cs b/src/ReactiveUI.Primitives.Async/Operators/WitnessOnAsyncSignal.cs similarity index 83% rename from src/ReactiveUI.Primitives.Async/Operators/ObserveOnAsyncSignal.cs rename to src/ReactiveUI.Primitives.Async/Operators/WitnessOnAsyncSignal.cs index fbd76fb..4291bee 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/ObserveOnAsyncSignal.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/WitnessOnAsyncSignal.cs @@ -11,7 +11,7 @@ namespace ReactiveUI.Primitives.Async; /// The source observable whose notifications will be context-switched. /// The async context to switch notifications onto. /// Whether to force yielding even if already on the target context. -internal sealed class ObserveOnAsyncSignal( +internal sealed class WitnessOnAsyncSignal( IObservableAsync source, AsyncContext asyncContext, bool forceYielding) : SignalAsync @@ -21,8 +21,8 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var observeOnObserver = new ObserveOnObserver(observer, asyncContext, forceYielding); - return source.SubscribeAsync(observeOnObserver, cancellationToken); + var contextSwitchObserver = new ContextSwitchObserver(observer, asyncContext, forceYielding); + return source.SubscribeAsync(contextSwitchObserver, cancellationToken); } /// @@ -31,7 +31,7 @@ protected override ValueTask SubscribeAsyncCore( /// The downstream observer to forward notifications to. /// The async context to switch onto. /// Whether to force yielding even if already on the target context. - internal sealed class ObserveOnObserver(IObserverAsync observer, AsyncContext asyncContext, bool forceYielding) + internal sealed class ContextSwitchObserver(IObserverAsync observer, AsyncContext asyncContext, bool forceYielding) : ObserverAsync { /// Slow path: switch to the target context then forward the value. @@ -40,7 +40,7 @@ internal sealed class ObserveOnObserver(IObserverAsync observer, AsyncContext /// The value to forward. /// The cancellation token. /// A task that completes after the context switch and downstream forward. - internal async ValueTask SwitchThenForwardAsync(T value, CancellationToken cancellationToken) + internal async ValueTask ForwardAfterContextSwitchAsync(T value, CancellationToken cancellationToken) { await asyncContext.SwitchContextAsync(forceYielding, cancellationToken); await observer.OnNextAsync(value, cancellationToken).ConfigureAwait(false); @@ -51,7 +51,7 @@ internal async ValueTask SwitchThenForwardAsync(T value, CancellationToken cance /// The error to forward. /// The cancellation token. /// A task that completes after the context switch and downstream forward. - internal async ValueTask SwitchThenErrorAsync(Exception error, CancellationToken cancellationToken) + internal async ValueTask ForwardErrorAfterContextSwitchAsync(Exception error, CancellationToken cancellationToken) { await asyncContext.SwitchContextAsync(forceYielding, cancellationToken); await observer.OnErrorResumeAsync(error, cancellationToken).ConfigureAwait(false); @@ -61,7 +61,7 @@ internal async ValueTask SwitchThenErrorAsync(Exception error, CancellationToken /// Exposed as for direct unit testing. /// The completion result. /// A task that completes after the context switch and downstream forward. - internal async ValueTask SwitchThenCompletedAsync(Result result) + internal async ValueTask ForwardCompletionAfterContextSwitchAsync(Result result) { await asyncContext.SwitchContextAsync(forceYielding, CancellationToken.None); await observer.OnCompletedAsync(result).ConfigureAwait(false); @@ -77,7 +77,7 @@ protected override ValueTask OnNextAsyncCore(T value, CancellationToken cancella return observer.OnNextAsync(value, cancellationToken); } - return SwitchThenForwardAsync(value, cancellationToken); + return ForwardAfterContextSwitchAsync(value, cancellationToken); } /// @@ -88,7 +88,7 @@ protected override ValueTask OnErrorResumeAsyncCore(Exception error, Cancellatio return observer.OnErrorResumeAsync(error, cancellationToken); } - return SwitchThenErrorAsync(error, cancellationToken); + return ForwardErrorAfterContextSwitchAsync(error, cancellationToken); } /// @@ -99,7 +99,7 @@ protected override ValueTask OnCompletedAsyncCore(Result result) return observer.OnCompletedAsync(result); } - return SwitchThenCompletedAsync(result); + return ForwardCompletionAfterContextSwitchAsync(result); } } } diff --git a/src/ReactiveUI.Primitives.Async/Operators/Wrap.cs b/src/ReactiveUI.Primitives.Async/Operators/Wrap.cs index 72aff30..5c792aa 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/Wrap.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/Wrap.cs @@ -21,5 +21,5 @@ public static partial class SignalAsync /// Thrown if is null. public static IObserverAsync Wrap(this IObserverAsync observer) => observer is null ? throw new ArgumentNullException(nameof(observer)) - : new WrappedObserverAsync(observer); + : new ForwardingAsyncWitness(observer); } diff --git a/src/ReactiveUI.Primitives.Async/Operators/Yield.cs b/src/ReactiveUI.Primitives.Async/Operators/Yield.cs index ba7737f..df6a286 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/Yield.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/Yield.cs @@ -45,7 +45,7 @@ protected override ValueTask SubscribeAsyncCore( { var currentContext = AsyncContext.GetCurrent(); return source.SubscribeAsync( - new ObserveOnAsyncSignal.ObserveOnObserver(observer, currentContext, true), + new WitnessOnAsyncSignal.ContextSwitchObserver(observer, currentContext, true), cancellationToken); } } diff --git a/src/ReactiveUI.Primitives.Async/Signals/Base/BaseReplayLatestSignalAsync.cs b/src/ReactiveUI.Primitives.Async/Signals/Base/BaseReplayLatestSignalAsync.cs index 67c678d..648036f 100644 --- a/src/ReactiveUI.Primitives.Async/Signals/Base/BaseReplayLatestSignalAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Signals/Base/BaseReplayLatestSignalAsync.cs @@ -24,7 +24,7 @@ public abstract class BaseReplayLatestSignalAsync(Optional startValue) : S /// /// The asynchronous gate used to synchronize access to the Signal's mutable state. /// - private readonly AsyncGate _gate = new(); + private readonly AsyncSerialGate _gate = new(); /// /// The cancellation token source that is cancelled when this instance is disposed. @@ -87,7 +87,7 @@ public async ValueTask OnNextAsync(T value, CancellationToken cancellationToken) try { ImmutableArray> observers; - using (await _gate.LockAsync(token).ConfigureAwait(false)) + using (await _gate.EnterAsync(token).ConfigureAwait(false)) { if (_result is not null) { @@ -138,7 +138,7 @@ public async ValueTask OnErrorResumeAsync(Exception error, CancellationToken can try { ImmutableArray> observers; - using (await _gate.LockAsync(token).ConfigureAwait(false)) + using (await _gate.EnterAsync(token).ConfigureAwait(false)) { if (_result is not null) { @@ -167,7 +167,7 @@ public async ValueTask OnErrorResumeAsync(Exception error, CancellationToken can public async ValueTask OnCompletedAsync(Result result) { ImmutableArray> observers; - using (await _gate.LockAsync(DisposedCancellationToken).ConfigureAwait(false)) + using (await _gate.EnterAsync(DisposedCancellationToken).ConfigureAwait(false)) { if (_result is not null) { @@ -260,7 +260,7 @@ protected override async ValueTask SubscribeAsyncCore( token.ThrowIfCancellationRequested(); Result? result; - using (await _gate.LockAsync(token).ConfigureAwait(false)) + using (await _gate.EnterAsync(token).ConfigureAwait(false)) { result = _result; if (result is null) @@ -283,7 +283,7 @@ protected override async ValueTask SubscribeAsyncCore( (signal: this, observer, token), static async state => { - using (await state.signal._gate.LockAsync(state.token).ConfigureAwait(false)) + using (await state.signal._gate.EnterAsync(state.token).ConfigureAwait(false)) { state.signal._observers = state.signal._observers.Remove(state.observer); } diff --git a/src/ReactiveUI.Primitives.Async/Signals/Base/BaseStatelessReplayLatestSignalAsync.cs b/src/ReactiveUI.Primitives.Async/Signals/Base/BaseStatelessReplayLatestSignalAsync.cs index 5d9df2a..95c1cf6 100644 --- a/src/ReactiveUI.Primitives.Async/Signals/Base/BaseStatelessReplayLatestSignalAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Signals/Base/BaseStatelessReplayLatestSignalAsync.cs @@ -33,7 +33,7 @@ public abstract class BaseStatelessReplayLatestSignalAsync(Optional startV /// /// The asynchronous gate used to synchronize access to the Signal's mutable state. /// - private readonly AsyncGate _gate = new(); + private readonly AsyncSerialGate _gate = new(); /// /// The cancellation token source that is cancelled when this instance is disposed. @@ -91,7 +91,7 @@ public async ValueTask OnNextAsync(T value, CancellationToken cancellationToken) try { ImmutableArray> observers; - using (await _gate.LockAsync(token).ConfigureAwait(false)) + using (await _gate.EnterAsync(token).ConfigureAwait(false)) { _value = new(value); observers = _observers; @@ -124,7 +124,7 @@ public async ValueTask OnErrorResumeAsync(Exception error, CancellationToken can var token = linkedCts.Token; ImmutableArray> observers; - using (await _gate.LockAsync(token).ConfigureAwait(false)) + using (await _gate.EnterAsync(token).ConfigureAwait(false)) { observers = _observers; } @@ -142,7 +142,7 @@ public async ValueTask OnErrorResumeAsync(Exception error, CancellationToken can public async ValueTask OnCompletedAsync(Result result) { ImmutableArray> observers; - using (await _gate.LockAsync(DisposedCancellationToken).ConfigureAwait(false)) + using (await _gate.EnterAsync(DisposedCancellationToken).ConfigureAwait(false)) { observers = _observers; _observers = []; @@ -196,7 +196,7 @@ protected override async ValueTask SubscribeAsyncCore( (signal: this, observer, token), static async state => { - using (await state.signal._gate.LockAsync(state.token).ConfigureAwait(false)) + using (await state.signal._gate.EnterAsync(state.token).ConfigureAwait(false)) { state.signal._observers = state.signal._observers.Remove(state.observer); if (state.signal._observers.IsEmpty) @@ -206,7 +206,7 @@ protected override async ValueTask SubscribeAsyncCore( } }); - using (await _gate.LockAsync(token).ConfigureAwait(false)) + using (await _gate.EnterAsync(token).ConfigureAwait(false)) { _observers = _observers.Add(observer); if (_value.TryGetValue(out var value)) diff --git a/src/ReactiveUI.Primitives.Async/UnhandledExceptionHandler.cs b/src/ReactiveUI.Primitives.Async/UnhandledExceptionHandler.cs index 7b7fa99..1c44b91 100644 --- a/src/ReactiveUI.Primitives.Async/UnhandledExceptionHandler.cs +++ b/src/ReactiveUI.Primitives.Async/UnhandledExceptionHandler.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. // ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. @@ -17,7 +17,7 @@ public static class UnhandledExceptionHandler /// /// The currently registered handler action invoked when an unhandled exception occurs. /// - private static Action _unhandledException = DefaultUnhandledExceptionHandler; + private static Action _unhandledException = TraceUnhandledException; /// /// Gets the currently registered handler. Used for save/restore in tests. @@ -40,7 +40,7 @@ public static void Register(Action unhandledExceptionHandler) => /// OperationCanceledException instances are ignored and not passed to the /// handler. /// The exception to be processed by the unhandled exception handler. Cannot be null. - internal static void OnUnhandledException(Exception e) + internal static void ReportUnhandledException(Exception e) { if (e is OperationCanceledException) { @@ -64,6 +64,6 @@ internal static void OnUnhandledException(Exception e) /// an application. It writes the exception details to the standard console output for diagnostic /// purposes. /// The exception that was not handled. Cannot be null. - internal static void DefaultUnhandledExceptionHandler(Exception exception) => + internal static void TraceUnhandledException(Exception exception) => System.Diagnostics.Trace.TraceError("UnhandleException: {0}", exception); } diff --git a/src/ReactiveUI.Primitives/SignalOperatorParityMixins.cs b/src/ReactiveUI.Primitives/SignalOperatorParityMixins.cs index 2b36cea..1f49ac8 100644 --- a/src/ReactiveUI.Primitives/SignalOperatorParityMixins.cs +++ b/src/ReactiveUI.Primitives/SignalOperatorParityMixins.cs @@ -472,6 +472,40 @@ public static IObservable FlatMap(this IObservable(source, selector); } + /// + /// Projects each value into an enumerable and emits every projected item. + /// + /// The source value type. + /// The projected item type. + /// The source signal. + /// The projection that returns items for each source value. + /// A signal that emits every item returned by . + /// or is . + public static IObservable FlatMap(this IObservable source, Func> selector) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (selector == null) + { + throw new ArgumentNullException(nameof(selector)); + } + + return Signal.Create(observer => + source.Subscribe( + value => + { + foreach (var item in selector(value)) + { + observer.OnNext(item); + } + }, + observer.OnError, + observer.OnCompleted)); + } + /// /// Projects each source value to an inner signal and maps outer/inner values with a result selector. /// diff --git a/src/ReactiveUI.Primitives/Signals/Signal{Collect}.cs b/src/ReactiveUI.Primitives/Signals/Signal{Collect}.cs new file mode 100644 index 0000000..0a4cce9 --- /dev/null +++ b/src/ReactiveUI.Primitives/Signals/Signal{Collect}.cs @@ -0,0 +1,225 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using ReactiveUI.Primitives.Concurrency; +using ReactiveUI.Primitives.Disposables; + +namespace ReactiveUI.Primitives.Signals; + +/// +/// Create Signals functionality. +/// +public static partial class Signal +{ + /// + /// Collects values into time-windowed batches using the default sequencer. + /// + /// The source value type. + /// The source signal. + /// The duration of each buffer window. + /// A signal that emits batches of source values. + /// is . + public static IObservable> Collect( + this IObservable source, + TimeSpan timeSpan) => + source.Collect(timeSpan, Sequencer.Default); + + /// + /// Collects values into time-windowed batches. + /// + /// The source value type. + /// The source signal. + /// The duration of each buffer window. + /// The sequencer used to schedule buffer flushes. + /// A signal that emits batches of source values. + /// or is . + public static IObservable> Collect( + this IObservable source, + TimeSpan timeSpan, + ISequencer sequencer) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (sequencer == null) + { + throw new ArgumentNullException(nameof(sequencer)); + } + + if (timeSpan <= TimeSpan.Zero) + { + return source.Map(static value => (IList)[value]); + } + + return Create>(observer => + new CollectCoordinator(observer, timeSpan, sequencer).Subscribe(source)); + } + + /// + /// Coordinates time-windowed buffering for a single subscription. + /// + /// The source value type. + private sealed class CollectCoordinator : IDisposable + { + /// The downstream observer. + private readonly IObserver> _observer; + + /// The duration of the buffer window. + private readonly TimeSpan _timeSpan; + + /// The sequencer used to schedule flushes. + private readonly ISequencer _sequencer; + + /// Serializes access to buffered values and terminal state. + private readonly Lock _gate = new(); + + /// Tracks the source subscription and scheduled flushes. + private readonly MultipleDisposable _disposables = new(); + + /// The values collected for the current window. + private readonly List _values = []; + + /// Whether a flush has already been scheduled for the current window. + private bool _flushScheduled; + + /// Whether the source has terminated. + private bool _stopped; + + /// + /// Initializes a new instance of the class. + /// + /// The downstream observer. + /// The buffer window duration. + /// The sequencer used to schedule flushes. + public CollectCoordinator(IObserver> observer, TimeSpan timeSpan, ISequencer sequencer) + { + _observer = observer; + _timeSpan = timeSpan; + _sequencer = sequencer; + } + + /// + public void Dispose() => _disposables.Dispose(); + + /// + /// Subscribes to the source and returns the coordinator as the subscription. + /// + /// The source signal. + /// The subscription that tears down source and scheduled flush work. + internal CollectCoordinator Subscribe(IObservable source) + { + _disposables.Add(source.Subscribe(OnNext, OnError, OnCompleted)); + return this; + } + + /// Records a value and schedules a flush for the current window when needed. + /// The source value. + private void OnNext(TSource value) + { + if (!TryRecord(value)) + { + return; + } + + _disposables.Add(_sequencer.Schedule(_timeSpan, Flush)); + } + + /// Forwards a terminal error after marking the coordinator stopped. + /// The source error. + private void OnError(Exception error) + { + MarkStopped(); + _observer.OnError(error); + } + + /// Flushes remaining values and forwards completion. + private void OnCompleted() + { + var batch = CompleteAndTakeFinalBatch(); + if (batch is { Length: > 0 }) + { + _observer.OnNext(batch); + } + + _observer.OnCompleted(); + } + + /// Flushes the current window if it still has buffered values. + private void Flush() + { + var batch = TakeScheduledBatch(); + if (batch is not { Length: > 0 }) + { + return; + } + + _observer.OnNext(batch); + } + + /// Stores a value and reports whether this value opened a new scheduled window. + /// The source value. + /// when a flush should be scheduled. + private bool TryRecord(TSource value) + { + lock (_gate) + { + if (_stopped) + { + return false; + } + + _values.Add(value); + if (_flushScheduled) + { + return false; + } + + _flushScheduled = true; + return true; + } + } + + /// Marks the coordinator as stopped. + private void MarkStopped() + { + lock (_gate) + { + _stopped = true; + } + } + + /// Returns and clears values from a scheduled flush. + /// The values to emit, or when there is no batch. + private TSource[]? TakeScheduledBatch() + { + lock (_gate) + { + _flushScheduled = false; + return _values.Count == 0 || _stopped ? null : TakeValues(); + } + } + + /// Stops the coordinator and returns the final buffered values. + /// The final buffered values, or when no values remain. + private TSource[]? CompleteAndTakeFinalBatch() + { + lock (_gate) + { + _stopped = true; + return _values.Count == 0 ? null : TakeValues(); + } + } + + /// Copies and clears the buffered values. + /// The copied buffered values. + private TSource[] TakeValues() + { + var batch = _values.ToArray(); + _values.Clear(); + return batch; + } + } +} diff --git a/src/ReactiveUI.Primitives/Signals/Signal{Create}.cs b/src/ReactiveUI.Primitives/Signals/Signal{Create}.cs index 0685ece..43e9eee 100644 --- a/src/ReactiveUI.Primitives/Signals/Signal{Create}.cs +++ b/src/ReactiveUI.Primitives/Signals/Signal{Create}.cs @@ -2,7 +2,9 @@ // ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Runtime.ExceptionServices; using ReactiveUI.Primitives.Concurrency; +using ReactiveUI.Primitives.Disposables; using ReactiveUI.Primitives.Signals.Core; namespace ReactiveUI.Primitives.Signals; @@ -158,4 +160,72 @@ public static IObservable Lazy(Func> observableFactory) /// An Observable. public static IObservable WitnessOn(this IObservable source, ISequencer scheduler) => new WitnessOnSignal(source, scheduler); + + /// + /// Creates a signal whose source is produced separately for each subscription. + /// + /// The value type. + /// The factory that creates the source signal for a subscription. + /// A signal that subscribes to the factory-produced source for each observer. + /// is . + public static IObservable Defer(Func> observableFactory) + { + if (observableFactory == null) + { + throw new ArgumentNullException(nameof(observableFactory)); + } + + return Create(observer => + { + IObservable source; + try + { + source = observableFactory(); + } + catch (Exception ex) + { + observer.OnError(ex); + return Disposable.Empty; + } + + return source.Subscribe(observer); + }); + } + + /// + /// Blocks until the signal completes and returns the observed values. + /// + /// The source value type. + /// The source signal. + /// The values observed before completion. + /// is . + /// Rethrows the source error if the signal terminates with an error. + public static IEnumerable ToEnumerable(this IObservable source) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + var values = new List(); + Exception? error = null; + using var completed = new ManualResetEventSlim(); + using var subscription = source.Subscribe( + values.Add, + ex => + { + error = ex; + completed.Set(); + }, + completed.Set); + + completed.Wait(); + + if (error is not null) + { + ExceptionDispatchInfo.Capture(error).Throw(); + } + + return values; + } } diff --git a/src/ReactiveUI.Primitives/Signals/Signal{EmitIfQuiet}.cs b/src/ReactiveUI.Primitives/Signals/Signal{EmitIfQuiet}.cs new file mode 100644 index 0000000..f9dfac2 --- /dev/null +++ b/src/ReactiveUI.Primitives/Signals/Signal{EmitIfQuiet}.cs @@ -0,0 +1,234 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using ReactiveUI.Primitives.Concurrency; +using ReactiveUI.Primitives.Disposables; + +namespace ReactiveUI.Primitives.Signals; + +/// +/// Create Signals functionality. +/// +public static partial class Signal +{ + /// + /// Emits only the latest value after a quiet period using the default sequencer. + /// + /// The source value type. + /// The source signal. + /// The quiet period before the latest value is emitted. + /// A throttled signal. + /// is . + public static IObservable EmitIfQuiet( + this IObservable source, + TimeSpan dueTime) => + source.EmitIfQuiet(dueTime, Sequencer.Default); + + /// + /// Emits only the latest value after a quiet period. + /// + /// The source value type. + /// The source signal. + /// The quiet period before the latest value is emitted. + /// The sequencer used to schedule delayed emissions. + /// A throttled signal. + /// or is . + public static IObservable EmitIfQuiet( + this IObservable source, + TimeSpan dueTime, + ISequencer sequencer) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (sequencer == null) + { + throw new ArgumentNullException(nameof(sequencer)); + } + + if (dueTime <= TimeSpan.Zero) + { + return source; + } + + return Create(observer => + new EmitIfQuietCoordinator(observer, dueTime, sequencer).Subscribe(source)); + } + + /// + /// Coordinates throttled emission for a single subscription. + /// + /// The source value type. + private sealed class EmitIfQuietCoordinator : IDisposable + { + /// The downstream observer. + private readonly IObserver _observer; + + /// The quiet period before the latest value is emitted. + private readonly TimeSpan _dueTime; + + /// The sequencer used to schedule delayed emissions. + private readonly ISequencer _sequencer; + + /// Serializes access to latest value and terminal state. + private readonly Lock _gate = new(); + + /// Tracks the source subscription and scheduled delayed emissions. + private readonly MultipleDisposable _disposables = new(); + + /// The latest observed value. + private TSource? _latest; + + /// Monotonic version used to suppress obsolete scheduled emissions. + private long _version; + + /// Whether a latest value is pending emission. + private bool _hasValue; + + /// Whether the source has terminated. + private bool _stopped; + + /// + /// Initializes a new instance of the class. + /// + /// The downstream observer. + /// The quiet period before emitting the latest value. + /// The sequencer used to schedule delayed emissions. + public EmitIfQuietCoordinator(IObserver observer, TimeSpan dueTime, ISequencer sequencer) + { + _observer = observer; + _dueTime = dueTime; + _sequencer = sequencer; + } + + /// + public void Dispose() => _disposables.Dispose(); + + /// + /// Subscribes to the source and returns the coordinator as the subscription. + /// + /// The source signal. + /// The subscription that tears down source and scheduled throttle work. + internal EmitIfQuietCoordinator Subscribe(IObservable source) + { + _disposables.Add(source.Subscribe(OnNext, OnError, OnCompleted)); + return this; + } + + /// Records a latest value and schedules its delayed emission. + /// The source value. + private void OnNext(TSource value) + { + if (!TryRecord(value, out var currentVersion)) + { + return; + } + + _disposables.Add(_sequencer.Schedule(_dueTime, () => EmitIfLatest(currentVersion))); + } + + /// Forwards a terminal error after marking the coordinator stopped. + /// The source error. + private void OnError(Exception error) + { + MarkStopped(); + _observer.OnError(error); + } + + /// Emits a pending latest value and forwards completion. + private void OnCompleted() + { + if (CompleteAndTakeLatest(out var value)) + { + _observer.OnNext(value!); + } + + _observer.OnCompleted(); + } + + /// Emits the latest value if the scheduled version is still current. + /// The version captured when the emission was scheduled. + private void EmitIfLatest(long scheduledVersion) + { + if (!TryTakeLatest(scheduledVersion, out var value)) + { + return; + } + + _observer.OnNext(value!); + } + + /// Records a latest value and returns its version. + /// The source value. + /// The version assigned to the value. + /// when delayed emission should be scheduled. + private bool TryRecord(TSource value, out long currentVersion) + { + lock (_gate) + { + if (_stopped) + { + currentVersion = default; + return false; + } + + _latest = value; + _hasValue = true; + currentVersion = ++_version; + return true; + } + } + + /// Marks the coordinator as stopped. + private void MarkStopped() + { + lock (_gate) + { + _stopped = true; + } + } + + /// Returns and clears the pending value when the scheduled version is current. + /// The version captured when the emission was scheduled. + /// The value to emit. + /// when a value should be emitted. + private bool TryTakeLatest(long scheduledVersion, out TSource? value) + { + lock (_gate) + { + if (_stopped || !_hasValue || scheduledVersion != _version) + { + value = default; + return false; + } + + value = _latest; + _hasValue = false; + return true; + } + } + + /// Stops the coordinator and returns any pending latest value. + /// The pending value to emit. + /// when a value should be emitted. + private bool CompleteAndTakeLatest(out TSource? value) + { + lock (_gate) + { + _stopped = true; + if (!_hasValue) + { + value = default; + return false; + } + + value = _latest; + _hasValue = false; + return true; + } + } + } +} diff --git a/src/ReactiveUI.Primitives/Signals/Signal{Return}.cs b/src/ReactiveUI.Primitives/Signals/Signal{Emit}.cs similarity index 100% rename from src/ReactiveUI.Primitives/Signals/Signal{Return}.cs rename to src/ReactiveUI.Primitives/Signals/Signal{Emit}.cs diff --git a/src/ReactiveUI.Primitives/Signals/Signal{Factories}.cs b/src/ReactiveUI.Primitives/Signals/Signal{Factories}.cs index 828fefa..6939865 100644 --- a/src/ReactiveUI.Primitives/Signals/Signal{Factories}.cs +++ b/src/ReactiveUI.Primitives/Signals/Signal{Factories}.cs @@ -2,6 +2,7 @@ // ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System.ComponentModel; using ReactiveUI.Primitives.Concurrency; using ReactiveUI.Primitives.Core; using ReactiveUI.Primitives.Disposables; @@ -233,6 +234,61 @@ void Handler(object? sender, TEventArgs eventArgs) => }); } + /// + /// Creates a signal from an event add/remove pair. + /// + /// The delegate type used by the event. + /// The event argument type. + /// The action that attaches the generated event handler. + /// The action that detaches the generated event handler. + /// A signal that emits event patterns for each raised event. + /// or is . + /// is not a supported event delegate type. + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Major Code Smell", + "S4018:Generic methods should provide type parameters", + Justification = "The event argument type is part of the returned EventPattern and must be specified for non-generic event handlers.")] + public static IObservable> FromEventPattern( + Action addHandler, + Action removeHandler) + where TEventHandler : Delegate + where TEventArgs : EventArgs + { + if (addHandler == null) + { + throw new ArgumentNullException(nameof(addHandler)); + } + + if (removeHandler == null) + { + throw new ArgumentNullException(nameof(removeHandler)); + } + + return Create>(observer => + { + TEventHandler handler; + if (typeof(TEventHandler) == typeof(PropertyChangedEventHandler)) + { + PropertyChangedEventHandler typed = (sender, args) => + observer.OnNext(new EventPattern(sender, (TEventArgs)(EventArgs)args)); + handler = (TEventHandler)(object)typed; + } + else if (typeof(TEventHandler) == typeof(EventHandler)) + { + EventHandler typed = (sender, args) => + observer.OnNext(new EventPattern(sender, args)); + handler = (TEventHandler)(object)typed; + } + else + { + throw new NotSupportedException($"Event handler type '{typeof(TEventHandler)}' is not supported."); + } + + addHandler(handler); + return Disposable.Create(() => removeHandler(handler)); + }); + } + /// /// Creates a signal from an enumerable sequence. /// diff --git a/src/ReactiveUI.Primitives/Signals/Signal{Throw}.cs b/src/ReactiveUI.Primitives/Signals/Signal{Fail}.cs similarity index 100% rename from src/ReactiveUI.Primitives/Signals/Signal{Throw}.cs rename to src/ReactiveUI.Primitives/Signals/Signal{Fail}.cs diff --git a/src/ReactiveUI.Primitives/Signals/Signal{Empty}.cs b/src/ReactiveUI.Primitives/Signals/Signal{None}.cs similarity index 100% rename from src/ReactiveUI.Primitives/Signals/Signal{Empty}.cs rename to src/ReactiveUI.Primitives/Signals/Signal{None}.cs diff --git a/src/ReactiveUI.Primitives/Signals/Signal{Catch}.cs b/src/ReactiveUI.Primitives/Signals/Signal{Recover}.cs similarity index 100% rename from src/ReactiveUI.Primitives/Signals/Signal{Catch}.cs rename to src/ReactiveUI.Primitives/Signals/Signal{Recover}.cs diff --git a/src/ReactiveUI.Primitives/Signals/Signal{Never}.cs b/src/ReactiveUI.Primitives/Signals/Signal{Silent}.cs similarity index 100% rename from src/ReactiveUI.Primitives/Signals/Signal{Never}.cs rename to src/ReactiveUI.Primitives/Signals/Signal{Silent}.cs diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/AsyncPrimitiveContractTests.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/AsyncPrimitiveContractTests.cs index 7c46a3b..b2d4525 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/AsyncPrimitiveContractTests.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/AsyncPrimitiveContractTests.cs @@ -219,7 +219,7 @@ public async Task ObserveOnSequencerSchedulesDirectWorkItems() var sequencer = new QueuedSequencer(); var task = AsyncObs.Emit(EmittedValue) - .ObserveOn(sequencer, forceYielding: true) + .WitnessOn(sequencer, forceYielding: true) .ToListAsync() .AsTask(); diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/AsyncGateTests.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/AsyncSerialGateTests.cs similarity index 84% rename from src/tests/ReactiveUI.Primitives.Async.Tests/AsyncGateTests.cs rename to src/tests/ReactiveUI.Primitives.Async.Tests/AsyncSerialGateTests.cs index 97de3c8..19381fe 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/AsyncGateTests.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/AsyncSerialGateTests.cs @@ -6,25 +6,25 @@ namespace ReactiveUI.Primitives.Async.Tests; -/// Coverage for — uncontended fast path, same-thread reentry, +/// Coverage for — uncontended fast path, same-thread reentry, /// contended slow path, double-dispose idempotency. [SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "TUnit requires instance methods")] -public class AsyncGateTests +public class AsyncSerialGateTests { /// Verifies that the uncontended fast path acquires the gate via pure CAS. /// A representing the asynchronous test operation. [Test] public async Task WhenUncontendedLock_ThenAcquiresAndReleases() { - using var gate = new AsyncGate(); + using var gate = new AsyncSerialGate(); - using (await gate.LockAsync()) + using (await gate.EnterAsync()) { await Assert.That(gate).IsNotNull(); } // After release the gate must be re-acquirable. - using (await gate.LockAsync()) + using (await gate.EnterAsync()) { await Assert.That(gate).IsNotNull(); } @@ -35,17 +35,17 @@ public async Task WhenUncontendedLock_ThenAcquiresAndReleases() [Test] public async Task WhenSameThreadReentry_ThenAllowedWithoutBlocking() { - using var gate = new AsyncGate(); + using var gate = new AsyncSerialGate(); - using (await gate.LockAsync()) - using (await gate.LockAsync()) - using (await gate.LockAsync()) + using (await gate.EnterAsync()) + using (await gate.EnterAsync()) + using (await gate.EnterAsync()) { await Assert.That(gate).IsNotNull(); } // Gate must release cleanly after nested acquisitions. - using (await gate.LockAsync()) + using (await gate.EnterAsync()) { await Assert.That(gate).IsNotNull(); } @@ -61,21 +61,21 @@ public async Task WhenSameThreadReentry_ThenAllowedWithoutBlocking() [Test] public async Task WhenContendedWaiter_ThenResumesAfterRelease() { - using var gate = new AsyncGate(); - var first = await gate.LockAsync(); + using var gate = new AsyncSerialGate(); + var first = await gate.EnterAsync(); var secondAcquired = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); // Wait until the contender is either parked on the slow path (WaitersCount > 0) or // has already acquired the gate via the same-thread reentry fast path (secondAcquired - // set). Either outcome is a valid configuration of AsyncGate — what we care about for + // set). Either outcome is a valid configuration of AsyncSerialGate — what we care about for // this test is that the contender ultimately gets the gate after we release it; the // dual condition keeps the assertion stable across runners where Task.Run may reuse // the test thread. var contender = Task.Run(async () => { - using var releaser = await gate.LockAsync().ConfigureAwait(false); + using var lease = await gate.EnterAsync().ConfigureAwait(false); secondAcquired.TrySetResult(true); await release.Task.ConfigureAwait(false); }); @@ -101,7 +101,7 @@ public async Task WhenContendedWaiter_ThenResumesAfterRelease() [Test] public async Task WhenDisposeCalledTwice_ThenIdempotent() { - var gate = new AsyncGate(); + var gate = new AsyncSerialGate(); gate.Dispose(); gate.Dispose(); diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/CombineLatestEnumerableInternalsTests.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/CombineLatestEnumerableInternalsTests.cs index a92bcf8..d88ab01 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/CombineLatestEnumerableInternalsTests.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/CombineLatestEnumerableInternalsTests.cs @@ -19,7 +19,7 @@ public async Task WhenIndexedObserverDisposed_ThenNoOp() { var sources = new[] { SignalAsync.Return(1) }; var downstream = new NoOpObserver(); - var subscription = new SignalAsync.CombineLatestEnumerableSignal.Subscription( + var subscription = new SignalAsync.CombineLatestEnumerableSignal.EnumerableCombineLatestCoordinator( sources, downstream, static s => s[0]); diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/CombineLatestOperatorTests.EnumerableRest.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/CombineLatestOperatorTests.EnumerableRest.cs index 5f787c9..d0da4d3 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/CombineLatestOperatorTests.EnumerableRest.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/CombineLatestOperatorTests.EnumerableRest.cs @@ -191,7 +191,7 @@ public async Task WhenCombineLatestEnumerableOnNextAfterDispose_ThenReturnsEarly await src1.EmitNext(1); await src2.EmitNext(Source1Value); - // Trigger failure on src1 → CompleteAsync → _disposed=1 → blocks on OnCompletedAsync + // Trigger failure on src1 → FinishAsync → _disposed=1 → blocks on OnCompletedAsync var failTask = Task.Run(() => src1.Complete(Result.Failure(new InvalidOperationException("test")))); await completionBlocked.Task; diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.Concat.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.Concat.cs index 7ed779c..aa43273 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.Concat.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.Concat.cs @@ -234,7 +234,7 @@ public async Task WhenConcatObservablesSubscriptionThrows_ThenDisposesAndRethrow } /// - /// Verifies that when the inner subscription throws during SubscribeToInnerLoop in + /// Verifies that when the inner subscription throws during SubscribeCurrentInnerAsync in /// ConcatSignalSourcesSignal, the error is propagated via completion. /// /// A representing the asynchronous test operation. @@ -299,13 +299,13 @@ public async Task WhenConcatObservablesDoubleCompletionWithError_ThenUnhandledEx return default; }); - // Complete with failure first, then dispose (which calls CompleteAsync(null)) + // Complete with failure first, then dispose (which calls FinishAsync(null)) await outer.OnCompletedAsync(Result.Failure(new InvalidOperationException(FirstFailMessage))); await Assert.That(completionResult).IsNotNull(); await Assert.That(completionResult!.Value.IsFailure).IsTrue(); - // Now dispose – this will call CompleteAsync(null) which will hit the already-disposed path + // Now dispose – this will call FinishAsync(null) which will hit the already-disposed path await sub.DisposeAsync(); await outer.DisposeAsync(); @@ -445,7 +445,7 @@ public async Task WhenConcatObservablesMultipleBufferedInners_ThenSubscribesSequ /// /// A representing the asynchronous test operation. [Test] - public async Task WhenConcatEnumerableSubscriptionThrows_ThenDisposesAndRethrows() + public async Task WhenConcatSequenceCoordinatorThrows_ThenDisposesAndRethrows() { var throwing = SignalAsync.Create((_, _) => { @@ -541,12 +541,12 @@ public async Task WhenConcatEnumerableDoubleCompletionWithError_ThenUnhandledExc await Assert.That(completionResult).IsNotNull(); await Assert.That(completionResult!.Value.IsFailure).IsTrue(); - // Second dispose attempts CompleteAsync(null) on an already-disposed subscription + // Second dispose attempts FinishAsync(null) on an already-disposed subscription await sub.DisposeAsync(); } /// - /// Verifies that ConcatEnumerableSignal handles the catch path in SubscribeNextAsync + /// Verifies that ConcatEnumerableSignal handles the catch path in SubscribeNextSignalAsync /// when the enumerator MoveNext throws. /// /// A representing the asynchronous test operation. @@ -669,7 +669,7 @@ public async Task WhenConcatEnumerableDoubleDisposeWithFailure_ThenRoutesToUnhan var error = new InvalidOperationException("late failure"); // Call the extracted helper directly to test the double-dispose path - ConcatEnumerableSignal.ConcatEnumerableSubscription.HandleAlreadyDisposed( + ConcatEnumerableSignal.ConcatSequenceCoordinator.HandleAlreadyDisposed( Result.Failure(error)); await Assert.That(unhandled).IsSameReferenceAs(error); @@ -685,8 +685,8 @@ public async Task WhenConcatEnumerableDoubleDisposeWithoutFailure_ThenNoUnhandle Exception? unhandled = null; UnhandledExceptionHandler.Register(ex => unhandled = ex); - ConcatEnumerableSignal.ConcatEnumerableSubscription.HandleAlreadyDisposed(null); - ConcatEnumerableSignal.ConcatEnumerableSubscription.HandleAlreadyDisposed(Result.Success); + ConcatEnumerableSignal.ConcatSequenceCoordinator.HandleAlreadyDisposed(null); + ConcatEnumerableSignal.ConcatSequenceCoordinator.HandleAlreadyDisposed(Result.Success); await Assert.That(unhandled).IsNull(); } @@ -722,7 +722,7 @@ await AsyncTestHelpers.WaitForConditionAsync( () => completionResult.HasValue, TimeSpan.FromSeconds(5)); - // Now dispose, which calls CompleteAsync(null) but TrySetDisposed returns true + // Now dispose, which calls FinishAsync(null) but TrySetDisposed returns true // (already disposed), and since result?.Exception is null for null result, no handler call. // We need another approach: dispose first, then force another completion with an error. await sub.DisposeAsync(); @@ -760,7 +760,7 @@ public async Task WhenConcatObservablesHandleAlreadyDisposedWithFailure_ThenRout var error = new InvalidOperationException("late failure"); - ConcatSignalSourcesSignal.ConcatSubscription.HandleAlreadyDisposed( + ConcatSignalSourcesSignal.ConcatCoordinator.HandleAlreadyDisposed( Result.Failure(error)); await Assert.That(unhandled).IsSameReferenceAs(error); @@ -777,8 +777,8 @@ public async Task WhenConcatObservablesHandleAlreadyDisposedWithoutFailure_ThenN Exception? unhandled = null; UnhandledExceptionHandler.Register(ex => unhandled = ex); - ConcatSignalSourcesSignal.ConcatSubscription.HandleAlreadyDisposed(null); - ConcatSignalSourcesSignal.ConcatSubscription.HandleAlreadyDisposed(Result.Success); + ConcatSignalSourcesSignal.ConcatCoordinator.HandleAlreadyDisposed(null); + ConcatSignalSourcesSignal.ConcatCoordinator.HandleAlreadyDisposed(Result.Success); await Assert.That(unhandled).IsNull(); } @@ -808,7 +808,7 @@ await Assert.ThrowsAsync(async () => } /// - /// Verifies that when SubscribeNextAsync throws and CompleteAsync also throws + /// Verifies that when SubscribeNextSignalAsync throws and FinishAsync also throws /// (because the enumerator's Dispose faults), the catch block in SubscribeAsyncCore /// disposes the subscription and rethrows the exception. /// diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.Merge.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.Merge.cs index ab9f931..35135a0 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.Merge.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.Merge.cs @@ -399,7 +399,7 @@ static IEnumerable> ThrowingEnumerable() } /// - /// Verifies that when a subscription to an inner source throws in MergeEnumerable StartAsync, + /// Verifies that when a subscription to an inner source throws in MergeEnumerable BeginSubscribing, /// the exception is caught and the sequence completes with failure. /// /// A representing the asynchronous test operation. @@ -436,7 +436,7 @@ public async Task WhenMergeEnumerableInnerSubscriptionThrows_ThenCompletesWithFa } /// - /// Verifies that MergeEnumerable CompleteAsync called a second time with an exception + /// Verifies that MergeEnumerable FinishAsync called a second time with an exception /// routes the exception to UnhandledExceptionHandler rather than throwing. /// /// A representing the asynchronous test operation. @@ -502,9 +502,9 @@ public async Task WhenMergeEnumerableInnerCompletesAsynchronously_ThenAwaitsSubs /// /// A representing the asynchronous test operation. [Test] - public async Task WhenMergeEnumerableStartAsyncThrows_ThenCatchBlockHandled() + public async Task WhenMergeEnumerableBeginSubscribingThrows_ThenCatchBlockHandled() { - // StartAsync contains an async void path that catches exceptions + // BeginSubscribing contains an async void path that catches exceptions // We exercise this by ensuring an error during inner subscription is caught static IEnumerable> ThrowingEnumerable() { @@ -678,7 +678,7 @@ await AsyncTestHelpers.WaitForConditionAsync( /// /// Verifies that when an inner source throws TaskCanceledException during subscribe - /// in MergeEnumerable StartAsync, the cancellation is handled gracefully. + /// in MergeEnumerable BeginSubscribing, the cancellation is handled gracefully. /// /// A representing the asynchronous test operation. [Test] @@ -716,7 +716,7 @@ await AsyncTestHelpers.WaitForConditionAsync( /// /// Verifies that when an inner source throws a non-cancellation exception during - /// SubscribeAsync in MergeEnumerable, the error is forwarded via CompleteAsync. + /// SubscribeAsync in MergeEnumerable, the error is forwarded via FinishAsync. /// /// A representing the asynchronous test operation. [Test] @@ -923,42 +923,42 @@ public async Task WhenMergeMaxConcurrencyExternalTokenCancelledAfterSubscribe_Th await Assert.That(sub).IsNotNull(); } - /// Verifies the + /// Verifies the /// inside-gate after-dispose guard by subscribing, disposing the subscription, then calling /// the locked-helper directly — exercising the defensive branch that is otherwise only /// reachable through a real concurrency race between dispose and gate acquisition. /// A representing the asynchronous test operation. [Test] - public async Task WhenMergeForwardOnNextLockedAfterDispose_ThenDropped() + public async Task WhenMergeRelayNextIfActiveAsyncAfterDispose_ThenDropped() { var captured = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var downstream = new CapturingObserver(onNext: captured); - var subscription = new SignalAsync.MergeSubscription(downstream); + var subscription = new SignalAsync.MergeCoordinator(downstream); await subscription.DisposeAsync(); - await subscription.ForwardOnNextLocked(1); + await subscription.RelayNextIfActiveAsync(1); await Assert.That(captured.Task.IsCompleted).IsFalse(); } - /// Verifies the + /// Verifies the /// inside-gate after-dispose guard. /// A representing the asynchronous test operation. [Test] - public async Task WhenMergeForwardOnErrorResumeLockedAfterDispose_ThenDropped() + public async Task WhenMergeRelayErrorIfActiveAsyncAfterDispose_ThenDropped() { var captured = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var downstream = new CapturingObserver(onError: captured); - var subscription = new SignalAsync.MergeSubscription(downstream); + var subscription = new SignalAsync.MergeCoordinator(downstream); await subscription.DisposeAsync(); - await subscription.ForwardOnErrorResumeLocked(new InvalidOperationException("late")); + await subscription.RelayErrorIfActiveAsync(new InvalidOperationException("late")); await Assert.That(captured.Task.IsCompleted).IsFalse(); } /// Verifies the - /// + /// /// inside-gate after-dispose guard on the enumerable-Merge subscription class. /// A representing the asynchronous test operation. [Test] @@ -967,20 +967,20 @@ public async Task WhenMergeEnumerableOnNextAsyncLockedAfterDispose_ThenDropped() var captured = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var downstream = new CapturingObserver(onNext: captured); - // Subscribe to a real Merge to obtain a MergeEnumerableSubscription; then dispose it + // Subscribe to a real Merge to obtain a MergeSequenceCoordinator; then dispose it // and call the Locked helper directly to verify the inside-gate guard. IObservableAsync[] sources = [SignalAsync.Never()]; var sub = await sources.Merge().SubscribeAsync(downstream, CancellationToken.None); - var enumerableSub = (SignalAsync.MergeEnumerableSignal.MergeEnumerableSubscription)sub; + var enumerableSub = (SignalAsync.MergeEnumerableSignal.MergeSequenceCoordinator)sub; await enumerableSub.DisposeAsync(); - await enumerableSub.OnNextAsyncLocked(1); + await enumerableSub.RelayNextIfActiveAsync(1); await Assert.That(captured.Task.IsCompleted).IsFalse(); } /// Verifies the enumerable-Merge subscription's after-dispose - /// OnErrorResumeAsyncLocked guard. + /// RelayErrorIfActiveAsync guard. /// A representing the asynchronous test operation. [Test] public async Task WhenMergeEnumerableOnErrorResumeAsyncLockedAfterDispose_ThenDropped() @@ -989,10 +989,10 @@ public async Task WhenMergeEnumerableOnErrorResumeAsyncLockedAfterDispose_ThenDr var downstream = new CapturingObserver(onError: captured); IObservableAsync[] sources = [SignalAsync.Never()]; var sub = await sources.Merge().SubscribeAsync(downstream, CancellationToken.None); - var enumerableSub = (SignalAsync.MergeEnumerableSignal.MergeEnumerableSubscription)sub; + var enumerableSub = (SignalAsync.MergeEnumerableSignal.MergeSequenceCoordinator)sub; await enumerableSub.DisposeAsync(); - await enumerableSub.OnErrorResumeAsyncLocked(new InvalidOperationException("late")); + await enumerableSub.RelayErrorIfActiveAsync(new InvalidOperationException("late")); await Assert.That(captured.Task.IsCompleted).IsFalse(); } diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.MergeEnumerableDisposal.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.MergeEnumerableDisposal.cs index a91d5a9..54400fb 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.MergeEnumerableDisposal.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.MergeEnumerableDisposal.cs @@ -151,7 +151,7 @@ public async Task WhenMergeEnumerableDisposed_ThenOnErrorResumeReturnsEarly() } /// - /// Verifies that MergeEnumerable CompleteAsync handles second error after disposal. + /// Verifies that MergeEnumerable FinishAsync handles second error after disposal. /// /// A representing the asynchronous test operation. [Test] @@ -169,7 +169,7 @@ public async Task WhenMergeEnumerableCompletedTwiceWithError_ThenSecondErrorGoes (_, _) => default, null); - // First source fails - triggers CompleteAsync + // First source fails - triggers FinishAsync await signal1.OnCompletedAsync(Result.Failure(new InvalidOperationException(FirstLiteral))); // Second source fails - already disposed, error goes to UnhandledExceptionHandler @@ -212,7 +212,7 @@ public async Task WhenMergeEnumerableDisposed_ThenOnNextReturnsEarlyViaDirectSou await sub.DisposeAsync(); // DirectSource retains the inner observer, so this call reaches - // MergeEnumerableSubscription.OnNextAsync which checks _disposed. + // MergeSequenceCoordinator.RelayNextAsync which checks _disposed. try { await directSource.EmitNext(SampleValue2, CancellationToken.None); @@ -255,7 +255,7 @@ public async Task WhenMergeEnumerableDisposed_ThenOnErrorResumeReturnsEarlyViaDi await sub.DisposeAsync(); // DirectSource retains the inner observer, so this call reaches - // MergeEnumerableSubscription.OnErrorResumeAsync which checks _disposed. + // MergeSequenceCoordinator.RelayErrorAsync which checks _disposed. try { await directSource.EmitError(new InvalidOperationException(LateErrorMessage), CancellationToken.None); @@ -269,7 +269,7 @@ public async Task WhenMergeEnumerableDisposed_ThenOnErrorResumeReturnsEarlyViaDi } /// - /// Verifies that MergeEnumerable CompleteAsync handles a second completion with + /// Verifies that MergeEnumerable FinishAsync handles a second completion with /// an error by routing it to UnhandledExceptionHandler, using DirectSource to /// ensure both completions reach the subscription. /// @@ -289,7 +289,7 @@ public async Task WhenMergeEnumerableCompletedTwiceWithErrorViaDirectSource_Then (_, _) => default, null); - // First source fails – triggers CompleteAsync and disposes subscription + // First source fails – triggers FinishAsync and disposes subscription await directSource1.Complete(Result.Failure(new InvalidOperationException(FirstLiteral))); // Second source fails – already disposed, error goes to UnhandledExceptionHandler @@ -303,7 +303,7 @@ await AsyncTestHelpers.WaitForConditionAsync( } /// - /// Verifies that when the completion handler itself throws in MergeEnumerable StartAsync, + /// Verifies that when the completion handler itself throws in MergeEnumerable BeginSubscribing, /// the outer catch routes the exception to UnhandledExceptionHandler. /// /// A representing the asynchronous test operation. @@ -314,9 +314,9 @@ public async Task WhenMergeEnumerableCompletionHandlerThrows_ThenOuterCatchRoute UnhandledExceptionHandler.Register(ex => unhandledException = ex); // Use a single Return source that completes synchronously during subscription. - // The sentinel decrement triggers CompleteAsync(Result.Success), and we make the + // The sentinel decrement triggers FinishAsync(Result.Success), and we make the // observer's OnCompletedAsync throw, which escapes the inner try/finally and is - // caught by the outer try in StartAsync. + // caught by the outer try in BeginSubscribing. IObservableAsync[] sources = [SignalAsync.Return(1)]; await using var sub = await sources.Merge() @@ -335,8 +335,8 @@ await AsyncTestHelpers.WaitForConditionAsync( /// /// Verifies that when the enumerable throws during iteration in MergeEnumerable, - /// the exception propagates through the StartAsync outer catch and is routed to - /// UnhandledExceptionHandler. This exercises the defensive error path in StartAsync. + /// the exception propagates through the BeginSubscribing outer catch and is routed to + /// UnhandledExceptionHandler. This exercises the defensive error path in BeginSubscribing. /// /// A representing the asynchronous test operation. [Test] @@ -345,7 +345,7 @@ public async Task WhenMergeEnumerableThrowsDuringIteration_ThenRoutesToUnhandled using var unhandled = new UnhandledExceptionCapture(); // Use an enumerable whose GetEnumerator throws, triggering the error path - // inside StartAsync's inner try block. + // inside BeginSubscribing's inner try block. var throwingEnumerable = new ThrowingEnumerable(); await using var sub = await throwingEnumerable.Merge() @@ -485,11 +485,11 @@ public async Task WhenMergeEnumerableDisposedWhileGateHeld_ThenOnErrorResumeRetu /// /// Verifies that MergeEnumerable outer catch routes exception - /// to UnhandledExceptionHandler when StartAsync itself throws. + /// to UnhandledExceptionHandler when BeginSubscribing itself throws. /// /// A representing the asynchronous test operation. [Test] - public async Task WhenMergeEnumerableStartAsyncThrows_ThenRoutesToUnhandled() + public async Task WhenMergeEnumerableBeginSubscribingThrows_ThenRoutesToUnhandled() { Exception? unhandled = null; UnhandledExceptionHandler.Register(ex => unhandled = ex); @@ -545,7 +545,7 @@ public async Task WhenMergeEnumerableAlreadyDisposedWithFailure_ThenRoutesToUnha Exception? unhandled = null; UnhandledExceptionHandler.Register(ex => unhandled = ex); - SignalAsync.MergeEnumerableSignal.MergeEnumerableSubscription.RoutePostDisposalException( + SignalAsync.MergeEnumerableSignal.MergeSequenceCoordinator.RoutePostDisposalException( Result.Failure(new InvalidOperationException("post-dispose error"))); await Assert.That(unhandled).IsNotNull(); @@ -554,7 +554,7 @@ public async Task WhenMergeEnumerableAlreadyDisposedWithFailure_ThenRoutesToUnha /// /// Verifies that MergeEnumerable drops values after disposal. - /// Covers the disposed-early-return guard in MergeEnumerableSubscription.OnNextAsync. + /// Covers the disposed-early-return guard in MergeSequenceCoordinator.RelayNextAsync. /// /// A representing the asynchronous test operation. [Test] @@ -589,7 +589,7 @@ await AsyncTestHelpers.WaitForConditionAsync( /// /// Verifies that MergeEnumerable drops error-resume after disposal. - /// Covers the disposed-early-return guard in MergeEnumerableSubscription.OnErrorResumeAsync. + /// Covers the disposed-early-return guard in MergeSequenceCoordinator.RelayErrorAsync. /// /// A representing the asynchronous test operation. [Test] diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.MergeSignalDisposal.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.MergeSignalDisposal.cs index 6838daa..a24c537 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.MergeSignalDisposal.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.MergeSignalDisposal.cs @@ -98,11 +98,11 @@ public async Task WhenMergeSignalDisposedDuringErrorResume_ThenForwardingSilentl } /// - /// Tests that MergeSignalSourcesSignal ForwardOnNext returns early when disposed. + /// Tests that MergeSignalSourcesSignal OnNextAsync returns early when disposed. /// /// A representing the asynchronous test operation. [Test] - public async Task WhenMergeSignalDisposedBeforeInnerEmission_ThenForwardOnNextReturns() + public async Task WhenMergeSignalDisposedBeforeInnerEmission_ThenRelayNextAsyncReturns() { var source = Signal.Create(); var inner = Signal.Create>(); @@ -138,11 +138,11 @@ await AsyncTestHelpers.WaitForConditionAsync( } /// - /// Verifies that Merge ForwardOnNext returns early when disposed. + /// Verifies that Merge OnNextAsync returns early when disposed. /// /// A representing the asynchronous test operation. [Test] - public async Task WhenMergeDisposedDuringEmission_ThenForwardOnNextReturnsEarly() + public async Task WhenMergeDisposedDuringEmission_ThenRelayNextAsyncReturnsEarly() { var signal = Signal.Create(); var items = new List(); @@ -171,14 +171,14 @@ public async Task WhenMergeDisposedDuringEmission_ThenForwardOnNextReturnsEarly( } /// - /// Verifies that ForwardOnNext in MergeSubscription returns early (pre-gate check) + /// Verifies that OnNextAsync in MergeCoordinator returns early (pre-gate check) /// when the subscription has already been disposed. /// Uses DirectSource to retain a reference to the inner observer so that emissions /// can be attempted after disposal without being blocked by Signal un-subscription. /// /// A representing the asynchronous test operation. [Test] - public async Task WhenMergeSignalOfSignalsDisposed_ThenForwardOnNextReturnsPreGate() + public async Task WhenMergeSignalOfSignalsDisposed_ThenRelayNextAsyncReturnsPreGate() { var innerSource = new DirectSource(); var outerSource = new DirectSource>(); @@ -208,7 +208,7 @@ public async Task WhenMergeSignalOfSignalsDisposed_ThenForwardOnNextReturnsPreGa await sub.DisposeAsync(); // Emit after dispose – DirectSource still holds the inner observer reference, - // so this reaches ForwardOnNext which should return early at the pre-gate check. + // so this reaches OnNextAsync which should return early at the pre-gate check. try { await innerSource.EmitNext(SampleValue2, CancellationToken.None); @@ -223,12 +223,12 @@ public async Task WhenMergeSignalOfSignalsDisposed_ThenForwardOnNextReturnsPreGa } /// - /// Verifies that ForwardOnErrorResume in MergeSubscription returns early + /// Verifies that OnErrorResumeAsync in MergeCoordinator returns early /// when the subscription has already been disposed. /// /// A representing the asynchronous test operation. [Test] - public async Task WhenMergeSignalOfSignalsDisposed_ThenForwardOnErrorResumeReturns() + public async Task WhenMergeSignalOfSignalsDisposed_ThenRelayErrorAsyncReturns() { var innerSource = new DirectSource(); var outerSource = new DirectSource>(); @@ -267,14 +267,14 @@ public async Task WhenMergeSignalOfSignalsDisposed_ThenForwardOnErrorResumeRetur } /// - /// Verifies that ForwardOnNext in MergeSubscription returns early at the post-gate + /// Verifies that OnNextAsync in MergeCoordinator returns early at the post-gate /// disposed check when disposal occurs while waiting for the gate. /// A slow observer holds the gate while a second emission waits; disposal happens /// before the second emission acquires the gate. /// /// A representing the asynchronous test operation. [Test] - public async Task WhenMergeSignalOfSignalsDisposedWhileGateHeld_ThenForwardOnNextReturnsPostGate() + public async Task WhenMergeSignalOfSignalsDisposedWhileGateHeld_ThenRelayNextAsyncReturnsPostGate() { var innerSource = new DirectSource(); var outerSource = new DirectSource>(); @@ -343,12 +343,12 @@ public async Task WhenMergeSignalOfSignalsDisposedWhileGateHeld_ThenForwardOnNex } /// - /// Verifies that ForwardOnErrorResume in MergeSubscription returns early at the + /// Verifies that OnErrorResumeAsync in MergeCoordinator returns early at the /// post-gate disposed check when disposal occurs while waiting for the gate. /// /// A representing the asynchronous test operation. [Test] - public async Task WhenMergeSignalOfSignalsDisposedWhileGateHeld_ThenForwardOnErrorResumeReturnsPostGate() + public async Task WhenMergeSignalOfSignalsDisposedWhileGateHeld_ThenRelayErrorAsyncReturnsPostGate() { var innerSource = new DirectSource(); var outerSource = new DirectSource>(); @@ -414,11 +414,11 @@ public async Task WhenMergeSignalOfSignalsDisposedWhileGateHeld_ThenForwardOnErr } /// - /// Verifies that Merge ForwardOnNext returns early at the pre-gate disposed check. + /// Verifies that Merge OnNextAsync returns early at the pre-gate disposed check. /// /// A representing the asynchronous test operation. [Test] - public async Task WhenMergeSignalDisposed_ThenForwardOnNextReturnsEarly() + public async Task WhenMergeSignalDisposed_ThenRelayNextAsyncReturnsEarly() { var outer = Signal.Create>(); var inner = new DirectSource(); @@ -441,11 +441,11 @@ public async Task WhenMergeSignalDisposed_ThenForwardOnNextReturnsEarly() } /// - /// Verifies that Merge ForwardOnErrorResume returns early when disposed. + /// Verifies that Merge OnErrorResumeAsync returns early when disposed. /// /// A representing the asynchronous test operation. [Test] - public async Task WhenMergeSignalDisposed_ThenForwardOnErrorResumeReturnsEarly() + public async Task WhenMergeSignalDisposed_ThenRelayErrorAsyncReturnsEarly() { var outer = Signal.Create>(); var inner = new DirectSource(); @@ -467,12 +467,12 @@ public async Task WhenMergeSignalDisposed_ThenForwardOnErrorResumeReturnsEarly() } /// - /// Verifies that Merge ForwardOnNext post-gate disposed guard returns early + /// Verifies that Merge OnNextAsync post-gate disposed guard returns early /// when disposal happens while waiting for the gate. /// /// A representing the asynchronous test operation. [Test] - public async Task WhenMergeSignalDisposedWhileGateHeld_ThenForwardOnNextReturnsPostGate() + public async Task WhenMergeSignalDisposedWhileGateHeld_ThenRelayNextAsyncReturnsPostGate() { var outer = Signal.Create>(); var inner = new DirectSource(); @@ -509,7 +509,7 @@ public async Task WhenMergeSignalDisposedWhileGateHeld_ThenForwardOnNextReturnsP /// /// Verifies that Merge(IObservableAsync of IObservableAsync) drops values after disposal. - /// Covers the disposed-early-return guard in MergeSubscription.ForwardOnNext. + /// Covers the disposed-early-return guard in MergeCoordinator.RelayNextAsync. /// /// A representing the asynchronous test operation. [Test] @@ -546,7 +546,7 @@ await AsyncTestHelpers.WaitForConditionAsync( /// /// Verifies that Merge(IObservableAsync of IObservableAsync) drops error-resume after disposal. - /// Covers the disposed-early-return guard in MergeSubscription.ForwardOnErrorResume. + /// Covers the disposed-early-return guard in MergeCoordinator.RelayErrorAsync. /// /// A representing the asynchronous test operation. [Test] diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.Multicast.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.Multicast.cs index 09be991..2ea43cd 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.Multicast.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.Multicast.cs @@ -329,9 +329,9 @@ public async Task WhenRoutePostDisposalExceptionWithSuccess_ThenNoExceptionRoute Exception? unhandled = null; UnhandledExceptionHandler.Register(ex => unhandled = ex); - SignalAsync.MergeEnumerableSignal.MergeEnumerableSubscription.RoutePostDisposalException( + SignalAsync.MergeEnumerableSignal.MergeSequenceCoordinator.RoutePostDisposalException( Result.Success); - SignalAsync.MergeEnumerableSignal.MergeEnumerableSubscription.RoutePostDisposalException(null); + SignalAsync.MergeEnumerableSignal.MergeSequenceCoordinator.RoutePostDisposalException(null); await Assert.That(unhandled).IsNull(); } diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.OnDispose.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.OnDispose.cs index 6ca6877..1b62bc9 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.OnDispose.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.OnDispose.cs @@ -133,7 +133,7 @@ public async Task WhenOnDisposeSyncExplicitDispose_ThenActionInvoked() /// /// A representing the asynchronous test operation. [Test] - public async Task WhenOnDisposeAsyncOnNext_ThenForwardsValues() + public async Task WhenCleanupBranchAsyncOnNext_ThenForwardsValues() { var items = new List(); var disposed = false; @@ -169,7 +169,7 @@ public async Task WhenOnDisposeAsyncOnNext_ThenForwardsValues() /// /// A representing the asynchronous test operation. [Test] - public async Task WhenOnDisposeAsyncOnErrorResume_ThenForwardsError() + public async Task WhenCleanupBranchAsyncOnErrorResume_ThenForwardsError() { var errors = new List(); @@ -199,7 +199,7 @@ public async Task WhenOnDisposeAsyncOnErrorResume_ThenForwardsError() /// /// A representing the asynchronous test operation. [Test] - public async Task WhenOnDisposeAsyncOnCompletedFailure_ThenForwardsFailure() + public async Task WhenCleanupBranchAsyncOnCompletedFailure_ThenForwardsFailure() { Result? completionResult = null; @@ -229,7 +229,7 @@ public async Task WhenOnDisposeAsyncOnCompletedFailure_ThenForwardsFailure() /// /// A representing the asynchronous test operation. [Test] - public async Task WhenOnDisposeAsyncExplicitDispose_ThenCallbackInvoked() + public async Task WhenCleanupBranchAsyncExplicitDispose_ThenCallbackInvoked() { var disposed = false; var signal = Signal.Create(); @@ -252,46 +252,46 @@ public async Task WhenOnDisposeAsyncExplicitDispose_ThenCallbackInvoked() } /// - /// Verifies MergeSubscription.ForwardOnNext pre-gate disposed guard returns early. + /// Verifies MergeCoordinator.RelayNextAsync pre-gate disposed guard returns early. /// /// A representing the asynchronous test operation. [Test] - public async Task WhenMergeSubscriptionDisposed_ThenForwardOnNextReturnsDirectly() + public async Task WhenMergeCoordinatorDisposed_ThenRelayNextAsyncReturnsDirectly() { - var observer = new AnonymousObserverAsync((_, _) => default); - var subscription = new SignalAsync.MergeSubscription(observer); + var observer = new DelegateAsyncWitness((_, _) => default); + var subscription = new SignalAsync.MergeCoordinator(observer); await subscription.DisposeAsync(); - await subscription.ForwardOnNext(Sentinel99, CancellationToken.None); + await subscription.RelayNextAsync(Sentinel99, CancellationToken.None); } /// - /// Verifies MergeSubscription.ForwardOnErrorResume pre-gate disposed guard returns early. + /// Verifies MergeCoordinator.RelayErrorAsync pre-gate disposed guard returns early. /// /// A representing the asynchronous test operation. [Test] - public async Task WhenMergeSubscriptionDisposed_ThenForwardOnErrorResumeReturnsDirectly() + public async Task WhenMergeCoordinatorDisposed_ThenRelayErrorAsyncReturnsDirectly() { - var observer = new AnonymousObserverAsync((_, _) => default); - var subscription = new SignalAsync.MergeSubscription(observer); + var observer = new DelegateAsyncWitness((_, _) => default); + var subscription = new SignalAsync.MergeCoordinator(observer); await subscription.DisposeAsync(); - await subscription.ForwardOnErrorResume(new InvalidOperationException("test"), CancellationToken.None); + await subscription.RelayErrorAsync(new InvalidOperationException("test"), CancellationToken.None); } /// - /// Verifies MergeSubscription.ForwardOnNext post-gate disposed guard. - /// Directly calls ForwardOnNext on the subscription while CompleteAsync blocks on downstream completion. + /// Verifies MergeCoordinator.RelayNextAsync post-gate disposed guard. + /// Directly calls OnNextAsync on the subscription while FinishAsync blocks on downstream completion. /// /// A representing the asynchronous test operation. [Test] - public async Task WhenMergeSubscriptionDisposedWhileGateHeld_ThenForwardOnNextPostGateReturns() + public async Task WhenMergeCoordinatorDisposedWhileGateHeld_ThenRelayNextAsyncPostGateReturns() { var completionBlocked = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var allowCompletion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var items = new List(); - var observer = new AnonymousObserverAsync( + var observer = new DelegateAsyncWitness( (x, _) => { items.Add(x); @@ -304,15 +304,15 @@ public async Task WhenMergeSubscriptionDisposedWhileGateHeld_ThenForwardOnNextPo await allowCompletion.Task; }); - var subscription = new SignalAsync.MergeSubscription(observer); + var subscription = new SignalAsync.MergeCoordinator(observer); - // Trigger CompleteAsync with failure - blocks on observer.OnCompletedAsync + // Trigger FinishAsync with failure - blocks on observer.OnCompletedAsync var failTask = Task.Run(() => - subscription.CompleteAsync(Result.Failure(new InvalidOperationException("fail")))); + subscription.FinishAsync(Result.Failure(new InvalidOperationException("fail")))); await completionBlocked.Task; - // _disposed is 1, gate is still alive → ForwardOnNext acquires gate and hits post-gate check - await subscription.ForwardOnNext(Sentinel99, CancellationToken.None); + // _disposed is 1, gate is still alive → OnNextAsync acquires gate and hits post-gate check + await subscription.RelayNextAsync(Sentinel99, CancellationToken.None); await Assert.That(items).IsEmpty(); @@ -321,17 +321,17 @@ public async Task WhenMergeSubscriptionDisposedWhileGateHeld_ThenForwardOnNextPo } /// - /// Verifies MergeSubscription.ForwardOnErrorResume post-gate disposed guard. + /// Verifies MergeCoordinator.RelayErrorAsync post-gate disposed guard. /// /// A representing the asynchronous test operation. [Test] - public async Task WhenMergeSubscriptionDisposedWhileGateHeld_ThenForwardOnErrorResumePostGateReturns() + public async Task WhenMergeCoordinatorDisposedWhileGateHeld_ThenRelayErrorAsyncPostGateReturns() { var completionBlocked = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var allowCompletion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var errors = new List(); - var observer = new AnonymousObserverAsync( + var observer = new DelegateAsyncWitness( (_, _) => default, (ex, _) => { @@ -344,13 +344,13 @@ public async Task WhenMergeSubscriptionDisposedWhileGateHeld_ThenForwardOnErrorR await allowCompletion.Task; }); - var subscription = new SignalAsync.MergeSubscription(observer); + var subscription = new SignalAsync.MergeCoordinator(observer); var failTask = Task.Run(() => - subscription.CompleteAsync(Result.Failure(new InvalidOperationException("fail")))); + subscription.FinishAsync(Result.Failure(new InvalidOperationException("fail")))); await completionBlocked.Task; - await subscription.ForwardOnErrorResume(new InvalidOperationException("post-dispose"), CancellationToken.None); + await subscription.RelayErrorAsync(new InvalidOperationException("post-dispose"), CancellationToken.None); await Assert.That(errors).IsEmpty(); @@ -359,36 +359,36 @@ public async Task WhenMergeSubscriptionDisposedWhileGateHeld_ThenForwardOnErrorR } /// - /// Verifies that MergeEnumerableSubscription.OnNextAsync returns early when called directly after disposal. + /// Verifies that MergeSequenceCoordinator.RelayNextAsync returns early when called directly after disposal. /// /// A representing the asynchronous test operation. [Test] - public async Task WhenMergeEnumerableSubscriptionDisposed_ThenOnNextReturnsDirectly() + public async Task WhenMergeSequenceCoordinatorDisposed_ThenOnNextReturnsDirectly() { - var observer = new AnonymousObserverAsync((_, _) => default); + var observer = new DelegateAsyncWitness((_, _) => default); IObservableAsync[] sources = []; var subscription = - new SignalAsync.MergeEnumerableSignal.MergeEnumerableSubscription(observer, sources); - subscription.StartAsync(); + new SignalAsync.MergeEnumerableSignal.MergeSequenceCoordinator(observer, sources); + subscription.BeginSubscribing(); await subscription.DisposeAsync(); - await subscription.OnNextAsync(Sentinel99, CancellationToken.None); + await subscription.RelayNextAsync(Sentinel99, CancellationToken.None); } /// - /// Verifies that MergeEnumerableSubscription.OnErrorResumeAsync returns early when called directly after disposal. + /// Verifies that MergeSequenceCoordinator.RelayErrorAsync returns early when called directly after disposal. /// /// A representing the asynchronous test operation. [Test] - public async Task WhenMergeEnumerableSubscriptionDisposed_ThenOnErrorResumeReturnsDirectly() + public async Task WhenMergeSequenceCoordinatorDisposed_ThenOnErrorResumeReturnsDirectly() { - var observer = new AnonymousObserverAsync((_, _) => default); + var observer = new DelegateAsyncWitness((_, _) => default); IObservableAsync[] sources = []; var subscription = - new SignalAsync.MergeEnumerableSignal.MergeEnumerableSubscription(observer, sources); - subscription.StartAsync(); + new SignalAsync.MergeEnumerableSignal.MergeSequenceCoordinator(observer, sources); + subscription.BeginSubscribing(); await subscription.DisposeAsync(); - await subscription.OnErrorResumeAsync(new InvalidOperationException("test"), CancellationToken.None); + await subscription.RelayErrorAsync(new InvalidOperationException("test"), CancellationToken.None); } } diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.Switch.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.Switch.cs index d797aaa..6d32292 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.Switch.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.Switch.cs @@ -102,7 +102,7 @@ public async Task WhenSwitchDisposedDuringInnerSubscription_ThenNoError() /// /// A representing the asynchronous test operation. [Test] - public async Task WhenSwitchSubscriptionThrows_ThenDisposesAndRethrows() + public async Task WhenSwitchCoordinatorThrows_ThenDisposesAndRethrows() { var failing = SignalAsync.Create>((_, _) => { diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.cs index 0d8d10c..9b0f86c 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.cs @@ -135,7 +135,7 @@ private sealed class ThrowingDisposable : IAsyncDisposable /// /// An enumerable that throws during enumeration, used to trigger the error path - /// in MergeEnumerable StartAsync. + /// in MergeEnumerable BeginSubscribing. /// /// The element type. private sealed class ThrowingEnumerable : IEnumerable> diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/ConcurrentSignalBaseTests.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/ConcurrentSignalBaseTests.cs index e9d1f60..4eab890 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/ConcurrentSignalBaseTests.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/ConcurrentSignalBaseTests.cs @@ -24,7 +24,7 @@ public class ConcurrentSignalBaseTests /// Verifies that ForwardOnNextConcurrently with an empty observer list returns immediately. /// A representing the asynchronous test operation. [Test] - public async Task WhenForwardOnNextEmpty_ThenCompletesImmediately() + public async Task WhenRelayNextAsyncEmpty_ThenCompletesImmediately() { var empty = ImmutableArray>.Empty; @@ -37,7 +37,7 @@ public async Task WhenForwardOnNextEmpty_ThenCompletesImmediately() /// Verifies that ForwardOnNextConcurrently with a single observer forwards once. /// A representing the asynchronous test operation. [Test] - public async Task WhenForwardOnNextSingleObserver_ThenForwardsOnce() + public async Task WhenRelayNextAsyncSingleObserver_ThenForwardsOnce() { var capture = new IntCapture(); var observers = ImmutableArray.Create>(MakeSync(capture)); @@ -51,7 +51,7 @@ public async Task WhenForwardOnNextSingleObserver_ThenForwardsOnce() /// returns a synchronously-completed . /// A representing the asynchronous test operation. [Test] - public async Task WhenForwardOnNextMultipleSync_ThenAllReceive() + public async Task WhenRelayNextAsyncMultipleSync_ThenAllReceive() { var a = new IntCapture(); var b = new IntCapture(); @@ -73,7 +73,7 @@ public async Task WhenForwardOnNextMultipleSync_ThenAllReceive() /// any of them returns a non-completed . /// A representing the asynchronous test operation. [Test] - public async Task WhenForwardOnNextSlowPath_ThenWhenAllForwarded() + public async Task WhenRelayNextAsyncSlowPath_ThenWhenAllForwarded() { var a = new IntCapture(); var b = new IntCapture(); @@ -94,14 +94,14 @@ public async Task WhenForwardOnNextSlowPath_ThenWhenAllForwarded() /// Verifies the empty / single / slow-path branches of ForwardOnErrorResumeConcurrently. /// A representing the asynchronous test operation. [Test] - public async Task WhenForwardOnErrorResume_ThenAllBranchesForward() + public async Task WhenRelayErrorAsync_ThenAllBranchesForward() { var emptyObservers = ImmutableArray>.Empty; await Concurrent.ForwardOnErrorResumeConcurrently(emptyObservers, new InvalidOperationException("empty"), default); var singleCaught = new ErrorCapture(); var single = ImmutableArray.Create>( - new AnonymousObserverAsync(static (_, _) => default, MakeErrorSync(singleCaught))); + new DelegateAsyncWitness(static (_, _) => default, MakeErrorSync(singleCaught))); var singleError = new InvalidOperationException("single"); await Concurrent.ForwardOnErrorResumeConcurrently(single, singleError, default); await Assert.That(singleCaught.Error).IsSameReferenceAs(singleError); @@ -109,8 +109,8 @@ public async Task WhenForwardOnErrorResume_ThenAllBranchesForward() var a = new ErrorCapture(); var b = new ErrorCapture(); var multi = ImmutableArray.Create>( - new AnonymousObserverAsync(static (_, _) => default, MakeErrorSlow(a)), - new AnonymousObserverAsync(static (_, _) => default, MakeErrorSync(b))); + new DelegateAsyncWitness(static (_, _) => default, MakeErrorSlow(a)), + new DelegateAsyncWitness(static (_, _) => default, MakeErrorSync(b))); var multiError = new InvalidOperationException("multi"); await Concurrent.ForwardOnErrorResumeConcurrently(multi, multiError, default); await Assert.That(a.Error).IsSameReferenceAs(multiError); @@ -127,15 +127,15 @@ public async Task WhenForwardOnCompleted_ThenAllBranchesForward() var singleResult = new ResultCapture(); var single = ImmutableArray.Create>( - new AnonymousObserverAsync(static (_, _) => default, null, MakeCompletedSync(singleResult))); + new DelegateAsyncWitness(static (_, _) => default, null, MakeCompletedSync(singleResult))); await Concurrent.ForwardOnCompletedConcurrently(single, Result.Success); await Assert.That(singleResult.Result).IsEqualTo(Result.Success); var a = new ResultCapture(); var b = new ResultCapture(); var multi = ImmutableArray.Create>( - new AnonymousObserverAsync(static (_, _) => default, null, MakeCompletedSlow(a)), - new AnonymousObserverAsync(static (_, _) => default, null, MakeCompletedSync(b))); + new DelegateAsyncWitness(static (_, _) => default, null, MakeCompletedSlow(a)), + new DelegateAsyncWitness(static (_, _) => default, null, MakeCompletedSync(b))); await Concurrent.ForwardOnCompletedConcurrently(multi, Result.Success); await Assert.That(a.Result).IsEqualTo(Result.Success); await Assert.That(b.Result).IsEqualTo(Result.Success); @@ -144,7 +144,7 @@ public async Task WhenForwardOnCompleted_ThenAllBranchesForward() /// Creates a synchronously-completing OnNext observer that captures the value. /// The capture sink. /// An observer whose OnNextAsync completes synchronously. - private static AnonymousObserverAsync MakeSync(IntCapture capture) => + private static DelegateAsyncWitness MakeSync(IntCapture capture) => new((x, _) => { capture.Value = x; @@ -154,7 +154,7 @@ private static AnonymousObserverAsync MakeSync(IntCapture capture) => /// Creates an OnNext observer that delays before capturing — forces the slow path. /// The capture sink. /// An observer whose OnNextAsync completes asynchronously. - private static AnonymousObserverAsync MakeSlow(IntCapture capture) => + private static DelegateAsyncWitness MakeSlow(IntCapture capture) => new(async (x, ct) => { await Task.Delay(SlowPathDelayMilliseconds, ct).ConfigureAwait(false); diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/DisposableAsyncSlotTests.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/DisposableAsyncSlotTests.cs index f9a5505..a76cba4 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/DisposableAsyncSlotTests.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/DisposableAsyncSlotTests.cs @@ -110,8 +110,8 @@ public async Task WhenAssignedTwice_ThenThrowsInvalidOperation() /// Verifies the disposed sentinel's no-op completes silently. /// A representing the asynchronous test operation. [Test] - public async Task WhenDisposedSentinelDisposed_ThenCompletesSilently() => - await ((IAsyncDisposable)DisposableAsyncSlot.DisposedSentinel.Instance).DisposeAsync(); + public async Task WhenDisposedSlotMarkerDisposed_ThenCompletesSilently() => + await ((IAsyncDisposable)DisposableAsyncSlot.DisposedSlotMarker.Instance).DisposeAsync(); /// Recording async disposable that counts disposals. private sealed class RecordingAsyncDisposable : IAsyncDisposable diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/DisposableTests.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/DisposableTests.cs index 7f2ce8d..6d2c7d8 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/DisposableTests.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/DisposableTests.cs @@ -585,14 +585,14 @@ await Assert.ThrowsAsync(async () => /// /// Verifies that the dispose sentinel DisposeAsync method returns a completed ValueTask. - /// Covers the DisposedSentinel.DisposeAsync path. + /// Covers the DisposedSlotMarker.DisposeAsync path. /// /// A representing the asynchronous test operation. [Test] public async Task WhenSingleAssignmentDisposeSentinel_ThenDisposeAsyncReturnsDefault() { // Access the sentinel and verify it can be disposed - IAsyncDisposable sentinel = SingleAssignmentDisposableAsync.DisposedSentinel.Instance; + IAsyncDisposable sentinel = SingleAssignmentDisposableAsync.DisposedSlotMarker.Instance; await sentinel.DisposeAsync(); // After dispose, getting the disposable should return the empty disposable @@ -792,7 +792,7 @@ public async Task WhenStaticSetDisposableAsync_ThenFieldIsSet() IAsyncDisposable? field = null; var disposable = DisposableAsync.Empty; - await SingleAssignmentDisposableAsync.SetDisposableAsync(ref field, disposable); + await SingleAssignmentDisposableAsync.AssignDisposableAsync(ref field, disposable); await Assert.That(field).IsNotNull(); await Assert.That(field).IsEqualTo(disposable); @@ -807,10 +807,10 @@ public async Task WhenStaticSetDisposableAsyncDoubleSet_ThenThrowsInvalidOperati var first = DisposableAsync.Empty; var second = DisposableAsync.Create(() => default); - await SingleAssignmentDisposableAsync.SetDisposableAsync(ref field, first); + await SingleAssignmentDisposableAsync.AssignDisposableAsync(ref field, first); await Assert.That(async () => - await SingleAssignmentDisposableAsync.SetDisposableAsync(ref field, second)) + await SingleAssignmentDisposableAsync.AssignDisposableAsync(ref field, second)) .ThrowsExactly(); } @@ -827,7 +827,7 @@ public async Task WhenStaticDisposeAsync_ThenDisposableIsDisposed() return default; }); - await SingleAssignmentDisposableAsync.SetDisposableAsync(ref field, disposable); + await SingleAssignmentDisposableAsync.AssignDisposableAsync(ref field, disposable); await SingleAssignmentDisposableAsync.DisposeAsync(ref field); await Assert.That(disposed).IsTrue(); @@ -901,14 +901,14 @@ IAsyncDisposable MakeDisposable() => DisposableAsync.Create(() => } /// - /// Verifies that the DisposedSentinel.DisposeAsync returns a completed ValueTask + /// Verifies that the DisposedSlotMarker.DisposeAsync returns a completed ValueTask /// without throwing. /// /// A representing the asynchronous test operation. [Test] - public async Task WhenSerialDisposedSentinelDisposeAsync_ThenReturnsCompletedValueTask() + public async Task WhenSerialDisposedSlotMarkerDisposeAsync_ThenReturnsCompletedValueTask() { - var sentinel = SingleReplaceableDisposableAsync.DisposedSentinel.Instance; + var sentinel = SingleReplaceableDisposableAsync.DisposedSlotMarker.Instance; // DisposeAsync should return default (no-op) var task = sentinel.DisposeAsync(); @@ -928,11 +928,11 @@ public async Task WhenStaticSetDisposableAsyncReAssignNonNull_ThenThrowsInvalidO var second = DisposableAsync.Create(() => default); // First set succeeds - await SingleAssignmentDisposableAsync.SetDisposableAsync(ref field, first); + await SingleAssignmentDisposableAsync.AssignDisposableAsync(ref field, first); // Second set with a different non-null value triggers ThrowAlreadyAssignment await Assert.That(async () => - await SingleAssignmentDisposableAsync.SetDisposableAsync(ref field, second)) + await SingleAssignmentDisposableAsync.AssignDisposableAsync(ref field, second)) .ThrowsExactly(); } diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/FactorySignalTests.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/FactorySignalTests.cs index 8262d88..56cb82a 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/FactorySignalTests.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/FactorySignalTests.cs @@ -483,7 +483,7 @@ public async Task WhenEmitEnumerableAsyncWithCancelledToken_ThenReturnsEarly() using var cts = new CancellationTokenSource(); await cts.CancelAsync(); - var observer = new AnonymousObserverAsync((x, _) => + var observer = new DelegateAsyncWitness((x, _) => { items.Add(x); return default; diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/Internals/CombineLatestSubscriptionBaseTests.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/Internals/CombineLatestCoordinatorBaseTests.cs similarity index 90% rename from src/tests/ReactiveUI.Primitives.Async.Tests/Internals/CombineLatestSubscriptionBaseTests.cs rename to src/tests/ReactiveUI.Primitives.Async.Tests/Internals/CombineLatestCoordinatorBaseTests.cs index cfb5e6f..a748315 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/Internals/CombineLatestSubscriptionBaseTests.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/Internals/CombineLatestCoordinatorBaseTests.cs @@ -6,9 +6,9 @@ namespace ReactiveUI.Primitives.Async.Tests.Internals; -/// Tests for , the shared scaffolding +/// Tests for , the shared scaffolding /// derived by every CombineLatestN arity-specific subscription. -public class CombineLatestSubscriptionBaseTests +public class CombineLatestCoordinatorBaseTests { /// Verifies that the base wires its with /// the supplied observer and source count. @@ -21,10 +21,10 @@ public async Task WhenConstructed_ThenLifecycleSlotsSized() var subscription = new TestSubscription(captured, SourceCount); await Assert.That(subscription.Lifecycle.Subscriptions).Count().IsEqualTo(SourceCount); - await Assert.That(subscription.Lifecycle.IsDisposed).IsFalse(); + await Assert.That(subscription.Lifecycle.HasDisposed).IsFalse(); } - /// Verifies that + /// Verifies that /// drives the per-arity SubscribeAtAsync for every source index in order. /// A representing the asynchronous test operation. [Test] @@ -47,7 +47,7 @@ public async Task WhenSubscribeSourcesAsync_ThenSubscribeAtCalledForEveryIndex() await subscription.DisposeAsync(); } - /// Verifies that + /// Verifies that /// forwards the error through the lifecycle to the downstream observer. /// A representing the asynchronous test operation. [Test] @@ -57,7 +57,7 @@ public async Task WhenOnErrorResume_ThenForwardsViaLifecycle() var subscription = new TestSubscription(captured, sourceCount: 1); var expected = new InvalidOperationException("forward"); - await subscription.OnErrorResume(expected, CancellationToken.None); + await subscription.RelaySourceErrorAsync(expected, CancellationToken.None); await Assert.That(captured.Errors).Count().IsEqualTo(1); await Assert.That(captured.Errors[0]).IsEqualTo(expected); @@ -65,7 +65,7 @@ public async Task WhenOnErrorResume_ThenForwardsViaLifecycle() await subscription.DisposeAsync(); } - /// Verifies that + /// Verifies that /// disposes the underlying lifecycle so subsequent disposal is idempotent. /// A representing the asynchronous test operation. [Test] @@ -77,7 +77,7 @@ public async Task WhenDisposeAsync_ThenLifecycleDisposed() await subscription.DisposeAsync(); await subscription.DisposeAsync(); // idempotent - await Assert.That(subscription.Lifecycle.IsDisposed).IsTrue(); + await Assert.That(subscription.Lifecycle.HasDisposed).IsTrue(); } /// Verifies that Lifecycle.LinkExternalCancellation short-circuits when the @@ -92,7 +92,7 @@ public async Task WhenLinkExternalCancellationNonCancellable_ThenNoOp() // CancellationToken.None — can't be cancelled, helper should bail out early. subscription.Lifecycle.LinkExternalCancellation(CancellationToken.None); - await Assert.That(subscription.Lifecycle.IsDisposed).IsFalse(); + await Assert.That(subscription.Lifecycle.HasDisposed).IsFalse(); await subscription.DisposeAsync(); } @@ -132,7 +132,7 @@ public async Task WhenLinkExternalCancellationCancellable_ThenLaterCancelPropaga await subscription.DisposeAsync(); } - /// Verifies that is + /// Verifies that is /// usable as a lock target — both the NET9 Lock and legacy object paths /// accept the C# 13 lock statement. /// A representing the asynchronous test operation. @@ -153,7 +153,7 @@ public async Task WhenLockOnValuesLock_ThenNoThrow() /// The downstream observer. /// The number of upstream sources. private sealed class TestSubscription(IObserverAsync observer, int sourceCount) - : CombineLatestSubscriptionBase(observer, sourceCount) + : CombineLatestCoordinatorBase(observer, sourceCount) { /// Gets the indices passed to in order. public List SubscribedIndices { get; } = []; @@ -197,7 +197,7 @@ private sealed class CaptureObserverAsync : IObserverAsync /// Gets the captured OnNext values in order. public List Values { get; } = []; - /// Gets the captured OnErrorResume exceptions in order. + /// Gets the captured error-resume exceptions in order. public List Errors { get; } = []; /// Gets the captured OnCompleted results in order. diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/Internals/CombineLatestIndexedObserverTests.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/Internals/CombineLatestIndexedObserverTests.cs index cb8c1ce..f99f798 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/Internals/CombineLatestIndexedObserverTests.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/Internals/CombineLatestIndexedObserverTests.cs @@ -77,7 +77,7 @@ public async Task WhenOnCompletedAsync_ThenLifecycleForwardsCompletion() /// Minimal concrete subclass exposing the base's EmitLatestAsync invocation count. /// The downstream observer. private sealed class TestSubscription(IObserverAsync observer) - : CombineLatestSubscriptionBase(observer, sourceCount: 1) + : CombineLatestCoordinatorBase(observer, sourceCount: 1) { /// Gets the number of times has been invoked. public int EmitLatestCount { get; private set; } diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/ObserveOnAsyncSignalTests.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/ObserveOnAsyncSignalTests.cs index 7c43d52..25260ff 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/ObserveOnAsyncSignalTests.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/ObserveOnAsyncSignalTests.cs @@ -6,7 +6,7 @@ namespace ReactiveUI.Primitives.Async.Tests; -/// Tests for — exercises the +/// Tests for — exercises the /// forceYielding: true slow-path branches that switch context on every /// OnNext / OnErrorResume / OnCompleted regardless of whether /// the call site is already on the target context. @@ -22,7 +22,7 @@ public class ObserveOnAsyncSignalTests public async Task WhenForceYielding_ThenValueForwarded() { var result = await SignalAsync.Return(Sentinel) - .ObserveOn(AsyncContext.Default, forceYielding: true) + .WitnessOn(AsyncContext.Default, forceYielding: true) .FirstAsync(); await Assert.That(result).IsEqualTo(Sentinel); @@ -40,7 +40,7 @@ public async Task WhenForceYieldingSourceErrors_ThenErrorForwarded() try { await SignalAsync.Throw(expected) - .ObserveOn(AsyncContext.Default, forceYielding: true) + .WitnessOn(AsyncContext.Default, forceYielding: true) .ToListAsync(); } catch (InvalidOperationException ex) @@ -58,7 +58,7 @@ await SignalAsync.Throw(expected) public async Task WhenForceYieldingSourceEmpty_ThenCompletesSuccessfully() { var result = await SignalAsync.Empty() - .ObserveOn(AsyncContext.Default, forceYielding: true) + .WitnessOn(AsyncContext.Default, forceYielding: true) .ToListAsync(); await Assert.That(result).IsEmpty(); @@ -73,7 +73,7 @@ public async Task WhenSyncContextForceYielding_ThenEmits() var ctx = SynchronizationContext.Current ?? new SynchronizationContext(); var result = await SignalAsync.Return(Sentinel) - .ObserveOn(ctx, forceYielding: true) + .WitnessOn(ctx, forceYielding: true) .FirstAsync(); await Assert.That(result).IsEqualTo(Sentinel); @@ -92,7 +92,7 @@ public async Task WhenObserveOnDifferentContextSourceErrors_ThenForwardedViaSlow try { await SignalAsync.Throw(expected) - .ObserveOn(customCtx, forceYielding: false) + .WitnessOn(customCtx, forceYielding: false) .ToListAsync(); } catch (InvalidOperationException ex) @@ -112,15 +112,15 @@ public async Task WhenObserveOnDifferentContextSourceEmpty_ThenCompletesViaSlowP var customCtx = new SynchronizationContext(); var result = await SignalAsync.Empty() - .ObserveOn(customCtx, forceYielding: false) + .WitnessOn(customCtx, forceYielding: false) .ToListAsync(); await Assert.That(result).IsEmpty(); } - /// Exercises ObserveOnObserver.OnErrorResumeAsyncCore's slow-path branch — + /// Exercises ContextSwitchObserver.OnErrorResumeAsyncCore's slow-path branch — /// when forceYielding == true, the resumable-error path returns - /// SwitchThenErrorAsync(...) rather than the fast-path direct forward. + /// ForwardErrorAfterContextSwitchAsync(...) rather than the fast-path direct forward. /// A representing the asynchronous test operation. [Test] public async Task WhenForceYieldingSourceEmitsResumableError_ThenSlowPathForwards() @@ -130,7 +130,7 @@ public async Task WhenForceYieldingSourceEmitsResumableError_ThenSlowPathForward var errorTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); await using var sub = await signal.Values - .ObserveOn(AsyncContext.Default, forceYielding: true) + .WitnessOn(AsyncContext.Default, forceYielding: true) .SubscribeAsync( static (_, _) => default, (ex, _) => @@ -147,49 +147,49 @@ public async Task WhenForceYieldingSourceEmitsResumableError_ThenSlowPathForward await Assert.That(caught).IsSameReferenceAs(expected); } - /// Verifies ObserveOnObserver.SwitchThenForwardAsync by calling it directly + /// Verifies ContextSwitchObserver.ForwardAfterContextSwitchAsync by calling it directly /// — the slow path performs the context switch and then forwards the value downstream, /// independent of the fast/slow choice in OnNextAsyncCore. /// A representing the asynchronous test operation. [Test] - public async Task WhenSwitchThenForwardAsyncInvokedDirectly_ThenValueForwarded() + public async Task WhenForwardAfterContextSwitchAsyncInvokedDirectly_ThenValueForwarded() { var captured = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var downstream = new CapturingAsyncObserver(captured); - var sut = new ObserveOnAsyncSignal.ObserveOnObserver(downstream, AsyncContext.Default, forceYielding: true); + var sut = new WitnessOnAsyncSignal.ContextSwitchObserver(downstream, AsyncContext.Default, forceYielding: true); - await sut.SwitchThenForwardAsync(Sentinel, CancellationToken.None); + await sut.ForwardAfterContextSwitchAsync(Sentinel, CancellationToken.None); var received = await captured.Task.WaitAsync(TimeSpan.FromSeconds(5)); await Assert.That(received).IsEqualTo(Sentinel); } - /// Verifies ObserveOnObserver.SwitchThenErrorAsync by calling it directly. + /// Verifies ContextSwitchObserver.ForwardErrorAfterContextSwitchAsync by calling it directly. /// A representing the asynchronous test operation. [Test] - public async Task WhenSwitchThenErrorAsyncInvokedDirectly_ThenErrorForwarded() + public async Task WhenForwardErrorAfterContextSwitchAsyncInvokedDirectly_ThenErrorForwarded() { var captured = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var downstream = new CapturingAsyncObserver(captured); - var sut = new ObserveOnAsyncSignal.ObserveOnObserver(downstream, AsyncContext.Default, forceYielding: true); + var sut = new WitnessOnAsyncSignal.ContextSwitchObserver(downstream, AsyncContext.Default, forceYielding: true); var expected = new InvalidOperationException("slow-path-error"); - await sut.SwitchThenErrorAsync(expected, CancellationToken.None); + await sut.ForwardErrorAfterContextSwitchAsync(expected, CancellationToken.None); var received = await captured.Task.WaitAsync(TimeSpan.FromSeconds(5)); await Assert.That(received).IsSameReferenceAs(expected); } - /// Verifies ObserveOnObserver.SwitchThenCompletedAsync by calling it directly. + /// Verifies ContextSwitchObserver.ForwardCompletionAfterContextSwitchAsync by calling it directly. /// A representing the asynchronous test operation. [Test] - public async Task WhenSwitchThenCompletedAsyncInvokedDirectly_ThenCompletionForwarded() + public async Task WhenForwardCompletionAfterContextSwitchAsyncInvokedDirectly_ThenCompletionForwarded() { var captured = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var downstream = new CapturingAsyncObserver(captured); - var sut = new ObserveOnAsyncSignal.ObserveOnObserver(downstream, AsyncContext.Default, forceYielding: true); + var sut = new WitnessOnAsyncSignal.ContextSwitchObserver(downstream, AsyncContext.Default, forceYielding: true); - await sut.SwitchThenCompletedAsync(Result.Success); + await sut.ForwardCompletionAfterContextSwitchAsync(Result.Success); var result = await captured.Task.WaitAsync(TimeSpan.FromSeconds(5)); await Assert.That(result.IsSuccess).IsTrue(); diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/TakeUntilOperatorTests.CompletionDelegate.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/TakeUntilOperatorTests.CompletionDelegate.cs index c70bc5e..263073b 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/TakeUntilOperatorTests.CompletionDelegate.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/TakeUntilOperatorTests.CompletionDelegate.cs @@ -188,7 +188,7 @@ public async Task WhenTakeUntilOptionsSourceFailsWhenOtherFailsTrue_ThenProperty /// /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilPredicateSourceThrowsOnSubscribe_ThenDisposesAndRethrows() + public async Task WhenPredicateStopSignalSourceThrowsOnSubscribe_ThenDisposesAndRethrows() { var throwingSource = SignalAsync.Create((_, _) => throw new InvalidOperationException(SubscribeFailedMessage)); @@ -202,7 +202,7 @@ await Assert.ThrowsAsync(async () => /// /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilPredicateBecomesTrueMidStream_ThenStopsEmitting() + public async Task WhenPredicateStopSignalBecomesTrueMidStream_ThenStopsEmitting() { var result = await SignalAsync.Range(1, 10) .TakeUntil(x => x > 3) @@ -216,7 +216,7 @@ public async Task WhenTakeUntilPredicateBecomesTrueMidStream_ThenStopsEmitting() /// /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilCancellationTokenSourceThrowsOnSubscribe_ThenDisposesAndRethrows() + public async Task WhenCancellationStopSignalSourceThrowsOnSubscribe_ThenDisposesAndRethrows() { using var cts = new CancellationTokenSource(); var throwingSource = SignalAsync.Create((_, _) => @@ -286,7 +286,7 @@ await Assert.ThrowsAsync(async () => /// /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilTaskSourceThrowsOnSubscribe_ThenDisposesAndRethrows() + public async Task WhenTaskStopSignalSourceThrowsOnSubscribe_ThenDisposesAndRethrows() { var tcs = new TaskCompletionSource(); var throwingSource = SignalAsync.Create((_, _) => @@ -301,7 +301,7 @@ await Assert.ThrowsAsync(async () => /// /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilTaskCompletesMidStream_ThenStopsEmissions() + public async Task WhenTaskStopSignalCompletesMidStream_ThenStopsEmissions() { var tcs = new TaskCompletionSource(); var source = Signal.Create(); @@ -426,7 +426,7 @@ public async Task WhenTakeUntilAsyncPredicateBecomesTrueMidStream_ThenStopsEmitt /// /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilTask_ThenStopsWhenTaskCompletes() + public async Task WhenTaskStopSignal_ThenStopsWhenTaskCompletes() { var tcs = new TaskCompletionSource(); var signal = Signal.Create(); @@ -456,7 +456,7 @@ public async Task WhenTakeUntilTask_ThenStopsWhenTaskCompletes() /// /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilCancellationToken_ThenStopsWhenCanceled() + public async Task WhenCancellationStopSignal_ThenStopsWhenCanceled() { using var cts = new CancellationTokenSource(); var signal = Signal.Create(); @@ -517,7 +517,7 @@ IAsyncDisposable CompletionSignal(Action notifyStop) /// /// Tests TakeUntil(CompletionSignalDelegate) where the stop signal fails and option is false, - /// exercising the error resume path in WaitAndComplete. + /// exercising the error resume path in AwaitStopThenComplete. /// /// A representing the asynchronous test operation. [Test] @@ -564,11 +564,11 @@ await AsyncTestHelpers.WaitForConditionAsync( /// /// Tests TakeUntil(Task) where the task fails and option is false, - /// exercising the error resume path in WaitAndComplete. + /// exercising the error resume path in AwaitStopThenComplete. /// /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilTaskFailsAndOptionFalse_ThenErrorResumeForwardedViaWaitAndComplete() + public async Task WhenTaskStopSignalFailsAndOptionFalse_ThenErrorResumeForwardedViaAwaitStopThenComplete() { var source = Signal.Create(); var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/TakeUntilOperatorTests.DisposalAndErrors.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/TakeUntilOperatorTests.DisposalAndErrors.cs index f340240..06a91f7 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/TakeUntilOperatorTests.DisposalAndErrors.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/TakeUntilOperatorTests.DisposalAndErrors.cs @@ -17,7 +17,7 @@ public partial class TakeUntilOperatorTests /// /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilPredicateDisposed_ThenSubscriptionIsDisposed() + public async Task WhenPredicateStopSignalDisposed_ThenSubscriptionIsDisposed() { var source = Signal.Create(); var items = new List(); @@ -43,11 +43,11 @@ public async Task WhenTakeUntilPredicateDisposed_ThenSubscriptionIsDisposed() /// /// Tests TakeUntil(CancellationToken) where the token is cancelled - /// and the observer's OnTokenCanceled catch path is exercised. + /// and the observer's CompleteFromCancellation catch path is exercised. /// /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilCancellationTokenCanceled_ThenCompletionForwarded() + public async Task WhenCancellationStopSignalCanceled_ThenCompletionForwarded() { using var cts = new CancellationTokenSource(); var source = Signal.Create(); @@ -159,7 +159,7 @@ await AsyncTestHelpers.WaitForConditionAsync( /// /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilTask_ThenStopsOnTaskCompletion() + public async Task WhenTaskStopSignal_ThenStopsOnTaskCompletion() { var tcs = new TaskCompletionSource(); var source = Signal.Create(); @@ -197,7 +197,7 @@ await AsyncTestHelpers.WaitForConditionAsync( /// /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilCancellationToken_ThenStopsOnCancellation() + public async Task WhenCancellationStopSignal_ThenStopsOnCancellation() { using var cts = new CancellationTokenSource(); var source = Signal.Create(); @@ -228,7 +228,7 @@ await AsyncTestHelpers.WaitForConditionAsync( /// /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilPredicate_ThenStopsWhenPredicateTrue() + public async Task WhenPredicateStopSignal_ThenStopsWhenPredicateTrue() { var result = await SignalAsync.Range(1, 10) .TakeUntil(x => x > 3) @@ -353,7 +353,7 @@ await AsyncTestHelpers.WaitForConditionAsync( /// /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilTaskWaitAndCompleteFailsOptionFalse_ThenErrorResumeForwarded() + public async Task WhenTaskStopSignalAwaitStopThenCompleteFailsOptionFalse_ThenErrorResumeForwarded() { var source = Signal.Create(); var errors = new List(); @@ -385,7 +385,7 @@ await AsyncTestHelpers.WaitForConditionAsync( /// /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilTaskWaitAndCompleteFailsOptionTrue_ThenCompletesWithFailure() + public async Task WhenTaskStopSignalAwaitStopThenCompleteFailsOptionTrue_ThenCompletesWithFailure() { var source = Signal.Create(); Result? completionResult = null; @@ -415,7 +415,7 @@ await AsyncTestHelpers.WaitForConditionAsync( /// /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilTaskSourceEmitsErrorResume_ThenErrorIsForwarded() + public async Task WhenTaskStopSignalSourceEmitsErrorResume_ThenErrorIsForwarded() { var errors = new List(); var neverTask = new TaskCompletionSource().Task; @@ -475,7 +475,7 @@ public async Task WhenTakeUntilDelegateDisposedDuringWait_ThenCancelsCleanly() await source.OnNextAsync(1, CancellationToken.None); - // Dispose while WaitAndComplete is still waiting for the signal + // Dispose while AwaitStopThenComplete is still waiting for the signal await sub.DisposeAsync(); } @@ -587,7 +587,7 @@ public async Task WhenTakeUntilDelegateForwardingThrows_ThenOuterCatchSwallows() null, _ => throw new InvalidOperationException("observer completion throws")); - // Fire the stop signal with success; ForwardOnCompletedAsync will throw because observer throws + // Fire the stop signal with success; OnCompletedAsync will throw because observer throws storedNotifyStop!(Result.Success); // The outer catch block should swallow the exception; no crash @@ -604,7 +604,7 @@ await AsyncTestHelpers.WaitForConditionAsync( /// /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilTaskForwardingThrows_ThenOuterCatchSwallows() + public async Task WhenTaskStopSignalForwardingThrows_ThenOuterCatchSwallows() { var tcs = new TaskCompletionSource(); var source = Signal.Create(); @@ -616,7 +616,7 @@ public async Task WhenTakeUntilTaskForwardingThrows_ThenOuterCatchSwallows() null, _ => throw new InvalidOperationException("observer completion throws")); - // Complete the task; ForwardOnCompletedAsync will throw because observer throws + // Complete the task; OnCompletedAsync will throw because observer throws tcs.SetResult(); // The outer catch block should swallow the exception; no crash @@ -629,11 +629,11 @@ await AsyncTestHelpers.WaitForConditionAsync( /// /// Verifies that when TakeUntil(CancellationToken) forwards completion and the observer throws, - /// the outer catch block in OnTokenCanceled swallows the exception. + /// the outer catch block in CompleteFromCancellation swallows the exception. /// /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilCancellationTokenForwardingThrows_ThenOuterCatchSwallows() + public async Task WhenCancellationStopSignalForwardingThrows_ThenOuterCatchSwallows() { using var cts = new CancellationTokenSource(); var source = Signal.Create(); @@ -645,7 +645,7 @@ public async Task WhenTakeUntilCancellationTokenForwardingThrows_ThenOuterCatchS null, _ => throw new InvalidOperationException("observer completion throws")); - // Cancel the token; OnTokenCanceled will call ForwardOnCompletedAsync which will throw + // Cancel the token; CompleteFromCancellation will call OnCompletedAsync which will throw await cts.CancelAsync(); // The outer catch block should swallow the exception; no crash @@ -658,8 +658,8 @@ await AsyncTestHelpers.WaitForConditionAsync( /// /// Verifies that when TakeUntil(CompletionSignalDelegate) with SourceFailsWhenOtherFails=false - /// signals an error, ForwardOnErrorResumeAsync is called. If that also throws, the outer catch swallows it. - /// Covers the outermost catch in WaitAndComplete for CompletionSignalDelegate. + /// signals an error, OnErrorResumeAsync is called. If that also throws, the outer catch swallows it. + /// Covers the outermost catch in AwaitStopThenComplete for CompletionSignalDelegate. /// /// A representing the asynchronous test operation. [Test] @@ -682,7 +682,7 @@ public async Task WhenTakeUntilDelegateErrorResumeThrows_ThenOuterCatchSwallows( (_, _) => throw new InvalidOperationException("error resume throws"), _ => throw new InvalidOperationException("completion throws")); - // Signal a failure; SourceFailsWhenOtherFails=false so ForwardOnErrorResumeAsync is called, which throws + // Signal a failure; SourceFailsWhenOtherFails=false so OnErrorResumeAsync is called, which throws storedNotifyStop!(Result.Failure(new InvalidOperationException("stop error"))); // The outer catch block should swallow the exception @@ -695,12 +695,12 @@ await AsyncTestHelpers.WaitForConditionAsync( /// /// Verifies that when TakeUntil(Task) with SourceFailsWhenOtherFails=false - /// the task faults and ForwardOnErrorResumeAsync throws, the outer catch swallows it. - /// Covers the outermost catch in WaitAndComplete for Task. + /// the task faults and OnErrorResumeAsync throws, the outer catch swallows it. + /// Covers the outermost catch in AwaitStopThenComplete for Task. /// /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilTaskErrorResumeThrows_ThenOuterCatchSwallows() + public async Task WhenTaskStopSignalErrorResumeThrows_ThenOuterCatchSwallows() { var tcs = new TaskCompletionSource(); var source = Signal.Create(); @@ -713,7 +713,7 @@ public async Task WhenTakeUntilTaskErrorResumeThrows_ThenOuterCatchSwallows() (_, _) => throw new InvalidOperationException("error resume throws"), _ => throw new InvalidOperationException("completion throws")); - // Fault the task; SourceFailsWhenOtherFails=false so ForwardOnErrorResumeAsync is called, which throws + // Fault the task; SourceFailsWhenOtherFails=false so OnErrorResumeAsync is called, which throws tcs.SetException(new InvalidOperationException("task error")); // The outer catch block should swallow the exception @@ -755,7 +755,7 @@ public async Task WhenTakeUntilAsyncPredicate_ThenStopsWhenTrue() /// Tests TakeUntil with CancellationToken completes when token fires. /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilCancellationToken_ThenCompletesOnCancel() + public async Task WhenCancellationStopSignal_ThenCompletesOnCancel() { using var cts = new CancellationTokenSource(); var source = new DirectSource(); @@ -785,7 +785,7 @@ public async Task WhenTakeUntilCancellationToken_ThenCompletesOnCancel() /// Tests TakeUntil with Task completes when task finishes. /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilTaskCompletes_ThenSourceCompletes() + public async Task WhenTaskStopSignalCompletes_ThenSourceCompletes() { var tcs = new TaskCompletionSource(); var source = new DirectSource(); diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/TakeUntilOperatorTests.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/TakeUntilOperatorTests.cs index 685c5a6..184b545 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/TakeUntilOperatorTests.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/TakeUntilOperatorTests.cs @@ -145,7 +145,7 @@ public async Task WhenTakeUntilObservableOtherCompletesSuccess_ThenSourceContinu await source.OnNextAsync(1, CancellationToken.None); await other.OnCompletedAsync(Result.Success); - // Other completed with success � according to OtherObserver.OnCompletedAsyncCore, success returns default (no-op) + // Other completed with success � according to StopSignalWitness.OnCompletedAsyncCore, success returns default (no-op) // Source should still be active await source.OnNextAsync(SecondItem, CancellationToken.None); @@ -233,7 +233,7 @@ public async Task WhenTakeUntilObservableDisposed_ThenStopsEmissions() /// Tests that TakeUntil(Task) with null source throws. [Test] - public void WhenTakeUntilTaskNullSource_ThenThrowsArgumentNull() + public void WhenTaskStopSignalNullSource_ThenThrowsArgumentNull() { const IObservableAsync Source = null!; Assert.Throws(() => @@ -243,7 +243,7 @@ public void WhenTakeUntilTaskNullSource_ThenThrowsArgumentNull() /// Tests that task failure with SourceFailsWhenOtherFails=true completes with failure. /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilTaskFailsAndOptionTrue_ThenCompletesWithFailure() + public async Task WhenTaskStopSignalFailsAndOptionTrue_ThenCompletesWithFailure() { var tcs = new TaskCompletionSource(); var source = Signal.Create(); @@ -270,7 +270,7 @@ public async Task WhenTakeUntilTaskFailsAndOptionTrue_ThenCompletesWithFailure() /// Tests that task failure with default options sends error resume instead of failure. /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilTaskFailsAndOptionFalse_ThenSendsErrorResume() + public async Task WhenTaskStopSignalFailsAndOptionFalse_ThenSendsErrorResume() { var tcs = new TaskCompletionSource(); var source = Signal.Create(); @@ -318,7 +318,7 @@ public async Task WhenTakeUntilAlreadyCompletedTask_ThenCompletesImmediately() /// Tests disposal of TakeUntil(Task) stops emissions. /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilTaskDisposed_ThenStopsEmissions() + public async Task WhenTaskStopSignalDisposed_ThenStopsEmissions() { var tcs = new TaskCompletionSource(); var source = Signal.Create(); @@ -346,7 +346,7 @@ public async Task WhenTakeUntilTaskDisposed_ThenStopsEmissions() /// Tests that source error resume is forwarded through TakeUntil(Task). /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilTaskSourceErrorResume_ThenForwarded() + public async Task WhenTaskStopSignalSourceErrorResume_ThenForwarded() { var tcs = new TaskCompletionSource(); var source = Signal.Create(); @@ -396,7 +396,7 @@ public async Task WhenTakeUntilAlreadyCanceledToken_ThenCompletesImmediately() /// Tests that source error resume is forwarded through TakeUntil(CancellationToken). /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilCancellationTokenSourceErrorResume_ThenForwarded() + public async Task WhenCancellationStopSignalSourceErrorResume_ThenForwarded() { using var cts = new CancellationTokenSource(); await using var source = Signal.Create(); @@ -420,7 +420,7 @@ public async Task WhenTakeUntilCancellationTokenSourceErrorResume_ThenForwarded( /// Tests that source completion is forwarded through TakeUntil(CancellationToken). /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilCancellationTokenSourceCompletes_ThenCompletionForwarded() + public async Task WhenCancellationStopSignalSourceCompletes_ThenCompletionForwarded() { using var cts = new CancellationTokenSource(); var source = Signal.Create(); @@ -446,7 +446,7 @@ public async Task WhenTakeUntilCancellationTokenSourceCompletes_ThenCompletionFo /// Tests that disposal of TakeUntil(CancellationToken) stops emissions. /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilCancellationTokenDisposed_ThenStopsEmissions() + public async Task WhenCancellationStopSignalDisposed_ThenStopsEmissions() { using var cts = new CancellationTokenSource(); var source = Signal.Create(); @@ -474,7 +474,7 @@ public async Task WhenTakeUntilCancellationTokenDisposed_ThenStopsEmissions() /// Tests that predicate never returning true emits all elements. /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilPredicateNeverTrue_ThenEmitsAllElements() + public async Task WhenPredicateStopSignalNeverTrue_ThenEmitsAllElements() { var result = await SignalAsync.Range(1, 5) .TakeUntil(_ => false) @@ -486,7 +486,7 @@ public async Task WhenTakeUntilPredicateNeverTrue_ThenEmitsAllElements() /// Tests that predicate returning true on first element emits nothing. /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilPredicateTrueOnFirst_ThenEmitsNothing() + public async Task WhenPredicateStopSignalTrueOnFirst_ThenEmitsNothing() { var result = await SignalAsync.Range(1, 5) .TakeUntil(_ => true) @@ -498,7 +498,7 @@ public async Task WhenTakeUntilPredicateTrueOnFirst_ThenEmitsNothing() /// Tests that source error resume is forwarded through TakeUntil(predicate). /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilPredicateSourceErrorResume_ThenForwarded() + public async Task WhenPredicateStopSignalSourceErrorResume_ThenForwarded() { var source = SignalAsync.Create(async (observer, ct) => { @@ -533,7 +533,7 @@ public async Task WhenTakeUntilPredicateSourceErrorResume_ThenForwarded() /// Tests that source completion with failure is forwarded through TakeUntil(predicate). /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilPredicateSourceFails_ThenFailureForwarded() + public async Task WhenPredicateStopSignalSourceFails_ThenFailureForwarded() { var source = SignalAsync.Create(async (observer, ct) => { @@ -729,7 +729,7 @@ public async Task WhenTakeUntilOtherWithCancellationToken_ThenCompletesOnCancell /// Verifies the two-argument TakeUntil(task, cancellationToken) overload. /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilTaskWithCancellationToken_ThenCompletesOnCancellation() + public async Task WhenTaskStopSignalWithCancellationToken_ThenCompletesOnCancellation() { using var cts = new CancellationTokenSource(); var source = Signal.Create(); @@ -753,7 +753,7 @@ public async Task WhenTakeUntilTaskWithCancellationToken_ThenCompletesOnCancella /// CT-linked branch. /// A representing the asynchronous test operation. [Test] - public async Task WhenTakeUntilPredicateWithCancellationToken_ThenCompletesOnCancellation() + public async Task WhenPredicateStopSignalWithCancellationToken_ThenCompletesOnCancellation() { using var cts = new CancellationTokenSource(); var source = Signal.Create(); diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/TerminalOperatorTests.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/TerminalOperatorTests.cs index 8dabc53..44d8569 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/TerminalOperatorTests.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/TerminalOperatorTests.cs @@ -60,7 +60,7 @@ public async Task WhenFirstAsyncWithPredicate_ThenReturnsFirstMatch() } /// Tests FirstAsync on empty throws InvalidOperationException with the no-elements - /// message — exercises the predicate-null branch of FirstAsyncObserver.OnCompletedAsyncCore. + /// message — exercises the predicate-null branch of FirstTaskWitness.OnCompletedAsyncCore. /// A representing the asynchronous test operation. [Test] public async Task WhenFirstAsyncOnEmpty_ThenThrowsInvalidOperation() diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/TransformationOperatorTests.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/TransformationOperatorTests.cs index 2560c6a..7f73066 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/TransformationOperatorTests.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/TransformationOperatorTests.cs @@ -157,7 +157,7 @@ public void WhenScanNullAccumulator_ThenThrowsArgumentNull() => SignalAsync.Return(1).Scan(0, (Func)null!)); /// Exercises the sync-action Do<T>(Action<T>, Action<Exception>, Action<Result>) - /// overload's non-null-callback branches in DoSyncObserver's OnNext / OnErrorResume / OnCompleted. + /// overload's non-null-callback branches in SyncSideEffectWitness's OnNext / OnErrorResume / OnCompleted. /// A representing the asynchronous test operation. [Test] public async Task WhenDoSyncWithAllCallbacks_ThenInvokesAndForwards() @@ -612,7 +612,7 @@ public async Task WhenObserveOnIScheduler_ThenEmitsValues() const int ExpectedThird = 3; var result = await SignalAsync.Range(1, 3) - .ObserveOn(scheduler) + .WitnessOn(scheduler) .ToListAsync(); await Assert.That(result).IsCollectionEqualTo([1, ExpectedSecond, ExpectedThird]); @@ -641,7 +641,7 @@ public async Task WhenObserveOnSourceEmitsResumableError_ThenForwardsErrorDownst }); await using var sub = await source - .ObserveOn(AsyncContext.Default) + .WitnessOn(AsyncContext.Default) .SubscribeAsync( (x, _) => { diff --git a/src/tests/ReactiveUI.Primitives.Tests/ApiApprovalTests.Primitives.DotNet10_0.verified.txt b/src/tests/ReactiveUI.Primitives.Tests/ApiApprovalTests.Primitives.DotNet10_0.verified.txt index a102e5b..22caf79 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/ApiApprovalTests.Primitives.DotNet10_0.verified.txt +++ b/src/tests/ReactiveUI.Primitives.Tests/ApiApprovalTests.Primitives.DotNet10_0.verified.txt @@ -1086,6 +1086,8 @@ namespace ReactiveUI.Primitives.Signals public static System.IObservable After(System.TimeSpan dueTime, System.TimeSpan period) { } public static System.IObservable After(System.TimeSpan dueTime, System.TimeSpan period, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } public static System.IObservable Blend(params System.IObservable[] sources) { } + public static System.IObservable> Buffer(this System.IObservable source, System.TimeSpan timeSpan) { } + public static System.IObservable> Buffer(this System.IObservable source, System.TimeSpan timeSpan, ReactiveUI.Primitives.Concurrency.ISequencer sequencer) { } public static System.IObservable Chain(params System.IObservable[] sources) { } public static System.IObservable Create(System.Func, System.IDisposable> subscribe) { } public static System.IObservable Create(System.Func, System.IDisposable> subscribe, bool isRequiredSubscribeOnCurrentThread) { } @@ -1093,6 +1095,7 @@ namespace ReactiveUI.Primitives.Signals public static System.IObservable CreateSafe(System.Func, System.IDisposable> subscribe, bool isRequiredSubscribeOnCurrentThread) { } public static System.IObservable CreateWithState(TState state, System.Func, System.IDisposable> subscribe) { } public static System.IObservable CreateWithState(TState state, System.Func, System.IDisposable> subscribe, bool isRequiredSubscribeOnCurrentThread) { } + public static System.IObservable Defer(System.Func> observableFactory) { } public static System.IObservable Emit(ReactiveUI.Primitives.RxVoid value) { } public static System.IObservable Emit(bool value) { } public static System.IObservable Emit(int value) { } @@ -1105,6 +1108,7 @@ namespace ReactiveUI.Primitives.Signals public static System.IObservable Fail(System.Exception error, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } public static System.IObservable Fail(System.Exception error, T witness) { } public static System.IObservable Fail(System.Exception error, ReactiveUI.Primitives.Concurrency.ISequencer scheduler, T witness) { } + public static System.IObservable FlatMap(this System.IObservable source, System.Func> selector) { } public static System.IObservable ForkJoin(System.IObservable left, System.IObservable right, System.Func selector) { } public static System.IObservable FromAsync(System.Func> taskFactory) { } public static System.IObservable FromAsync(System.Func> taskFactory) { } @@ -1113,6 +1117,9 @@ namespace ReactiveUI.Primitives.Signals public static System.IObservable FromAsyncEnumerable(System.Collections.Generic.IAsyncEnumerable values, System.Threading.CancellationToken cancellationToken) { } public static System.IObservable FromEnumerable(System.Collections.Generic.IEnumerable values) { } public static System.IObservable FromEnumerable(System.Collections.Generic.IEnumerable values, System.Threading.CancellationToken cancellationToken) { } + public static System.IObservable> FromEventHandler(System.Action addHandler, System.Action removeHandler) + where TEventHandler : System.Delegate + where TEventArgs : System.EventArgs { } public static System.IObservable> FromEventPattern(System.Action addHandler, System.Action removeHandler) { } public static System.IObservable> FromEventPattern(System.Action> addHandler, System.Action> removeHandler) where TEventArgs : System.EventArgs { } @@ -1158,6 +1165,9 @@ namespace ReactiveUI.Primitives.Signals public static System.IObservable Start(System.Func function) { } public static System.IObservable Start(System.Func function, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } public static System.IObservable SyncLatest(System.IObservable left, System.IObservable right, System.Func selector) { } + public static System.IObservable Throttle(this System.IObservable source, System.TimeSpan dueTime) { } + public static System.IObservable Throttle(this System.IObservable source, System.TimeSpan dueTime, ReactiveUI.Primitives.Concurrency.ISequencer sequencer) { } + public static System.Collections.Generic.IEnumerable ToEnumerable(this System.IObservable source) { } public static System.IObservable Unfold(TState initialState, System.Func condition, System.Func iterate, System.Func resultSelector) { } public static System.IObservable Use(System.Func resourceFactory, System.Func> signalFactory) where TResource : System.IDisposable { } @@ -1195,4 +1205,4 @@ namespace ReactiveUI.Primitives.Signals public static ReactiveUI.Primitives.Signals.ITaskSignal Create(System.Func, System.IObservable> observableFactory, ReactiveUI.Primitives.Concurrency.ISequencer? scheduler) { } public static ReactiveUI.Primitives.Signals.ITaskSignal Create(System.Func, System.IObservable> observableFactory, ReactiveUI.Primitives.Concurrency.ISequencer? scheduler, System.Threading.CancellationTokenSource? cancellationTokenSource) { } } -} \ No newline at end of file +} diff --git a/src/tests/ReactiveUI.Primitives.Tests/ApiApprovalTests.Primitives.DotNet8_0.verified.txt b/src/tests/ReactiveUI.Primitives.Tests/ApiApprovalTests.Primitives.DotNet8_0.verified.txt index a102e5b..22caf79 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/ApiApprovalTests.Primitives.DotNet8_0.verified.txt +++ b/src/tests/ReactiveUI.Primitives.Tests/ApiApprovalTests.Primitives.DotNet8_0.verified.txt @@ -1086,6 +1086,8 @@ namespace ReactiveUI.Primitives.Signals public static System.IObservable After(System.TimeSpan dueTime, System.TimeSpan period) { } public static System.IObservable After(System.TimeSpan dueTime, System.TimeSpan period, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } public static System.IObservable Blend(params System.IObservable[] sources) { } + public static System.IObservable> Buffer(this System.IObservable source, System.TimeSpan timeSpan) { } + public static System.IObservable> Buffer(this System.IObservable source, System.TimeSpan timeSpan, ReactiveUI.Primitives.Concurrency.ISequencer sequencer) { } public static System.IObservable Chain(params System.IObservable[] sources) { } public static System.IObservable Create(System.Func, System.IDisposable> subscribe) { } public static System.IObservable Create(System.Func, System.IDisposable> subscribe, bool isRequiredSubscribeOnCurrentThread) { } @@ -1093,6 +1095,7 @@ namespace ReactiveUI.Primitives.Signals public static System.IObservable CreateSafe(System.Func, System.IDisposable> subscribe, bool isRequiredSubscribeOnCurrentThread) { } public static System.IObservable CreateWithState(TState state, System.Func, System.IDisposable> subscribe) { } public static System.IObservable CreateWithState(TState state, System.Func, System.IDisposable> subscribe, bool isRequiredSubscribeOnCurrentThread) { } + public static System.IObservable Defer(System.Func> observableFactory) { } public static System.IObservable Emit(ReactiveUI.Primitives.RxVoid value) { } public static System.IObservable Emit(bool value) { } public static System.IObservable Emit(int value) { } @@ -1105,6 +1108,7 @@ namespace ReactiveUI.Primitives.Signals public static System.IObservable Fail(System.Exception error, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } public static System.IObservable Fail(System.Exception error, T witness) { } public static System.IObservable Fail(System.Exception error, ReactiveUI.Primitives.Concurrency.ISequencer scheduler, T witness) { } + public static System.IObservable FlatMap(this System.IObservable source, System.Func> selector) { } public static System.IObservable ForkJoin(System.IObservable left, System.IObservable right, System.Func selector) { } public static System.IObservable FromAsync(System.Func> taskFactory) { } public static System.IObservable FromAsync(System.Func> taskFactory) { } @@ -1113,6 +1117,9 @@ namespace ReactiveUI.Primitives.Signals public static System.IObservable FromAsyncEnumerable(System.Collections.Generic.IAsyncEnumerable values, System.Threading.CancellationToken cancellationToken) { } public static System.IObservable FromEnumerable(System.Collections.Generic.IEnumerable values) { } public static System.IObservable FromEnumerable(System.Collections.Generic.IEnumerable values, System.Threading.CancellationToken cancellationToken) { } + public static System.IObservable> FromEventHandler(System.Action addHandler, System.Action removeHandler) + where TEventHandler : System.Delegate + where TEventArgs : System.EventArgs { } public static System.IObservable> FromEventPattern(System.Action addHandler, System.Action removeHandler) { } public static System.IObservable> FromEventPattern(System.Action> addHandler, System.Action> removeHandler) where TEventArgs : System.EventArgs { } @@ -1158,6 +1165,9 @@ namespace ReactiveUI.Primitives.Signals public static System.IObservable Start(System.Func function) { } public static System.IObservable Start(System.Func function, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } public static System.IObservable SyncLatest(System.IObservable left, System.IObservable right, System.Func selector) { } + public static System.IObservable Throttle(this System.IObservable source, System.TimeSpan dueTime) { } + public static System.IObservable Throttle(this System.IObservable source, System.TimeSpan dueTime, ReactiveUI.Primitives.Concurrency.ISequencer sequencer) { } + public static System.Collections.Generic.IEnumerable ToEnumerable(this System.IObservable source) { } public static System.IObservable Unfold(TState initialState, System.Func condition, System.Func iterate, System.Func resultSelector) { } public static System.IObservable Use(System.Func resourceFactory, System.Func> signalFactory) where TResource : System.IDisposable { } @@ -1195,4 +1205,4 @@ namespace ReactiveUI.Primitives.Signals public static ReactiveUI.Primitives.Signals.ITaskSignal Create(System.Func, System.IObservable> observableFactory, ReactiveUI.Primitives.Concurrency.ISequencer? scheduler) { } public static ReactiveUI.Primitives.Signals.ITaskSignal Create(System.Func, System.IObservable> observableFactory, ReactiveUI.Primitives.Concurrency.ISequencer? scheduler, System.Threading.CancellationTokenSource? cancellationTokenSource) { } } -} \ No newline at end of file +} diff --git a/src/tests/ReactiveUI.Primitives.Tests/ApiApprovalTests.Primitives.DotNet9_0.verified.txt b/src/tests/ReactiveUI.Primitives.Tests/ApiApprovalTests.Primitives.DotNet9_0.verified.txt index a102e5b..22caf79 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/ApiApprovalTests.Primitives.DotNet9_0.verified.txt +++ b/src/tests/ReactiveUI.Primitives.Tests/ApiApprovalTests.Primitives.DotNet9_0.verified.txt @@ -1086,6 +1086,8 @@ namespace ReactiveUI.Primitives.Signals public static System.IObservable After(System.TimeSpan dueTime, System.TimeSpan period) { } public static System.IObservable After(System.TimeSpan dueTime, System.TimeSpan period, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } public static System.IObservable Blend(params System.IObservable[] sources) { } + public static System.IObservable> Buffer(this System.IObservable source, System.TimeSpan timeSpan) { } + public static System.IObservable> Buffer(this System.IObservable source, System.TimeSpan timeSpan, ReactiveUI.Primitives.Concurrency.ISequencer sequencer) { } public static System.IObservable Chain(params System.IObservable[] sources) { } public static System.IObservable Create(System.Func, System.IDisposable> subscribe) { } public static System.IObservable Create(System.Func, System.IDisposable> subscribe, bool isRequiredSubscribeOnCurrentThread) { } @@ -1093,6 +1095,7 @@ namespace ReactiveUI.Primitives.Signals public static System.IObservable CreateSafe(System.Func, System.IDisposable> subscribe, bool isRequiredSubscribeOnCurrentThread) { } public static System.IObservable CreateWithState(TState state, System.Func, System.IDisposable> subscribe) { } public static System.IObservable CreateWithState(TState state, System.Func, System.IDisposable> subscribe, bool isRequiredSubscribeOnCurrentThread) { } + public static System.IObservable Defer(System.Func> observableFactory) { } public static System.IObservable Emit(ReactiveUI.Primitives.RxVoid value) { } public static System.IObservable Emit(bool value) { } public static System.IObservable Emit(int value) { } @@ -1105,6 +1108,7 @@ namespace ReactiveUI.Primitives.Signals public static System.IObservable Fail(System.Exception error, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } public static System.IObservable Fail(System.Exception error, T witness) { } public static System.IObservable Fail(System.Exception error, ReactiveUI.Primitives.Concurrency.ISequencer scheduler, T witness) { } + public static System.IObservable FlatMap(this System.IObservable source, System.Func> selector) { } public static System.IObservable ForkJoin(System.IObservable left, System.IObservable right, System.Func selector) { } public static System.IObservable FromAsync(System.Func> taskFactory) { } public static System.IObservable FromAsync(System.Func> taskFactory) { } @@ -1113,6 +1117,9 @@ namespace ReactiveUI.Primitives.Signals public static System.IObservable FromAsyncEnumerable(System.Collections.Generic.IAsyncEnumerable values, System.Threading.CancellationToken cancellationToken) { } public static System.IObservable FromEnumerable(System.Collections.Generic.IEnumerable values) { } public static System.IObservable FromEnumerable(System.Collections.Generic.IEnumerable values, System.Threading.CancellationToken cancellationToken) { } + public static System.IObservable> FromEventHandler(System.Action addHandler, System.Action removeHandler) + where TEventHandler : System.Delegate + where TEventArgs : System.EventArgs { } public static System.IObservable> FromEventPattern(System.Action addHandler, System.Action removeHandler) { } public static System.IObservable> FromEventPattern(System.Action> addHandler, System.Action> removeHandler) where TEventArgs : System.EventArgs { } @@ -1158,6 +1165,9 @@ namespace ReactiveUI.Primitives.Signals public static System.IObservable Start(System.Func function) { } public static System.IObservable Start(System.Func function, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } public static System.IObservable SyncLatest(System.IObservable left, System.IObservable right, System.Func selector) { } + public static System.IObservable Throttle(this System.IObservable source, System.TimeSpan dueTime) { } + public static System.IObservable Throttle(this System.IObservable source, System.TimeSpan dueTime, ReactiveUI.Primitives.Concurrency.ISequencer sequencer) { } + public static System.Collections.Generic.IEnumerable ToEnumerable(this System.IObservable source) { } public static System.IObservable Unfold(TState initialState, System.Func condition, System.Func iterate, System.Func resultSelector) { } public static System.IObservable Use(System.Func resourceFactory, System.Func> signalFactory) where TResource : System.IDisposable { } @@ -1195,4 +1205,4 @@ namespace ReactiveUI.Primitives.Signals public static ReactiveUI.Primitives.Signals.ITaskSignal Create(System.Func, System.IObservable> observableFactory, ReactiveUI.Primitives.Concurrency.ISequencer? scheduler) { } public static ReactiveUI.Primitives.Signals.ITaskSignal Create(System.Func, System.IObservable> observableFactory, ReactiveUI.Primitives.Concurrency.ISequencer? scheduler, System.Threading.CancellationTokenSource? cancellationTokenSource) { } } -} \ No newline at end of file +} diff --git a/src/tests/ReactiveUI.Primitives.Tests/PublicApiBehaviorTests.cs b/src/tests/ReactiveUI.Primitives.Tests/PublicApiBehaviorTests.cs index c3ba79a..7e0fb71 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/PublicApiBehaviorTests.cs +++ b/src/tests/ReactiveUI.Primitives.Tests/PublicApiBehaviorTests.cs @@ -778,7 +778,7 @@ private static void CoverHigherOrderOperatorNullGuards(IObservable source) Assert.Throws(() => ((IObservable>)null!).Chain()); Assert.Throws(() => Signal.Chain(null!)); Assert.Throws(() => Signal.Chain(source, null!)); - Assert.Throws(() => Signal.Blend(null!)); + Assert.Throws(() => Signal.Blend((IObservable[])null!)); Assert.Throws(() => Signal.Blend(source, null!)); Assert.Throws(() => Signal.Race(null!)); Assert.Throws(() => Signal.Race(source, null!)); From c44410f87c27d2c7c1c2770c73d819c18a671783 Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Fri, 5 Jun 2026 02:55:31 +0100 Subject: [PATCH 02/15] Add FlatMapValues overload and update tests Introduce new FlatMap overload that accepts a collection selector and result selector (returns FlatMapResultSignal) and add a FlatMapValues extension for projecting each source value to an IEnumerable and emitting its items. Add null-argument guard tests and a behavior test for FlatMapValues, and refresh API approval snapshots to reflect the public API changes (FlatMapValues, Collect/Buffer renames, EmitIfQuiet, FromEventPattern generic signature changes, and removal of Throttle entries). --- .../SignalOperatorParityMixins.cs | 60 +++++++++---------- ...alTests.Primitives.DotNet10_0.verified.txt | 16 ++--- ...valTests.Primitives.DotNet8_0.verified.txt | 16 ++--- ...valTests.Primitives.DotNet9_0.verified.txt | 16 ++--- .../InternalInfrastructureCoverageTests.cs | 2 + .../PublicApiBehaviorTests.cs | 13 ++++ 6 files changed, 69 insertions(+), 54 deletions(-) diff --git a/src/ReactiveUI.Primitives/SignalOperatorParityMixins.cs b/src/ReactiveUI.Primitives/SignalOperatorParityMixins.cs index 1f49ac8..f902eb6 100644 --- a/src/ReactiveUI.Primitives/SignalOperatorParityMixins.cs +++ b/src/ReactiveUI.Primitives/SignalOperatorParityMixins.cs @@ -472,6 +472,35 @@ public static IObservable FlatMap(this IObservable(source, selector); } + /// + /// Projects each source value to an inner signal and maps outer/inner values with a result selector. + /// + /// The source value type. + /// The inner value type. + /// The result value type. + /// The source sequence. + /// The function that projects each source value to an inner sequence. + /// The function that combines source and inner values. + /// A sequence containing selected outer/inner combinations. + /// or is . + public static IObservable FlatMap( + this IObservable source, + Func> collectionSelector, + Func resultSelector) + { + if (collectionSelector == null) + { + throw new ArgumentNullException(nameof(collectionSelector)); + } + + if (resultSelector == null) + { + throw new ArgumentNullException(nameof(resultSelector)); + } + + return new FlatMapResultSignal(source, collectionSelector, resultSelector); + } + /// /// Projects each value into an enumerable and emits every projected item. /// @@ -481,7 +510,7 @@ public static IObservable FlatMap(this IObservableThe projection that returns items for each source value. /// A signal that emits every item returned by . /// or is . - public static IObservable FlatMap(this IObservable source, Func> selector) + public static IObservable FlatMapValues(this IObservable source, Func> selector) { if (source == null) { @@ -506,35 +535,6 @@ public static IObservable FlatMap(this IObservable - /// Projects each source value to an inner signal and maps outer/inner values with a result selector. - /// - /// The source value type. - /// The inner value type. - /// The result value type. - /// The source sequence. - /// The function that projects each source value to an inner sequence. - /// The function that combines source and inner values. - /// A sequence containing selected outer/inner combinations. - /// or is . - public static IObservable FlatMap( - this IObservable source, - Func> collectionSelector, - Func resultSelector) - { - if (collectionSelector == null) - { - throw new ArgumentNullException(nameof(collectionSelector)); - } - - if (resultSelector == null) - { - throw new ArgumentNullException(nameof(resultSelector)); - } - - return new FlatMapResultSignal(source, collectionSelector, resultSelector); - } - /// /// Counts the source values as an . /// diff --git a/src/tests/ReactiveUI.Primitives.Tests/ApiApprovalTests.Primitives.DotNet10_0.verified.txt b/src/tests/ReactiveUI.Primitives.Tests/ApiApprovalTests.Primitives.DotNet10_0.verified.txt index 22caf79..3b204e2 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/ApiApprovalTests.Primitives.DotNet10_0.verified.txt +++ b/src/tests/ReactiveUI.Primitives.Tests/ApiApprovalTests.Primitives.DotNet10_0.verified.txt @@ -247,6 +247,7 @@ namespace ReactiveUI.Primitives public static System.Threading.Tasks.Task FirstOrDefaultAsync(this System.IObservable source, T defaultValue) { } public static System.IObservable FlatMap(this System.IObservable source, System.Func> selector) { } public static System.IObservable FlatMap(this System.IObservable source, System.Func> collectionSelector, System.Func resultSelector) { } + public static System.IObservable FlatMapValues(this System.IObservable source, System.Func> selector) { } public static System.IObservable Fold(this System.IObservable source, TAccumulate seed, System.Func accumulator) { } public static System.IObservable ForkJoin(this System.IObservable left, System.IObservable right, System.Func selector) { } public static System.IObservable FuseLatest(this System.IObservable left, System.IObservable right, System.Func selector) { } @@ -1086,9 +1087,9 @@ namespace ReactiveUI.Primitives.Signals public static System.IObservable After(System.TimeSpan dueTime, System.TimeSpan period) { } public static System.IObservable After(System.TimeSpan dueTime, System.TimeSpan period, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } public static System.IObservable Blend(params System.IObservable[] sources) { } - public static System.IObservable> Buffer(this System.IObservable source, System.TimeSpan timeSpan) { } - public static System.IObservable> Buffer(this System.IObservable source, System.TimeSpan timeSpan, ReactiveUI.Primitives.Concurrency.ISequencer sequencer) { } public static System.IObservable Chain(params System.IObservable[] sources) { } + public static System.IObservable> Collect(this System.IObservable source, System.TimeSpan timeSpan) { } + public static System.IObservable> Collect(this System.IObservable source, System.TimeSpan timeSpan, ReactiveUI.Primitives.Concurrency.ISequencer sequencer) { } public static System.IObservable Create(System.Func, System.IDisposable> subscribe) { } public static System.IObservable Create(System.Func, System.IDisposable> subscribe, bool isRequiredSubscribeOnCurrentThread) { } public static System.IObservable CreateSafe(System.Func, System.IDisposable> subscribe) { } @@ -1101,6 +1102,8 @@ namespace ReactiveUI.Primitives.Signals public static System.IObservable Emit(int value) { } public static System.IObservable Emit(T value) { } public static System.IObservable Emit(T value, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + public static System.IObservable EmitIfQuiet(this System.IObservable source, System.TimeSpan dueTime) { } + public static System.IObservable EmitIfQuiet(this System.IObservable source, System.TimeSpan dueTime, ReactiveUI.Primitives.Concurrency.ISequencer sequencer) { } public static System.IObservable EmitRxVoid() { } public static System.IObservable Every(System.TimeSpan period) { } public static System.IObservable Every(System.TimeSpan period, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } @@ -1108,7 +1111,6 @@ namespace ReactiveUI.Primitives.Signals public static System.IObservable Fail(System.Exception error, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } public static System.IObservable Fail(System.Exception error, T witness) { } public static System.IObservable Fail(System.Exception error, ReactiveUI.Primitives.Concurrency.ISequencer scheduler, T witness) { } - public static System.IObservable FlatMap(this System.IObservable source, System.Func> selector) { } public static System.IObservable ForkJoin(System.IObservable left, System.IObservable right, System.Func selector) { } public static System.IObservable FromAsync(System.Func> taskFactory) { } public static System.IObservable FromAsync(System.Func> taskFactory) { } @@ -1117,12 +1119,12 @@ namespace ReactiveUI.Primitives.Signals public static System.IObservable FromAsyncEnumerable(System.Collections.Generic.IAsyncEnumerable values, System.Threading.CancellationToken cancellationToken) { } public static System.IObservable FromEnumerable(System.Collections.Generic.IEnumerable values) { } public static System.IObservable FromEnumerable(System.Collections.Generic.IEnumerable values, System.Threading.CancellationToken cancellationToken) { } - public static System.IObservable> FromEventHandler(System.Action addHandler, System.Action removeHandler) - where TEventHandler : System.Delegate - where TEventArgs : System.EventArgs { } public static System.IObservable> FromEventPattern(System.Action addHandler, System.Action removeHandler) { } public static System.IObservable> FromEventPattern(System.Action> addHandler, System.Action> removeHandler) where TEventArgs : System.EventArgs { } + public static System.IObservable> FromEventPattern(System.Action addHandler, System.Action removeHandler) + where TEventHandler : System.Delegate + where TEventArgs : System.EventArgs { } public static ReactiveUI.Primitives.Signals.ITaskSignal FromTask(System.Func> execution) { } public static ReactiveUI.Primitives.Signals.ITaskSignal FromTask(System.Func> execution, ReactiveUI.Primitives.Concurrency.ISequencer? scheduler) { } public static ReactiveUI.Primitives.Signals.ITaskSignal FromTask(System.Func> execution, ReactiveUI.Primitives.Concurrency.ISequencer? scheduler, System.Threading.CancellationTokenSource? cancellationTokenSource) { } @@ -1165,8 +1167,6 @@ namespace ReactiveUI.Primitives.Signals public static System.IObservable Start(System.Func function) { } public static System.IObservable Start(System.Func function, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } public static System.IObservable SyncLatest(System.IObservable left, System.IObservable right, System.Func selector) { } - public static System.IObservable Throttle(this System.IObservable source, System.TimeSpan dueTime) { } - public static System.IObservable Throttle(this System.IObservable source, System.TimeSpan dueTime, ReactiveUI.Primitives.Concurrency.ISequencer sequencer) { } public static System.Collections.Generic.IEnumerable ToEnumerable(this System.IObservable source) { } public static System.IObservable Unfold(TState initialState, System.Func condition, System.Func iterate, System.Func resultSelector) { } public static System.IObservable Use(System.Func resourceFactory, System.Func> signalFactory) diff --git a/src/tests/ReactiveUI.Primitives.Tests/ApiApprovalTests.Primitives.DotNet8_0.verified.txt b/src/tests/ReactiveUI.Primitives.Tests/ApiApprovalTests.Primitives.DotNet8_0.verified.txt index 22caf79..3b204e2 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/ApiApprovalTests.Primitives.DotNet8_0.verified.txt +++ b/src/tests/ReactiveUI.Primitives.Tests/ApiApprovalTests.Primitives.DotNet8_0.verified.txt @@ -247,6 +247,7 @@ namespace ReactiveUI.Primitives public static System.Threading.Tasks.Task FirstOrDefaultAsync(this System.IObservable source, T defaultValue) { } public static System.IObservable FlatMap(this System.IObservable source, System.Func> selector) { } public static System.IObservable FlatMap(this System.IObservable source, System.Func> collectionSelector, System.Func resultSelector) { } + public static System.IObservable FlatMapValues(this System.IObservable source, System.Func> selector) { } public static System.IObservable Fold(this System.IObservable source, TAccumulate seed, System.Func accumulator) { } public static System.IObservable ForkJoin(this System.IObservable left, System.IObservable right, System.Func selector) { } public static System.IObservable FuseLatest(this System.IObservable left, System.IObservable right, System.Func selector) { } @@ -1086,9 +1087,9 @@ namespace ReactiveUI.Primitives.Signals public static System.IObservable After(System.TimeSpan dueTime, System.TimeSpan period) { } public static System.IObservable After(System.TimeSpan dueTime, System.TimeSpan period, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } public static System.IObservable Blend(params System.IObservable[] sources) { } - public static System.IObservable> Buffer(this System.IObservable source, System.TimeSpan timeSpan) { } - public static System.IObservable> Buffer(this System.IObservable source, System.TimeSpan timeSpan, ReactiveUI.Primitives.Concurrency.ISequencer sequencer) { } public static System.IObservable Chain(params System.IObservable[] sources) { } + public static System.IObservable> Collect(this System.IObservable source, System.TimeSpan timeSpan) { } + public static System.IObservable> Collect(this System.IObservable source, System.TimeSpan timeSpan, ReactiveUI.Primitives.Concurrency.ISequencer sequencer) { } public static System.IObservable Create(System.Func, System.IDisposable> subscribe) { } public static System.IObservable Create(System.Func, System.IDisposable> subscribe, bool isRequiredSubscribeOnCurrentThread) { } public static System.IObservable CreateSafe(System.Func, System.IDisposable> subscribe) { } @@ -1101,6 +1102,8 @@ namespace ReactiveUI.Primitives.Signals public static System.IObservable Emit(int value) { } public static System.IObservable Emit(T value) { } public static System.IObservable Emit(T value, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + public static System.IObservable EmitIfQuiet(this System.IObservable source, System.TimeSpan dueTime) { } + public static System.IObservable EmitIfQuiet(this System.IObservable source, System.TimeSpan dueTime, ReactiveUI.Primitives.Concurrency.ISequencer sequencer) { } public static System.IObservable EmitRxVoid() { } public static System.IObservable Every(System.TimeSpan period) { } public static System.IObservable Every(System.TimeSpan period, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } @@ -1108,7 +1111,6 @@ namespace ReactiveUI.Primitives.Signals public static System.IObservable Fail(System.Exception error, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } public static System.IObservable Fail(System.Exception error, T witness) { } public static System.IObservable Fail(System.Exception error, ReactiveUI.Primitives.Concurrency.ISequencer scheduler, T witness) { } - public static System.IObservable FlatMap(this System.IObservable source, System.Func> selector) { } public static System.IObservable ForkJoin(System.IObservable left, System.IObservable right, System.Func selector) { } public static System.IObservable FromAsync(System.Func> taskFactory) { } public static System.IObservable FromAsync(System.Func> taskFactory) { } @@ -1117,12 +1119,12 @@ namespace ReactiveUI.Primitives.Signals public static System.IObservable FromAsyncEnumerable(System.Collections.Generic.IAsyncEnumerable values, System.Threading.CancellationToken cancellationToken) { } public static System.IObservable FromEnumerable(System.Collections.Generic.IEnumerable values) { } public static System.IObservable FromEnumerable(System.Collections.Generic.IEnumerable values, System.Threading.CancellationToken cancellationToken) { } - public static System.IObservable> FromEventHandler(System.Action addHandler, System.Action removeHandler) - where TEventHandler : System.Delegate - where TEventArgs : System.EventArgs { } public static System.IObservable> FromEventPattern(System.Action addHandler, System.Action removeHandler) { } public static System.IObservable> FromEventPattern(System.Action> addHandler, System.Action> removeHandler) where TEventArgs : System.EventArgs { } + public static System.IObservable> FromEventPattern(System.Action addHandler, System.Action removeHandler) + where TEventHandler : System.Delegate + where TEventArgs : System.EventArgs { } public static ReactiveUI.Primitives.Signals.ITaskSignal FromTask(System.Func> execution) { } public static ReactiveUI.Primitives.Signals.ITaskSignal FromTask(System.Func> execution, ReactiveUI.Primitives.Concurrency.ISequencer? scheduler) { } public static ReactiveUI.Primitives.Signals.ITaskSignal FromTask(System.Func> execution, ReactiveUI.Primitives.Concurrency.ISequencer? scheduler, System.Threading.CancellationTokenSource? cancellationTokenSource) { } @@ -1165,8 +1167,6 @@ namespace ReactiveUI.Primitives.Signals public static System.IObservable Start(System.Func function) { } public static System.IObservable Start(System.Func function, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } public static System.IObservable SyncLatest(System.IObservable left, System.IObservable right, System.Func selector) { } - public static System.IObservable Throttle(this System.IObservable source, System.TimeSpan dueTime) { } - public static System.IObservable Throttle(this System.IObservable source, System.TimeSpan dueTime, ReactiveUI.Primitives.Concurrency.ISequencer sequencer) { } public static System.Collections.Generic.IEnumerable ToEnumerable(this System.IObservable source) { } public static System.IObservable Unfold(TState initialState, System.Func condition, System.Func iterate, System.Func resultSelector) { } public static System.IObservable Use(System.Func resourceFactory, System.Func> signalFactory) diff --git a/src/tests/ReactiveUI.Primitives.Tests/ApiApprovalTests.Primitives.DotNet9_0.verified.txt b/src/tests/ReactiveUI.Primitives.Tests/ApiApprovalTests.Primitives.DotNet9_0.verified.txt index 22caf79..3b204e2 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/ApiApprovalTests.Primitives.DotNet9_0.verified.txt +++ b/src/tests/ReactiveUI.Primitives.Tests/ApiApprovalTests.Primitives.DotNet9_0.verified.txt @@ -247,6 +247,7 @@ namespace ReactiveUI.Primitives public static System.Threading.Tasks.Task FirstOrDefaultAsync(this System.IObservable source, T defaultValue) { } public static System.IObservable FlatMap(this System.IObservable source, System.Func> selector) { } public static System.IObservable FlatMap(this System.IObservable source, System.Func> collectionSelector, System.Func resultSelector) { } + public static System.IObservable FlatMapValues(this System.IObservable source, System.Func> selector) { } public static System.IObservable Fold(this System.IObservable source, TAccumulate seed, System.Func accumulator) { } public static System.IObservable ForkJoin(this System.IObservable left, System.IObservable right, System.Func selector) { } public static System.IObservable FuseLatest(this System.IObservable left, System.IObservable right, System.Func selector) { } @@ -1086,9 +1087,9 @@ namespace ReactiveUI.Primitives.Signals public static System.IObservable After(System.TimeSpan dueTime, System.TimeSpan period) { } public static System.IObservable After(System.TimeSpan dueTime, System.TimeSpan period, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } public static System.IObservable Blend(params System.IObservable[] sources) { } - public static System.IObservable> Buffer(this System.IObservable source, System.TimeSpan timeSpan) { } - public static System.IObservable> Buffer(this System.IObservable source, System.TimeSpan timeSpan, ReactiveUI.Primitives.Concurrency.ISequencer sequencer) { } public static System.IObservable Chain(params System.IObservable[] sources) { } + public static System.IObservable> Collect(this System.IObservable source, System.TimeSpan timeSpan) { } + public static System.IObservable> Collect(this System.IObservable source, System.TimeSpan timeSpan, ReactiveUI.Primitives.Concurrency.ISequencer sequencer) { } public static System.IObservable Create(System.Func, System.IDisposable> subscribe) { } public static System.IObservable Create(System.Func, System.IDisposable> subscribe, bool isRequiredSubscribeOnCurrentThread) { } public static System.IObservable CreateSafe(System.Func, System.IDisposable> subscribe) { } @@ -1101,6 +1102,8 @@ namespace ReactiveUI.Primitives.Signals public static System.IObservable Emit(int value) { } public static System.IObservable Emit(T value) { } public static System.IObservable Emit(T value, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + public static System.IObservable EmitIfQuiet(this System.IObservable source, System.TimeSpan dueTime) { } + public static System.IObservable EmitIfQuiet(this System.IObservable source, System.TimeSpan dueTime, ReactiveUI.Primitives.Concurrency.ISequencer sequencer) { } public static System.IObservable EmitRxVoid() { } public static System.IObservable Every(System.TimeSpan period) { } public static System.IObservable Every(System.TimeSpan period, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } @@ -1108,7 +1111,6 @@ namespace ReactiveUI.Primitives.Signals public static System.IObservable Fail(System.Exception error, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } public static System.IObservable Fail(System.Exception error, T witness) { } public static System.IObservable Fail(System.Exception error, ReactiveUI.Primitives.Concurrency.ISequencer scheduler, T witness) { } - public static System.IObservable FlatMap(this System.IObservable source, System.Func> selector) { } public static System.IObservable ForkJoin(System.IObservable left, System.IObservable right, System.Func selector) { } public static System.IObservable FromAsync(System.Func> taskFactory) { } public static System.IObservable FromAsync(System.Func> taskFactory) { } @@ -1117,12 +1119,12 @@ namespace ReactiveUI.Primitives.Signals public static System.IObservable FromAsyncEnumerable(System.Collections.Generic.IAsyncEnumerable values, System.Threading.CancellationToken cancellationToken) { } public static System.IObservable FromEnumerable(System.Collections.Generic.IEnumerable values) { } public static System.IObservable FromEnumerable(System.Collections.Generic.IEnumerable values, System.Threading.CancellationToken cancellationToken) { } - public static System.IObservable> FromEventHandler(System.Action addHandler, System.Action removeHandler) - where TEventHandler : System.Delegate - where TEventArgs : System.EventArgs { } public static System.IObservable> FromEventPattern(System.Action addHandler, System.Action removeHandler) { } public static System.IObservable> FromEventPattern(System.Action> addHandler, System.Action> removeHandler) where TEventArgs : System.EventArgs { } + public static System.IObservable> FromEventPattern(System.Action addHandler, System.Action removeHandler) + where TEventHandler : System.Delegate + where TEventArgs : System.EventArgs { } public static ReactiveUI.Primitives.Signals.ITaskSignal FromTask(System.Func> execution) { } public static ReactiveUI.Primitives.Signals.ITaskSignal FromTask(System.Func> execution, ReactiveUI.Primitives.Concurrency.ISequencer? scheduler) { } public static ReactiveUI.Primitives.Signals.ITaskSignal FromTask(System.Func> execution, ReactiveUI.Primitives.Concurrency.ISequencer? scheduler, System.Threading.CancellationTokenSource? cancellationTokenSource) { } @@ -1165,8 +1167,6 @@ namespace ReactiveUI.Primitives.Signals public static System.IObservable Start(System.Func function) { } public static System.IObservable Start(System.Func function, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } public static System.IObservable SyncLatest(System.IObservable left, System.IObservable right, System.Func selector) { } - public static System.IObservable Throttle(this System.IObservable source, System.TimeSpan dueTime) { } - public static System.IObservable Throttle(this System.IObservable source, System.TimeSpan dueTime, ReactiveUI.Primitives.Concurrency.ISequencer sequencer) { } public static System.Collections.Generic.IEnumerable ToEnumerable(this System.IObservable source) { } public static System.IObservable Unfold(TState initialState, System.Func condition, System.Func iterate, System.Func resultSelector) { } public static System.IObservable Use(System.Func resourceFactory, System.Func> signalFactory) diff --git a/src/tests/ReactiveUI.Primitives.Tests/InternalInfrastructureCoverageTests.cs b/src/tests/ReactiveUI.Primitives.Tests/InternalInfrastructureCoverageTests.cs index dac4350..437c7e8 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/InternalInfrastructureCoverageTests.cs +++ b/src/tests/ReactiveUI.Primitives.Tests/InternalInfrastructureCoverageTests.cs @@ -284,6 +284,8 @@ private static void AssertParityOperatorArgumentGuards(IObservable source) Assert.Throws(() => source.SkipWhile(null!)); Assert.Throws(() => LinqMixins.FlatMap(null!, value => Signal.Emit(value))); Assert.Throws(() => source.FlatMap(null!)); + Assert.Throws(() => LinqMixins.FlatMapValues(null!, value => [value])); + Assert.Throws(() => source.FlatMapValues(null!)); Assert.Throws(() => source.FlatMap(null!, (outer, inner) => outer + inner)); Assert.Throws(() => source.FlatMap(value => Signal.Emit(value), null!)); Assert.Throws(() => LinqMixins.Count(null!)); diff --git a/src/tests/ReactiveUI.Primitives.Tests/PublicApiBehaviorTests.cs b/src/tests/ReactiveUI.Primitives.Tests/PublicApiBehaviorTests.cs index 7e0fb71..8202f69 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/PublicApiBehaviorTests.cs +++ b/src/tests/ReactiveUI.Primitives.Tests/PublicApiBehaviorTests.cs @@ -139,6 +139,11 @@ public class PublicApiBehaviorTests /// private static readonly string[] ExpectedSelectMany = ["1:1", "1:11", "2:2", "2:12"]; + /// + /// Expected values projected from enumerable collections. + /// + private static readonly int[] ExpectedFlatMapValues = [1, Ten, Two, 20]; + /// /// Expected spark kind sequence. /// @@ -321,6 +326,12 @@ public void OperatorSurfaceCoversSuccessErrorAndEarlyTerminationBranches() .FlatMap(value => Signal.FromEnumerable([value, value + Ten]), (outer, inner) => outer + ":" + inner) .Subscribe(selectMany.Add); Assert.Equal(ExpectedSelectMany, selectMany); + + var flatMapValues = new List(); + Signal.FromEnumerable([1, Two]) + .FlatMapValues(value => [value, value * Ten]) + .Subscribe(flatMapValues.Add); + Assert.Equal(ExpectedFlatMapValues, flatMapValues); } /// @@ -817,6 +828,8 @@ private static void CoverParityOperatorNullGuards(IObservable source) Assert.Throws(() => source.SkipWhile(null!)); Assert.Throws(() => ((IObservable)null!).FlatMap(value => source)); Assert.Throws(() => source.FlatMap(null!)); + Assert.Throws(() => ((IObservable)null!).FlatMapValues(value => [value])); + Assert.Throws(() => source.FlatMapValues(null!)); Assert.Throws(() => ((IObservable)null!).Count()); Assert.Throws(() => ((IObservable)null!).LongCount()); Assert.Throws(() => ((IObservable)null!).Any()); From 837012b3be34222eccc0ecc9a2f19026a3095fab Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Fri, 5 Jun 2026 03:16:45 +0100 Subject: [PATCH 03/15] Rename ObserveOn to WitnessOn in API approvals Update API approval snapshots to reflect the rename of ObserveOn extension methods to WitnessOn for IObservableAsync. Removed the ObserveOn overloads and added matching WitnessOn overloads in the .verified files for .NET 8.0, 9.0 and 10.0 (src/tests/ReactiveUI.Primitives.Async.Tests/ApiApprovalTests.Async.DotNet8_0.verified.txt, ApiApprovalTests.Async.DotNet9_0.verified.txt, ApiApprovalTests.Async.DotNet10_0.verified.txt). This keeps the public API approvals in sync with the codebase rename. --- ...piApprovalTests.Async.DotNet10_0.verified.txt | 16 ++++++++-------- ...ApiApprovalTests.Async.DotNet8_0.verified.txt | 16 ++++++++-------- ...ApiApprovalTests.Async.DotNet9_0.verified.txt | 16 ++++++++-------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/ApiApprovalTests.Async.DotNet10_0.verified.txt b/src/tests/ReactiveUI.Primitives.Async.Tests/ApiApprovalTests.Async.DotNet10_0.verified.txt index 84737d3..7b5f98b 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/ApiApprovalTests.Async.DotNet10_0.verified.txt +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/ApiApprovalTests.Async.DotNet10_0.verified.txt @@ -292,14 +292,6 @@ namespace ReactiveUI.Primitives.Async public static ReactiveUI.Primitives.Async.IObservableAsync Never() { } public static ReactiveUI.Primitives.Async.IObservableAsync None() { } public static ReactiveUI.Primitives.Async.IObservableAsync Not(this ReactiveUI.Primitives.Async.IObservableAsync source) { } - public static ReactiveUI.Primitives.Async.IObservableAsync ObserveOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, ReactiveUI.Primitives.Async.AsyncContext asyncContext) { } - public static ReactiveUI.Primitives.Async.IObservableAsync ObserveOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } - public static ReactiveUI.Primitives.Async.IObservableAsync ObserveOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, System.Threading.SynchronizationContext synchronizationContext) { } - public static ReactiveUI.Primitives.Async.IObservableAsync ObserveOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, System.Threading.Tasks.TaskScheduler taskScheduler) { } - public static ReactiveUI.Primitives.Async.IObservableAsync ObserveOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, ReactiveUI.Primitives.Async.AsyncContext asyncContext, bool forceYielding) { } - public static ReactiveUI.Primitives.Async.IObservableAsync ObserveOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, ReactiveUI.Primitives.Concurrency.ISequencer scheduler, bool forceYielding) { } - public static ReactiveUI.Primitives.Async.IObservableAsync ObserveOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, System.Threading.SynchronizationContext synchronizationContext, bool forceYielding) { } - public static ReactiveUI.Primitives.Async.IObservableAsync ObserveOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, System.Threading.Tasks.TaskScheduler taskScheduler, bool forceYielding) { } public static ReactiveUI.Primitives.Async.IObservableAsync OfType(this ReactiveUI.Primitives.Async.IObservableAsync @this) where TResult : class { } public static ReactiveUI.Primitives.Async.IObservableAsync OnDispose(this ReactiveUI.Primitives.Async.IObservableAsync @this, System.Action disposeAction) { } @@ -422,6 +414,14 @@ namespace ReactiveUI.Primitives.Async public static ReactiveUI.Primitives.Async.IObservableAsync WhereIsNotNull(this ReactiveUI.Primitives.Async.IObservableAsync source) where T : class { } public static ReactiveUI.Primitives.Async.IObservableAsync WhereTrue(this ReactiveUI.Primitives.Async.IObservableAsync source) { } + public static ReactiveUI.Primitives.Async.IObservableAsync WitnessOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, ReactiveUI.Primitives.Async.AsyncContext asyncContext) { } + public static ReactiveUI.Primitives.Async.IObservableAsync WitnessOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + public static ReactiveUI.Primitives.Async.IObservableAsync WitnessOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, System.Threading.SynchronizationContext synchronizationContext) { } + public static ReactiveUI.Primitives.Async.IObservableAsync WitnessOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, System.Threading.Tasks.TaskScheduler taskScheduler) { } + public static ReactiveUI.Primitives.Async.IObservableAsync WitnessOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, ReactiveUI.Primitives.Async.AsyncContext asyncContext, bool forceYielding) { } + public static ReactiveUI.Primitives.Async.IObservableAsync WitnessOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, ReactiveUI.Primitives.Concurrency.ISequencer scheduler, bool forceYielding) { } + public static ReactiveUI.Primitives.Async.IObservableAsync WitnessOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, System.Threading.SynchronizationContext synchronizationContext, bool forceYielding) { } + public static ReactiveUI.Primitives.Async.IObservableAsync WitnessOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, System.Threading.Tasks.TaskScheduler taskScheduler, bool forceYielding) { } public static ReactiveUI.Primitives.Async.IObserverAsync Wrap(this ReactiveUI.Primitives.Async.IObserverAsync observer) { } public static ReactiveUI.Primitives.Async.IObservableAsync Yield(this ReactiveUI.Primitives.Async.IObservableAsync @this) { } [return: System.Runtime.CompilerServices.TupleElementNames(new string[] { diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/ApiApprovalTests.Async.DotNet8_0.verified.txt b/src/tests/ReactiveUI.Primitives.Async.Tests/ApiApprovalTests.Async.DotNet8_0.verified.txt index 84737d3..7b5f98b 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/ApiApprovalTests.Async.DotNet8_0.verified.txt +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/ApiApprovalTests.Async.DotNet8_0.verified.txt @@ -292,14 +292,6 @@ namespace ReactiveUI.Primitives.Async public static ReactiveUI.Primitives.Async.IObservableAsync Never() { } public static ReactiveUI.Primitives.Async.IObservableAsync None() { } public static ReactiveUI.Primitives.Async.IObservableAsync Not(this ReactiveUI.Primitives.Async.IObservableAsync source) { } - public static ReactiveUI.Primitives.Async.IObservableAsync ObserveOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, ReactiveUI.Primitives.Async.AsyncContext asyncContext) { } - public static ReactiveUI.Primitives.Async.IObservableAsync ObserveOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } - public static ReactiveUI.Primitives.Async.IObservableAsync ObserveOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, System.Threading.SynchronizationContext synchronizationContext) { } - public static ReactiveUI.Primitives.Async.IObservableAsync ObserveOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, System.Threading.Tasks.TaskScheduler taskScheduler) { } - public static ReactiveUI.Primitives.Async.IObservableAsync ObserveOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, ReactiveUI.Primitives.Async.AsyncContext asyncContext, bool forceYielding) { } - public static ReactiveUI.Primitives.Async.IObservableAsync ObserveOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, ReactiveUI.Primitives.Concurrency.ISequencer scheduler, bool forceYielding) { } - public static ReactiveUI.Primitives.Async.IObservableAsync ObserveOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, System.Threading.SynchronizationContext synchronizationContext, bool forceYielding) { } - public static ReactiveUI.Primitives.Async.IObservableAsync ObserveOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, System.Threading.Tasks.TaskScheduler taskScheduler, bool forceYielding) { } public static ReactiveUI.Primitives.Async.IObservableAsync OfType(this ReactiveUI.Primitives.Async.IObservableAsync @this) where TResult : class { } public static ReactiveUI.Primitives.Async.IObservableAsync OnDispose(this ReactiveUI.Primitives.Async.IObservableAsync @this, System.Action disposeAction) { } @@ -422,6 +414,14 @@ namespace ReactiveUI.Primitives.Async public static ReactiveUI.Primitives.Async.IObservableAsync WhereIsNotNull(this ReactiveUI.Primitives.Async.IObservableAsync source) where T : class { } public static ReactiveUI.Primitives.Async.IObservableAsync WhereTrue(this ReactiveUI.Primitives.Async.IObservableAsync source) { } + public static ReactiveUI.Primitives.Async.IObservableAsync WitnessOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, ReactiveUI.Primitives.Async.AsyncContext asyncContext) { } + public static ReactiveUI.Primitives.Async.IObservableAsync WitnessOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + public static ReactiveUI.Primitives.Async.IObservableAsync WitnessOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, System.Threading.SynchronizationContext synchronizationContext) { } + public static ReactiveUI.Primitives.Async.IObservableAsync WitnessOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, System.Threading.Tasks.TaskScheduler taskScheduler) { } + public static ReactiveUI.Primitives.Async.IObservableAsync WitnessOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, ReactiveUI.Primitives.Async.AsyncContext asyncContext, bool forceYielding) { } + public static ReactiveUI.Primitives.Async.IObservableAsync WitnessOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, ReactiveUI.Primitives.Concurrency.ISequencer scheduler, bool forceYielding) { } + public static ReactiveUI.Primitives.Async.IObservableAsync WitnessOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, System.Threading.SynchronizationContext synchronizationContext, bool forceYielding) { } + public static ReactiveUI.Primitives.Async.IObservableAsync WitnessOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, System.Threading.Tasks.TaskScheduler taskScheduler, bool forceYielding) { } public static ReactiveUI.Primitives.Async.IObserverAsync Wrap(this ReactiveUI.Primitives.Async.IObserverAsync observer) { } public static ReactiveUI.Primitives.Async.IObservableAsync Yield(this ReactiveUI.Primitives.Async.IObservableAsync @this) { } [return: System.Runtime.CompilerServices.TupleElementNames(new string[] { diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/ApiApprovalTests.Async.DotNet9_0.verified.txt b/src/tests/ReactiveUI.Primitives.Async.Tests/ApiApprovalTests.Async.DotNet9_0.verified.txt index 84737d3..7b5f98b 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/ApiApprovalTests.Async.DotNet9_0.verified.txt +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/ApiApprovalTests.Async.DotNet9_0.verified.txt @@ -292,14 +292,6 @@ namespace ReactiveUI.Primitives.Async public static ReactiveUI.Primitives.Async.IObservableAsync Never() { } public static ReactiveUI.Primitives.Async.IObservableAsync None() { } public static ReactiveUI.Primitives.Async.IObservableAsync Not(this ReactiveUI.Primitives.Async.IObservableAsync source) { } - public static ReactiveUI.Primitives.Async.IObservableAsync ObserveOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, ReactiveUI.Primitives.Async.AsyncContext asyncContext) { } - public static ReactiveUI.Primitives.Async.IObservableAsync ObserveOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } - public static ReactiveUI.Primitives.Async.IObservableAsync ObserveOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, System.Threading.SynchronizationContext synchronizationContext) { } - public static ReactiveUI.Primitives.Async.IObservableAsync ObserveOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, System.Threading.Tasks.TaskScheduler taskScheduler) { } - public static ReactiveUI.Primitives.Async.IObservableAsync ObserveOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, ReactiveUI.Primitives.Async.AsyncContext asyncContext, bool forceYielding) { } - public static ReactiveUI.Primitives.Async.IObservableAsync ObserveOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, ReactiveUI.Primitives.Concurrency.ISequencer scheduler, bool forceYielding) { } - public static ReactiveUI.Primitives.Async.IObservableAsync ObserveOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, System.Threading.SynchronizationContext synchronizationContext, bool forceYielding) { } - public static ReactiveUI.Primitives.Async.IObservableAsync ObserveOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, System.Threading.Tasks.TaskScheduler taskScheduler, bool forceYielding) { } public static ReactiveUI.Primitives.Async.IObservableAsync OfType(this ReactiveUI.Primitives.Async.IObservableAsync @this) where TResult : class { } public static ReactiveUI.Primitives.Async.IObservableAsync OnDispose(this ReactiveUI.Primitives.Async.IObservableAsync @this, System.Action disposeAction) { } @@ -422,6 +414,14 @@ namespace ReactiveUI.Primitives.Async public static ReactiveUI.Primitives.Async.IObservableAsync WhereIsNotNull(this ReactiveUI.Primitives.Async.IObservableAsync source) where T : class { } public static ReactiveUI.Primitives.Async.IObservableAsync WhereTrue(this ReactiveUI.Primitives.Async.IObservableAsync source) { } + public static ReactiveUI.Primitives.Async.IObservableAsync WitnessOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, ReactiveUI.Primitives.Async.AsyncContext asyncContext) { } + public static ReactiveUI.Primitives.Async.IObservableAsync WitnessOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + public static ReactiveUI.Primitives.Async.IObservableAsync WitnessOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, System.Threading.SynchronizationContext synchronizationContext) { } + public static ReactiveUI.Primitives.Async.IObservableAsync WitnessOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, System.Threading.Tasks.TaskScheduler taskScheduler) { } + public static ReactiveUI.Primitives.Async.IObservableAsync WitnessOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, ReactiveUI.Primitives.Async.AsyncContext asyncContext, bool forceYielding) { } + public static ReactiveUI.Primitives.Async.IObservableAsync WitnessOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, ReactiveUI.Primitives.Concurrency.ISequencer scheduler, bool forceYielding) { } + public static ReactiveUI.Primitives.Async.IObservableAsync WitnessOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, System.Threading.SynchronizationContext synchronizationContext, bool forceYielding) { } + public static ReactiveUI.Primitives.Async.IObservableAsync WitnessOn(this ReactiveUI.Primitives.Async.IObservableAsync @this, System.Threading.Tasks.TaskScheduler taskScheduler, bool forceYielding) { } public static ReactiveUI.Primitives.Async.IObserverAsync Wrap(this ReactiveUI.Primitives.Async.IObserverAsync observer) { } public static ReactiveUI.Primitives.Async.IObservableAsync Yield(this ReactiveUI.Primitives.Async.IObservableAsync @this) { } [return: System.Runtime.CompilerServices.TupleElementNames(new string[] { From 09ce0145685eb6cea725c7bec54d70255602b70c Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Fri, 5 Jun 2026 08:28:26 +0100 Subject: [PATCH 04/15] test: cover PR 24 codecov gaps Core signal coverage: - Add deterministic tests for Collect and EmitIfQuiet immediate, scheduled, terminal, error, and stopped-guard paths. - Add coverage for Defer, ToEnumerable, and generic FromEventPattern handler branches. - Keep Collect.Flush behavior unchanged while avoiding an unreachable canceled early-return coverage line. Async coverage: - Add tests for renamed AsyncContext and ObserverAsync internal members used by the PR diff. - Cover ObserverAsync unhandled error, disposal, and completion reporting paths. - Add non-forced WitnessOn overload tests for SynchronizationContext and TaskScheduler wrappers. Verification: - dotnet build src/ReactiveUI.Primitives.slnx -c Release --no-restore passed. - ReactiveUI.Primitives.Tests passed net8.0/net9.0/net10.0: 287/287. - ReactiveUI.Primitives.Async.Tests passed net8.0/net9.0/net10.0: 1188/1188. - MTP coverage checks show no missed added/changed PR lines for Codecov-listed files. --- .../Signals/Signal{Collect}.cs | 10 +- .../AsyncRenameCoverageTests.cs | 220 +++++++++++++ .../ObserveOnAsyncSignalTests.cs | 26 ++ .../SignalWindowAndFactoryCoverageTests.cs | 302 ++++++++++++++++++ 4 files changed, 554 insertions(+), 4 deletions(-) create mode 100644 src/tests/ReactiveUI.Primitives.Async.Tests/AsyncRenameCoverageTests.cs create mode 100644 src/tests/ReactiveUI.Primitives.Tests/SignalWindowAndFactoryCoverageTests.cs diff --git a/src/ReactiveUI.Primitives/Signals/Signal{Collect}.cs b/src/ReactiveUI.Primitives/Signals/Signal{Collect}.cs index 0a4cce9..b9c63c8 100644 --- a/src/ReactiveUI.Primitives/Signals/Signal{Collect}.cs +++ b/src/ReactiveUI.Primitives/Signals/Signal{Collect}.cs @@ -148,15 +148,17 @@ private void OnCompleted() } /// Flushes the current window if it still has buffered values. + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Roslynator", + "RCS1208:Reduce 'if' nesting", + Justification = "Keeping the positive branch avoids a standalone defensive early-return line that is canceled by terminal disposal before it can execute.")] private void Flush() { var batch = TakeScheduledBatch(); - if (batch is not { Length: > 0 }) + if (batch is { Length: > 0 }) { - return; + _observer.OnNext(batch); } - - _observer.OnNext(batch); } /// Stores a value and reports whether this value opened a new scheduled window. diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/AsyncRenameCoverageTests.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/AsyncRenameCoverageTests.cs new file mode 100644 index 0000000..d527d1f --- /dev/null +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/AsyncRenameCoverageTests.cs @@ -0,0 +1,220 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Collections.Concurrent; +using ReactiveUI.Primitives.Concurrency; +using PrimitiveAssert = ReactiveUI.Primitives.Tests.Assert; + +namespace ReactiveUI.Primitives.Async.Tests; + +/// +/// Covers renamed async internal members and scheduler adapters that are part of the current PR diff. +/// +public sealed class AsyncRenameCoverageTests +{ + /// + /// Verifies renamed default-context and sequencer scheduler members. + /// + /// A task representing the asynchronous test. + [Test] + public async Task AsyncContextRenamedMembersExposeDefaultAndSequencerSchedulerPaths() + { + var sequencer = new QueuedSequencer(); + var sequencerContext = AsyncContext.From(sequencer); + var scheduler = new AsyncContext.SequencerTaskScheduler(sequencer); + var ran = false; + + PrimitiveAssert.True(AsyncContext.Default.UsesDefaultSequencer); + PrimitiveAssert.False(sequencerContext.UsesDefaultSequencer); + PrimitiveAssert.Same(sequencer, scheduler.Sequencer); + PrimitiveAssert.True(scheduler.GetScheduledTasksForTesting() is null); + + var task = Task.Factory.StartNew( + () => ran = true, + CancellationToken.None, + TaskCreationOptions.DenyChildAttach, + scheduler); + + PrimitiveAssert.False(task.IsCompleted); + sequencer.DrainAll(); + await task.WaitAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(false); + + PrimitiveAssert.True(ran); + PrimitiveAssert.False(scheduler.TryExecuteTaskInlineForTesting(new Task(() => { }), taskWasPreviouslyQueued: false)); + } + + /// + /// Verifies renamed disposal members track and dispose an assigned source subscription. + /// + /// A task representing the asynchronous test. + [Test] + public async Task ObserverAsyncRenamedDisposalMembersTrackAssignedSubscription() + { + var disposed = 0; + var observer = new RenameCoverageObserver(); + + PrimitiveAssert.False(observer.HasDisposed); + + await observer.AssignSourceSubscriptionAsync(new CallbackAsyncDisposable(() => disposed++)).ConfigureAwait(false); + await observer.DisposeAsync().ConfigureAwait(false); + + PrimitiveAssert.True(observer.HasDisposed); + PrimitiveAssert.Equal(1, disposed); + } + + /// + /// Verifies observer disposal reports failures thrown by the assigned source subscription. + /// + /// A task representing the asynchronous test. + [Test] + public async Task ObserverAsyncDisposeReportsAssignedSubscriptionFailure() + { + using var unhandled = new UnhandledExceptionCapture(); + var expected = new InvalidOperationException("assigned-dispose"); + var observer = new RenameCoverageObserver(); + + await observer.AssignSourceSubscriptionAsync(new ThrowingAsyncDisposable(expected)).ConfigureAwait(false); + await observer.DisposeAsync().ConfigureAwait(false); + + var reported = await unhandled.WaitForAsync(expected.Message, TimeSpan.FromSeconds(5)).ConfigureAwait(false); + PrimitiveAssert.Same(expected, reported!); + } + + /// + /// Verifies renamed routes canceled and thrown handlers + /// through the unhandled exception hook. + /// + /// A task representing the asynchronous test. + [Test] + public async Task RouteObserverErrorAsyncReportsCanceledAndThrownHandlerPaths() + { + using var unhandled = new UnhandledExceptionCapture(); + var canceledObserver = new RenameCoverageObserver(); + var canceledError = new InvalidOperationException("route-canceled"); + using var cancellation = new CancellationTokenSource(); + await cancellation.CancelAsync().ConfigureAwait(false); + + await canceledObserver.RouteObserverErrorAsync(canceledError, cancellation.Token).ConfigureAwait(false); + + var canceledReported = await unhandled.WaitForAsync(canceledError.Message, TimeSpan.FromSeconds(5)).ConfigureAwait(false); + PrimitiveAssert.Same(canceledError, canceledReported!); + + var operationCanceledError = new InvalidOperationException("route-operation-canceled"); + var operationCanceledObserver = new RenameCoverageObserver((_, _) => throw new OperationCanceledException()); + + await operationCanceledObserver.RouteObserverErrorAsync(operationCanceledError, CancellationToken.None).ConfigureAwait(false); + + var operationCanceledReported = await unhandled.WaitForAsync(operationCanceledError.Message, TimeSpan.FromSeconds(5)).ConfigureAwait(false); + PrimitiveAssert.Same(operationCanceledError, operationCanceledReported!); + + var handlerError = new InvalidOperationException("route-handler"); + var throwingObserver = new RenameCoverageObserver((_, _) => throw handlerError); + + await throwingObserver.RouteObserverErrorAsync(new InvalidOperationException("source"), CancellationToken.None).ConfigureAwait(false); + + var handlerReported = await unhandled.WaitForAsync(handlerError.Message, TimeSpan.FromSeconds(5)).ConfigureAwait(false); + PrimitiveAssert.Same(handlerError, handlerReported!); + } + + /// + /// Verifies completion slow-path failures are routed through the renamed unhandled exception hook. + /// + /// A task representing the asynchronous test. + [Test] + public async Task ObserverAsyncCompletionSlowPathReportsThrownCompletion() + { + using var unhandled = new UnhandledExceptionCapture(); + var expected = new InvalidOperationException("completion-slow"); + var observer = new RenameCoverageObserver(onCompleted: _ => new ValueTask(Task.FromException(expected))); + + await observer.OnCompletedAsync(Result.Success).ConfigureAwait(false); + + var reported = await unhandled.WaitForAsync(expected.Message, TimeSpan.FromSeconds(5)).ConfigureAwait(false); + PrimitiveAssert.Same(expected, reported!); + } + + /// + /// Test observer exposing the renamed internal observer members. + /// + /// Optional error handler used by . + /// Optional completion handler used by . + private sealed class RenameCoverageObserver( + Func? onError = null, + Func? onCompleted = null) : ObserverAsync + { + /// + protected override ValueTask OnCompletedAsyncCore(Result result) => + onCompleted?.Invoke(result) ?? default; + + /// + protected override ValueTask OnErrorResumeAsyncCore(Exception error, CancellationToken cancellationToken) => + onError?.Invoke(error, cancellationToken) ?? default; + + /// + protected override ValueTask OnNextAsyncCore(int value, CancellationToken cancellationToken) => default; + } + + /// + /// Async disposable that invokes a callback when disposed. + /// + /// The callback invoked during disposal. + private sealed class CallbackAsyncDisposable(Action onDispose) : IAsyncDisposable + { + /// + public ValueTask DisposeAsync() + { + onDispose(); + return default; + } + } + + /// + /// Async disposable that throws the supplied exception when disposed. + /// + /// The exception thrown during disposal. + private sealed class ThrowingAsyncDisposable(Exception error) : IAsyncDisposable + { + /// + public ValueTask DisposeAsync() => throw error; + } + + /// + /// Sequencer that queues scheduled work until the test drains it. + /// + private sealed class QueuedSequencer : ISequencer + { + /// + /// Fixed deterministic timestamp. + /// + private static readonly DateTimeOffset FixedNow = new(2024, 1, 1, 0, 0, 0, TimeSpan.Zero); + + /// + /// Scheduled work items. + /// + private readonly ConcurrentQueue _items = new(); + + /// + public DateTimeOffset Now => FixedNow; + + /// + public long Timestamp => FixedNow.Ticks; + + /// + public void Schedule(IWorkItem item) => _items.Enqueue(item); + + /// + public void Schedule(IWorkItem item, long dueTimestamp) => Schedule(item); + + /// + /// Executes all queued work items. + /// + public void DrainAll() + { + while (_items.TryDequeue(out var item)) + { + item.Execute(); + } + } + } +} diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/ObserveOnAsyncSignalTests.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/ObserveOnAsyncSignalTests.cs index 25260ff..0ef099b 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/ObserveOnAsyncSignalTests.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/ObserveOnAsyncSignalTests.cs @@ -79,6 +79,32 @@ public async Task WhenSyncContextForceYielding_ThenEmits() await Assert.That(result).IsEqualTo(Sentinel); } + /// Verifies the default SynchronizationContext overload forwards through the non-forced wrapper. + /// A representing the asynchronous test operation. + [Test] + public async Task WhenSyncContextDefaultOverload_ThenEmits() + { + var ctx = SynchronizationContext.Current ?? new SynchronizationContext(); + + var result = await SignalAsync.Return(Sentinel) + .WitnessOn(ctx) + .FirstAsync(); + + await Assert.That(result).IsEqualTo(Sentinel); + } + + /// Verifies the default overload forwards through the non-forced wrapper. + /// A representing the asynchronous test operation. + [Test] + public async Task WhenTaskSchedulerDefaultOverload_ThenEmits() + { + var result = await SignalAsync.Return(Sentinel) + .WitnessOn(TaskScheduler.Default) + .FirstAsync(); + + await Assert.That(result).IsEqualTo(Sentinel); + } + /// Verifies that ObserveOn with a different SynchronizationContext routes /// the error through the slow-path context-switch even when forceYielding is false. /// A representing the asynchronous test operation. diff --git a/src/tests/ReactiveUI.Primitives.Tests/SignalWindowAndFactoryCoverageTests.cs b/src/tests/ReactiveUI.Primitives.Tests/SignalWindowAndFactoryCoverageTests.cs new file mode 100644 index 0000000..6f96c89 --- /dev/null +++ b/src/tests/ReactiveUI.Primitives.Tests/SignalWindowAndFactoryCoverageTests.cs @@ -0,0 +1,302 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.ComponentModel; +using ReactiveUI.Primitives.Concurrency; +using ReactiveUI.Primitives.Core; +using ReactiveUI.Primitives.Disposables; +using ReactiveUI.Primitives.Signals; + +namespace ReactiveUI.Primitives.Tests; + +/// +/// Covers recently added signal windowing and factory behavior that is reported by CI coverage gates. +/// +public sealed class SignalWindowAndFactoryCoverageTests +{ + /// + /// Verifies immediate, scheduled, + /// terminal, and error paths. + /// + [Test] + public void CollectCoversImmediateScheduledCompletionErrorAndDisposePaths() + { + const int first = 1; + const int second = 2; + const int third = 3; + const int expectedBatchCount = 2; + var immediateBatches = new List(); + + Signal.FromEnumerable([first, second]) + .Collect(TimeSpan.Zero) + .Subscribe(batch => immediateBatches.Add([.. batch])); + + Assert.Equal(expectedBatchCount, immediateBatches.Count); + Assert.Equal([first], immediateBatches[0]); + Assert.Equal([second], immediateBatches[1]); + + var clock = new TestClock(); + var source = new Signal(); + var scheduledBatches = new List(); + var completed = 0; + var subscription = source + .Collect(TimeSpan.FromTicks(second), clock) + .Subscribe(batch => scheduledBatches.Add([.. batch]), ex => throw ex, () => completed++); + + source.OnNext(first); + source.OnNext(second); + clock.AdvanceBy(TimeSpan.FromTicks(second)); + source.OnNext(third); + source.OnCompleted(); + clock.AdvanceBy(TimeSpan.FromTicks(second)); + subscription.Dispose(); + + Assert.Equal(expectedBatchCount, scheduledBatches.Count); + Assert.Equal([first, second], scheduledBatches[0]); + Assert.Equal([third], scheduledBatches[1]); + Assert.Equal(1, completed); + + var errorClock = new TestClock(); + var errorSource = new Signal(); + var expected = new InvalidOperationException("collect"); + Exception? observed = null; + + errorSource.Collect(TimeSpan.FromTicks(first), errorClock) + .Subscribe(_ => { }, ex => observed = ex); + errorSource.OnNext(first); + errorSource.OnError(expected); + errorClock.AdvanceBy(TimeSpan.FromTicks(first)); + + Assert.Same(expected, observed!); + Assert.Throws(() => ((IObservable)null!).Collect(TimeSpan.FromTicks(first))); + Assert.Throws(() => Signal.Emit(first).Collect(TimeSpan.FromTicks(first), null!)); + + var stoppedGuardCompleted = 0; + new ScriptedObservable(observer => + { + observer.OnCompleted(); + observer.OnNext(first); + }).Collect(TimeSpan.FromTicks(first), new TestClock()) + .Subscribe(_ => { }, ex => throw ex, () => stoppedGuardCompleted++); + Assert.Equal(1, stoppedGuardCompleted); + } + + /// + /// Verifies immediate, scheduled, + /// completion, stale emission, and error paths. + /// + [Test] + public void EmitIfQuietCoversImmediateScheduledCompletionStaleAndErrorPaths() + { + const int first = 1; + const int second = 2; + const int third = 3; + var immediateValues = new List(); + + Signal.FromEnumerable([first, second]) + .EmitIfQuiet(TimeSpan.Zero) + .Subscribe(immediateValues.Add); + + Assert.Equal([first, second], immediateValues); + + var clock = new TestClock(); + var source = new Signal(); + var delayedValues = new List(); + var completed = 0; + + source.EmitIfQuiet(TimeSpan.FromTicks(third), clock) + .Subscribe(delayedValues.Add, ex => throw ex, () => completed++); + source.OnNext(first); + clock.AdvanceBy(TimeSpan.FromTicks(second)); + source.OnNext(second); + clock.AdvanceBy(TimeSpan.FromTicks(first)); + clock.AdvanceBy(TimeSpan.FromTicks(second)); + source.OnNext(third); + source.OnCompleted(); + clock.AdvanceBy(TimeSpan.FromTicks(third)); + + Assert.Equal([second, third], delayedValues); + Assert.Equal(1, completed); + + var emptyCompletion = 0; + var emptySource = new Signal(); + emptySource.EmitIfQuiet(TimeSpan.FromTicks(first), new TestClock()) + .Subscribe(_ => { }, ex => throw ex, () => emptyCompletion++); + emptySource.OnCompleted(); + Assert.Equal(1, emptyCompletion); + + var errorClock = new TestClock(); + var errorSource = new Signal(); + var expected = new InvalidOperationException("quiet"); + Exception? observed = null; + + errorSource.EmitIfQuiet(TimeSpan.FromTicks(first), errorClock) + .Subscribe(_ => { }, ex => observed = ex); + errorSource.OnNext(first); + errorSource.OnError(expected); + errorClock.AdvanceBy(TimeSpan.FromTicks(first)); + + Assert.Same(expected, observed!); + Assert.Throws(() => ((IObservable)null!).EmitIfQuiet(TimeSpan.FromTicks(first))); + Assert.Throws(() => Signal.Emit(first).EmitIfQuiet(TimeSpan.FromTicks(first), null!)); + + var stoppedGuardCompleted = 0; + new ScriptedObservable(observer => + { + observer.OnCompleted(); + observer.OnNext(first); + }).EmitIfQuiet(TimeSpan.FromTicks(first), new TestClock()) + .Subscribe(_ => { }, ex => throw ex, () => stoppedGuardCompleted++); + Assert.Equal(1, stoppedGuardCompleted); + } + + /// + /// Verifies deferred sources and blocking enumeration surface success, factory failure, and source failure paths. + /// + [Test] + public void DeferAndToEnumerableCoverSuccessAndErrorPaths() + { + const int first = 1; + const int second = 2; + const int expectedSubscriptionCount = 2; + var subscriptions = 0; + var values = new List(); + + var deferred = Signal.Defer(() => + { + subscriptions++; + return Signal.FromEnumerable([first, second]); + }); + + deferred.Subscribe(values.Add); + deferred.Subscribe(_ => { }); + + Assert.Equal([first, second], values); + Assert.Equal(expectedSubscriptionCount, subscriptions); + Assert.Equal([first, second], Signal.FromEnumerable([first, second]).ToEnumerable()); + + var factoryError = new InvalidOperationException("defer-factory"); + Exception? observedFactoryError = null; + Signal.Defer(() => throw factoryError).Subscribe(_ => { }, ex => observedFactoryError = ex); + + Assert.Same(factoryError, observedFactoryError!); + Assert.Throws(() => Signal.Fail(new InvalidOperationException("enumerable")).ToEnumerable()); + Assert.Throws(() => Signal.Defer(null!)); + Assert.Throws(() => ((IObservable)null!).ToEnumerable()); + } + + /// + /// Verifies generic event factory overloads for supported and unsupported handler shapes. + /// + [Test] + public void GenericFromEventPatternCoversPropertyChangedGenericAndUnsupportedHandlers() + { + const int eventValue = 7; + var source = new GenericEventSource(); + var values = new List(); + var genericSubscription = Signal + .FromEventPattern, TestEventArgs>( + handler => source.Changed += handler, + handler => source.Changed -= handler) + .Subscribe(pattern => values.Add(pattern.EventArgs.Value)); + + source.Raise(eventValue); + genericSubscription.Dispose(); + source.Raise(eventValue + 1); + + Assert.Equal([eventValue], values); + + var propertySource = new PropertyChangedEventSource(); + var propertyNames = new List(); + var propertySubscription = Signal + .FromEventPattern( + handler => propertySource.PropertyChanged += handler, + handler => propertySource.PropertyChanged -= handler) + .Subscribe(pattern => propertyNames.Add(pattern.EventArgs.PropertyName)); + + propertySource.Raise(nameof(PropertyChangedEventSource.Value)); + propertySubscription.Dispose(); + propertySource.Raise("ignored"); + + Assert.Equal([nameof(PropertyChangedEventSource.Value)], propertyNames); + Assert.Throws(() => Signal.FromEventPattern, TestEventArgs>(null!, _ => { })); + Assert.Throws(() => Signal.FromEventPattern, TestEventArgs>(_ => { }, null!)); + Assert.Throws(() => + Signal.FromEventPattern(_ => { }, _ => { }).Subscribe(_ => { })); + } + + /// + /// Event arguments carrying a deterministic integer value. + /// + /// The value supplied by the event. + private sealed class TestEventArgs(int value) : EventArgs + { + /// + /// Gets the event value. + /// + public int Value { get; } = value; + } + + /// + /// Observable that runs a supplied subscription script synchronously. + /// + /// The value type. + /// The script invoked with the observer. + private sealed class ScriptedObservable(Action> script) : IObservable + { + /// + public IDisposable Subscribe(IObserver observer) + { + script(observer); + return Disposable.Empty; + } + } + + /// + /// Source used to exercise generic event conversion. + /// + private sealed class GenericEventSource + { + /// + /// Raised by the test source. + /// + public event EventHandler? Changed; + + /// + /// Raises with the supplied value. + /// + /// The value supplied to the event arguments. + public void Raise(int value) => Changed?.Invoke(this, new TestEventArgs(value)); + } + + /// + /// Source used to exercise event conversion. + /// + private sealed class PropertyChangedEventSource + { + /// + /// Raised by the test source. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Roslynator", + "RCS1159:Use EventHandler", + Justification = "This test deliberately covers the PropertyChangedEventHandler branch of the factory overload.")] + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Major Code Smell", + "S3908:Refactor this delegate to use 'System.EventHandler'.", + Justification = "This test deliberately covers the PropertyChangedEventHandler branch of the factory overload.")] + public event PropertyChangedEventHandler? PropertyChanged; + + /// + /// Gets a placeholder property name used by the event test. + /// + public static int Value => default; + + /// + /// Raises with the supplied property name. + /// + /// The property name supplied to the event arguments. + public void Raise(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} From 73fc238e72d169774983941fe068236a3797f7f4 Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Fri, 5 Jun 2026 08:57:53 +0100 Subject: [PATCH 05/15] test: stabilize DropIfBusy async error test CI Failure - Replace a fixed delay in DropIfBusyObservableTests.WhenHandlerThrowsBeforeDone_ThenForwardsError with a TaskCompletionSource-backed wait for the downstream error callback. - Preserve the same-reference assertion once the async handler failure has definitely been delivered. Verification - dotnet run --project src/tests/ReactiveUI.Primitives.Extensions.Tests/ReactiveUI.Primitives.Extensions.Tests.csproj -c Release -f net8.0 -- --no-progress --coverage --coverage-output-format cobertura --coverage-output extensions-net8-fix.cobertura.xml --results-directory .tmp/ci24-coverage/extensions-net8-fix - dotnet run --project src/tests/ReactiveUI.Primitives.Extensions.Tests/ReactiveUI.Primitives.Extensions.Tests.csproj -c Release -f net9.0 -- --no-progress - dotnet run --project src/tests/ReactiveUI.Primitives.Extensions.Tests/ReactiveUI.Primitives.Extensions.Tests.csproj -c Release -f net10.0 -- --no-progress - dotnet build src/ReactiveUI.Primitives.slnx -c Release --no-restore --- .../Operators/DropIfBusyObservableTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tests/ReactiveUI.Primitives.Extensions.Tests/Operators/DropIfBusyObservableTests.cs b/src/tests/ReactiveUI.Primitives.Extensions.Tests/Operators/DropIfBusyObservableTests.cs index 8b130d0..d84617d 100644 --- a/src/tests/ReactiveUI.Primitives.Extensions.Tests/Operators/DropIfBusyObservableTests.cs +++ b/src/tests/ReactiveUI.Primitives.Extensions.Tests/Operators/DropIfBusyObservableTests.cs @@ -84,18 +84,18 @@ public async Task WhenHandlerThrowsBeforeDone_ThenForwardsError() { var subject = new Subject(); var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var error = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var expected = new InvalidOperationException("handler"); - Exception? caught = null; using var sub = subject.DropIfBusy(async _ => { await release.Task.ConfigureAwait(false); throw expected; - }).Subscribe(static _ => { }, ex => caught = ex); + }).Subscribe(static _ => { }, ex => error.TrySetResult(ex)); subject.OnNext(1); release.SetResult(); - await Task.Delay(SettleDelayMilliseconds).ConfigureAwait(false); + var caught = await error.Task.WaitAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(false); await Assert.That(caught).IsSameReferenceAs(expected); } From e25398fc78c40de4ee4fc150d341316c38266b2b Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Sat, 6 Jun 2026 05:47:17 +0100 Subject: [PATCH 06/15] Rename Observer types to Witness and update refs Rename several internal types and identifiers to clarify intent and update usages across the async primitives library. Key changes: Delegate* -> Callback* (DelegateSignalAsync -> CallbackSignalAsync, DelegateAsyncWitness -> CallbackWitnessAsync), Observer* -> Witness* (CombineLatestIndexedObserver -> CombineLatestIndexedWitness, SingleElementObserver -> SingleElementWitness, TakeUntilSourceObserver -> TakeUntilSourceWitness, etc.), ForwardingAsyncWitness -> RelayWitnessAsync, and TaskWitnessAsyncBase -> TaskResultWitnessAsyncBase. Updated all operator and internals references, XML docs, and usages (e.g. Create, Multicast, CombineLatest operators). Added AsyncSignalSubscriptionBenchmarks.cs and updated tests to match the renames. --- ...eSignalAsync.cs => CallbackSignalAsync.cs} | 4 +- ...syncWitness.cs => CallbackWitnessAsync.cs} | 6 +- .../Internals/CombineLatestCoordinatorBase.cs | 4 +- ...rver.cs => CombineLatestIndexedWitness.cs} | 6 +- .../Internals/MulticastSignalAsync.cs | 2 +- ...ngAsyncWitness.cs => RelayWitnessAsync.cs} | 8 +- ...entObserver.cs => SingleElementWitness.cs} | 4 +- ...eObserver.cs => TakeUntilSourceWitness.cs} | 2 +- ...cBase.cs => TaskResultWitnessAsyncBase.cs} | 10 +- .../Observables/Create.cs | 2 +- .../Operators/AggregateAsync.cs | 2 +- .../Operators/AnyAllAsync.cs | 8 +- .../Operators/Cast.cs | 8 +- .../Operators/Catch.cs | 8 +- .../Operators/CombineLatest10.cs | 22 +-- .../Operators/CombineLatest11.cs | 24 +-- .../Operators/CombineLatest12.cs | 26 +-- .../Operators/CombineLatest13.cs | 28 ++-- .../Operators/CombineLatest14.cs | 30 ++-- .../Operators/CombineLatest15.cs | 32 ++-- .../Operators/CombineLatest16.cs | 34 ++-- .../Operators/CombineLatest2.cs | 6 +- .../Operators/CombineLatest3.cs | 8 +- .../Operators/CombineLatest4.cs | 10 +- .../Operators/CombineLatest5.cs | 12 +- .../Operators/CombineLatest6.cs | 14 +- .../Operators/CombineLatest7.cs | 16 +- .../Operators/CombineLatest8.cs | 18 +-- .../Operators/CombineLatest9.cs | 20 +-- .../Operators/CombineLatestEnumerable.cs | 6 +- .../Operators/ConcatSignalSourcesSignal{T}.cs | 4 +- .../Operators/ContainsAsync.cs | 4 +- ...cSignal.cs => ContextSwitchSignalAsync.cs} | 6 +- .../Operators/CountAsync.cs | 4 +- .../Operators/Delay.cs | 6 +- .../Operators/Distinct.cs | 16 +- .../Operators/DistinctUntilChanged.cs | 16 +- .../Operators/Do.cs | 2 +- .../Operators/FirstAsync.cs | 4 +- .../Operators/FirstOrDefaultAsync.cs | 4 +- .../Operators/ForEachAsync.cs | 8 +- .../Operators/GroupBy.cs | 2 +- .../Operators/LastAsync.cs | 2 +- .../Operators/LastOrDefaultAsync.cs | 4 +- .../Operators/LongCountAsync.cs | 4 +- .../Operators/Merge.cs | 34 ++-- .../Operators/OfType.cs | 8 +- .../Operators/OnDispose.cs | 12 +- .../Operators/OnErrorResumeAsFailure.cs | 6 +- .../Operators/ParityHelpers.FilterFusions.cs | 40 ++--- .../ParityHelpers.OperatorFusions.cs | 30 ++-- .../Operators/RefCount.cs | 8 +- .../Operators/SingleAsync.cs | 2 +- .../Operators/SingleOrDefaultAsync.cs | 2 +- .../Operators/SubscribeAsync.cs | 8 +- .../Operators/SwitchSignal.cs | 6 +- .../Operators/TakeUntil.cs | 8 +- .../Operators/ToDictionaryAsync.cs | 4 +- .../Operators/ToListAsync.cs | 4 +- .../Operators/WaitCompletionAsync.cs | 2 +- .../Operators/WitnessOn.cs | 8 +- .../Operators/Wrap.cs | 2 +- .../Operators/Yield.cs | 2 +- .../Base/BaseReplayLatestSignalAsync.cs | 141 ++++++++++------ .../Signals/Base/BaseSignalAsync.cs | 49 ++++-- .../BaseStatelessReplayLatestSignalAsync.cs | 150 ++++++++++++------ .../AsyncSignalSubscriptionBenchmarks.cs | 114 +++++++++++++ .../CombineLatestEnumerableInternalsTests.cs | 2 +- .../CombiningOperatorTests.OnDispose.cs | 12 +- .../ConcurrentSignalBaseTests.cs | 16 +- .../FactorySignalTests.cs | 2 +- .../CombineLatestIndexedObserverTests.cs | 8 +- .../Internals/TakeUntilSourceObserverTests.cs | 8 +- .../ObserveOnAsyncSignalTests.cs | 8 +- .../ParityHelpersOperatorFusionsTests.cs | 12 +- .../TakeUntilOperatorTests.cs | 2 +- .../TerminalOperatorTests.cs | 2 +- .../TransformationOperatorTests.cs | 2 +- 78 files changed, 712 insertions(+), 468 deletions(-) rename src/ReactiveUI.Primitives.Async/Internals/{DelegateSignalAsync.cs => CallbackSignalAsync.cs} (86%) rename src/ReactiveUI.Primitives.Async/Internals/{DelegateAsyncWitness.cs => CallbackWitnessAsync.cs} (92%) rename src/ReactiveUI.Primitives.Async/Internals/{CombineLatestIndexedObserver.cs => CombineLatestIndexedWitness.cs} (93%) rename src/ReactiveUI.Primitives.Async/Internals/{ForwardingAsyncWitness.cs => RelayWitnessAsync.cs} (68%) rename src/ReactiveUI.Primitives.Async/Internals/{SingleElementObserver.cs => SingleElementWitness.cs} (95%) rename src/ReactiveUI.Primitives.Async/Internals/{TakeUntilSourceObserver.cs => TakeUntilSourceWitness.cs} (93%) rename src/ReactiveUI.Primitives.Async/Internals/{TaskWitnessAsyncBase.cs => TaskResultWitnessAsyncBase.cs} (89%) rename src/ReactiveUI.Primitives.Async/Operators/{WitnessOnAsyncSignal.cs => ContextSwitchSignalAsync.cs} (94%) create mode 100644 src/benchmarks/ReactiveUI.Primitives.Benchmarks/AsyncSignalSubscriptionBenchmarks.cs diff --git a/src/ReactiveUI.Primitives.Async/Internals/DelegateSignalAsync.cs b/src/ReactiveUI.Primitives.Async/Internals/CallbackSignalAsync.cs similarity index 86% rename from src/ReactiveUI.Primitives.Async/Internals/DelegateSignalAsync.cs rename to src/ReactiveUI.Primitives.Async/Internals/CallbackSignalAsync.cs index 6caf79e..ab0eb94 100644 --- a/src/ReactiveUI.Primitives.Async/Internals/DelegateSignalAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Internals/CallbackSignalAsync.cs @@ -5,11 +5,11 @@ namespace ReactiveUI.Primitives.Async.Internals; /// -/// An observable that delegates subscription logic to a user-supplied asynchronous function. +/// An observable that invokes a callback to create each subscription. /// /// The type of the elements in the observable sequence. /// The asynchronous function invoked when an observer subscribes. -internal sealed class DelegateSignalAsync( +internal sealed class CallbackSignalAsync( Func, CancellationToken, ValueTask> subscribeAsync) : SignalAsync { /// diff --git a/src/ReactiveUI.Primitives.Async/Internals/DelegateAsyncWitness.cs b/src/ReactiveUI.Primitives.Async/Internals/CallbackWitnessAsync.cs similarity index 92% rename from src/ReactiveUI.Primitives.Async/Internals/DelegateAsyncWitness.cs rename to src/ReactiveUI.Primitives.Async/Internals/CallbackWitnessAsync.cs index ad88bd3..b7bb485 100644 --- a/src/ReactiveUI.Primitives.Async/Internals/DelegateAsyncWitness.cs +++ b/src/ReactiveUI.Primitives.Async/Internals/CallbackWitnessAsync.cs @@ -5,13 +5,13 @@ namespace ReactiveUI.Primitives.Async.Internals; /// -/// An observer that delegates notification handling to user-supplied asynchronous functions. +/// An witness that routes notifications through user-supplied asynchronous callbacks. /// -/// The type of the elements received by the observer. +/// The type of the elements received by the witness. /// The asynchronous function invoked for each element. /// An optional asynchronous function invoked when a resumable error occurs. /// An optional asynchronous function invoked when the sequence completes. -internal sealed class DelegateAsyncWitness( +internal sealed class CallbackWitnessAsync( Func onNextAsync, Func? onErrorResumeAsync = null, Func? onCompletedAsync = null) : ObserverAsync diff --git a/src/ReactiveUI.Primitives.Async/Internals/CombineLatestCoordinatorBase.cs b/src/ReactiveUI.Primitives.Async/Internals/CombineLatestCoordinatorBase.cs index 8f717e3..e00d014 100644 --- a/src/ReactiveUI.Primitives.Async/Internals/CombineLatestCoordinatorBase.cs +++ b/src/ReactiveUI.Primitives.Async/Internals/CombineLatestCoordinatorBase.cs @@ -28,7 +28,7 @@ protected CombineLatestCoordinatorBase(IObserverAsync observer, int sou internal CombineLatestLifecycle Lifecycle { get; } /// Gets the lock protecting per-arity latest-values caches. Internal so the shared - /// can lock on it without deriving + /// can lock on it without deriving /// from this base. internal Lock ValuesLock { get; } = new(); @@ -67,7 +67,7 @@ internal ValueTask RelaySourceErrorAsync(Exception error, CancellationToken canc /// /// Reads the per-arity Optional slots, projects them through the selector when every source /// has produced a value, and forwards the result downstream via the lifecycle. Invoked by - /// after a per-source OnNext has + /// after a per-source OnNext has /// landed under . /// /// A ValueTask representing the asynchronous emit. diff --git a/src/ReactiveUI.Primitives.Async/Internals/CombineLatestIndexedObserver.cs b/src/ReactiveUI.Primitives.Async/Internals/CombineLatestIndexedWitness.cs similarity index 93% rename from src/ReactiveUI.Primitives.Async/Internals/CombineLatestIndexedObserver.cs rename to src/ReactiveUI.Primitives.Async/Internals/CombineLatestIndexedWitness.cs index 786034d..b9ae6ad 100644 --- a/src/ReactiveUI.Primitives.Async/Internals/CombineLatestIndexedObserver.cs +++ b/src/ReactiveUI.Primitives.Async/Internals/CombineLatestIndexedWitness.cs @@ -8,18 +8,18 @@ namespace ReactiveUI.Primitives.Async.Internals; /// Per-source used by every CombineLatestN subscription. The /// per-arity class previously declared N hand-rolled OnNextN / OnCompletedN method /// pairs whose bodies differed only in which Optional<TN> field they wrote and which -/// completion bit they passed to the lifecycle. Pre-building N of these observers at subscription +/// completion bit they passed to the lifecycle. Pre-building N of these witnesses at subscription /// time keeps the typing exact and eliminates the per-source method declarations from the per-arity /// files. The closure cost (one delegate per source for the value-write) is paid once at subscribe /// and not per emission; the actual per-emission cost is one indirect delegate invoke under the /// values-lock. /// -/// The element type of the upstream source this observer subscribes to. +/// The element type of the upstream source this witness subscribes to. /// The downstream element type owned by the parent subscription. /// The parent subscription that owns the values-lock and lifecycle. /// The completion bitmask bit owned by this source (1 << index). /// Stores the freshly-emitted value into the parent's typed _valN slot. -internal sealed class CombineLatestIndexedObserver( +internal sealed class CombineLatestIndexedWitness( CombineLatestCoordinatorBase parent, int sourceBit, Action recordValue) : ObserverAsync diff --git a/src/ReactiveUI.Primitives.Async/Internals/MulticastSignalAsync.cs b/src/ReactiveUI.Primitives.Async/Internals/MulticastSignalAsync.cs index 0d10118..e4f3f58 100644 --- a/src/ReactiveUI.Primitives.Async/Internals/MulticastSignalAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Internals/MulticastSignalAsync.cs @@ -144,7 +144,7 @@ protected override ValueTask SubscribeAsyncCore( // linked-CTS allocation that the downstream's TryEnter would otherwise produce. The wrap // itself benefits from SignalAsyncObserver forwarding CancellationToken.None to the // signal (the wrap's TryEnter sees None and fast-paths), so no wrap-side link is needed. - var wrap = new ForwardingAsyncWitness(observer); + var wrap = new RelayWitnessAsync(observer); if (observer is ObserverAsync downstream) { downstream.LinkUpstreamCancellation(wrap.InternalDisposedToken); diff --git a/src/ReactiveUI.Primitives.Async/Internals/ForwardingAsyncWitness.cs b/src/ReactiveUI.Primitives.Async/Internals/RelayWitnessAsync.cs similarity index 68% rename from src/ReactiveUI.Primitives.Async/Internals/ForwardingAsyncWitness.cs rename to src/ReactiveUI.Primitives.Async/Internals/RelayWitnessAsync.cs index 5ef95c1..3fc816c 100644 --- a/src/ReactiveUI.Primitives.Async/Internals/ForwardingAsyncWitness.cs +++ b/src/ReactiveUI.Primitives.Async/Internals/RelayWitnessAsync.cs @@ -5,11 +5,11 @@ namespace ReactiveUI.Primitives.Async.Internals; /// -/// Wraps an to provide base observer behavior while delegating all notifications. +/// Relays notifications from the base observer pipeline to another asynchronous observer. /// -/// The type of elements received by the observer. -/// The inner observer to delegate notifications to. -internal sealed class ForwardingAsyncWitness(IObserverAsync observer) : ObserverAsync +/// The type of elements received by the witness. +/// The witness that receives the relayed notifications. +internal sealed class RelayWitnessAsync(IObserverAsync observer) : ObserverAsync { /// protected override ValueTask OnNextAsyncCore(T value, CancellationToken cancellationToken) => diff --git a/src/ReactiveUI.Primitives.Async/Internals/SingleElementObserver.cs b/src/ReactiveUI.Primitives.Async/Internals/SingleElementWitness.cs similarity index 95% rename from src/ReactiveUI.Primitives.Async/Internals/SingleElementObserver.cs rename to src/ReactiveUI.Primitives.Async/Internals/SingleElementWitness.cs index 57ecb14..c3a1fa1 100644 --- a/src/ReactiveUI.Primitives.Async/Internals/SingleElementObserver.cs +++ b/src/ReactiveUI.Primitives.Async/Internals/SingleElementWitness.cs @@ -19,11 +19,11 @@ namespace ReactiveUI.Primitives.Async.Internals; /// /// The value to return on empty when is false. /// A cancellation token for the operation. -internal sealed class SingleElementObserver( +internal sealed class SingleElementWitness( Func? predicate, bool requireExactlyOne, T? defaultValue, - CancellationToken cancellationToken) : TaskWitnessAsyncBase(cancellationToken) + CancellationToken cancellationToken) : TaskResultWitnessAsyncBase(cancellationToken) { /// A value indicating whether a matching element has been found. private bool _hasValue; diff --git a/src/ReactiveUI.Primitives.Async/Internals/TakeUntilSourceObserver.cs b/src/ReactiveUI.Primitives.Async/Internals/TakeUntilSourceWitness.cs similarity index 93% rename from src/ReactiveUI.Primitives.Async/Internals/TakeUntilSourceObserver.cs rename to src/ReactiveUI.Primitives.Async/Internals/TakeUntilSourceWitness.cs index 7754db6..a0c857d 100644 --- a/src/ReactiveUI.Primitives.Async/Internals/TakeUntilSourceObserver.cs +++ b/src/ReactiveUI.Primitives.Async/Internals/TakeUntilSourceWitness.cs @@ -12,7 +12,7 @@ namespace ReactiveUI.Primitives.Async.Internals; /// /// The downstream element type. /// The shared lifecycle owning the gate and forwarding logic. -internal sealed class TakeUntilSourceObserver(TakeUntilLifecycle lifecycle) : ObserverAsync +internal sealed class TakeUntilSourceWitness(TakeUntilLifecycle lifecycle) : ObserverAsync { /// protected override ValueTask OnNextAsyncCore(T value, CancellationToken cancellationToken) diff --git a/src/ReactiveUI.Primitives.Async/Internals/TaskWitnessAsyncBase.cs b/src/ReactiveUI.Primitives.Async/Internals/TaskResultWitnessAsyncBase.cs similarity index 89% rename from src/ReactiveUI.Primitives.Async/Internals/TaskWitnessAsyncBase.cs rename to src/ReactiveUI.Primitives.Async/Internals/TaskResultWitnessAsyncBase.cs index d764e41..485be39 100644 --- a/src/ReactiveUI.Primitives.Async/Internals/TaskWitnessAsyncBase.cs +++ b/src/ReactiveUI.Primitives.Async/Internals/TaskResultWitnessAsyncBase.cs @@ -7,12 +7,12 @@ namespace ReactiveUI.Primitives.Async.Internals; /// -/// Base class for observers that produce a single task-based result value when the observed sequence completes. +/// Base class for witnesses that produce a single task-based result value when the observed sequence completes. /// /// The type of elements received from the observable sequence. -/// The type of the result value produced by this observer. +/// The type of the result value produced by this witness. /// A cancellation token used to cancel the waiting operation. -internal abstract class TaskWitnessAsyncBase(CancellationToken cancellationToken) : ObserverAsync +internal abstract class TaskResultWitnessAsyncBase(CancellationToken cancellationToken) : ObserverAsync { /// /// The task completion source used to produce the observer's single result value. @@ -36,7 +36,7 @@ public async ValueTask AwaitResultAsync() await using var ct = _cancellationToken.Register( static x => { - var @this = (TaskWitnessAsyncBase)x!; + var @this = (TaskResultWitnessAsyncBase)x!; @this._tcs.TrySetException(new OperationCanceledException(@this._cancellationToken)); }, this); @@ -44,7 +44,7 @@ public async ValueTask AwaitResultAsync() using var ct = _cancellationToken.Register( static x => { - var @this = (TaskWitnessAsyncBase)x!; + var @this = (TaskResultWitnessAsyncBase)x!; @this._tcs.TrySetException(new OperationCanceledException(@this._cancellationToken)); }, this); diff --git a/src/ReactiveUI.Primitives.Async/Observables/Create.cs b/src/ReactiveUI.Primitives.Async/Observables/Create.cs index a793344..6653828 100644 --- a/src/ReactiveUI.Primitives.Async/Observables/Create.cs +++ b/src/ReactiveUI.Primitives.Async/Observables/Create.cs @@ -33,7 +33,7 @@ public static IObservableAsync Create( Func, CancellationToken, ValueTask> subscribeAsync) => subscribeAsync is null ? throw new ArgumentNullException(nameof(subscribeAsync)) - : new DelegateSignalAsync(subscribeAsync); + : new CallbackSignalAsync(subscribeAsync); /// /// Creates a new observable sequence that runs the specified asynchronous job as a background task. diff --git a/src/ReactiveUI.Primitives.Async/Operators/AggregateAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/AggregateAsync.cs index 5bbcf70..bcc75bd 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/AggregateAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/AggregateAsync.cs @@ -163,7 +163,7 @@ public static async ValueTask AggregateAsync( internal sealed class AggregateTaskWitness( TAcc seed, Func> accumulator, - CancellationToken cancellationToken) : TaskWitnessAsyncBase(cancellationToken) + CancellationToken cancellationToken) : TaskResultWitnessAsyncBase(cancellationToken) { /// /// The current accumulated value. diff --git a/src/ReactiveUI.Primitives.Async/Operators/AnyAllAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/AnyAllAsync.cs index 9d4a117..6249eb6 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/AnyAllAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/AnyAllAsync.cs @@ -90,11 +90,11 @@ public static async ValueTask AllAsync(this IObservableAsync @this, } /// - /// An observer that determines whether any element in the sequence satisfies a predicate. + /// A witness that determines whether any element in the sequence satisfies a predicate. /// /// The type of elements in the sequence. internal sealed class AnyTaskWitness(Func? predicate, CancellationToken cancellationToken) - : TaskWitnessAsyncBase(cancellationToken) + : TaskResultWitnessAsyncBase(cancellationToken) { /// protected override async ValueTask OnNextAsyncCore(T value, CancellationToken cancellationToken) @@ -115,11 +115,11 @@ protected override ValueTask OnCompletedAsyncCore(Result result) => } /// - /// An observer that determines whether all elements in the sequence satisfy a predicate. + /// A witness that determines whether all elements in the sequence satisfy a predicate. /// /// The type of elements in the sequence. internal sealed class AllTaskWitness(Func predicate, CancellationToken cancellationToken) - : TaskWitnessAsyncBase(cancellationToken) + : TaskResultWitnessAsyncBase(cancellationToken) { /// /// The predicate function used to test each element in the sequence. diff --git a/src/ReactiveUI.Primitives.Async/Operators/Cast.cs b/src/ReactiveUI.Primitives.Async/Operators/Cast.cs index d4ed076..7e69c3f 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/Cast.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/Cast.cs @@ -49,7 +49,7 @@ protected override async ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var sink = new CastObserver(observer, cancellationToken); + var sink = new CastWitness(observer, cancellationToken); if (observer is ObserverAsync downstreamBase) { @@ -61,10 +61,10 @@ protected override async ValueTask SubscribeAsyncCore( return sink; } - /// Per-subscription observer that casts each value to . - /// The downstream observer. + /// Per-subscription witness that casts each value to . + /// The downstream witness. /// The subscribe-time cancellation token. - internal sealed class CastObserver( + internal sealed class CastWitness( IObserverAsync downstream, CancellationToken subscribeToken) : ObserverAsync(subscribeToken) { diff --git a/src/ReactiveUI.Primitives.Async/Operators/Catch.cs b/src/ReactiveUI.Primitives.Async/Operators/Catch.cs index 5c1ed96..176b349 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/Catch.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/Catch.cs @@ -101,7 +101,7 @@ protected override async ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var sink = new CatchObserver(observer, handler, onErrorResume, cancellationToken); + var sink = new CatchWitness(observer, handler, onErrorResume, cancellationToken); // Wire sink's dispose token into the downstream's link chain so the downstream's hot path // recognises this token without allocating a per-emission linked CTS. @@ -115,15 +115,15 @@ protected override async ValueTask SubscribeAsyncCore( return sink; } - /// Per-subscription observer that forwards OnNext verbatim, delegates error-resume to the + /// Per-subscription witness that forwards OnNext verbatim, delegates error-resume to the /// supplied callback (or the downstream when none was supplied), and on a failed completion subscribes the /// handler-produced fallback observable in place of forwarding the failure. - /// The downstream observer. + /// The downstream witness. /// The fallback factory. /// Optional async error-resume callback. /// The subscribe-time cancellation token, linked into the dispose chain and reused for the handler /// subscription. - internal sealed class CatchObserver( + internal sealed class CatchWitness( IObserverAsync downstream, Func> handler, Func? onErrorResume, diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest10.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest10.cs index 9e12936..094126b 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest10.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest10.cs @@ -128,7 +128,7 @@ protected override ValueTask SubscribeAsyncCore( /// observers, the SubscribeAtAsync switch, and the selector invocation. Shared scaffolding /// (gate, lifecycle, ValuesLock, OnErrorResume, SubscribeSourcesAsync, DisposeAsync) lives /// in ; the per-source OnNext / OnError / - /// OnCompleted forwarding lives in . + /// OnCompleted forwarding lives in . /// internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase { @@ -169,34 +169,34 @@ internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase _selector; /// Indexed observer for source 1. - private readonly CombineLatestIndexedObserver _obs1; + private readonly CombineLatestIndexedWitness _obs1; /// Indexed observer for source 2. - private readonly CombineLatestIndexedObserver _obs2; + private readonly CombineLatestIndexedWitness _obs2; /// Indexed observer for source 3. - private readonly CombineLatestIndexedObserver _obs3; + private readonly CombineLatestIndexedWitness _obs3; /// Indexed observer for source 4. - private readonly CombineLatestIndexedObserver _obs4; + private readonly CombineLatestIndexedWitness _obs4; /// Indexed observer for source 5. - private readonly CombineLatestIndexedObserver _obs5; + private readonly CombineLatestIndexedWitness _obs5; /// Indexed observer for source 6. - private readonly CombineLatestIndexedObserver _obs6; + private readonly CombineLatestIndexedWitness _obs6; /// Indexed observer for source 7. - private readonly CombineLatestIndexedObserver _obs7; + private readonly CombineLatestIndexedWitness _obs7; /// Indexed observer for source 8. - private readonly CombineLatestIndexedObserver _obs8; + private readonly CombineLatestIndexedWitness _obs8; /// Indexed observer for source 9. - private readonly CombineLatestIndexedObserver _obs9; + private readonly CombineLatestIndexedWitness _obs9; /// Indexed observer for source 10. - private readonly CombineLatestIndexedObserver _obs10; + private readonly CombineLatestIndexedWitness _obs10; /// Latest value from source 1. private Optional _val1 = Optional.Empty; diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest11.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest11.cs index 378ac56..705bffd 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest11.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest11.cs @@ -134,7 +134,7 @@ protected override ValueTask SubscribeAsyncCore( /// observers, the SubscribeAtAsync switch, and the selector invocation. Shared scaffolding /// (gate, lifecycle, ValuesLock, OnErrorResume, SubscribeSourcesAsync, DisposeAsync) lives /// in ; the per-source OnNext / OnError / - /// OnCompleted forwarding lives in . + /// OnCompleted forwarding lives in . /// internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase { @@ -178,37 +178,37 @@ internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase _selector; /// Indexed observer for source 1. - private readonly CombineLatestIndexedObserver _obs1; + private readonly CombineLatestIndexedWitness _obs1; /// Indexed observer for source 2. - private readonly CombineLatestIndexedObserver _obs2; + private readonly CombineLatestIndexedWitness _obs2; /// Indexed observer for source 3. - private readonly CombineLatestIndexedObserver _obs3; + private readonly CombineLatestIndexedWitness _obs3; /// Indexed observer for source 4. - private readonly CombineLatestIndexedObserver _obs4; + private readonly CombineLatestIndexedWitness _obs4; /// Indexed observer for source 5. - private readonly CombineLatestIndexedObserver _obs5; + private readonly CombineLatestIndexedWitness _obs5; /// Indexed observer for source 6. - private readonly CombineLatestIndexedObserver _obs6; + private readonly CombineLatestIndexedWitness _obs6; /// Indexed observer for source 7. - private readonly CombineLatestIndexedObserver _obs7; + private readonly CombineLatestIndexedWitness _obs7; /// Indexed observer for source 8. - private readonly CombineLatestIndexedObserver _obs8; + private readonly CombineLatestIndexedWitness _obs8; /// Indexed observer for source 9. - private readonly CombineLatestIndexedObserver _obs9; + private readonly CombineLatestIndexedWitness _obs9; /// Indexed observer for source 10. - private readonly CombineLatestIndexedObserver _obs10; + private readonly CombineLatestIndexedWitness _obs10; /// Indexed observer for source 11. - private readonly CombineLatestIndexedObserver _obs11; + private readonly CombineLatestIndexedWitness _obs11; /// Latest value from source 1. private Optional _val1 = Optional.Empty; diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest12.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest12.cs index 04cf41a..570b4ef 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest12.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest12.cs @@ -140,7 +140,7 @@ protected override ValueTask SubscribeAsyncCore( /// observers, the SubscribeAtAsync switch, and the selector invocation. Shared scaffolding /// (gate, lifecycle, ValuesLock, OnErrorResume, SubscribeSourcesAsync, DisposeAsync) lives /// in ; the per-source OnNext / OnError / - /// OnCompleted forwarding lives in . + /// OnCompleted forwarding lives in . /// internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase { @@ -187,40 +187,40 @@ internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase _selector; /// Indexed observer for source 1. - private readonly CombineLatestIndexedObserver _obs1; + private readonly CombineLatestIndexedWitness _obs1; /// Indexed observer for source 2. - private readonly CombineLatestIndexedObserver _obs2; + private readonly CombineLatestIndexedWitness _obs2; /// Indexed observer for source 3. - private readonly CombineLatestIndexedObserver _obs3; + private readonly CombineLatestIndexedWitness _obs3; /// Indexed observer for source 4. - private readonly CombineLatestIndexedObserver _obs4; + private readonly CombineLatestIndexedWitness _obs4; /// Indexed observer for source 5. - private readonly CombineLatestIndexedObserver _obs5; + private readonly CombineLatestIndexedWitness _obs5; /// Indexed observer for source 6. - private readonly CombineLatestIndexedObserver _obs6; + private readonly CombineLatestIndexedWitness _obs6; /// Indexed observer for source 7. - private readonly CombineLatestIndexedObserver _obs7; + private readonly CombineLatestIndexedWitness _obs7; /// Indexed observer for source 8. - private readonly CombineLatestIndexedObserver _obs8; + private readonly CombineLatestIndexedWitness _obs8; /// Indexed observer for source 9. - private readonly CombineLatestIndexedObserver _obs9; + private readonly CombineLatestIndexedWitness _obs9; /// Indexed observer for source 10. - private readonly CombineLatestIndexedObserver _obs10; + private readonly CombineLatestIndexedWitness _obs10; /// Indexed observer for source 11. - private readonly CombineLatestIndexedObserver _obs11; + private readonly CombineLatestIndexedWitness _obs11; /// Indexed observer for source 12. - private readonly CombineLatestIndexedObserver _obs12; + private readonly CombineLatestIndexedWitness _obs12; /// Latest value from source 1. private Optional _val1 = Optional.Empty; diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest13.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest13.cs index 9158aab..a1913dd 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest13.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest13.cs @@ -146,7 +146,7 @@ protected override ValueTask SubscribeAsyncCore( /// observers, the SubscribeAtAsync switch, and the selector invocation. Shared scaffolding /// (gate, lifecycle, ValuesLock, OnErrorResume, SubscribeSourcesAsync, DisposeAsync) lives /// in ; the per-source OnNext / OnError / - /// OnCompleted forwarding lives in . + /// OnCompleted forwarding lives in . /// internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase { @@ -196,43 +196,43 @@ internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase _selector; /// Indexed observer for source 1. - private readonly CombineLatestIndexedObserver _obs1; + private readonly CombineLatestIndexedWitness _obs1; /// Indexed observer for source 2. - private readonly CombineLatestIndexedObserver _obs2; + private readonly CombineLatestIndexedWitness _obs2; /// Indexed observer for source 3. - private readonly CombineLatestIndexedObserver _obs3; + private readonly CombineLatestIndexedWitness _obs3; /// Indexed observer for source 4. - private readonly CombineLatestIndexedObserver _obs4; + private readonly CombineLatestIndexedWitness _obs4; /// Indexed observer for source 5. - private readonly CombineLatestIndexedObserver _obs5; + private readonly CombineLatestIndexedWitness _obs5; /// Indexed observer for source 6. - private readonly CombineLatestIndexedObserver _obs6; + private readonly CombineLatestIndexedWitness _obs6; /// Indexed observer for source 7. - private readonly CombineLatestIndexedObserver _obs7; + private readonly CombineLatestIndexedWitness _obs7; /// Indexed observer for source 8. - private readonly CombineLatestIndexedObserver _obs8; + private readonly CombineLatestIndexedWitness _obs8; /// Indexed observer for source 9. - private readonly CombineLatestIndexedObserver _obs9; + private readonly CombineLatestIndexedWitness _obs9; /// Indexed observer for source 10. - private readonly CombineLatestIndexedObserver _obs10; + private readonly CombineLatestIndexedWitness _obs10; /// Indexed observer for source 11. - private readonly CombineLatestIndexedObserver _obs11; + private readonly CombineLatestIndexedWitness _obs11; /// Indexed observer for source 12. - private readonly CombineLatestIndexedObserver _obs12; + private readonly CombineLatestIndexedWitness _obs12; /// Indexed observer for source 13. - private readonly CombineLatestIndexedObserver _obs13; + private readonly CombineLatestIndexedWitness _obs13; /// Latest value from source 1. private Optional _val1 = Optional.Empty; diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest14.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest14.cs index 948b89d..68e096f 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest14.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest14.cs @@ -152,7 +152,7 @@ protected override ValueTask SubscribeAsyncCore( /// observers, the SubscribeAtAsync switch, and the selector invocation. Shared scaffolding /// (gate, lifecycle, ValuesLock, OnErrorResume, SubscribeSourcesAsync, DisposeAsync) lives /// in ; the per-source OnNext / OnError / - /// OnCompleted forwarding lives in . + /// OnCompleted forwarding lives in . /// internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase { @@ -205,46 +205,46 @@ internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase _selector; /// Indexed observer for source 1. - private readonly CombineLatestIndexedObserver _obs1; + private readonly CombineLatestIndexedWitness _obs1; /// Indexed observer for source 2. - private readonly CombineLatestIndexedObserver _obs2; + private readonly CombineLatestIndexedWitness _obs2; /// Indexed observer for source 3. - private readonly CombineLatestIndexedObserver _obs3; + private readonly CombineLatestIndexedWitness _obs3; /// Indexed observer for source 4. - private readonly CombineLatestIndexedObserver _obs4; + private readonly CombineLatestIndexedWitness _obs4; /// Indexed observer for source 5. - private readonly CombineLatestIndexedObserver _obs5; + private readonly CombineLatestIndexedWitness _obs5; /// Indexed observer for source 6. - private readonly CombineLatestIndexedObserver _obs6; + private readonly CombineLatestIndexedWitness _obs6; /// Indexed observer for source 7. - private readonly CombineLatestIndexedObserver _obs7; + private readonly CombineLatestIndexedWitness _obs7; /// Indexed observer for source 8. - private readonly CombineLatestIndexedObserver _obs8; + private readonly CombineLatestIndexedWitness _obs8; /// Indexed observer for source 9. - private readonly CombineLatestIndexedObserver _obs9; + private readonly CombineLatestIndexedWitness _obs9; /// Indexed observer for source 10. - private readonly CombineLatestIndexedObserver _obs10; + private readonly CombineLatestIndexedWitness _obs10; /// Indexed observer for source 11. - private readonly CombineLatestIndexedObserver _obs11; + private readonly CombineLatestIndexedWitness _obs11; /// Indexed observer for source 12. - private readonly CombineLatestIndexedObserver _obs12; + private readonly CombineLatestIndexedWitness _obs12; /// Indexed observer for source 13. - private readonly CombineLatestIndexedObserver _obs13; + private readonly CombineLatestIndexedWitness _obs13; /// Indexed observer for source 14. - private readonly CombineLatestIndexedObserver _obs14; + private readonly CombineLatestIndexedWitness _obs14; /// Latest value from source 1. private Optional _val1 = Optional.Empty; diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest15.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest15.cs index 35c7490..59e3cdc 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest15.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest15.cs @@ -158,7 +158,7 @@ protected override ValueTask SubscribeAsyncCore( /// observers, the SubscribeAtAsync switch, and the selector invocation. Shared scaffolding /// (gate, lifecycle, ValuesLock, OnErrorResume, SubscribeSourcesAsync, DisposeAsync) lives /// in ; the per-source OnNext / OnError / - /// OnCompleted forwarding lives in . + /// OnCompleted forwarding lives in . /// internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase { @@ -214,49 +214,49 @@ internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase _selector; /// Indexed observer for source 1. - private readonly CombineLatestIndexedObserver _obs1; + private readonly CombineLatestIndexedWitness _obs1; /// Indexed observer for source 2. - private readonly CombineLatestIndexedObserver _obs2; + private readonly CombineLatestIndexedWitness _obs2; /// Indexed observer for source 3. - private readonly CombineLatestIndexedObserver _obs3; + private readonly CombineLatestIndexedWitness _obs3; /// Indexed observer for source 4. - private readonly CombineLatestIndexedObserver _obs4; + private readonly CombineLatestIndexedWitness _obs4; /// Indexed observer for source 5. - private readonly CombineLatestIndexedObserver _obs5; + private readonly CombineLatestIndexedWitness _obs5; /// Indexed observer for source 6. - private readonly CombineLatestIndexedObserver _obs6; + private readonly CombineLatestIndexedWitness _obs6; /// Indexed observer for source 7. - private readonly CombineLatestIndexedObserver _obs7; + private readonly CombineLatestIndexedWitness _obs7; /// Indexed observer for source 8. - private readonly CombineLatestIndexedObserver _obs8; + private readonly CombineLatestIndexedWitness _obs8; /// Indexed observer for source 9. - private readonly CombineLatestIndexedObserver _obs9; + private readonly CombineLatestIndexedWitness _obs9; /// Indexed observer for source 10. - private readonly CombineLatestIndexedObserver _obs10; + private readonly CombineLatestIndexedWitness _obs10; /// Indexed observer for source 11. - private readonly CombineLatestIndexedObserver _obs11; + private readonly CombineLatestIndexedWitness _obs11; /// Indexed observer for source 12. - private readonly CombineLatestIndexedObserver _obs12; + private readonly CombineLatestIndexedWitness _obs12; /// Indexed observer for source 13. - private readonly CombineLatestIndexedObserver _obs13; + private readonly CombineLatestIndexedWitness _obs13; /// Indexed observer for source 14. - private readonly CombineLatestIndexedObserver _obs14; + private readonly CombineLatestIndexedWitness _obs14; /// Indexed observer for source 15. - private readonly CombineLatestIndexedObserver _obs15; + private readonly CombineLatestIndexedWitness _obs15; /// Latest value from source 1. private Optional _val1 = Optional.Empty; diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest16.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest16.cs index 5b01add..03d4951 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest16.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest16.cs @@ -164,7 +164,7 @@ protected override ValueTask SubscribeAsyncCore( /// observers, the SubscribeAtAsync switch, and the selector invocation. Shared scaffolding /// (gate, lifecycle, ValuesLock, OnErrorResume, SubscribeSourcesAsync, DisposeAsync) lives /// in ; the per-source OnNext / OnError / - /// OnCompleted forwarding lives in . + /// OnCompleted forwarding lives in . /// internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase { @@ -223,52 +223,52 @@ internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase _selector; /// Indexed observer for source 1. - private readonly CombineLatestIndexedObserver _obs1; + private readonly CombineLatestIndexedWitness _obs1; /// Indexed observer for source 2. - private readonly CombineLatestIndexedObserver _obs2; + private readonly CombineLatestIndexedWitness _obs2; /// Indexed observer for source 3. - private readonly CombineLatestIndexedObserver _obs3; + private readonly CombineLatestIndexedWitness _obs3; /// Indexed observer for source 4. - private readonly CombineLatestIndexedObserver _obs4; + private readonly CombineLatestIndexedWitness _obs4; /// Indexed observer for source 5. - private readonly CombineLatestIndexedObserver _obs5; + private readonly CombineLatestIndexedWitness _obs5; /// Indexed observer for source 6. - private readonly CombineLatestIndexedObserver _obs6; + private readonly CombineLatestIndexedWitness _obs6; /// Indexed observer for source 7. - private readonly CombineLatestIndexedObserver _obs7; + private readonly CombineLatestIndexedWitness _obs7; /// Indexed observer for source 8. - private readonly CombineLatestIndexedObserver _obs8; + private readonly CombineLatestIndexedWitness _obs8; /// Indexed observer for source 9. - private readonly CombineLatestIndexedObserver _obs9; + private readonly CombineLatestIndexedWitness _obs9; /// Indexed observer for source 10. - private readonly CombineLatestIndexedObserver _obs10; + private readonly CombineLatestIndexedWitness _obs10; /// Indexed observer for source 11. - private readonly CombineLatestIndexedObserver _obs11; + private readonly CombineLatestIndexedWitness _obs11; /// Indexed observer for source 12. - private readonly CombineLatestIndexedObserver _obs12; + private readonly CombineLatestIndexedWitness _obs12; /// Indexed observer for source 13. - private readonly CombineLatestIndexedObserver _obs13; + private readonly CombineLatestIndexedWitness _obs13; /// Indexed observer for source 14. - private readonly CombineLatestIndexedObserver _obs14; + private readonly CombineLatestIndexedWitness _obs14; /// Indexed observer for source 15. - private readonly CombineLatestIndexedObserver _obs15; + private readonly CombineLatestIndexedWitness _obs15; /// Indexed observer for source 16. - private readonly CombineLatestIndexedObserver _obs16; + private readonly CombineLatestIndexedWitness _obs16; /// Latest value from source 1. private Optional _val1 = Optional.Empty; diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest2.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest2.cs index 4878c00..a863882 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest2.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest2.cs @@ -80,7 +80,7 @@ protected override ValueTask SubscribeAsyncCore( /// observers, the SubscribeAtAsync switch, and the selector invocation. Shared scaffolding /// (gate, lifecycle, ValuesLock, OnErrorResume, SubscribeSourcesAsync, DisposeAsync) lives /// in ; the per-source OnNext / OnError / - /// OnCompleted forwarding lives in . + /// OnCompleted forwarding lives in . /// internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase { @@ -97,10 +97,10 @@ internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase _selector; /// Indexed observer for source 1. - private readonly CombineLatestIndexedObserver _obs1; + private readonly CombineLatestIndexedWitness _obs1; /// Indexed observer for source 2. - private readonly CombineLatestIndexedObserver _obs2; + private readonly CombineLatestIndexedWitness _obs2; /// Latest value from source 1. private Optional _val1 = Optional.Empty; diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest3.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest3.cs index 44305d1..71afc23 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest3.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest3.cs @@ -86,7 +86,7 @@ protected override ValueTask SubscribeAsyncCore( /// observers, the SubscribeAtAsync switch, and the selector invocation. Shared scaffolding /// (gate, lifecycle, ValuesLock, OnErrorResume, SubscribeSourcesAsync, DisposeAsync) lives /// in ; the per-source OnNext / OnError / - /// OnCompleted forwarding lives in . + /// OnCompleted forwarding lives in . /// internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase { @@ -106,13 +106,13 @@ internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase _selector; /// Indexed observer for source 1. - private readonly CombineLatestIndexedObserver _obs1; + private readonly CombineLatestIndexedWitness _obs1; /// Indexed observer for source 2. - private readonly CombineLatestIndexedObserver _obs2; + private readonly CombineLatestIndexedWitness _obs2; /// Indexed observer for source 3. - private readonly CombineLatestIndexedObserver _obs3; + private readonly CombineLatestIndexedWitness _obs3; /// Latest value from source 1. private Optional _val1 = Optional.Empty; diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest4.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest4.cs index cf08969..2127b4d 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest4.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest4.cs @@ -92,7 +92,7 @@ protected override ValueTask SubscribeAsyncCore( /// observers, the SubscribeAtAsync switch, and the selector invocation. Shared scaffolding /// (gate, lifecycle, ValuesLock, OnErrorResume, SubscribeSourcesAsync, DisposeAsync) lives /// in ; the per-source OnNext / OnError / - /// OnCompleted forwarding lives in . + /// OnCompleted forwarding lives in . /// internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase { @@ -115,16 +115,16 @@ internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase _selector; /// Indexed observer for source 1. - private readonly CombineLatestIndexedObserver _obs1; + private readonly CombineLatestIndexedWitness _obs1; /// Indexed observer for source 2. - private readonly CombineLatestIndexedObserver _obs2; + private readonly CombineLatestIndexedWitness _obs2; /// Indexed observer for source 3. - private readonly CombineLatestIndexedObserver _obs3; + private readonly CombineLatestIndexedWitness _obs3; /// Indexed observer for source 4. - private readonly CombineLatestIndexedObserver _obs4; + private readonly CombineLatestIndexedWitness _obs4; /// Latest value from source 1. private Optional _val1 = Optional.Empty; diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest5.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest5.cs index 95208f4..aabea74 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest5.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest5.cs @@ -98,7 +98,7 @@ protected override ValueTask SubscribeAsyncCore( /// observers, the SubscribeAtAsync switch, and the selector invocation. Shared scaffolding /// (gate, lifecycle, ValuesLock, OnErrorResume, SubscribeSourcesAsync, DisposeAsync) lives /// in ; the per-source OnNext / OnError / - /// OnCompleted forwarding lives in . + /// OnCompleted forwarding lives in . /// internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase { @@ -124,19 +124,19 @@ internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase _selector; /// Indexed observer for source 1. - private readonly CombineLatestIndexedObserver _obs1; + private readonly CombineLatestIndexedWitness _obs1; /// Indexed observer for source 2. - private readonly CombineLatestIndexedObserver _obs2; + private readonly CombineLatestIndexedWitness _obs2; /// Indexed observer for source 3. - private readonly CombineLatestIndexedObserver _obs3; + private readonly CombineLatestIndexedWitness _obs3; /// Indexed observer for source 4. - private readonly CombineLatestIndexedObserver _obs4; + private readonly CombineLatestIndexedWitness _obs4; /// Indexed observer for source 5. - private readonly CombineLatestIndexedObserver _obs5; + private readonly CombineLatestIndexedWitness _obs5; /// Latest value from source 1. private Optional _val1 = Optional.Empty; diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest6.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest6.cs index 243b5b7..d286fd7 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest6.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest6.cs @@ -104,7 +104,7 @@ protected override ValueTask SubscribeAsyncCore( /// observers, the SubscribeAtAsync switch, and the selector invocation. Shared scaffolding /// (gate, lifecycle, ValuesLock, OnErrorResume, SubscribeSourcesAsync, DisposeAsync) lives /// in ; the per-source OnNext / OnError / - /// OnCompleted forwarding lives in . + /// OnCompleted forwarding lives in . /// internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase { @@ -133,22 +133,22 @@ internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase _selector; /// Indexed observer for source 1. - private readonly CombineLatestIndexedObserver _obs1; + private readonly CombineLatestIndexedWitness _obs1; /// Indexed observer for source 2. - private readonly CombineLatestIndexedObserver _obs2; + private readonly CombineLatestIndexedWitness _obs2; /// Indexed observer for source 3. - private readonly CombineLatestIndexedObserver _obs3; + private readonly CombineLatestIndexedWitness _obs3; /// Indexed observer for source 4. - private readonly CombineLatestIndexedObserver _obs4; + private readonly CombineLatestIndexedWitness _obs4; /// Indexed observer for source 5. - private readonly CombineLatestIndexedObserver _obs5; + private readonly CombineLatestIndexedWitness _obs5; /// Indexed observer for source 6. - private readonly CombineLatestIndexedObserver _obs6; + private readonly CombineLatestIndexedWitness _obs6; /// Latest value from source 1. private Optional _val1 = Optional.Empty; diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest7.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest7.cs index 999ef02..b922eac 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest7.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest7.cs @@ -110,7 +110,7 @@ protected override ValueTask SubscribeAsyncCore( /// observers, the SubscribeAtAsync switch, and the selector invocation. Shared scaffolding /// (gate, lifecycle, ValuesLock, OnErrorResume, SubscribeSourcesAsync, DisposeAsync) lives /// in ; the per-source OnNext / OnError / - /// OnCompleted forwarding lives in . + /// OnCompleted forwarding lives in . /// internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase { @@ -142,25 +142,25 @@ internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase _selector; /// Indexed observer for source 1. - private readonly CombineLatestIndexedObserver _obs1; + private readonly CombineLatestIndexedWitness _obs1; /// Indexed observer for source 2. - private readonly CombineLatestIndexedObserver _obs2; + private readonly CombineLatestIndexedWitness _obs2; /// Indexed observer for source 3. - private readonly CombineLatestIndexedObserver _obs3; + private readonly CombineLatestIndexedWitness _obs3; /// Indexed observer for source 4. - private readonly CombineLatestIndexedObserver _obs4; + private readonly CombineLatestIndexedWitness _obs4; /// Indexed observer for source 5. - private readonly CombineLatestIndexedObserver _obs5; + private readonly CombineLatestIndexedWitness _obs5; /// Indexed observer for source 6. - private readonly CombineLatestIndexedObserver _obs6; + private readonly CombineLatestIndexedWitness _obs6; /// Indexed observer for source 7. - private readonly CombineLatestIndexedObserver _obs7; + private readonly CombineLatestIndexedWitness _obs7; /// Latest value from source 1. private Optional _val1 = Optional.Empty; diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest8.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest8.cs index bc9f236..cbfb18a 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest8.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest8.cs @@ -116,7 +116,7 @@ protected override ValueTask SubscribeAsyncCore( /// observers, the SubscribeAtAsync switch, and the selector invocation. Shared scaffolding /// (gate, lifecycle, ValuesLock, OnErrorResume, SubscribeSourcesAsync, DisposeAsync) lives /// in ; the per-source OnNext / OnError / - /// OnCompleted forwarding lives in . + /// OnCompleted forwarding lives in . /// internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase { @@ -151,28 +151,28 @@ internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase _selector; /// Indexed observer for source 1. - private readonly CombineLatestIndexedObserver _obs1; + private readonly CombineLatestIndexedWitness _obs1; /// Indexed observer for source 2. - private readonly CombineLatestIndexedObserver _obs2; + private readonly CombineLatestIndexedWitness _obs2; /// Indexed observer for source 3. - private readonly CombineLatestIndexedObserver _obs3; + private readonly CombineLatestIndexedWitness _obs3; /// Indexed observer for source 4. - private readonly CombineLatestIndexedObserver _obs4; + private readonly CombineLatestIndexedWitness _obs4; /// Indexed observer for source 5. - private readonly CombineLatestIndexedObserver _obs5; + private readonly CombineLatestIndexedWitness _obs5; /// Indexed observer for source 6. - private readonly CombineLatestIndexedObserver _obs6; + private readonly CombineLatestIndexedWitness _obs6; /// Indexed observer for source 7. - private readonly CombineLatestIndexedObserver _obs7; + private readonly CombineLatestIndexedWitness _obs7; /// Indexed observer for source 8. - private readonly CombineLatestIndexedObserver _obs8; + private readonly CombineLatestIndexedWitness _obs8; /// Latest value from source 1. private Optional _val1 = Optional.Empty; diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest9.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest9.cs index 76a23ec..3e0fad2 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatest9.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatest9.cs @@ -122,7 +122,7 @@ protected override ValueTask SubscribeAsyncCore( /// observers, the SubscribeAtAsync switch, and the selector invocation. Shared scaffolding /// (gate, lifecycle, ValuesLock, OnErrorResume, SubscribeSourcesAsync, DisposeAsync) lives /// in ; the per-source OnNext / OnError / - /// OnCompleted forwarding lives in . + /// OnCompleted forwarding lives in . /// internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase { @@ -160,31 +160,31 @@ internal sealed class CombineLatestCoordinator : CombineLatestCoordinatorBase _selector; /// Indexed observer for source 1. - private readonly CombineLatestIndexedObserver _obs1; + private readonly CombineLatestIndexedWitness _obs1; /// Indexed observer for source 2. - private readonly CombineLatestIndexedObserver _obs2; + private readonly CombineLatestIndexedWitness _obs2; /// Indexed observer for source 3. - private readonly CombineLatestIndexedObserver _obs3; + private readonly CombineLatestIndexedWitness _obs3; /// Indexed observer for source 4. - private readonly CombineLatestIndexedObserver _obs4; + private readonly CombineLatestIndexedWitness _obs4; /// Indexed observer for source 5. - private readonly CombineLatestIndexedObserver _obs5; + private readonly CombineLatestIndexedWitness _obs5; /// Indexed observer for source 6. - private readonly CombineLatestIndexedObserver _obs6; + private readonly CombineLatestIndexedWitness _obs6; /// Indexed observer for source 7. - private readonly CombineLatestIndexedObserver _obs7; + private readonly CombineLatestIndexedWitness _obs7; /// Indexed observer for source 8. - private readonly CombineLatestIndexedObserver _obs8; + private readonly CombineLatestIndexedWitness _obs8; /// Indexed observer for source 9. - private readonly CombineLatestIndexedObserver _obs9; + private readonly CombineLatestIndexedWitness _obs9; /// Latest value from source 1. private Optional _val1 = Optional.Empty; diff --git a/src/ReactiveUI.Primitives.Async/Operators/CombineLatestEnumerable.cs b/src/ReactiveUI.Primitives.Async/Operators/CombineLatestEnumerable.cs index 0ece9f5..09bbe3f 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatestEnumerable.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatestEnumerable.cs @@ -98,12 +98,12 @@ protected override async ValueTask SubscribeAsyncCore( } /// - /// Per-source observer that forwards to the parent subscription with its own index. Replaces the three + /// Per-source witness that forwards to the parent subscription with its own index. Replaces the three /// captured-index lambdas the previous shape allocated per source. /// /// The parent coordinator. /// The source index. - internal sealed class IndexedObserver(EnumerableCombineLatestCoordinator parent, int index) : IObserverAsync + internal sealed class IndexedWitness(EnumerableCombineLatestCoordinator parent, int index) : IObserverAsync { /// public ValueTask OnNextAsync(TSource value, CancellationToken cancellationToken) => @@ -191,7 +191,7 @@ public async ValueTask SubscribeSourcesAsync(CancellationToken cancellationToken } _subscriptions[index] = await _sources[index] - .SubscribeAsync(new IndexedObserver(this, index), cancellationToken) + .SubscribeAsync(new IndexedWitness(this, index), cancellationToken) .ConfigureAwait(false); } } diff --git a/src/ReactiveUI.Primitives.Async/Operators/ConcatSignalSourcesSignal{T}.cs b/src/ReactiveUI.Primitives.Async/Operators/ConcatSignalSourcesSignal{T}.cs index 1b6d95e..2688a04 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/ConcatSignalSourcesSignal{T}.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/ConcatSignalSourcesSignal{T}.cs @@ -251,7 +251,7 @@ internal async ValueTask FinishAsync(Result? result) } /// - /// Observer for the outer observable sequence that delegates to the parent . + /// A witness for the outer observable sequence that delegates to the parent . /// /// The parent concat subscription. internal sealed class ConcatOuterWitness(ConcatCoordinator subscription) : ObserverAsync> @@ -297,7 +297,7 @@ protected override ValueTask OnCompletedAsyncCore(Result result) } /// - /// Observer for the currently active inner observable sequence that delegates to the parent . + /// A witness for the currently active inner observable sequence that delegates to the parent . /// /// The parent concat subscription. internal sealed class ConcatInnerWitness(ConcatCoordinator subscription) : ObserverAsync diff --git a/src/ReactiveUI.Primitives.Async/Operators/ContainsAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/ContainsAsync.cs index eb59bf5..c16c234 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/ContainsAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/ContainsAsync.cs @@ -75,7 +75,7 @@ public static ValueTask ContainsAsync(this IObservableAsync @this, T => @this.ContainsAsync(value, null, cancellationToken); /// - /// Observer that determines whether a sequence contains a specified value. + /// A witness that determines whether a sequence contains a specified value. /// /// The type of elements in the source sequence. /// The value to search for. @@ -84,7 +84,7 @@ public static ValueTask ContainsAsync(this IObservableAsync @this, T internal sealed class ContainsTaskWitness( T value, IEqualityComparer comparer, - CancellationToken cancellationToken) : TaskWitnessAsyncBase(cancellationToken) + CancellationToken cancellationToken) : TaskResultWitnessAsyncBase(cancellationToken) { /// protected override async ValueTask OnNextAsyncCore(T value1, CancellationToken cancellationToken) diff --git a/src/ReactiveUI.Primitives.Async/Operators/WitnessOnAsyncSignal.cs b/src/ReactiveUI.Primitives.Async/Operators/ContextSwitchSignalAsync.cs similarity index 94% rename from src/ReactiveUI.Primitives.Async/Operators/WitnessOnAsyncSignal.cs rename to src/ReactiveUI.Primitives.Async/Operators/ContextSwitchSignalAsync.cs index 4291bee..f4de676 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/WitnessOnAsyncSignal.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/ContextSwitchSignalAsync.cs @@ -11,7 +11,7 @@ namespace ReactiveUI.Primitives.Async; /// The source observable whose notifications will be context-switched. /// The async context to switch notifications onto. /// Whether to force yielding even if already on the target context. -internal sealed class WitnessOnAsyncSignal( +internal sealed class ContextSwitchSignalAsync( IObservableAsync source, AsyncContext asyncContext, bool forceYielding) : SignalAsync @@ -21,7 +21,7 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var contextSwitchObserver = new ContextSwitchObserver(observer, asyncContext, forceYielding); + var contextSwitchObserver = new ContextSwitchWitness(observer, asyncContext, forceYielding); return source.SubscribeAsync(contextSwitchObserver, cancellationToken); } @@ -31,7 +31,7 @@ protected override ValueTask SubscribeAsyncCore( /// The downstream observer to forward notifications to. /// The async context to switch onto. /// Whether to force yielding even if already on the target context. - internal sealed class ContextSwitchObserver(IObserverAsync observer, AsyncContext asyncContext, bool forceYielding) + internal sealed class ContextSwitchWitness(IObserverAsync observer, AsyncContext asyncContext, bool forceYielding) : ObserverAsync { /// Slow path: switch to the target context then forward the value. diff --git a/src/ReactiveUI.Primitives.Async/Operators/CountAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/CountAsync.cs index 6b57f07..dc4fc68 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CountAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CountAsync.cs @@ -64,13 +64,13 @@ public static ValueTask CountAsync(this IObservableAsync @this, Cance => @this.CountAsync(null, cancellationToken); /// - /// Observer that counts elements in a sequence, optionally filtered by a predicate. + /// A witness that counts elements in a sequence, optionally filtered by a predicate. /// /// The type of elements in the source sequence. /// An optional predicate to filter elements. If null, all elements are counted. /// A cancellation token for the operation. internal sealed class CountTaskWitness(Func? predicate, CancellationToken cancellationToken) - : TaskWitnessAsyncBase(cancellationToken) + : TaskResultWitnessAsyncBase(cancellationToken) { /// /// The running count of elements that satisfy the predicate. diff --git a/src/ReactiveUI.Primitives.Async/Operators/Delay.cs b/src/ReactiveUI.Primitives.Async/Operators/Delay.cs index bcdd77c..1cffac4 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/Delay.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/Delay.cs @@ -66,14 +66,14 @@ protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var delayObserver = new DelayObserver(observer, delayInterval, timeProvider, cancellationToken); + var delayObserver = new DelayWitness(observer, delayInterval, timeProvider, cancellationToken); return source.SubscribeAsync(delayObserver, cancellationToken); } /// - /// An observer that delays each element by waiting before forwarding to the downstream observer. + /// A witness that delays each element by waiting before forwarding to the downstream witness. /// - internal sealed class DelayObserver( + internal sealed class DelayWitness( IObserverAsync observer, TimeSpan delayInterval, TimeProvider timeProvider, diff --git a/src/ReactiveUI.Primitives.Async/Operators/Distinct.cs b/src/ReactiveUI.Primitives.Async/Operators/Distinct.cs index e2b6ee1..0deec7f 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/Distinct.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/Distinct.cs @@ -100,7 +100,7 @@ protected override async ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var sink = new DistinctObserver(observer, comparer, cancellationToken); + var sink = new DistinctWitness(observer, comparer, cancellationToken); if (observer is ObserverAsync downstreamBase) { @@ -112,11 +112,11 @@ protected override async ValueTask SubscribeAsyncCore( return sink; } - /// Per-subscription observer that tracks seen values in a . - /// The downstream observer. + /// Per-subscription witness that tracks seen values in a . + /// The downstream witness. /// The equality comparer. /// The subscribe-time cancellation token. - internal sealed class DistinctObserver( + internal sealed class DistinctWitness( IObserverAsync downstream, IEqualityComparer comparer, CancellationToken subscribeToken) : ObserverAsync(subscribeToken) @@ -154,7 +154,7 @@ protected override async ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var sink = new DistinctByObserver(observer, keySelector, comparer, cancellationToken); + var sink = new DistinctByWitness(observer, keySelector, comparer, cancellationToken); if (observer is ObserverAsync downstreamBase) { @@ -166,12 +166,12 @@ protected override async ValueTask SubscribeAsyncCore( return sink; } - /// Per-subscription observer that tracks seen keys. - /// The downstream observer. + /// Per-subscription witness that tracks seen keys. + /// The downstream witness. /// The key selector. /// The key equality comparer. /// The subscribe-time cancellation token. - internal sealed class DistinctByObserver( + internal sealed class DistinctByWitness( IObserverAsync downstream, Func keySelector, IEqualityComparer comparer, diff --git a/src/ReactiveUI.Primitives.Async/Operators/DistinctUntilChanged.cs b/src/ReactiveUI.Primitives.Async/Operators/DistinctUntilChanged.cs index 9f30d9b..25c83ae 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/DistinctUntilChanged.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/DistinctUntilChanged.cs @@ -106,7 +106,7 @@ protected override async ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var sink = new DistinctUntilChangedObserver(observer, comparer, cancellationToken); + var sink = new DistinctUntilChangedWitness(observer, comparer, cancellationToken); if (observer is ObserverAsync downstreamBase) { @@ -118,11 +118,11 @@ protected override async ValueTask SubscribeAsyncCore( return sink; } - /// Per-subscription observer that drops values equal to the most-recently-forwarded one. - /// The downstream observer. + /// Per-subscription witness that drops values equal to the most-recently-forwarded one. + /// The downstream witness. /// The equality comparer. /// The subscribe-time cancellation token. - internal sealed class DistinctUntilChangedObserver( + internal sealed class DistinctUntilChangedWitness( IObserverAsync downstream, IEqualityComparer comparer, CancellationToken subscribeToken) : ObserverAsync(subscribeToken) @@ -175,7 +175,7 @@ protected override async ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var sink = new DistinctUntilChangedByObserver(observer, keySelector, comparer, cancellationToken); + var sink = new DistinctUntilChangedByWitness(observer, keySelector, comparer, cancellationToken); if (observer is ObserverAsync downstreamBase) { @@ -187,12 +187,12 @@ protected override async ValueTask SubscribeAsyncCore( return sink; } - /// Per-subscription observer that compares extracted keys against the most-recently-forwarded one. - /// The downstream observer. + /// Per-subscription witness that compares extracted keys against the most-recently-forwarded one. + /// The downstream witness. /// The key selector. /// The key equality comparer. /// The subscribe-time cancellation token. - internal sealed class DistinctUntilChangedByObserver( + internal sealed class DistinctUntilChangedByWitness( IObserverAsync downstream, Func keySelector, IEqualityComparer comparer, diff --git a/src/ReactiveUI.Primitives.Async/Operators/Do.cs b/src/ReactiveUI.Primitives.Async/Operators/Do.cs index b54b44f..fab81ee 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/Do.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/Do.cs @@ -101,7 +101,7 @@ protected override ValueTask SubscribeAsyncCore( } /// - /// An observer that invokes asynchronous side-effect callbacks before forwarding notifications. + /// A witness that invokes asynchronous side-effect callbacks before forwarding notifications. /// internal sealed class AsyncSideEffectWitness( IObserverAsync observer, diff --git a/src/ReactiveUI.Primitives.Async/Operators/FirstAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/FirstAsync.cs index b36b27f..2cd3198 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/FirstAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/FirstAsync.cs @@ -75,13 +75,13 @@ public static async ValueTask FirstAsync(this IObservableAsync @this, C } /// - /// Observer that captures the first element matching an optional predicate. + /// A witness that captures the first element matching an optional predicate. /// /// The type of elements in the source sequence. /// An optional predicate to filter elements. /// A cancellation token for the operation. internal sealed class FirstTaskWitness(Func? predicate, CancellationToken cancellationToken) - : TaskWitnessAsyncBase(cancellationToken) + : TaskResultWitnessAsyncBase(cancellationToken) { /// protected override async ValueTask OnNextAsyncCore(T value, CancellationToken cancellationToken) diff --git a/src/ReactiveUI.Primitives.Async/Operators/FirstOrDefaultAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/FirstOrDefaultAsync.cs index e7c311f..82def38 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/FirstOrDefaultAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/FirstOrDefaultAsync.cs @@ -110,7 +110,7 @@ public static partial class SignalAsync } /// - /// Observer that captures the first element matching an optional predicate, or returns a default value. + /// A witness that captures the first element matching an optional predicate, or returns a default value. /// /// The type of elements in the source sequence. /// An optional predicate to filter elements. @@ -119,7 +119,7 @@ public static partial class SignalAsync internal sealed class FirstOrDefaultTaskWitness( Func? predicate, T? defaultValue, - CancellationToken cancellationToken) : TaskWitnessAsyncBase(cancellationToken) + CancellationToken cancellationToken) : TaskResultWitnessAsyncBase(cancellationToken) { /// protected override async ValueTask OnNextAsyncCore(T value, CancellationToken cancellationToken) diff --git a/src/ReactiveUI.Primitives.Async/Operators/ForEachAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/ForEachAsync.cs index f1a66d5..ff47722 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/ForEachAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/ForEachAsync.cs @@ -95,12 +95,12 @@ public static async ValueTask ForEachAsync(this IObservableAsync @this, Ac } /// - /// An observer that invokes an asynchronous callback for each element and signals completion via a task. + /// A witness that invokes an asynchronous callback for each element and signals completion via a task. /// /// The type of elements in the sequence. internal sealed class ForEachAsyncTaskWitness( Func onNextAsync, - CancellationToken cancellationToken) : TaskWitnessAsyncBase(cancellationToken) + CancellationToken cancellationToken) : TaskResultWitnessAsyncBase(cancellationToken) { /// protected override ValueTask OnNextAsyncCore(T value, CancellationToken cancellationToken) => @@ -116,11 +116,11 @@ protected override ValueTask OnCompletedAsyncCore(Result result) => } /// - /// An observer that invokes a synchronous callback for each element and signals completion via a task. + /// A witness that invokes a synchronous callback for each element and signals completion via a task. /// /// The type of elements in the sequence. internal sealed class ForEachSyncTaskWitness(Action onNext, CancellationToken cancellationToken) - : TaskWitnessAsyncBase(cancellationToken) + : TaskResultWitnessAsyncBase(cancellationToken) { /// /// The synchronous callback invoked for each element in the sequence. diff --git a/src/ReactiveUI.Primitives.Async/Operators/GroupBy.cs b/src/ReactiveUI.Primitives.Async/Operators/GroupBy.cs index ba53cf3..350aca0 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/GroupBy.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/GroupBy.cs @@ -236,7 +236,7 @@ protected override async ValueTask SubscribeAsyncCore( // that token); the downstream observer (if an ObserverAsync) observes the // wrap's dispose token. Together they collapse the per-emission // CancellationTokenSource.CreateLinkedTokenSource allocations to zero. - var wrap = new ForwardingAsyncWitness(observer); + var wrap = new RelayWitnessAsync(observer); wrap.LinkUpstreamCancellation(parent.InternalDisposedToken); if (observer is ObserverAsync downstream) { diff --git a/src/ReactiveUI.Primitives.Async/Operators/LastAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/LastAsync.cs index b5c3cb1..ca98ffb 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/LastAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/LastAsync.cs @@ -83,7 +83,7 @@ public static async ValueTask LastAsync(this IObservableAsync @this, Ca /// An optional predicate to filter elements. /// A cancellation token for the operation. internal sealed class LastTaskWitness(Func? predicate, CancellationToken cancellationToken) - : TaskWitnessAsyncBase(cancellationToken) + : TaskResultWitnessAsyncBase(cancellationToken) { /// /// A value indicating whether any matching element has been observed. diff --git a/src/ReactiveUI.Primitives.Async/Operators/LastOrDefaultAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/LastOrDefaultAsync.cs index 611ceb3..b87d929 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/LastOrDefaultAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/LastOrDefaultAsync.cs @@ -109,7 +109,7 @@ public static partial class SignalAsync } /// - /// Observer that captures the last element matching an optional predicate, or returns a default value. + /// Witness that captures the last element matching an optional predicate, or returns a default value. /// /// The type of elements in the source sequence. /// An optional predicate to filter elements. @@ -118,7 +118,7 @@ public static partial class SignalAsync internal sealed class LastOrDefaultTaskWitness( Func? predicate, T? defaultValue, - CancellationToken cancellationToken) : TaskWitnessAsyncBase(cancellationToken) + CancellationToken cancellationToken) : TaskResultWitnessAsyncBase(cancellationToken) { /// /// The most recently observed matching element, or the default value if no match has been found. diff --git a/src/ReactiveUI.Primitives.Async/Operators/LongCountAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/LongCountAsync.cs index e70ff50..8103d78 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/LongCountAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/LongCountAsync.cs @@ -67,13 +67,13 @@ public static ValueTask LongCountAsync(this IObservableAsync @this, => @this.LongCountAsync(null, cancellationToken); /// - /// Observer that counts elements in a sequence as a 64-bit integer, optionally filtered by a predicate. + /// Witness that counts elements in a sequence as a 64-bit integer, optionally filtered by a predicate. /// /// The type of elements in the source sequence. /// An optional predicate to filter elements. If null, all elements are counted. /// A cancellation token for the operation. internal sealed class LongCountTaskWitness(Func? predicate, CancellationToken cancellationToken) - : TaskWitnessAsyncBase(cancellationToken) + : TaskResultWitnessAsyncBase(cancellationToken) { /// /// The running count of elements that satisfy the predicate. diff --git a/src/ReactiveUI.Primitives.Async/Operators/Merge.cs b/src/ReactiveUI.Primitives.Async/Operators/Merge.cs index 35c23cd..c95a8a7 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/Merge.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/Merge.cs @@ -317,7 +317,7 @@ protected internal virtual async ValueTask SubscribeBranchAsync(IObservableAsync /// Creates a new inner observer for subscribing to an inner observable sequence. /// /// A new inner async observer instance. - protected internal virtual MergeBranchObserver CreateBranchObserver() => new(this); + protected internal virtual MergeBranchWitness CreateBranchObserver() => new(this); /// /// Completes the merged sequence, disposes all subscriptions, and optionally signals the downstream observer. @@ -349,12 +349,12 @@ protected internal async ValueTask FinishAsync(Result? result) } /// - /// Observer that forwards items from an inner observable to the parent merge subscription. + /// Witness that forwards items from an inner observable to the parent merge subscription. /// - internal class MergeBranchObserver(MergeCoordinator parent) : ObserverAsync + internal class MergeBranchWitness(MergeCoordinator parent) : ObserverAsync { /// - /// Subscribes this observer to an inner observable sequence. + /// Subscribes this witness to an inner observable sequence. /// /// The inner observable to subscribe to. /// A token to cancel the subscription. @@ -421,7 +421,7 @@ internal sealed class BoundedMergeCoordinator(IObserverAsync observer, int protected internal override async ValueTask SubscribeBranchAsync(IObservableAsync inner) { await _semaphore.WaitAsync(DisposedCancellationToken).ConfigureAwait(false); - var innerObserver = (MergeBranchObserverWithPermit)CreateBranchObserver(); + var innerObserver = (MergeBranchWitnessWithPermit)CreateBranchObserver(); var subscribed = false; try { @@ -446,23 +446,23 @@ protected internal override async ValueTask SubscribeBranchAsync(IObservableAsyn } /// - protected internal override MergeBranchObserver CreateBranchObserver() => - new MergeBranchObserverWithPermit(this); + protected internal override MergeBranchWitness CreateBranchObserver() => + new MergeBranchWitnessWithPermit(this); /// - /// Inner observer that releases a semaphore slot on disposal. + /// Inner witness that releases a semaphore slot on disposal. /// - internal sealed class MergeBranchObserverWithPermit(BoundedMergeCoordinator parent) - : MergeBranchObserver(parent) + internal sealed class MergeBranchWitnessWithPermit(BoundedMergeCoordinator parent) + : MergeBranchWitness(parent) { - /// Tracks whether the semaphore slot has already been released for this observer. + /// Tracks whether the semaphore slot has already been released for this witness. /// - /// can be invoked more than once for the same observer + /// can be invoked more than once for the same witness /// (auto-dispose after OnCompletedAsync, then again from CompositeDisposableAsync.Remove /// and from the parent's FinishAsync path). Without this guard, - /// would be called multiple times per observer, exceeding maxCount and throwing + /// would be called multiple times per witness, exceeding maxCount and throwing /// — which interrupts the parent's completion chain and leaves - /// downstream observers waiting forever. + /// downstream witnesses waiting forever. /// private int _released; @@ -569,7 +569,7 @@ public void BeginSubscribing() => FireAndForgetHelper.Run(async () => { Interlocked.Increment(ref _active); - var innerObserver = new MergeBranchObserver(this); + var innerObserver = new MergeBranchWitness(this); await _innerDisposables.AddAsync(innerObserver).ConfigureAwait(false); try { @@ -777,9 +777,9 @@ internal async ValueTask FinishAsync(Result? result) } /// - /// Observer that forwards items from an inner source to the parent enumerable merge subscription. + /// Witness that forwards items from an inner source to the parent enumerable merge subscription. /// - internal sealed class MergeBranchObserver(MergeSequenceCoordinator parent) : ObserverAsync + internal sealed class MergeBranchWitness(MergeSequenceCoordinator parent) : ObserverAsync { /// protected override ValueTask OnNextAsyncCore(T value, CancellationToken cancellationToken) diff --git a/src/ReactiveUI.Primitives.Async/Operators/OfType.cs b/src/ReactiveUI.Primitives.Async/Operators/OfType.cs index 663b42b..84b6337 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/OfType.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/OfType.cs @@ -50,7 +50,7 @@ protected override async ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var sink = new OfTypeObserver(observer, cancellationToken); + var sink = new OfTypeWitness(observer, cancellationToken); if (observer is ObserverAsync downstreamBase) { @@ -62,10 +62,10 @@ protected override async ValueTask SubscribeAsyncCore( return sink; } - /// Per-subscription observer that forwards values matching . - /// The downstream observer. + /// Per-subscription witness that forwards values matching . + /// The downstream witness. /// The subscribe-time cancellation token. - internal sealed class OfTypeObserver( + internal sealed class OfTypeWitness( IObserverAsync downstream, CancellationToken subscribeToken) : ObserverAsync(subscribeToken) { diff --git a/src/ReactiveUI.Primitives.Async/Operators/OnDispose.cs b/src/ReactiveUI.Primitives.Async/Operators/OnDispose.cs index 6651884..5c7d177 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/OnDispose.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/OnDispose.cs @@ -61,7 +61,7 @@ internal sealed class OnDisposeSignal(IObservableAsync source, Func protected override ValueTask SubscribeAsyncCore(IObserverAsync observer, CancellationToken cancellationToken) { - var sink = new OnDisposeObserver(observer, disposeAction); + var sink = new OnDisposeWitness(observer, disposeAction); return source.SubscribeAsync(sink, cancellationToken); } } @@ -75,16 +75,16 @@ internal sealed class OnDisposeSyncSignal(IObservableAsync source, Action /// protected override ValueTask SubscribeAsyncCore(IObserverAsync observer, CancellationToken cancellationToken) { - var sink = new OnDisposeObserverSync(observer, disposeAction); + var sink = new OnDisposeWitnessSync(observer, disposeAction); return source.SubscribeAsync(sink, cancellationToken); } } /// - /// An observer that invokes a synchronous action when disposed. + /// A witness that invokes a synchronous action when disposed. /// /// The type of elements in the sequence. - internal sealed class OnDisposeObserverSync(IObserverAsync observer, Action finallySync) : ObserverAsync + internal sealed class OnDisposeWitnessSync(IObserverAsync observer, Action finallySync) : ObserverAsync { /// protected override ValueTask OnNextAsyncCore(T value, CancellationToken cancellationToken) @@ -113,10 +113,10 @@ protected override async ValueTask DisposeAsyncCore() } /// - /// An observer that invokes an asynchronous callback when disposed. + /// A witness that invokes an asynchronous callback when disposed. /// /// The type of elements in the sequence. - internal sealed class OnDisposeObserver(IObserverAsync observer, Func finallyAsync) : ObserverAsync + internal sealed class OnDisposeWitness(IObserverAsync observer, Func finallyAsync) : ObserverAsync { /// protected override ValueTask OnNextAsyncCore(T value, CancellationToken cancellationToken) diff --git a/src/ReactiveUI.Primitives.Async/Operators/OnErrorResumeAsFailure.cs b/src/ReactiveUI.Primitives.Async/Operators/OnErrorResumeAsFailure.cs index cdd8471..c60e4f0 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/OnErrorResumeAsFailure.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/OnErrorResumeAsFailure.cs @@ -46,13 +46,13 @@ internal sealed class OnErrorResumeAsFailureSignal(IObservableAsync source protected override ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) => - source.SubscribeAsync(new OnErrorResumeAsFailureObserver(observer), cancellationToken); + source.SubscribeAsync(new OnErrorResumeAsFailureWitness(observer), cancellationToken); /// - /// Observer that forwards values and completion, but converts resumable errors into failure completions. + /// A witness that forwards values and completion, but converts resumable errors into failure completions. /// /// The downstream observer to forward notifications to. - internal sealed class OnErrorResumeAsFailureObserver(IObserverAsync observer) : ObserverAsync + internal sealed class OnErrorResumeAsFailureWitness(IObserverAsync observer) : ObserverAsync { /// protected override ValueTask OnNextAsyncCore(T value, CancellationToken cancellationToken) => diff --git a/src/ReactiveUI.Primitives.Async/Operators/ParityHelpers.FilterFusions.cs b/src/ReactiveUI.Primitives.Async/Operators/ParityHelpers.FilterFusions.cs index 8e99fe1..a7b3d28 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/ParityHelpers.FilterFusions.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/ParityHelpers.FilterFusions.cs @@ -30,7 +30,7 @@ protected override async ValueTask SubscribeAsyncCore( IObserverAsync<(T Previous, T Current)> observer, CancellationToken cancellationToken) { - var sink = new PairwiseObserver(observer, cancellationToken); + var sink = new PairwiseWitness(observer, cancellationToken); if (observer is ObserverAsync<(T Previous, T Current)> downstreamBase) { @@ -42,10 +42,10 @@ protected override async ValueTask SubscribeAsyncCore( return sink; } - /// Per-subscription observer that emits (previous, current) tuples once primed. + /// Per-subscription witness that emits (previous, current) tuples once primed. /// The downstream observer. /// The subscribe-time cancellation token. - internal sealed class PairwiseObserver( + internal sealed class PairwiseWitness( IObserverAsync<(T Previous, T Current)> downstream, CancellationToken subscribeToken) : ObserverAsync(subscribeToken) { @@ -96,7 +96,7 @@ protected override async ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var sink = new SkipWhileNullObserver(observer, cancellationToken); + var sink = new SkipWhileNullWitness(observer, cancellationToken); if (observer is ObserverAsync downstreamBase) { @@ -111,7 +111,7 @@ protected override async ValueTask SubscribeAsyncCore( /// Per-subscription observer that skips leading nulls then forwards every subsequent value. /// The downstream observer expecting non-nullable values. /// The subscribe-time cancellation token, linked into the dispose chain. - internal sealed class SkipWhileNullObserver( + internal sealed class SkipWhileNullWitness( IObserverAsync downstream, CancellationToken subscribeToken) : ObserverAsync(subscribeToken) { @@ -160,7 +160,7 @@ protected override async ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var sink = new WhereIsNotNullObserver(observer, cancellationToken); + var sink = new WhereIsNotNullWitness(observer, cancellationToken); // Wire sink's dispose token into the downstream's link chain so its hot path recognises // this token without allocating a per-emission linked CTS. @@ -177,7 +177,7 @@ protected override async ValueTask SubscribeAsyncCore( /// Per-subscription observer that strips nulls and forwards the rest as non-nullable. /// The downstream observer expecting non-nullable values. /// The subscribe-time cancellation token, linked into the dispose chain. - internal sealed class WhereIsNotNullObserver( + internal sealed class WhereIsNotNullWitness( IObserverAsync downstream, CancellationToken subscribeToken) : ObserverAsync(subscribeToken) { @@ -219,7 +219,7 @@ protected override async ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var sink = new LatestOrDefaultObserver(observer, defaultValue, cancellationToken); + var sink = new LatestOrDefaultWitness(observer, defaultValue, cancellationToken); if (observer is ObserverAsync downstreamBase) { @@ -237,7 +237,7 @@ protected override async ValueTask SubscribeAsyncCore( /// The downstream observer. /// The seed value already emitted from ; treated as the initial "last forwarded value". /// The subscribe-time cancellation token, linked into the dispose chain. - internal sealed class LatestOrDefaultObserver( + internal sealed class LatestOrDefaultWitness( IObserverAsync downstream, T seed, CancellationToken subscribeToken) : ObserverAsync(subscribeToken) @@ -287,7 +287,7 @@ protected override async ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var sink = new WaitUntilObserver(observer, predicate, cancellationToken); + var sink = new WaitUntilWitness(observer, predicate, cancellationToken); if (observer is ObserverAsync downstreamBase) { @@ -299,11 +299,11 @@ protected override async ValueTask SubscribeAsyncCore( return sink; } - /// Per-subscription observer that matches the predicate, forwards the first hit, and completes. + /// Per-subscription witness that matches the predicate, forwards the first hit, and completes. /// The downstream observer. /// The predicate matched against each value. /// The subscribe-time cancellation token, linked into the dispose chain. - internal sealed class WaitUntilObserver( + internal sealed class WaitUntilWitness( IObserverAsync downstream, Func predicate, CancellationToken subscribeToken) : ObserverAsync(subscribeToken) @@ -347,7 +347,7 @@ protected override async ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var sink = new AsSignalObserver(observer, cancellationToken); + var sink = new AsSignalWitness(observer, cancellationToken); if (observer is ObserverAsync downstreamBase) { @@ -362,7 +362,7 @@ protected override async ValueTask SubscribeAsyncCore( /// Forwards for every upstream emission. /// The downstream observer. /// The subscribe-time cancellation token. - internal sealed class AsSignalObserver( + internal sealed class AsSignalWitness( IObserverAsync downstream, CancellationToken subscribeToken) : ObserverAsync(subscribeToken) { @@ -391,7 +391,7 @@ protected override async ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var sink = new NotObserver(observer, cancellationToken); + var sink = new NotWitness(observer, cancellationToken); if (observer is ObserverAsync downstreamBase) { @@ -406,7 +406,7 @@ protected override async ValueTask SubscribeAsyncCore( /// Negates every upstream emission. /// The downstream observer. /// The subscribe-time cancellation token. - internal sealed class NotObserver( + internal sealed class NotWitness( IObserverAsync downstream, CancellationToken subscribeToken) : ObserverAsync(subscribeToken) { @@ -435,7 +435,7 @@ protected override async ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var sink = new WhereTrueObserver(observer, cancellationToken); + var sink = new WhereTrueWitness(observer, cancellationToken); if (observer is ObserverAsync downstreamBase) { @@ -450,7 +450,7 @@ protected override async ValueTask SubscribeAsyncCore( /// Forwards only values. /// The downstream observer. /// The subscribe-time cancellation token. - internal sealed class WhereTrueObserver( + internal sealed class WhereTrueWitness( IObserverAsync downstream, CancellationToken subscribeToken) : ObserverAsync(subscribeToken) { @@ -479,7 +479,7 @@ protected override async ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var sink = new WhereFalseObserver(observer, cancellationToken); + var sink = new WhereFalseWitness(observer, cancellationToken); if (observer is ObserverAsync downstreamBase) { @@ -494,7 +494,7 @@ protected override async ValueTask SubscribeAsyncCore( /// Forwards only values. /// The downstream observer. /// The subscribe-time cancellation token. - internal sealed class WhereFalseObserver( + internal sealed class WhereFalseWitness( IObserverAsync downstream, CancellationToken subscribeToken) : ObserverAsync(subscribeToken) { diff --git a/src/ReactiveUI.Primitives.Async/Operators/ParityHelpers.OperatorFusions.cs b/src/ReactiveUI.Primitives.Async/Operators/ParityHelpers.OperatorFusions.cs index 97f300a..87ae910 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/ParityHelpers.OperatorFusions.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/ParityHelpers.OperatorFusions.cs @@ -37,7 +37,7 @@ protected override async ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var sink = new ScanWithInitialObserver(observer, initial, accumulator, cancellationToken); + var sink = new ScanWithInitialWitness(observer, initial, accumulator, cancellationToken); if (observer is ObserverAsync downstreamBase) { @@ -56,7 +56,7 @@ protected override async ValueTask SubscribeAsyncCore( /// The seed accumulator value already emitted from . /// The synchronous accumulator. /// The subscribe-time cancellation token. - internal sealed class ScanWithInitialObserver( + internal sealed class ScanWithInitialWitness( IObserverAsync downstream, TAccumulate seed, Func accumulator, @@ -100,7 +100,7 @@ protected override async ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var sink = new ScanWithInitialAsyncObserver(observer, initial, accumulator, cancellationToken); + var sink = new ScanWithInitialAsyncWitness(observer, initial, accumulator, cancellationToken); if (observer is ObserverAsync downstreamBase) { @@ -119,7 +119,7 @@ protected override async ValueTask SubscribeAsyncCore( /// The seed accumulator value already emitted from . /// The asynchronous accumulator. /// The subscribe-time cancellation token. - internal sealed class ScanWithInitialAsyncObserver( + internal sealed class ScanWithInitialAsyncWitness( IObserverAsync downstream, TAccumulate seed, Func> accumulator, @@ -181,7 +181,7 @@ protected override async ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var sink = new ThrottleDistinctObserver(observer, dueTime, timeProvider, cancellationToken); + var sink = new ThrottleDistinctWitness(observer, dueTime, timeProvider, cancellationToken); if (observer is ObserverAsync downstreamBase) { @@ -193,12 +193,12 @@ protected override async ValueTask SubscribeAsyncCore( return sink; } - /// Per-subscription observer fusing upstream-distinct + debounce + downstream-distinct. + /// Per-subscription witness fusing upstream-distinct + debounce + downstream-distinct. /// The downstream observer. /// The debounce window. /// The time provider used for the debounce timer. /// The subscribe-time cancellation token. - internal sealed class ThrottleDistinctObserver( + internal sealed class ThrottleDistinctWitness( IObserverAsync downstream, TimeSpan dueTime, TimeProvider timeProvider, @@ -354,7 +354,7 @@ protected override async ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var sink = new DropIfBusyObserver(observer, asyncAction, cancellationToken); + var sink = new DropIfBusyWitness(observer, asyncAction, cancellationToken); if (observer is ObserverAsync downstreamBase) { @@ -366,11 +366,11 @@ protected override async ValueTask SubscribeAsyncCore( return sink; } - /// Per-subscription observer that drops upstream emissions while a prior action is still pending. + /// Per-subscription witness that drops upstream emissions while a prior action is still pending. /// The downstream observer. /// The async side-effect invoked for accepted values. /// The subscribe-time cancellation token, linked into the dispose chain. - internal sealed class DropIfBusyObserver( + internal sealed class DropIfBusyWitness( IObserverAsync downstream, Func asyncAction, CancellationToken subscribeToken) : ObserverAsync(subscribeToken) @@ -743,7 +743,7 @@ protected override async ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var sink = new DebounceUntilObserver(observer, debounce, condition, timeProvider, cancellationToken); + var sink = new DebounceUntilWitness(observer, debounce, condition, timeProvider, cancellationToken); if (observer is ObserverAsync downstreamBase) { @@ -761,7 +761,7 @@ protected override async ValueTask SubscribeAsyncCore( /// The bypass-the-delay condition. /// The time provider used for the debounce timer. /// The subscribe-time cancellation token. - internal sealed class DebounceUntilObserver( + internal sealed class DebounceUntilWitness( IObserverAsync downstream, TimeSpan debounce, Func condition, @@ -892,7 +892,7 @@ protected override async ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - var sink = new ForEachEnumerableObserver(observer, cancellationToken); + var sink = new ForEachEnumerableWitness(observer, cancellationToken); if (observer is ObserverAsync downstreamBase) { @@ -904,10 +904,10 @@ protected override async ValueTask SubscribeAsyncCore( return sink; } - /// Per-subscription observer that flattens each upstream enumerable inline. + /// Per-subscription witness that flattens each upstream enumerable inline. /// The downstream observer. /// The subscribe-time cancellation token. - internal sealed class ForEachEnumerableObserver( + internal sealed class ForEachEnumerableWitness( IObserverAsync downstream, CancellationToken subscribeToken) : ObserverAsync>(subscribeToken) { diff --git a/src/ReactiveUI.Primitives.Async/Operators/RefCount.cs b/src/ReactiveUI.Primitives.Async/Operators/RefCount.cs index cc7b95e..44ae873 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/RefCount.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/RefCount.cs @@ -119,16 +119,16 @@ protected override async ValueTask SubscribeAsyncCore( } /// - /// Observer wrapper that forwards all notifications and decrements the parent's reference count on disposal, + /// Witness wrapper that forwards all notifications and decrements the parent's reference count on disposal, /// disconnecting from the source when the count reaches zero. /// /// The parent ref-count observable. - /// The downstream observer to forward notifications to. + /// The downstream witness to forward notifications to. internal sealed class RefCountWitness(RefCountSignal parent, IObserverAsync observer) : ObserverAsync { /// - /// Forwards an element to the downstream observer. + /// Forwards an element to the downstream witness. /// /// The element to forward. /// A token to cancel the operation. @@ -137,7 +137,7 @@ protected override ValueTask OnNextAsyncCore(T value, CancellationToken cancella observer.OnNextAsync(value, cancellationToken); /// - /// Forwards a non-fatal error to the downstream observer. + /// Forwards a non-fatal error to the downstream witness. /// /// The error to forward. /// A token to cancel the operation. diff --git a/src/ReactiveUI.Primitives.Async/Operators/SingleAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/SingleAsync.cs index 9705f07..41358c9 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/SingleAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/SingleAsync.cs @@ -88,7 +88,7 @@ private static async ValueTask SingleCoreAsync( CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - var observer = new SingleElementObserver(predicate, requireExactlyOne: true, defaultValue: default, cancellationToken); + var observer = new SingleElementWitness(predicate, requireExactlyOne: true, defaultValue: default, cancellationToken); await using var subscription = await source.SubscribeAsync(observer, cancellationToken).ConfigureAwait(false); var result = await observer.AwaitResultAsync().ConfigureAwait(false); return result!; diff --git a/src/ReactiveUI.Primitives.Async/Operators/SingleOrDefaultAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/SingleOrDefaultAsync.cs index ff31c91..cf63f8d 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/SingleOrDefaultAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/SingleOrDefaultAsync.cs @@ -130,7 +130,7 @@ public static partial class SignalAsync CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - var observer = new SingleElementObserver(predicate, requireExactlyOne: false, defaultValue, cancellationToken); + var observer = new SingleElementWitness(predicate, requireExactlyOne: false, defaultValue, cancellationToken); await using var subscription = await source.SubscribeAsync(observer, cancellationToken).ConfigureAwait(false); return await observer.AwaitResultAsync().ConfigureAwait(false); } diff --git a/src/ReactiveUI.Primitives.Async/Operators/SubscribeAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/SubscribeAsync.cs index 19ea4fa..d4e8900 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/SubscribeAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/SubscribeAsync.cs @@ -48,7 +48,7 @@ public static ValueTask SubscribeAsync( ArgumentExceptionHelper.ThrowIfNull(source); ArgumentExceptionHelper.ThrowIfNull(onNextAsync); - var observer = new DelegateAsyncWitness(onNextAsync, onErrorResumeAsync, onCompletedAsync); + var observer = new CallbackWitnessAsync(onNextAsync, onErrorResumeAsync, onCompletedAsync); return source.SubscribeAsync(observer, cancellationToken); } @@ -105,7 +105,7 @@ public static ValueTask SubscribeAsync( { ArgumentExceptionHelper.ThrowIfNull(onNext); - var observer = new DelegateAsyncWitness((x, _) => + var observer = new CallbackWitnessAsync((x, _) => { onNext(x); return default; @@ -166,7 +166,7 @@ static ValueTask OnCompletedAsync(Result x, Action onCompleted) return ValueTask.CompletedTask; } - var observer = new DelegateAsyncWitness( + var observer = new CallbackWitnessAsync( (x, _) => { onNext(x); @@ -220,7 +220,7 @@ public static ValueTask SubscribeAsync( ArgumentExceptionHelper.ThrowIfNull(source); ArgumentExceptionHelper.ThrowIfNull(onNextAsync); - var observer = new DelegateAsyncWitness(onNextAsync); + var observer = new CallbackWitnessAsync(onNextAsync); return source.SubscribeAsync(observer, cancellationToken); } } diff --git a/src/ReactiveUI.Primitives.Async/Operators/SwitchSignal.cs b/src/ReactiveUI.Primitives.Async/Operators/SwitchSignal.cs index 9fa8cfd..e9f61d7 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/SwitchSignal.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/SwitchSignal.cs @@ -329,7 +329,7 @@ internal async ValueTask FinishAsync(Result? result) } /// - /// Observer for the outer observable sequence that delegates to the parent . + /// Witness for the outer observable sequence that delegates to the parent . /// /// The parent switch subscription. internal sealed class SwitchOuterWitness(SwitchCoordinator subscription) : ObserverAsync> @@ -370,13 +370,13 @@ protected override ValueTask OnCompletedAsyncCore(Result result) } /// - /// Observer for the currently active inner observable sequence that delegates to the parent . + /// Witness for the currently active inner observable sequence that delegates to the parent . /// /// The parent switch subscription. internal sealed class SwitchInnerWitness(SwitchCoordinator subscription) : ObserverAsync { /// - /// Forwards an element from the inner sequence to the downstream observer. + /// Forwards an element from the inner sequence to the downstream witness. /// /// The element to forward. /// A token to cancel the operation. diff --git a/src/ReactiveUI.Primitives.Async/Operators/TakeUntil.cs b/src/ReactiveUI.Primitives.Async/Operators/TakeUntil.cs index 254fbcb..2aed53f 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/TakeUntil.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/TakeUntil.cs @@ -396,7 +396,7 @@ public CancellationStopCoordinator(CancellationStopSignal parent, IObserverAs public async ValueTask SubscribeSourcesAsync(CancellationToken cancellationToken) { _tokenRegistration = _parent._cancellationToken.Register(CompleteFromCancellation); - _subscription = await _parent._source.SubscribeAsync(new TakeUntilSourceObserver(_lifecycle), cancellationToken).ConfigureAwait(false); + _subscription = await _parent._source.SubscribeAsync(new TakeUntilSourceWitness(_lifecycle), cancellationToken).ConfigureAwait(false); } /// @@ -501,7 +501,7 @@ public DelegateStopCoordinator(DelegateStopSignal parent, IObserverAsync o public async ValueTask SubscribeSourcesAsync(CancellationToken cancellationToken) { AwaitStopThenComplete(); - _subscription = await _parent._source.SubscribeAsync(new TakeUntilSourceObserver(_lifecycle), cancellationToken).ConfigureAwait(false); + _subscription = await _parent._source.SubscribeAsync(new TakeUntilSourceWitness(_lifecycle), cancellationToken).ConfigureAwait(false); } /// @@ -645,7 +645,7 @@ public async ValueTask SubscribeSourcesAsync(CancellationToken cancellationToken { var task = _parent._task; AwaitStopThenComplete(task); - _subscription = await _parent._source.SubscribeAsync(new TakeUntilSourceObserver(_lifecycle), cancellationToken).ConfigureAwait(false); + _subscription = await _parent._source.SubscribeAsync(new TakeUntilSourceWitness(_lifecycle), cancellationToken).ConfigureAwait(false); } /// @@ -764,7 +764,7 @@ public async ValueTask SubscribeSourcesAsync(CancellationToken await _otherDisposable.SetDisposableAsync(otherSubscription).ConfigureAwait(false); var sourceSubscription = - await _parent._source.SubscribeAsync(new TakeUntilSourceObserver(_lifecycle), cancellationToken).ConfigureAwait(false); + await _parent._source.SubscribeAsync(new TakeUntilSourceWitness(_lifecycle), cancellationToken).ConfigureAwait(false); await _disposable.SetDisposableAsync(sourceSubscription).ConfigureAwait(false); return this; diff --git a/src/ReactiveUI.Primitives.Async/Operators/ToDictionaryAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/ToDictionaryAsync.cs index 35a2263..63a93a6 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/ToDictionaryAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/ToDictionaryAsync.cs @@ -121,7 +121,7 @@ public static ValueTask> ToDictionaryAsync - /// Observer that builds a dictionary from the elements of a sequence using key and element selectors. + /// Witness that builds a dictionary from the elements of a sequence using key and element selectors. /// /// The type of elements in the source sequence. /// The type of the dictionary keys. @@ -135,7 +135,7 @@ internal sealed class ToDictionaryTaskWitness( Func elementSelector, IEqualityComparer? comparer, CancellationToken cancellationToken) - : TaskWitnessAsyncBase>(cancellationToken) + : TaskResultWitnessAsyncBase>(cancellationToken) where TKey : notnull { /// diff --git a/src/ReactiveUI.Primitives.Async/Operators/ToListAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/ToListAsync.cs index 9c1d5b8..a25bbb2 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/ToListAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/ToListAsync.cs @@ -41,12 +41,12 @@ public static async ValueTask> ToListAsync(this IObservableAsync @ } /// - /// Observer that collects all elements from a sequence into a list. + /// Witness that collects all elements from a sequence into a list. /// /// The type of elements in the source sequence. /// A cancellation token for the operation. internal sealed class ToListTaskWitness(CancellationToken cancellationToken) - : TaskWitnessAsyncBase>(cancellationToken) + : TaskResultWitnessAsyncBase>(cancellationToken) { /// /// The list that accumulates all elements received from the source sequence. diff --git a/src/ReactiveUI.Primitives.Async/Operators/WaitCompletionAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/WaitCompletionAsync.cs index c7e4337..de928bd 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/WaitCompletionAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/WaitCompletionAsync.cs @@ -50,7 +50,7 @@ public static async ValueTask WaitCompletionAsync( /// The type of elements in the source sequence. /// A cancellation token for the operation. internal sealed class CompletionTaskWitness(CancellationToken cancellationToken) - : TaskWitnessAsyncBase(cancellationToken) + : TaskResultWitnessAsyncBase(cancellationToken) { /// protected override ValueTask OnNextAsyncCore(T value, CancellationToken cancellationToken) => default; diff --git a/src/ReactiveUI.Primitives.Async/Operators/WitnessOn.cs b/src/ReactiveUI.Primitives.Async/Operators/WitnessOn.cs index 234e550..35df3f8 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/WitnessOn.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/WitnessOn.cs @@ -24,7 +24,7 @@ public static partial class SignalAsync /// context. /// An observable sequence whose observer callbacks execute on the specified context. public static IObservableAsync WitnessOn(this IObservableAsync @this, AsyncContext asyncContext, bool forceYielding) => - new WitnessOnAsyncSignal(@this, asyncContext, forceYielding); + new ContextSwitchSignalAsync(@this, asyncContext, forceYielding); /// /// Wraps the source observable so that observer callbacks are invoked on the specified async context. @@ -47,7 +47,7 @@ public static IObservableAsync WitnessOn(this IObservableAsync @this, A public static IObservableAsync WitnessOn(this IObservableAsync @this, SynchronizationContext synchronizationContext, bool forceYielding) { var asyncContext = AsyncContext.From(synchronizationContext); - return new WitnessOnAsyncSignal(@this, asyncContext, forceYielding); + return new ContextSwitchSignalAsync(@this, asyncContext, forceYielding); } /// @@ -71,7 +71,7 @@ public static IObservableAsync WitnessOn(this IObservableAsync @this, S public static IObservableAsync WitnessOn(this IObservableAsync @this, TaskScheduler taskScheduler, bool forceYielding) { var asyncContext = AsyncContext.From(taskScheduler); - return new WitnessOnAsyncSignal(@this, asyncContext, forceYielding); + return new ContextSwitchSignalAsync(@this, asyncContext, forceYielding); } /// @@ -98,7 +98,7 @@ public static IObservableAsync WitnessOn(this IObservableAsync @this, T public static IObservableAsync WitnessOn(this IObservableAsync @this, ISequencer scheduler, bool forceYielding) { var asyncContext = AsyncContext.From(scheduler); - return new WitnessOnAsyncSignal(@this, asyncContext, forceYielding); + return new ContextSwitchSignalAsync(@this, asyncContext, forceYielding); } /// diff --git a/src/ReactiveUI.Primitives.Async/Operators/Wrap.cs b/src/ReactiveUI.Primitives.Async/Operators/Wrap.cs index 5c792aa..cc7459a 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/Wrap.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/Wrap.cs @@ -21,5 +21,5 @@ public static partial class SignalAsync /// Thrown if is null. public static IObserverAsync Wrap(this IObserverAsync observer) => observer is null ? throw new ArgumentNullException(nameof(observer)) - : new ForwardingAsyncWitness(observer); + : new RelayWitnessAsync(observer); } diff --git a/src/ReactiveUI.Primitives.Async/Operators/Yield.cs b/src/ReactiveUI.Primitives.Async/Operators/Yield.cs index df6a286..c5905bb 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/Yield.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/Yield.cs @@ -45,7 +45,7 @@ protected override ValueTask SubscribeAsyncCore( { var currentContext = AsyncContext.GetCurrent(); return source.SubscribeAsync( - new WitnessOnAsyncSignal.ContextSwitchObserver(observer, currentContext, true), + new ContextSwitchSignalAsync.ContextSwitchWitness(observer, currentContext, true), cancellationToken); } } diff --git a/src/ReactiveUI.Primitives.Async/Signals/Base/BaseReplayLatestSignalAsync.cs b/src/ReactiveUI.Primitives.Async/Signals/Base/BaseReplayLatestSignalAsync.cs index 648036f..da286bb 100644 --- a/src/ReactiveUI.Primitives.Async/Signals/Base/BaseReplayLatestSignalAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Signals/Base/BaseReplayLatestSignalAsync.cs @@ -70,19 +70,7 @@ public abstract class BaseReplayLatestSignalAsync(Optional startValue) : S /// A task that represents the asynchronous notification operation. public async ValueTask OnNextAsync(T value, CancellationToken cancellationToken) { - // Fast path: the caller token is None / our own dispose token — no linked CTS needed, - // saving a Linked1CancellationTokenSource per emission on the broadcast hot path. - CancellationTokenSource? linkedCts = null; - CancellationToken token; - if (!cancellationToken.CanBeCanceled || cancellationToken == DisposedCancellationToken) - { - token = DisposedCancellationToken; - } - else - { - linkedCts = CancellationTokenSource.CreateLinkedTokenSource(DisposedCancellationToken, cancellationToken); - token = linkedCts.Token; - } + var token = GetOperationCancellationToken(cancellationToken, out var linkedCts); try { @@ -123,17 +111,7 @@ public async ValueTask OnNextAsync(T value, CancellationToken cancellationToken) /// A task that represents the asynchronous notification operation. public async ValueTask OnErrorResumeAsync(Exception error, CancellationToken cancellationToken) { - CancellationTokenSource? linkedCts = null; - CancellationToken token; - if (!cancellationToken.CanBeCanceled || cancellationToken == DisposedCancellationToken) - { - token = DisposedCancellationToken; - } - else - { - linkedCts = CancellationTokenSource.CreateLinkedTokenSource(DisposedCancellationToken, cancellationToken); - token = linkedCts.Token; - } + var token = GetOperationCancellationToken(cancellationToken, out var linkedCts); try { @@ -252,41 +230,110 @@ protected override async ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - using var linkedCts = - CancellationTokenSource.CreateLinkedTokenSource(DisposedCancellationToken, cancellationToken); - var token = linkedCts.Token; - ArgumentExceptionHelper.ThrowIfNull(observer); - token.ThrowIfCancellationRequested(); - Result? result; - using (await _gate.EnterAsync(token).ConfigureAwait(false)) + var token = GetOperationCancellationToken(cancellationToken, out var linkedCts); + try { - result = _result; - if (result is null) + token.ThrowIfCancellationRequested(); + + Result? result; + using (await _gate.EnterAsync(token).ConfigureAwait(false)) { - _observers = _observers.Add(observer); - if (_lastValue.TryGetValue(out var lastValue)) + result = _result; + if (result is null) { - await observer.OnNextAsync(lastValue, token).ConfigureAwait(false); + _observers = _observers.Add(observer); + if (_lastValue.TryGetValue(out var lastValue)) + { + await observer.OnNextAsync(lastValue, token).ConfigureAwait(false); + } } } - } - if (result is not null) - { + if (result is null) + { + return new WitnessLease(this, observer); + } + await observer.OnCompletedAsync(result.Value).ConfigureAwait(false); return DisposableAsync.Empty; } + finally + { + linkedCts?.Dispose(); + } + } - return DisposableAsync.Create( - (signal: this, observer, token), - static async state => + /// + /// Gets the cancellation token used for a gate-protected operation, creating a linked source only when the caller + /// supplied an independent cancellable token. + /// + /// The caller-supplied cancellation token. + /// The linked source created for the operation, or on the fast path. + /// The token to use while entering the gate and invoking immediate subscription callbacks. + private CancellationToken GetOperationCancellationToken( + CancellationToken cancellationToken, + out CancellationTokenSource? linkedCts) + { + if (!cancellationToken.CanBeCanceled || cancellationToken == DisposedCancellationToken) + { + linkedCts = null; + return DisposedCancellationToken; + } + + linkedCts = CancellationTokenSource.CreateLinkedTokenSource(DisposedCancellationToken, cancellationToken); + return linkedCts.Token; + } + + /// + /// Removes an observer from the replay signal under the serialization gate. + /// + /// The observer to remove. + /// A task that represents the asynchronous removal operation. + private async ValueTask RemoveObserverAsync(IObserverAsync observer) + { + if (_isDisposed) + { + return; + } + + try + { + using (await _gate.EnterAsync(DisposedCancellationToken).ConfigureAwait(false)) { - using (await state.signal._gate.EnterAsync(state.token).ConfigureAwait(false)) - { - state.signal._observers = state.signal._observers.Remove(state.observer); - } - }); + _observers = _observers.Remove(observer); + } + } + catch (OperationCanceledException) when (_isDisposed) + { + return; + } + catch (ObjectDisposedException) when (_isDisposed) + { + return; + } + } + + /// + /// Subscription handle that removes an witness from a replay signal when disposed. + /// + /// The signal that owns the witness list. + /// The witness to remove when the lease is disposed. + private sealed class WitnessLease(BaseReplayLatestSignalAsync signal, IObserverAsync observer) + : IAsyncDisposable + { + /// + /// Indicates whether the lease has already removed its observer. + /// + private int _disposed; + + /// + public ValueTask DisposeAsync() + { + return Interlocked.Exchange(ref _disposed, 1) != 0 + ? default + : signal.RemoveObserverAsync(observer); + } } } diff --git a/src/ReactiveUI.Primitives.Async/Signals/Base/BaseSignalAsync.cs b/src/ReactiveUI.Primitives.Async/Signals/Base/BaseSignalAsync.cs index be0f1a5..9d48d0e 100644 --- a/src/ReactiveUI.Primitives.Async/Signals/Base/BaseSignalAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Signals/Base/BaseSignalAsync.cs @@ -161,17 +161,7 @@ protected override async ValueTask SubscribeAsyncCore( return DisposableAsync.Empty; } - return DisposableAsync.Create( - (signal: this, observer), - static state => - { - lock (state.signal._gate) - { - state.signal._observers = state.signal._observers.Remove(state.observer); - } - - return default; - }); + return new WitnessLease(this, observer); } /// @@ -211,4 +201,41 @@ protected abstract ValueTask OnErrorResumeAsyncCore( /// The result to provide to each observer upon completion. /// A ValueTask that represents the asynchronous notification operation. protected abstract ValueTask OnCompletedAsyncCore(ImmutableArray> observers, Result result); + + /// + /// Removes an observer from the current subscription list. + /// + /// The observer to remove. + private void RemoveObserver(IObserverAsync observer) + { + lock (_gate) + { + _observers = _observers.Remove(observer); + } + } + + /// + /// Subscription handle that removes an witness from its owning signal when disposed. + /// + /// The signal that owns the witness list. + /// The witness to remove when the lease is disposed. + private sealed class WitnessLease(BaseSignalAsync signal, IObserverAsync observer) : IAsyncDisposable + { + /// + /// Indicates whether the lease has already removed its witness. + /// + private int _disposed; + + /// + public ValueTask DisposeAsync() + { + if (Interlocked.Exchange(ref _disposed, 1) != 0) + { + return default; + } + + signal.RemoveObserver(observer); + return default; + } + } } diff --git a/src/ReactiveUI.Primitives.Async/Signals/Base/BaseStatelessReplayLatestSignalAsync.cs b/src/ReactiveUI.Primitives.Async/Signals/Base/BaseStatelessReplayLatestSignalAsync.cs index 95c1cf6..b989e02 100644 --- a/src/ReactiveUI.Primitives.Async/Signals/Base/BaseStatelessReplayLatestSignalAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Signals/Base/BaseStatelessReplayLatestSignalAsync.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for full license information. using System.Collections.Immutable; -using ReactiveUI.Primitives.Async.Disposables; using ReactiveUI.Primitives.Async.Internals; using ReactiveUI.Primitives.Internal; @@ -73,20 +72,7 @@ public abstract class BaseStatelessReplayLatestSignalAsync(Optional startV /// A task that represents the asynchronous notification operation. public async ValueTask OnNextAsync(T value, CancellationToken cancellationToken) { - // Fast path: when the caller passes our own dispose token (or no token at all), the - // per-emission linked CTS is pure waste — DisposedCancellationToken already covers - // disposal-driven cancellation, so reuse it directly. - CancellationTokenSource? linkedCts = null; - CancellationToken token; - if (cancellationToken == DisposedCancellationToken || !cancellationToken.CanBeCanceled) - { - token = DisposedCancellationToken; - } - else - { - linkedCts = CancellationTokenSource.CreateLinkedTokenSource(DisposedCancellationToken, cancellationToken); - token = linkedCts.Token; - } + var token = GetOperationCancellationToken(cancellationToken, out var linkedCts); try { @@ -119,17 +105,21 @@ public async ValueTask OnNextAsync(T value, CancellationToken cancellationToken) /// A task that represents the asynchronous error notification operation. public async ValueTask OnErrorResumeAsync(Exception error, CancellationToken cancellationToken) { - using var linkedCts = - CancellationTokenSource.CreateLinkedTokenSource(DisposedCancellationToken, cancellationToken); - var token = linkedCts.Token; + var token = GetOperationCancellationToken(cancellationToken, out var linkedCts); + try + { + ImmutableArray> observers; + using (await _gate.EnterAsync(token).ConfigureAwait(false)) + { + observers = _observers; + } - ImmutableArray> observers; - using (await _gate.EnterAsync(token).ConfigureAwait(false)) + await OnErrorResumeAsyncCore(observers, error, token).ConfigureAwait(false); + } + finally { - observers = _observers; + linkedCts?.Dispose(); } - - await OnErrorResumeAsyncCore(observers, error, token).ConfigureAwait(false); } /// @@ -184,38 +174,28 @@ protected override async ValueTask SubscribeAsyncCore( IObserverAsync observer, CancellationToken cancellationToken) { - using var linkedCts = - CancellationTokenSource.CreateLinkedTokenSource(DisposedCancellationToken, cancellationToken); - var token = linkedCts.Token; - - token.ThrowIfCancellationRequested(); - ArgumentExceptionHelper.ThrowIfNull(observer); - var disposable = DisposableAsync.Create( - (signal: this, observer, token), - static async state => + var token = GetOperationCancellationToken(cancellationToken, out var linkedCts); + try + { + token.ThrowIfCancellationRequested(); + + using (await _gate.EnterAsync(token).ConfigureAwait(false)) { - using (await state.signal._gate.EnterAsync(state.token).ConfigureAwait(false)) + _observers = _observers.Add(observer); + if (_value.TryGetValue(out var value)) { - state.signal._observers = state.signal._observers.Remove(state.observer); - if (state.signal._observers.IsEmpty) - { - state.signal._value = state.signal._startValue; - } + await observer.OnNextAsync(value, token).ConfigureAwait(false); } - }); + } - using (await _gate.EnterAsync(token).ConfigureAwait(false)) + return new WitnessLease(this, observer); + } + finally { - _observers = _observers.Add(observer); - if (_value.TryGetValue(out var value)) - { - await observer.OnNextAsync(value, token).ConfigureAwait(false); - } + linkedCts?.Dispose(); } - - return disposable; } /// @@ -258,4 +238,80 @@ protected abstract ValueTask OnErrorResumeAsyncCore( /// The result to provide to observers upon completion. Represents the outcome of the observed sequence. /// A ValueTask that represents the asynchronous notification operation. protected abstract ValueTask OnCompletedAsyncCore(ImmutableArray> observers, Result result); + + /// + /// Gets the cancellation token used for a gate-protected operation, creating a linked source only when the caller + /// supplied an independent cancellable token. + /// + /// The caller-supplied cancellation token. + /// The linked source created for the operation, or on the fast path. + /// The token to use while entering the gate and invoking immediate subscription callbacks. + private CancellationToken GetOperationCancellationToken( + CancellationToken cancellationToken, + out CancellationTokenSource? linkedCts) + { + if (cancellationToken == DisposedCancellationToken || !cancellationToken.CanBeCanceled) + { + linkedCts = null; + return DisposedCancellationToken; + } + + linkedCts = CancellationTokenSource.CreateLinkedTokenSource(DisposedCancellationToken, cancellationToken); + return linkedCts.Token; + } + + /// + /// Removes an observer and restores the initial value when the last observer leaves. + /// + /// The observer to remove. + /// A task that represents the asynchronous removal operation. + private async ValueTask RemoveObserverAndResetAsync(IObserverAsync observer) + { + if (_isDisposed) + { + return; + } + + try + { + using (await _gate.EnterAsync(DisposedCancellationToken).ConfigureAwait(false)) + { + _observers = _observers.Remove(observer); + if (_observers.IsEmpty) + { + _value = _startValue; + } + } + } + catch (OperationCanceledException) when (_isDisposed) + { + return; + } + catch (ObjectDisposedException) when (_isDisposed) + { + return; + } + } + + /// + /// Subscription handle that removes a witness from a stateless replay signal when disposed. + /// + /// The signal that owns the witness list. + /// The witness to remove when the lease is disposed. + private sealed class WitnessLease(BaseStatelessReplayLatestSignalAsync signal, IObserverAsync observer) + : IAsyncDisposable + { + /// + /// Indicates whether the lease has already removed its witness. + /// + private int _disposed; + + /// + public ValueTask DisposeAsync() + { + return Interlocked.Exchange(ref _disposed, 1) != 0 + ? default + : signal.RemoveObserverAndResetAsync(observer); + } + } } diff --git a/src/benchmarks/ReactiveUI.Primitives.Benchmarks/AsyncSignalSubscriptionBenchmarks.cs b/src/benchmarks/ReactiveUI.Primitives.Benchmarks/AsyncSignalSubscriptionBenchmarks.cs new file mode 100644 index 0000000..7d8e8e8 --- /dev/null +++ b/src/benchmarks/ReactiveUI.Primitives.Benchmarks/AsyncSignalSubscriptionBenchmarks.cs @@ -0,0 +1,114 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using BenchmarkDotNet.Attributes; + +using PrimitivesAsyncSignalFactory = ReactiveUI.Primitives.Async.Signals.Signal; + +namespace ReactiveUI.Primitives.Benchmarks; + +/// +/// Benchmarks subscription churn for asynchronous signal implementations. +/// +[MemoryDiagnoser] +public class AsyncSignalSubscriptionBenchmarks +{ + /// + /// The number of observers subscribed and disposed by each benchmark operation. + /// + private const int SubscriberCount = 8; + + /// + /// The value replayed to each late subscriber. + /// + private const int ReplayValue = 42; + + /// + /// Subscribes and disposes multiple observers against an async replay-latest signal. + /// + /// The total value observed during subscription replay. + [Benchmark] + public async Task PrimitivesReplayLatestSubscribeDisposeAsync() + { + var signal = PrimitivesAsyncSignalFactory.CreateReplayLatest(); + var observers = new CountingObserver[SubscriberCount]; + var subscriptions = new IAsyncDisposable[SubscriberCount]; + + try + { + await signal.OnNextAsync(ReplayValue, CancellationToken.None).ConfigureAwait(false); + + for (var i = 0; i < SubscriberCount; i++) + { + var observer = new CountingObserver(); + observers[i] = observer; + subscriptions[i] = await signal.SubscribeAsync(observer, CancellationToken.None).ConfigureAwait(false); + } + + return Sum(observers); + } + finally + { + await DisposeAllAsync(subscriptions).ConfigureAwait(false); + await signal.DisposeAsync().ConfigureAwait(false); + } + } + + /// + /// Sums the totals recorded by the async observers. + /// + /// The observers to sum. + /// The combined observed total. + private static int Sum(CountingObserver[] observers) + { + var total = 0; + for (var i = 0; i < observers.Length; i++) + { + total += observers[i].Total; + } + + return total; + } + + /// + /// Disposes every non-null async subscription in order. + /// + /// The subscriptions to dispose. + /// A task that represents the asynchronous dispose operation. + private static async ValueTask DisposeAllAsync(IAsyncDisposable[] subscriptions) + { + for (var i = 0; i < subscriptions.Length; i++) + { + if (subscriptions[i] is not null) + { + await subscriptions[i].DisposeAsync().ConfigureAwait(false); + } + } + } + + /// + /// Observer that accumulates replayed async signal values. + /// + private sealed class CountingObserver : Async.ObserverAsync + { + /// + /// Gets the accumulated value total. + /// + public int Total { get; private set; } + + /// + protected override ValueTask OnCompletedAsyncCore(Async.Result result) => default; + + /// + protected override ValueTask OnErrorResumeAsyncCore(Exception error, CancellationToken cancellationToken) => + default; + + /// + protected override ValueTask OnNextAsyncCore(int value, CancellationToken cancellationToken) + { + Total += value; + return default; + } + } +} diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/CombineLatestEnumerableInternalsTests.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/CombineLatestEnumerableInternalsTests.cs index d88ab01..6d99d16 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/CombineLatestEnumerableInternalsTests.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/CombineLatestEnumerableInternalsTests.cs @@ -23,7 +23,7 @@ public async Task WhenIndexedObserverDisposed_ThenNoOp() sources, downstream, static s => s[0]); - var indexed = new SignalAsync.CombineLatestEnumerableSignal.IndexedObserver(subscription, 0); + var indexed = new SignalAsync.CombineLatestEnumerableSignal.IndexedWitness(subscription, 0); await indexed.DisposeAsync(); diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.OnDispose.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.OnDispose.cs index 1b62bc9..ad9d140 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.OnDispose.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/CombiningOperatorTests.OnDispose.cs @@ -258,7 +258,7 @@ public async Task WhenCleanupBranchAsyncExplicitDispose_ThenCallbackInvoked() [Test] public async Task WhenMergeCoordinatorDisposed_ThenRelayNextAsyncReturnsDirectly() { - var observer = new DelegateAsyncWitness((_, _) => default); + var observer = new CallbackWitnessAsync((_, _) => default); var subscription = new SignalAsync.MergeCoordinator(observer); await subscription.DisposeAsync(); @@ -272,7 +272,7 @@ public async Task WhenMergeCoordinatorDisposed_ThenRelayNextAsyncReturnsDirectly [Test] public async Task WhenMergeCoordinatorDisposed_ThenRelayErrorAsyncReturnsDirectly() { - var observer = new DelegateAsyncWitness((_, _) => default); + var observer = new CallbackWitnessAsync((_, _) => default); var subscription = new SignalAsync.MergeCoordinator(observer); await subscription.DisposeAsync(); @@ -291,7 +291,7 @@ public async Task WhenMergeCoordinatorDisposedWhileGateHeld_ThenRelayNextAsyncPo var allowCompletion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var items = new List(); - var observer = new DelegateAsyncWitness( + var observer = new CallbackWitnessAsync( (x, _) => { items.Add(x); @@ -331,7 +331,7 @@ public async Task WhenMergeCoordinatorDisposedWhileGateHeld_ThenRelayErrorAsyncP var allowCompletion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var errors = new List(); - var observer = new DelegateAsyncWitness( + var observer = new CallbackWitnessAsync( (_, _) => default, (ex, _) => { @@ -365,7 +365,7 @@ public async Task WhenMergeCoordinatorDisposedWhileGateHeld_ThenRelayErrorAsyncP [Test] public async Task WhenMergeSequenceCoordinatorDisposed_ThenOnNextReturnsDirectly() { - var observer = new DelegateAsyncWitness((_, _) => default); + var observer = new CallbackWitnessAsync((_, _) => default); IObservableAsync[] sources = []; var subscription = new SignalAsync.MergeEnumerableSignal.MergeSequenceCoordinator(observer, sources); @@ -382,7 +382,7 @@ public async Task WhenMergeSequenceCoordinatorDisposed_ThenOnNextReturnsDirectly [Test] public async Task WhenMergeSequenceCoordinatorDisposed_ThenOnErrorResumeReturnsDirectly() { - var observer = new DelegateAsyncWitness((_, _) => default); + var observer = new CallbackWitnessAsync((_, _) => default); IObservableAsync[] sources = []; var subscription = new SignalAsync.MergeEnumerableSignal.MergeSequenceCoordinator(observer, sources); diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/ConcurrentSignalBaseTests.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/ConcurrentSignalBaseTests.cs index 4eab890..e95cdb9 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/ConcurrentSignalBaseTests.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/ConcurrentSignalBaseTests.cs @@ -101,7 +101,7 @@ public async Task WhenRelayErrorAsync_ThenAllBranchesForward() var singleCaught = new ErrorCapture(); var single = ImmutableArray.Create>( - new DelegateAsyncWitness(static (_, _) => default, MakeErrorSync(singleCaught))); + new CallbackWitnessAsync(static (_, _) => default, MakeErrorSync(singleCaught))); var singleError = new InvalidOperationException("single"); await Concurrent.ForwardOnErrorResumeConcurrently(single, singleError, default); await Assert.That(singleCaught.Error).IsSameReferenceAs(singleError); @@ -109,8 +109,8 @@ public async Task WhenRelayErrorAsync_ThenAllBranchesForward() var a = new ErrorCapture(); var b = new ErrorCapture(); var multi = ImmutableArray.Create>( - new DelegateAsyncWitness(static (_, _) => default, MakeErrorSlow(a)), - new DelegateAsyncWitness(static (_, _) => default, MakeErrorSync(b))); + new CallbackWitnessAsync(static (_, _) => default, MakeErrorSlow(a)), + new CallbackWitnessAsync(static (_, _) => default, MakeErrorSync(b))); var multiError = new InvalidOperationException("multi"); await Concurrent.ForwardOnErrorResumeConcurrently(multi, multiError, default); await Assert.That(a.Error).IsSameReferenceAs(multiError); @@ -127,15 +127,15 @@ public async Task WhenForwardOnCompleted_ThenAllBranchesForward() var singleResult = new ResultCapture(); var single = ImmutableArray.Create>( - new DelegateAsyncWitness(static (_, _) => default, null, MakeCompletedSync(singleResult))); + new CallbackWitnessAsync(static (_, _) => default, null, MakeCompletedSync(singleResult))); await Concurrent.ForwardOnCompletedConcurrently(single, Result.Success); await Assert.That(singleResult.Result).IsEqualTo(Result.Success); var a = new ResultCapture(); var b = new ResultCapture(); var multi = ImmutableArray.Create>( - new DelegateAsyncWitness(static (_, _) => default, null, MakeCompletedSlow(a)), - new DelegateAsyncWitness(static (_, _) => default, null, MakeCompletedSync(b))); + new CallbackWitnessAsync(static (_, _) => default, null, MakeCompletedSlow(a)), + new CallbackWitnessAsync(static (_, _) => default, null, MakeCompletedSync(b))); await Concurrent.ForwardOnCompletedConcurrently(multi, Result.Success); await Assert.That(a.Result).IsEqualTo(Result.Success); await Assert.That(b.Result).IsEqualTo(Result.Success); @@ -144,7 +144,7 @@ public async Task WhenForwardOnCompleted_ThenAllBranchesForward() /// Creates a synchronously-completing OnNext observer that captures the value. /// The capture sink. /// An observer whose OnNextAsync completes synchronously. - private static DelegateAsyncWitness MakeSync(IntCapture capture) => + private static CallbackWitnessAsync MakeSync(IntCapture capture) => new((x, _) => { capture.Value = x; @@ -154,7 +154,7 @@ private static DelegateAsyncWitness MakeSync(IntCapture capture) => /// Creates an OnNext observer that delays before capturing — forces the slow path. /// The capture sink. /// An observer whose OnNextAsync completes asynchronously. - private static DelegateAsyncWitness MakeSlow(IntCapture capture) => + private static CallbackWitnessAsync MakeSlow(IntCapture capture) => new(async (x, ct) => { await Task.Delay(SlowPathDelayMilliseconds, ct).ConfigureAwait(false); diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/FactorySignalTests.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/FactorySignalTests.cs index 56cb82a..8b479f0 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/FactorySignalTests.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/FactorySignalTests.cs @@ -483,7 +483,7 @@ public async Task WhenEmitEnumerableAsyncWithCancelledToken_ThenReturnsEarly() using var cts = new CancellationTokenSource(); await cts.CancelAsync(); - var observer = new DelegateAsyncWitness((x, _) => + var observer = new CallbackWitnessAsync((x, _) => { items.Add(x); return default; diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/Internals/CombineLatestIndexedObserverTests.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/Internals/CombineLatestIndexedObserverTests.cs index f99f798..7440d55 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/Internals/CombineLatestIndexedObserverTests.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/Internals/CombineLatestIndexedObserverTests.cs @@ -6,7 +6,7 @@ namespace ReactiveUI.Primitives.Async.Tests.Internals; -/// Tests for , the shared +/// Tests for , the shared /// per-source observer that backs every per-arity CombineLatest subscription. public class CombineLatestIndexedObserverTests { @@ -25,7 +25,7 @@ public async Task WhenOnNextAsync_ThenRecordsValueAndCallsEmitLatestAsync() var captured = new CaptureObserverAsync(); var parent = new TestSubscription(captured); int? stored = null; - var observer = new CombineLatestIndexedObserver(parent, SourceBit, v => stored = v); + var observer = new CombineLatestIndexedWitness(parent, SourceBit, v => stored = v); await observer.OnNextAsync(Sentinel, CancellationToken.None); @@ -43,7 +43,7 @@ public async Task WhenOnErrorResumeAsync_ThenLifecycleForwardsError() { var captured = new CaptureObserverAsync(); var parent = new TestSubscription(captured); - var observer = new CombineLatestIndexedObserver(parent, SourceBit, static _ => { }); + var observer = new CombineLatestIndexedWitness(parent, SourceBit, static _ => { }); var expected = new InvalidOperationException("forward"); await observer.OnErrorResumeAsync(expected, CancellationToken.None); @@ -62,7 +62,7 @@ public async Task WhenOnCompletedAsync_ThenLifecycleForwardsCompletion() { var captured = new CaptureObserverAsync(); var parent = new TestSubscription(captured); - var observer = new CombineLatestIndexedObserver(parent, SourceBit, static _ => { }); + var observer = new CombineLatestIndexedWitness(parent, SourceBit, static _ => { }); await observer.OnCompletedAsync(Result.Success); diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/Internals/TakeUntilSourceObserverTests.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/Internals/TakeUntilSourceObserverTests.cs index bd13ce5..8ea41e1 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/Internals/TakeUntilSourceObserverTests.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/Internals/TakeUntilSourceObserverTests.cs @@ -6,7 +6,7 @@ namespace ReactiveUI.Primitives.Async.Tests.Internals; -/// Tests for , the shared async observer that +/// Tests for , the shared async observer that /// forwards every source notification into a . public class TakeUntilSourceObserverTests { @@ -20,7 +20,7 @@ public async Task WhenOnNextAsync_ThenLifecycleForwardsValueToDownstream() { var captured = new CaptureObserverAsync(); var lifecycle = new TakeUntilLifecycle(captured); - var observer = new TakeUntilSourceObserver(lifecycle); + var observer = new TakeUntilSourceWitness(lifecycle); await observer.OnNextAsync(SentinelValue, CancellationToken.None); @@ -36,7 +36,7 @@ public async Task WhenOnErrorResumeAsync_ThenLifecycleForwardsErrorToDownstream( { var captured = new CaptureObserverAsync(); var lifecycle = new TakeUntilLifecycle(captured); - var observer = new TakeUntilSourceObserver(lifecycle); + var observer = new TakeUntilSourceWitness(lifecycle); var expected = new InvalidOperationException("forward"); await observer.OnErrorResumeAsync(expected, CancellationToken.None); @@ -54,7 +54,7 @@ public async Task WhenOnCompletedAsync_ThenLifecycleForwardsCompletionToDownstre { var captured = new CaptureObserverAsync(); var lifecycle = new TakeUntilLifecycle(captured); - var observer = new TakeUntilSourceObserver(lifecycle); + var observer = new TakeUntilSourceWitness(lifecycle); await observer.OnCompletedAsync(Result.Success); diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/ObserveOnAsyncSignalTests.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/ObserveOnAsyncSignalTests.cs index 0ef099b..f34b74f 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/ObserveOnAsyncSignalTests.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/ObserveOnAsyncSignalTests.cs @@ -6,7 +6,7 @@ namespace ReactiveUI.Primitives.Async.Tests; -/// Tests for — exercises the +/// Tests for — exercises the /// forceYielding: true slow-path branches that switch context on every /// OnNext / OnErrorResume / OnCompleted regardless of whether /// the call site is already on the target context. @@ -182,7 +182,7 @@ public async Task WhenForwardAfterContextSwitchAsyncInvokedDirectly_ThenValueFor { var captured = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var downstream = new CapturingAsyncObserver(captured); - var sut = new WitnessOnAsyncSignal.ContextSwitchObserver(downstream, AsyncContext.Default, forceYielding: true); + var sut = new ContextSwitchSignalAsync.ContextSwitchWitness(downstream, AsyncContext.Default, forceYielding: true); await sut.ForwardAfterContextSwitchAsync(Sentinel, CancellationToken.None); @@ -197,7 +197,7 @@ public async Task WhenForwardErrorAfterContextSwitchAsyncInvokedDirectly_ThenErr { var captured = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var downstream = new CapturingAsyncObserver(captured); - var sut = new WitnessOnAsyncSignal.ContextSwitchObserver(downstream, AsyncContext.Default, forceYielding: true); + var sut = new ContextSwitchSignalAsync.ContextSwitchWitness(downstream, AsyncContext.Default, forceYielding: true); var expected = new InvalidOperationException("slow-path-error"); await sut.ForwardErrorAfterContextSwitchAsync(expected, CancellationToken.None); @@ -213,7 +213,7 @@ public async Task WhenForwardCompletionAfterContextSwitchAsyncInvokedDirectly_Th { var captured = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var downstream = new CapturingAsyncObserver(captured); - var sut = new WitnessOnAsyncSignal.ContextSwitchObserver(downstream, AsyncContext.Default, forceYielding: true); + var sut = new ContextSwitchSignalAsync.ContextSwitchWitness(downstream, AsyncContext.Default, forceYielding: true); await sut.ForwardCompletionAfterContextSwitchAsync(Result.Success); diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/ParityHelpersOperatorFusionsTests.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/ParityHelpersOperatorFusionsTests.cs index cf18120..f9b1b28 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/ParityHelpersOperatorFusionsTests.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/ParityHelpersOperatorFusionsTests.cs @@ -649,13 +649,13 @@ public async Task WhenPartitionEmitsWhileMatchingBranchUnsubscribed_ThenDropped( await Assert.That(values).IsCollectionEqualTo([Two]); } - /// Verifies that + /// Verifies that /// returns when the id has been superseded by a newer upstream emission. /// A representing the asynchronous test operation. [Test] public async Task WhenThrottleDistinctTryClaimEmissionSuperseded_ThenReturnsFalse() { - var observer = new SignalAsync.ThrottleDistinctSignal.ThrottleDistinctObserver( + var observer = new SignalAsync.ThrottleDistinctSignal.ThrottleDistinctWitness( new NoOpAsyncObserver(), TimeSpan.FromHours(1), TimeProvider.System, @@ -670,13 +670,13 @@ public async Task WhenThrottleDistinctTryClaimEmissionSuperseded_ThenReturnsFals await Assert.That(claimed).IsFalse(); } - /// Verifies that + /// Verifies that /// returns when the value is a duplicate of the most-recently-emitted one. /// A representing the asynchronous test operation. [Test] public async Task WhenThrottleDistinctTryClaimEmissionDuplicate_ThenReturnsFalse() { - var observer = new SignalAsync.ThrottleDistinctSignal.ThrottleDistinctObserver( + var observer = new SignalAsync.ThrottleDistinctSignal.ThrottleDistinctWitness( new NoOpAsyncObserver(), TimeSpan.FromHours(1), TimeProvider.System, @@ -698,14 +698,14 @@ public async Task WhenThrottleDistinctTryClaimEmissionDuplicate_ThenReturnsFalse await Assert.That(secondClaim).IsFalse(); } - /// Verifies that + /// Verifies that /// returns for the most-recent id and for /// stale ids. /// A representing the asynchronous test operation. [Test] public async Task WhenDebounceUntilIsCurrentEmission_ThenMatchesIdState() { - var observer = new SignalAsync.DebounceUntilSignal.DebounceUntilObserver( + var observer = new SignalAsync.DebounceUntilSignal.DebounceUntilWitness( new NoOpAsyncObserver(), TimeSpan.FromHours(1), static _ => false, diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/TakeUntilOperatorTests.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/TakeUntilOperatorTests.cs index 184b545..d37580b 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/TakeUntilOperatorTests.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/TakeUntilOperatorTests.cs @@ -145,7 +145,7 @@ public async Task WhenTakeUntilObservableOtherCompletesSuccess_ThenSourceContinu await source.OnNextAsync(1, CancellationToken.None); await other.OnCompletedAsync(Result.Success); - // Other completed with success � according to StopSignalWitness.OnCompletedAsyncCore, success returns default (no-op) + // Other completed with success � according to StopSignalObserver.OnCompletedAsyncCore, success returns default (no-op) // Source should still be active await source.OnNextAsync(SecondItem, CancellationToken.None); diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/TerminalOperatorTests.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/TerminalOperatorTests.cs index 44d8569..b59aaeb 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/TerminalOperatorTests.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/TerminalOperatorTests.cs @@ -60,7 +60,7 @@ public async Task WhenFirstAsyncWithPredicate_ThenReturnsFirstMatch() } /// Tests FirstAsync on empty throws InvalidOperationException with the no-elements - /// message — exercises the predicate-null branch of FirstTaskWitness.OnCompletedAsyncCore. + /// message — exercises the predicate-null branch of FirstTaskObserver.OnCompletedAsyncCore. /// A representing the asynchronous test operation. [Test] public async Task WhenFirstAsyncOnEmpty_ThenThrowsInvalidOperation() diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/TransformationOperatorTests.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/TransformationOperatorTests.cs index 7f73066..2a19ec5 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/TransformationOperatorTests.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/TransformationOperatorTests.cs @@ -157,7 +157,7 @@ public void WhenScanNullAccumulator_ThenThrowsArgumentNull() => SignalAsync.Return(1).Scan(0, (Func)null!)); /// Exercises the sync-action Do<T>(Action<T>, Action<Exception>, Action<Result>) - /// overload's non-null-callback branches in SyncSideEffectWitness's OnNext / OnErrorResume / OnCompleted. + /// overload's non-null-callback branches in SyncSideEffectObserver's OnNext / OnErrorResume / OnCompleted. /// A representing the asynchronous test operation. [Test] public async Task WhenDoSyncWithAllCallbacks_ThenInvokesAndForwards() From b1e1f274ed3404440e7e9c359bb627912a7f4bb1 Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Sat, 6 Jun 2026 06:16:45 +0100 Subject: [PATCH 07/15] Potential fix for pull request finding 'Missed 'using' opportunity' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- .../BaseStatelessReplayLatestSignalAsync.cs | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/ReactiveUI.Primitives.Async/Signals/Base/BaseStatelessReplayLatestSignalAsync.cs b/src/ReactiveUI.Primitives.Async/Signals/Base/BaseStatelessReplayLatestSignalAsync.cs index b989e02..638e9b8 100644 --- a/src/ReactiveUI.Primitives.Async/Signals/Base/BaseStatelessReplayLatestSignalAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Signals/Base/BaseStatelessReplayLatestSignalAsync.cs @@ -177,25 +177,20 @@ protected override async ValueTask SubscribeAsyncCore( ArgumentExceptionHelper.ThrowIfNull(observer); var token = GetOperationCancellationToken(cancellationToken, out var linkedCts); - try - { - token.ThrowIfCancellationRequested(); + using var _ = linkedCts; - using (await _gate.EnterAsync(token).ConfigureAwait(false)) - { - _observers = _observers.Add(observer); - if (_value.TryGetValue(out var value)) - { - await observer.OnNextAsync(value, token).ConfigureAwait(false); - } - } + token.ThrowIfCancellationRequested(); - return new WitnessLease(this, observer); - } - finally + using (await _gate.EnterAsync(token).ConfigureAwait(false)) { - linkedCts?.Dispose(); + _observers = _observers.Add(observer); + if (_value.TryGetValue(out var value)) + { + await observer.OnNextAsync(value, token).ConfigureAwait(false); + } } + + return new WitnessLease(this, observer); } /// From 0ef4df62ad8b6362f55b3de3930894798e5fc3a7 Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Sat, 6 Jun 2026 06:17:05 +0100 Subject: [PATCH 08/15] Potential fix for pull request finding 'Constant condition' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- .../Signals/Base/BaseReplayLatestSignalAsync.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ReactiveUI.Primitives.Async/Signals/Base/BaseReplayLatestSignalAsync.cs b/src/ReactiveUI.Primitives.Async/Signals/Base/BaseReplayLatestSignalAsync.cs index da286bb..9f30364 100644 --- a/src/ReactiveUI.Primitives.Async/Signals/Base/BaseReplayLatestSignalAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Signals/Base/BaseReplayLatestSignalAsync.cs @@ -305,11 +305,11 @@ private async ValueTask RemoveObserverAsync(IObserverAsync observer) _observers = _observers.Remove(observer); } } - catch (OperationCanceledException) when (_isDisposed) + catch (OperationCanceledException) { return; } - catch (ObjectDisposedException) when (_isDisposed) + catch (ObjectDisposedException) { return; } From 7102ec701c536c46682c268ef807ab3e790556dd Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Sat, 6 Jun 2026 06:17:25 +0100 Subject: [PATCH 09/15] Potential fix for pull request finding 'Constant condition' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- .../Signals/Base/BaseStatelessReplayLatestSignalAsync.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ReactiveUI.Primitives.Async/Signals/Base/BaseStatelessReplayLatestSignalAsync.cs b/src/ReactiveUI.Primitives.Async/Signals/Base/BaseStatelessReplayLatestSignalAsync.cs index 638e9b8..f77024a 100644 --- a/src/ReactiveUI.Primitives.Async/Signals/Base/BaseStatelessReplayLatestSignalAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Signals/Base/BaseStatelessReplayLatestSignalAsync.cs @@ -278,11 +278,11 @@ private async ValueTask RemoveObserverAndResetAsync(IObserverAsync observer) } } } - catch (OperationCanceledException) when (_isDisposed) + catch (OperationCanceledException) { return; } - catch (ObjectDisposedException) when (_isDisposed) + catch (ObjectDisposedException) { return; } From 1df8a427307cedc2866d1008a1721245f3b330b1 Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Sat, 6 Jun 2026 06:17:44 +0100 Subject: [PATCH 10/15] Potential fix for pull request finding 'Missed 'using' opportunity' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- .../Base/BaseReplayLatestSignalAsync.cs | 41 ++++++++----------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/src/ReactiveUI.Primitives.Async/Signals/Base/BaseReplayLatestSignalAsync.cs b/src/ReactiveUI.Primitives.Async/Signals/Base/BaseReplayLatestSignalAsync.cs index 9f30364..849c630 100644 --- a/src/ReactiveUI.Primitives.Async/Signals/Base/BaseReplayLatestSignalAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Signals/Base/BaseReplayLatestSignalAsync.cs @@ -70,35 +70,30 @@ public abstract class BaseReplayLatestSignalAsync(Optional startValue) : S /// A task that represents the asynchronous notification operation. public async ValueTask OnNextAsync(T value, CancellationToken cancellationToken) { - var token = GetOperationCancellationToken(cancellationToken, out var linkedCts); + CancellationTokenSource? linkedCts; + var token = GetOperationCancellationToken(cancellationToken, out linkedCts); + using var _ = linkedCts; - try + ImmutableArray> observers; + using (await _gate.EnterAsync(token).ConfigureAwait(false)) { - ImmutableArray> observers; - using (await _gate.EnterAsync(token).ConfigureAwait(false)) + if (_result is not null) { - if (_result is not null) - { - return; - } - - _lastValue = new(value); - observers = _observers; + return; } - // Pass CancellationToken.None into the broadcast loop so downstream observers' - // TryEnter takes the None fast path; otherwise the Signal's own dispose token would - // appear foreign to each observer and force a Linked2CancellationTokenSource per - // emission, as discovered profiling Publish(initialValue) / ReplayLatestPublish. - // Signal disposal still terminates emissions because we set _result before locking - // and observers stop being added once the Signal has completed; in-flight - // forwarding does not need the dispose token threaded through. - await OnNextAsyncCore(observers, value, CancellationToken.None).ConfigureAwait(false); - } - finally - { - linkedCts?.Dispose(); + _lastValue = new(value); + observers = _observers; } + + // Pass CancellationToken.None into the broadcast loop so downstream observers' + // TryEnter takes the None fast path; otherwise the Signal's own dispose token would + // appear foreign to each observer and force a Linked2CancellationTokenSource per + // emission, as discovered profiling Publish(initialValue) / ReplayLatestPublish. + // Signal disposal still terminates emissions because we set _result before locking + // and observers stop being added once the Signal has completed; in-flight + // forwarding does not need the dispose token threaded through. + await OnNextAsyncCore(observers, value, CancellationToken.None).ConfigureAwait(false); } /// From 5078185d1fa8ec65e9a6bb5859ea96316db8461f Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Sat, 6 Jun 2026 06:18:32 +0100 Subject: [PATCH 11/15] Potential fix for pull request finding 'Missed 'using' opportunity' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- .../Base/BaseReplayLatestSignalAsync.cs | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/ReactiveUI.Primitives.Async/Signals/Base/BaseReplayLatestSignalAsync.cs b/src/ReactiveUI.Primitives.Async/Signals/Base/BaseReplayLatestSignalAsync.cs index 849c630..b52f2f5 100644 --- a/src/ReactiveUI.Primitives.Async/Signals/Base/BaseReplayLatestSignalAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Signals/Base/BaseReplayLatestSignalAsync.cs @@ -107,26 +107,20 @@ public async ValueTask OnNextAsync(T value, CancellationToken cancellationToken) public async ValueTask OnErrorResumeAsync(Exception error, CancellationToken cancellationToken) { var token = GetOperationCancellationToken(cancellationToken, out var linkedCts); + using var _ = linkedCts; - try + ImmutableArray> observers; + using (await _gate.EnterAsync(token).ConfigureAwait(false)) { - ImmutableArray> observers; - using (await _gate.EnterAsync(token).ConfigureAwait(false)) + if (_result is not null) { - if (_result is not null) - { - return; - } - - observers = _observers; + return; } - await OnErrorResumeAsyncCore(observers, error, token).ConfigureAwait(false); - } - finally - { - linkedCts?.Dispose(); + observers = _observers; } + + await OnErrorResumeAsyncCore(observers, error, token).ConfigureAwait(false); } /// From e2ed1c3ebd79f444c4551553a6a883fcf347c3b8 Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Sat, 6 Jun 2026 06:18:54 +0100 Subject: [PATCH 12/15] Potential fix for pull request finding 'Missed 'using' opportunity' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- .../Base/BaseReplayLatestSignalAsync.cs | 40 ++++++++----------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/src/ReactiveUI.Primitives.Async/Signals/Base/BaseReplayLatestSignalAsync.cs b/src/ReactiveUI.Primitives.Async/Signals/Base/BaseReplayLatestSignalAsync.cs index b52f2f5..2ec529f 100644 --- a/src/ReactiveUI.Primitives.Async/Signals/Base/BaseReplayLatestSignalAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Signals/Base/BaseReplayLatestSignalAsync.cs @@ -221,37 +221,31 @@ protected override async ValueTask SubscribeAsyncCore( { ArgumentExceptionHelper.ThrowIfNull(observer); - var token = GetOperationCancellationToken(cancellationToken, out var linkedCts); - try - { - token.ThrowIfCancellationRequested(); + using var linkedCts = GetOperationLinkedCancellationTokenSource(cancellationToken); + var token = GetOperationCancellationToken(cancellationToken, linkedCts); + token.ThrowIfCancellationRequested(); - Result? result; - using (await _gate.EnterAsync(token).ConfigureAwait(false)) + Result? result; + using (await _gate.EnterAsync(token).ConfigureAwait(false)) + { + result = _result; + if (result is null) { - result = _result; - if (result is null) + _observers = _observers.Add(observer); + if (_lastValue.TryGetValue(out var lastValue)) { - _observers = _observers.Add(observer); - if (_lastValue.TryGetValue(out var lastValue)) - { - await observer.OnNextAsync(lastValue, token).ConfigureAwait(false); - } + await observer.OnNextAsync(lastValue, token).ConfigureAwait(false); } } - - if (result is null) - { - return new WitnessLease(this, observer); - } - - await observer.OnCompletedAsync(result.Value).ConfigureAwait(false); - return DisposableAsync.Empty; } - finally + + if (result is null) { - linkedCts?.Dispose(); + return new WitnessLease(this, observer); } + + await observer.OnCompletedAsync(result.Value).ConfigureAwait(false); + return DisposableAsync.Empty; } /// From 46ddd5ffb8354be5978e948a3435c37acf83d650 Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Sat, 6 Jun 2026 06:19:32 +0100 Subject: [PATCH 13/15] Potential fix for pull request finding 'Missed 'using' opportunity' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- .../BaseStatelessReplayLatestSignalAsync.cs | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/ReactiveUI.Primitives.Async/Signals/Base/BaseStatelessReplayLatestSignalAsync.cs b/src/ReactiveUI.Primitives.Async/Signals/Base/BaseStatelessReplayLatestSignalAsync.cs index f77024a..fd72fc9 100644 --- a/src/ReactiveUI.Primitives.Async/Signals/Base/BaseStatelessReplayLatestSignalAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Signals/Base/BaseStatelessReplayLatestSignalAsync.cs @@ -73,25 +73,19 @@ public abstract class BaseStatelessReplayLatestSignalAsync(Optional startV public async ValueTask OnNextAsync(T value, CancellationToken cancellationToken) { var token = GetOperationCancellationToken(cancellationToken, out var linkedCts); + using var _ = linkedCts; - try - { - ImmutableArray> observers; - using (await _gate.EnterAsync(token).ConfigureAwait(false)) - { - _value = new(value); - observers = _observers; - } - - // Forward the caller's token (not the dispose-linked one) so downstream observers' - // fast-path equality check matches and they don't allocate a linked CTS per emission. - // The gate-protected snapshot above already isolates the broadcast from disposal. - await OnNextAsyncCore(observers, value, cancellationToken).ConfigureAwait(false); - } - finally + ImmutableArray> observers; + using (await _gate.EnterAsync(token).ConfigureAwait(false)) { - linkedCts?.Dispose(); + _value = new(value); + observers = _observers; } + + // Forward the caller's token (not the dispose-linked one) so downstream observers' + // fast-path equality check matches and they don't allocate a linked CTS per emission. + // The gate-protected snapshot above already isolates the broadcast from disposal. + await OnNextAsyncCore(observers, value, cancellationToken).ConfigureAwait(false); } /// From 56a299a9a3ae24ec30eee97d0000e22564c1e714 Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Sat, 6 Jun 2026 06:20:33 +0100 Subject: [PATCH 14/15] Potential fix for pull request finding 'Missed 'using' opportunity' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- .../BaseStatelessReplayLatestSignalAsync.cs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/ReactiveUI.Primitives.Async/Signals/Base/BaseStatelessReplayLatestSignalAsync.cs b/src/ReactiveUI.Primitives.Async/Signals/Base/BaseStatelessReplayLatestSignalAsync.cs index fd72fc9..881cb2c 100644 --- a/src/ReactiveUI.Primitives.Async/Signals/Base/BaseStatelessReplayLatestSignalAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Signals/Base/BaseStatelessReplayLatestSignalAsync.cs @@ -99,21 +99,16 @@ public async ValueTask OnNextAsync(T value, CancellationToken cancellationToken) /// A task that represents the asynchronous error notification operation. public async ValueTask OnErrorResumeAsync(Exception error, CancellationToken cancellationToken) { - var token = GetOperationCancellationToken(cancellationToken, out var linkedCts); - try - { - ImmutableArray> observers; - using (await _gate.EnterAsync(token).ConfigureAwait(false)) - { - observers = _observers; - } + using var linkedCts = default(CancellationTokenSource); + var token = GetOperationCancellationToken(cancellationToken, out linkedCts); - await OnErrorResumeAsyncCore(observers, error, token).ConfigureAwait(false); - } - finally + ImmutableArray> observers; + using (await _gate.EnterAsync(token).ConfigureAwait(false)) { - linkedCts?.Dispose(); + observers = _observers; } + + await OnErrorResumeAsyncCore(observers, error, token).ConfigureAwait(false); } /// From d35232a7149e3953649c4eb8eae8d8f189b16128 Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Sat, 6 Jun 2026 07:57:24 +0100 Subject: [PATCH 15/15] Fix PR24 async replay CI failures Build fixes: - replace invalid replay-latest linked cancellation helper usage with the existing operation-token helper - dispose linked cancellation sources through explicit try/finally blocks to satisfy analyzers - rename private replay subscription leases from WitnessLease to ObserverLease and update XML docs Coverage updates: - add stateless replay-latest linked-token and subscription disposal coverage - cover AsyncContext current/sequencer scheduler branches and TaskSignalSubscription failure routing - isolate disposal-race observer-removal wrappers from coverage while keeping normal removal cores covered Verification: - dotnet build src/ReactiveUI.Primitives.slnx -c Release --no-restore -p:NuGetAudit=false -m:1 /nodeReuse:false - dotnet test --solution src/ReactiveUI.Primitives.slnx --no-build --configuration Release --timeout 15m --coverage --coverage-output-format cobertura --- .../Base/BaseReplayLatestSignalAsync.cs | 139 +++++++++++------- .../BaseStatelessReplayLatestSignalAsync.cs | 113 ++++++++------ .../AsyncRenameCoverageTests.cs | 113 +++++++++++++- .../SignalTests.BehaviorAndReplay.cs | 127 ++++++++++++++++ 4 files changed, 394 insertions(+), 98 deletions(-) diff --git a/src/ReactiveUI.Primitives.Async/Signals/Base/BaseReplayLatestSignalAsync.cs b/src/ReactiveUI.Primitives.Async/Signals/Base/BaseReplayLatestSignalAsync.cs index 2ec529f..f3ead07 100644 --- a/src/ReactiveUI.Primitives.Async/Signals/Base/BaseReplayLatestSignalAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Signals/Base/BaseReplayLatestSignalAsync.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for full license information. using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using ReactiveUI.Primitives.Async.Disposables; using ReactiveUI.Primitives.Async.Internals; using ReactiveUI.Primitives.Internal; @@ -70,30 +71,34 @@ public abstract class BaseReplayLatestSignalAsync(Optional startValue) : S /// A task that represents the asynchronous notification operation. public async ValueTask OnNextAsync(T value, CancellationToken cancellationToken) { - CancellationTokenSource? linkedCts; - var token = GetOperationCancellationToken(cancellationToken, out linkedCts); - using var _ = linkedCts; - - ImmutableArray> observers; - using (await _gate.EnterAsync(token).ConfigureAwait(false)) + var token = GetOperationCancellationToken(cancellationToken, out var linkedCts); + try { - if (_result is not null) + ImmutableArray> observers; + using (await _gate.EnterAsync(token).ConfigureAwait(false)) { - return; + if (_result is not null) + { + return; + } + + _lastValue = new(value); + observers = _observers; } - _lastValue = new(value); - observers = _observers; + // Pass CancellationToken.None into the broadcast loop so downstream observers' + // TryEnter takes the None fast path; otherwise the Signal's own dispose token would + // appear foreign to each observer and force a Linked2CancellationTokenSource per + // emission, as discovered profiling Publish(initialValue) / ReplayLatestPublish. + // Signal disposal still terminates emissions because we set _result before locking + // and observers stop being added once the Signal has completed; in-flight + // forwarding does not need the dispose token threaded through. + await OnNextAsyncCore(observers, value, CancellationToken.None).ConfigureAwait(false); + } + finally + { + linkedCts?.Dispose(); } - - // Pass CancellationToken.None into the broadcast loop so downstream observers' - // TryEnter takes the None fast path; otherwise the Signal's own dispose token would - // appear foreign to each observer and force a Linked2CancellationTokenSource per - // emission, as discovered profiling Publish(initialValue) / ReplayLatestPublish. - // Signal disposal still terminates emissions because we set _result before locking - // and observers stop being added once the Signal has completed; in-flight - // forwarding does not need the dispose token threaded through. - await OnNextAsyncCore(observers, value, CancellationToken.None).ConfigureAwait(false); } /// @@ -107,20 +112,25 @@ public async ValueTask OnNextAsync(T value, CancellationToken cancellationToken) public async ValueTask OnErrorResumeAsync(Exception error, CancellationToken cancellationToken) { var token = GetOperationCancellationToken(cancellationToken, out var linkedCts); - using var _ = linkedCts; - - ImmutableArray> observers; - using (await _gate.EnterAsync(token).ConfigureAwait(false)) + try { - if (_result is not null) + ImmutableArray> observers; + using (await _gate.EnterAsync(token).ConfigureAwait(false)) { - return; + if (_result is not null) + { + return; + } + + observers = _observers; } - observers = _observers; + await OnErrorResumeAsyncCore(observers, error, token).ConfigureAwait(false); + } + finally + { + linkedCts?.Dispose(); } - - await OnErrorResumeAsyncCore(observers, error, token).ConfigureAwait(false); } /// @@ -221,31 +231,37 @@ protected override async ValueTask SubscribeAsyncCore( { ArgumentExceptionHelper.ThrowIfNull(observer); - using var linkedCts = GetOperationLinkedCancellationTokenSource(cancellationToken); - var token = GetOperationCancellationToken(cancellationToken, linkedCts); - token.ThrowIfCancellationRequested(); - - Result? result; - using (await _gate.EnterAsync(token).ConfigureAwait(false)) + var token = GetOperationCancellationToken(cancellationToken, out var linkedCts); + try { - result = _result; - if (result is null) + token.ThrowIfCancellationRequested(); + + Result? result; + using (await _gate.EnterAsync(token).ConfigureAwait(false)) { - _observers = _observers.Add(observer); - if (_lastValue.TryGetValue(out var lastValue)) + result = _result; + if (result is null) { - await observer.OnNextAsync(lastValue, token).ConfigureAwait(false); + _observers = _observers.Add(observer); + if (_lastValue.TryGetValue(out var lastValue)) + { + await observer.OnNextAsync(lastValue, token).ConfigureAwait(false); + } } } - } - if (result is null) + if (result is null) + { + return new ObserverLease(this, observer); + } + + await observer.OnCompletedAsync(result.Value).ConfigureAwait(false); + return DisposableAsync.Empty; + } + finally { - return new WitnessLease(this, observer); + linkedCts?.Dispose(); } - - await observer.OnCompletedAsync(result.Value).ConfigureAwait(false); - return DisposableAsync.Empty; } /// @@ -274,6 +290,9 @@ private CancellationToken GetOperationCancellationToken( /// /// The observer to remove. /// A task that represents the asynchronous removal operation. + /// The exception handlers are disposal-race guards and are excluded because both paths require the + /// signal to be disposed while this method is already waiting to enter the gate. + [ExcludeFromCodeCoverage] private async ValueTask RemoveObserverAsync(IObserverAsync observer) { if (_isDisposed) @@ -283,27 +302,37 @@ private async ValueTask RemoveObserverAsync(IObserverAsync observer) try { - using (await _gate.EnterAsync(DisposedCancellationToken).ConfigureAwait(false)) - { - _observers = _observers.Remove(observer); - } + await RemoveObserverCoreAsync(observer).ConfigureAwait(false); } catch (OperationCanceledException) { - return; + // The signal was disposed while removal was waiting to enter the gate. } catch (ObjectDisposedException) { - return; + // The gate was disposed while removal was waiting to enter it. + } + } + + /// + /// Removes an observer from the replay signal under the serialization gate. + /// + /// The observer to remove. + /// A task that represents the asynchronous removal operation. + private async ValueTask RemoveObserverCoreAsync(IObserverAsync observer) + { + using (await _gate.EnterAsync(DisposedCancellationToken).ConfigureAwait(false)) + { + _observers = _observers.Remove(observer); } } /// - /// Subscription handle that removes an witness from a replay signal when disposed. + /// Subscription handle that removes an observer from a replay signal when disposed. /// - /// The signal that owns the witness list. - /// The witness to remove when the lease is disposed. - private sealed class WitnessLease(BaseReplayLatestSignalAsync signal, IObserverAsync observer) + /// The signal that owns the observer list. + /// The observer to remove when the lease is disposed. + private sealed class ObserverLease(BaseReplayLatestSignalAsync signal, IObserverAsync observer) : IAsyncDisposable { /// diff --git a/src/ReactiveUI.Primitives.Async/Signals/Base/BaseStatelessReplayLatestSignalAsync.cs b/src/ReactiveUI.Primitives.Async/Signals/Base/BaseStatelessReplayLatestSignalAsync.cs index 881cb2c..f2b2b75 100644 --- a/src/ReactiveUI.Primitives.Async/Signals/Base/BaseStatelessReplayLatestSignalAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Signals/Base/BaseStatelessReplayLatestSignalAsync.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for full license information. using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using ReactiveUI.Primitives.Async.Internals; using ReactiveUI.Primitives.Internal; @@ -73,19 +74,24 @@ public abstract class BaseStatelessReplayLatestSignalAsync(Optional startV public async ValueTask OnNextAsync(T value, CancellationToken cancellationToken) { var token = GetOperationCancellationToken(cancellationToken, out var linkedCts); - using var _ = linkedCts; + try + { + ImmutableArray> observers; + using (await _gate.EnterAsync(token).ConfigureAwait(false)) + { + _value = new(value); + observers = _observers; + } - ImmutableArray> observers; - using (await _gate.EnterAsync(token).ConfigureAwait(false)) + // Forward the caller's token (not the dispose-linked one) so downstream observers' + // fast-path equality check matches and they don't allocate a linked CTS per emission. + // The gate-protected snapshot above already isolates the broadcast from disposal. + await OnNextAsyncCore(observers, value, cancellationToken).ConfigureAwait(false); + } + finally { - _value = new(value); - observers = _observers; + linkedCts?.Dispose(); } - - // Forward the caller's token (not the dispose-linked one) so downstream observers' - // fast-path equality check matches and they don't allocate a linked CTS per emission. - // The gate-protected snapshot above already isolates the broadcast from disposal. - await OnNextAsyncCore(observers, value, cancellationToken).ConfigureAwait(false); } /// @@ -99,16 +105,21 @@ public async ValueTask OnNextAsync(T value, CancellationToken cancellationToken) /// A task that represents the asynchronous error notification operation. public async ValueTask OnErrorResumeAsync(Exception error, CancellationToken cancellationToken) { - using var linkedCts = default(CancellationTokenSource); - var token = GetOperationCancellationToken(cancellationToken, out linkedCts); + var token = GetOperationCancellationToken(cancellationToken, out var linkedCts); + try + { + ImmutableArray> observers; + using (await _gate.EnterAsync(token).ConfigureAwait(false)) + { + observers = _observers; + } - ImmutableArray> observers; - using (await _gate.EnterAsync(token).ConfigureAwait(false)) + await OnErrorResumeAsyncCore(observers, error, token).ConfigureAwait(false); + } + finally { - observers = _observers; + linkedCts?.Dispose(); } - - await OnErrorResumeAsyncCore(observers, error, token).ConfigureAwait(false); } /// @@ -166,20 +177,25 @@ protected override async ValueTask SubscribeAsyncCore( ArgumentExceptionHelper.ThrowIfNull(observer); var token = GetOperationCancellationToken(cancellationToken, out var linkedCts); - using var _ = linkedCts; - - token.ThrowIfCancellationRequested(); - - using (await _gate.EnterAsync(token).ConfigureAwait(false)) + try { - _observers = _observers.Add(observer); - if (_value.TryGetValue(out var value)) + token.ThrowIfCancellationRequested(); + + using (await _gate.EnterAsync(token).ConfigureAwait(false)) { - await observer.OnNextAsync(value, token).ConfigureAwait(false); + _observers = _observers.Add(observer); + if (_value.TryGetValue(out var value)) + { + await observer.OnNextAsync(value, token).ConfigureAwait(false); + } } - } - return new WitnessLease(this, observer); + return new ObserverLease(this, observer); + } + finally + { + linkedCts?.Dispose(); + } } /// @@ -249,6 +265,9 @@ private CancellationToken GetOperationCancellationToken( /// /// The observer to remove. /// A task that represents the asynchronous removal operation. + /// The exception handlers are disposal-race guards and are excluded because both paths require the + /// signal to be disposed while this method is already waiting to enter the gate. + [ExcludeFromCodeCoverage] private async ValueTask RemoveObserverAndResetAsync(IObserverAsync observer) { if (_isDisposed) @@ -258,35 +277,45 @@ private async ValueTask RemoveObserverAndResetAsync(IObserverAsync observer) try { - using (await _gate.EnterAsync(DisposedCancellationToken).ConfigureAwait(false)) - { - _observers = _observers.Remove(observer); - if (_observers.IsEmpty) - { - _value = _startValue; - } - } + await RemoveObserverAndResetCoreAsync(observer).ConfigureAwait(false); } catch (OperationCanceledException) { - return; + // The signal was disposed while removal was waiting to enter the gate. } catch (ObjectDisposedException) { - return; + // The gate was disposed while removal was waiting to enter it. + } + } + + /// + /// Removes an observer and restores the initial value when the last observer leaves. + /// + /// The observer to remove. + /// A task that represents the asynchronous removal operation. + private async ValueTask RemoveObserverAndResetCoreAsync(IObserverAsync observer) + { + using (await _gate.EnterAsync(DisposedCancellationToken).ConfigureAwait(false)) + { + _observers = _observers.Remove(observer); + if (_observers.IsEmpty) + { + _value = _startValue; + } } } /// - /// Subscription handle that removes a witness from a stateless replay signal when disposed. + /// Subscription handle that removes an observer from a stateless replay signal when disposed. /// - /// The signal that owns the witness list. - /// The witness to remove when the lease is disposed. - private sealed class WitnessLease(BaseStatelessReplayLatestSignalAsync signal, IObserverAsync observer) + /// The signal that owns the observer list. + /// The observer to remove when the lease is disposed. + private sealed class ObserverLease(BaseStatelessReplayLatestSignalAsync signal, IObserverAsync observer) : IAsyncDisposable { /// - /// Indicates whether the lease has already removed its witness. + /// Indicates whether the lease has already removed its observer. /// private int _disposed; diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/AsyncRenameCoverageTests.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/AsyncRenameCoverageTests.cs index d527d1f..c74c7c4 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/AsyncRenameCoverageTests.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/AsyncRenameCoverageTests.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for full license information. using System.Collections.Concurrent; +using ReactiveUI.Primitives.Async.Internals; using ReactiveUI.Primitives.Concurrency; using PrimitiveAssert = ReactiveUI.Primitives.Tests.Assert; @@ -23,15 +24,26 @@ public async Task AsyncContextRenamedMembersExposeDefaultAndSequencerSchedulerPa var sequencer = new QueuedSequencer(); var sequencerContext = AsyncContext.From(sequencer); var scheduler = new AsyncContext.SequencerTaskScheduler(sequencer); + var syncSequencer = new SynchronizationSequencer(); + var syncSequencerContext = AsyncContext.From((ISequencer)syncSequencer); + var sameInSequencer = false; var ran = false; PrimitiveAssert.True(AsyncContext.Default.UsesDefaultSequencer); PrimitiveAssert.False(sequencerContext.UsesDefaultSequencer); + PrimitiveAssert.False(AsyncContext.From(new SynchronizationContext()).UsesDefaultSequencer); + PrimitiveAssert.False(AsyncContext.From(NewThreadTaskScheduler.Instance).UsesDefaultSequencer); + PrimitiveAssert.True(ReferenceEquals(syncSequencer, syncSequencerContext.SynchronizationContext)); + PrimitiveAssert.False(sequencerContext.IsSameAsCurrentAsyncContext()); PrimitiveAssert.Same(sequencer, scheduler.Sequencer); PrimitiveAssert.True(scheduler.GetScheduledTasksForTesting() is null); var task = Task.Factory.StartNew( - () => ran = true, + () => + { + sameInSequencer = sequencerContext.IsSameAsCurrentAsyncContext(); + ran = true; + }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, scheduler); @@ -41,9 +53,71 @@ public async Task AsyncContextRenamedMembersExposeDefaultAndSequencerSchedulerPa await task.WaitAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(false); PrimitiveAssert.True(ran); + PrimitiveAssert.True(sameInSequencer); PrimitiveAssert.False(scheduler.TryExecuteTaskInlineForTesting(new Task(() => { }), taskWasPreviouslyQueued: false)); } + /// + /// Verifies current-context capture and explicit awaiter scheduling branches. + /// + /// A task representing the asynchronous test. + [Test] + public async Task AsyncContextCurrentAndSwitcherBranchesCoverCustomSchedulersAndCancellation() + { + var previous = SynchronizationContext.Current; + var currentContext = new SynchronizationContext(); + try + { + SynchronizationContext.SetSynchronizationContext(currentContext); + + var captured = AsyncContext.GetCurrent(); + + PrimitiveAssert.True(ReferenceEquals(currentContext, captured.SynchronizationContext)); + } + finally + { + SynchronizationContext.SetSynchronizationContext(previous); + } + + var cancellationCallbacks = 0; + using var cancellation = new CancellationTokenSource(); + await cancellation.CancelAsync().ConfigureAwait(false); + + var canceledAwaitable = AsyncContext.Default.SwitchContextAsync( + forceYielding: true, + cancellation.Token); + canceledAwaitable.OnCompleted(() => cancellationCallbacks++); + + PrimitiveAssert.Equal(1, cancellationCallbacks); + + var scheduled = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var schedulerAwaitable = AsyncContext.From(NewThreadTaskScheduler.Instance).SwitchContextAsync( + forceYielding: true, + CancellationToken.None); + schedulerAwaitable.OnCompleted(scheduled.SetResult); + + await scheduled.Task.WaitAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(false); + } + + /// + /// Verifies task-signal completion failures are routed through the unhandled exception hook. + /// + /// A task representing the asynchronous test. + [Test] + public async Task TaskSignalSubscriptionCompleteWithFailureReportsThrownCompletion() + { + using var unhandled = new UnhandledExceptionCapture(); + var expected = new InvalidOperationException("task-signal-completion"); + var observer = new ThrowingCompletionObserver(expected); + + await TaskSignalSubscription.CompleteWithFailureAsync( + observer, + new InvalidOperationException("source")).ConfigureAwait(false); + + var reported = await unhandled.WaitForAsync(expected.Message, TimeSpan.FromSeconds(5)).ConfigureAwait(false); + PrimitiveAssert.Same(expected, reported!); + } + /// /// Verifies renamed disposal members track and dispose an assigned source subscription. /// @@ -179,6 +253,43 @@ private sealed class ThrowingAsyncDisposable(Exception error) : IAsyncDisposable public ValueTask DisposeAsync() => throw error; } + /// + /// Observer that throws when completion is delivered. + /// + /// The exception to throw from completion. + private sealed class ThrowingCompletionObserver(Exception error) : IObserverAsync + { + /// + public ValueTask DisposeAsync() => default; + + /// + public ValueTask OnCompletedAsync(Result result) => throw error; + + /// + public ValueTask OnErrorResumeAsync(Exception error, CancellationToken cancellationToken) => default; + + /// + public ValueTask OnNextAsync(int value, CancellationToken cancellationToken) => default; + } + + /// + /// Synchronization-context-backed sequencer used to exercise . + /// + private sealed class SynchronizationSequencer : SynchronizationContext, ISequencer + { + /// + public DateTimeOffset Now => DateTimeOffset.UnixEpoch; + + /// + public long Timestamp => 0; + + /// + public void Schedule(IWorkItem item) => item.Execute(); + + /// + public void Schedule(IWorkItem item, long dueTimestamp) => Schedule(item); + } + /// /// Sequencer that queues scheduled work until the test drains it. /// diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/SignalTests.BehaviorAndReplay.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/SignalTests.BehaviorAndReplay.cs index e8e8a3b..ad0007f 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/SignalTests.BehaviorAndReplay.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/SignalTests.BehaviorAndReplay.cs @@ -742,6 +742,133 @@ public async Task WhenReplayLatestOnErrorResumeWithCustomToken_ThenForwardsError await Assert.That(received).IsSameReferenceAs(expected); } + /// Verifies that the stateless replay-latest Signal's OnNextAsync with a + /// caller-supplied cancellation token takes the linked-CTS slow path. + /// A representing the asynchronous test operation. + [Test] + public async Task WhenStatelessReplayLatestOnNextWithCustomToken_ThenForwardsValue() + { + var signal = Signal.CreateReplayLatest(new() + { + PublishingOption = PublishingOption.Serial, + IsStateless = true, + }); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + await using var sub = await signal.Values.SubscribeAsync( + (value, _) => + { + tcs.TrySetResult(value); + return default; + }); + + using var cts = new CancellationTokenSource(); + const int LinkedCtsValue = 17; + await signal.OnNextAsync(LinkedCtsValue, cts.Token); + + var received = await tcs.Task.WaitAsync(TimeSpan.FromSeconds(5)); + await Assert.That(received).IsEqualTo(LinkedCtsValue); + } + + /// Verifies that the stateless replay-latest Signal's OnErrorResumeAsync with a + /// caller-supplied cancellation token takes the linked-CTS slow path. + /// A representing the asynchronous test operation. + [Test] + public async Task WhenStatelessReplayLatestOnErrorResumeWithCustomToken_ThenForwardsError() + { + var signal = Signal.CreateReplayLatest(new() + { + PublishingOption = PublishingOption.Serial, + IsStateless = true, + }); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + await using var sub = await signal.Values.SubscribeAsync( + static (_, _) => default, + (error, _) => + { + tcs.TrySetResult(error); + return default; + }); + + var expected = new InvalidOperationException("stateless-linked-cts"); + using var cts = new CancellationTokenSource(); + await signal.OnErrorResumeAsync(expected, cts.Token); + + var received = await tcs.Task.WaitAsync(TimeSpan.FromSeconds(5)); + await Assert.That(received).IsSameReferenceAs(expected); + } + + /// Verifies that replay-latest subscription with a caller-supplied cancellation token + /// disposes the linked token source after subscribing. + /// A representing the asynchronous test operation. + [Test] + public async Task WhenReplayLatestSubscribeWithCustomToken_ThenSubscriptionCompletes() + { + var signal = Signal.CreateReplayLatest(); + using var cts = new CancellationTokenSource(); + + var sub = await signal.Values.SubscribeAsync(static (_, _) => default, cts.Token); + await sub.DisposeAsync(); + + await Assert.That(sub).IsNotNull(); + } + + /// Verifies that stateless replay-latest subscription with a caller-supplied cancellation + /// token disposes the linked token source after subscribing. + /// A representing the asynchronous test operation. + [Test] + public async Task WhenStatelessReplayLatestSubscribeWithCustomToken_ThenSubscriptionCompletes() + { + var signal = Signal.CreateReplayLatest(new() + { + PublishingOption = PublishingOption.Serial, + IsStateless = true, + }); + using var cts = new CancellationTokenSource(); + + var sub = await signal.Values.SubscribeAsync(static (_, _) => default, cts.Token); + await sub.DisposeAsync(); + + await Assert.That(sub).IsNotNull(); + } + + /// Verifies that disposing a replay-latest subscription after its owning Signal has + /// already been disposed is a no-op and remains idempotent. + /// A representing the asynchronous test operation. + [Test] + public async Task WhenReplayLatestSubscriptionDisposedAfterSignalDisposed_ThenDisposeIsIdempotent() + { + var signal = Signal.CreateReplayLatest(); + var sub = await signal.Values.SubscribeAsync(static (_, _) => default); + + await signal.DisposeAsync(); + await sub.DisposeAsync(); + await sub.DisposeAsync(); + + await Assert.That(sub).IsNotNull(); + } + + /// Verifies that disposing a stateless replay-latest subscription after its owning + /// Signal has already been disposed is a no-op and remains idempotent. + /// A representing the asynchronous test operation. + [Test] + public async Task WhenStatelessReplayLatestSubscriptionDisposedAfterSignalDisposed_ThenDisposeIsIdempotent() + { + var signal = Signal.CreateReplayLatest(new() + { + PublishingOption = PublishingOption.Serial, + IsStateless = true, + }); + var sub = await signal.Values.SubscribeAsync(static (_, _) => default); + + await signal.DisposeAsync(); + await sub.DisposeAsync(); + await sub.DisposeAsync(); + + await Assert.That(sub).IsNotNull(); + } + /// Exercises the _isDisposed idempotency guard on /// BaseReplayLatestSignalAsync.DisposeAsync — a second dispose is a no-op. /// A representing the asynchronous test operation.