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/AnonymousSignalAsync.cs b/src/ReactiveUI.Primitives.Async/Internals/CallbackSignalAsync.cs similarity index 85% rename from src/ReactiveUI.Primitives.Async/Internals/AnonymousSignalAsync.cs rename to src/ReactiveUI.Primitives.Async/Internals/CallbackSignalAsync.cs index 861e56e..ab0eb94 100644 --- a/src/ReactiveUI.Primitives.Async/Internals/AnonymousSignalAsync.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 AnonymousSignalAsync( +internal sealed class CallbackSignalAsync( Func, CancellationToken, ValueTask> subscribeAsync) : SignalAsync { /// diff --git a/src/ReactiveUI.Primitives.Async/Internals/AnonymousObserverAsync.cs b/src/ReactiveUI.Primitives.Async/Internals/CallbackWitnessAsync.cs similarity index 80% rename from src/ReactiveUI.Primitives.Async/Internals/AnonymousObserverAsync.cs rename to src/ReactiveUI.Primitives.Async/Internals/CallbackWitnessAsync.cs index b6c8f89..b7bb485 100644 --- a/src/ReactiveUI.Primitives.Async/Internals/AnonymousObserverAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Internals/CallbackWitnessAsync.cs @@ -1,17 +1,17 @@ -// 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. 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 AnonymousObserverAsync( +internal sealed class CallbackWitnessAsync( 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/CombineLatestSubscriptionBase.cs b/src/ReactiveUI.Primitives.Async/Internals/CombineLatestCoordinatorBase.cs similarity index 82% rename from src/ReactiveUI.Primitives.Async/Internals/CombineLatestSubscriptionBase.cs rename to src/ReactiveUI.Primitives.Async/Internals/CombineLatestCoordinatorBase.cs index ac80816..e00d014 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); } @@ -28,7 +28,7 @@ protected CombineLatestSubscriptionBase(IObserverAsync observer, int so 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(); @@ -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); @@ -67,7 +67,7 @@ internal ValueTask OnErrorResume(Exception error, CancellationToken cancellation /// /// 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. @@ -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/CombineLatestIndexedWitness.cs similarity index 91% rename from src/ReactiveUI.Primitives.Async/Internals/CombineLatestIndexedObserver.cs rename to src/ReactiveUI.Primitives.Async/Internals/CombineLatestIndexedWitness.cs index c23940b..b9ae6ad 100644 --- a/src/ReactiveUI.Primitives.Async/Internals/CombineLatestIndexedObserver.cs +++ b/src/ReactiveUI.Primitives.Async/Internals/CombineLatestIndexedWitness.cs @@ -8,19 +8,19 @@ 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( - CombineLatestSubscriptionBase parent, +internal sealed class CombineLatestIndexedWitness( + 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/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/MulticastSignalAsync.cs b/src/ReactiveUI.Primitives.Async/Internals/MulticastSignalAsync.cs index 0823a70..e4f3f58 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 RelayWitnessAsync(observer); if (observer is ObserverAsync downstream) { downstream.LinkUpstreamCancellation(wrap.InternalDisposedToken); diff --git a/src/ReactiveUI.Primitives.Async/Internals/WrappedObserverAsync.cs b/src/ReactiveUI.Primitives.Async/Internals/RelayWitnessAsync.cs similarity index 61% rename from src/ReactiveUI.Primitives.Async/Internals/WrappedObserverAsync.cs rename to src/ReactiveUI.Primitives.Async/Internals/RelayWitnessAsync.cs index 77bde0a..3fc816c 100644 --- a/src/ReactiveUI.Primitives.Async/Internals/WrappedObserverAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Internals/RelayWitnessAsync.cs @@ -1,15 +1,15 @@ -// 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. 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 WrappedObserverAsync(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 85% rename from src/ReactiveUI.Primitives.Async/Internals/SingleElementObserver.cs rename to src/ReactiveUI.Primitives.Async/Internals/SingleElementWitness.cs index 8dc75f8..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) : TaskObserverAsyncBase(cancellationToken) + CancellationToken cancellationToken) : TaskResultWitnessAsyncBase(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/TakeUntilSourceWitness.cs similarity index 82% rename from src/ReactiveUI.Primitives.Async/Internals/TakeUntilSourceObserver.cs rename to src/ReactiveUI.Primitives.Async/Internals/TakeUntilSourceWitness.cs index 72a2a19..a0c857d 100644 --- a/src/ReactiveUI.Primitives.Async/Internals/TakeUntilSourceObserver.cs +++ b/src/ReactiveUI.Primitives.Async/Internals/TakeUntilSourceWitness.cs @@ -12,23 +12,23 @@ 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) { _ = 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/TaskObserverAsyncBase.cs b/src/ReactiveUI.Primitives.Async/Internals/TaskResultWitnessAsyncBase.cs similarity index 80% rename from src/ReactiveUI.Primitives.Async/Internals/TaskObserverAsyncBase.cs rename to src/ReactiveUI.Primitives.Async/Internals/TaskResultWitnessAsyncBase.cs index 2dd81bf..485be39 100644 --- a/src/ReactiveUI.Primitives.Async/Internals/TaskObserverAsyncBase.cs +++ b/src/ReactiveUI.Primitives.Async/Internals/TaskResultWitnessAsyncBase.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,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 TaskObserverAsyncBase(CancellationToken cancellationToken) : ObserverAsync +internal abstract class TaskResultWitnessAsyncBase(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 = (TaskResultWitnessAsyncBase)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 = (TaskResultWitnessAsyncBase)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/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/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..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 AnonymousSignalAsync(subscribeAsync); + : new CallbackSignalAsync(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..bcc75bd 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) : TaskResultWitnessAsyncBase(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..6249eb6 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. + /// A witness 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) + : TaskResultWitnessAsyncBase(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. + /// A witness 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) + : TaskResultWitnessAsyncBase(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..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) { @@ -57,14 +57,14 @@ 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; } - /// 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 6cf7f7f..176b349 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; }); @@ -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. @@ -111,19 +111,19 @@ 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; } - /// 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, @@ -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..094126b 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 / - /// OnCompleted forwarding lives in . + /// 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; @@ -169,34 +169,34 @@ internal sealed class CombineLatestSubscription : CombineLatestSubscriptionBase< private readonly Func _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; @@ -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..705bffd 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 / - /// OnCompleted forwarding lives in . + /// 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; @@ -178,37 +178,37 @@ internal sealed class CombineLatestSubscription : CombineLatestSubscriptionBase< private readonly Func _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; @@ -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..570b4ef 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 / - /// OnCompleted forwarding lives in . + /// 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; @@ -187,40 +187,40 @@ internal sealed class CombineLatestSubscription : CombineLatestSubscriptionBase< private readonly Func _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; @@ -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..a1913dd 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 / - /// OnCompleted forwarding lives in . + /// 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; @@ -196,43 +196,43 @@ internal sealed class CombineLatestSubscription : CombineLatestSubscriptionBase< private readonly Func _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; @@ -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..68e096f 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 / - /// OnCompleted forwarding lives in . + /// 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; @@ -205,46 +205,46 @@ internal sealed class CombineLatestSubscription : CombineLatestSubscriptionBase< private readonly Func _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; @@ -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..59e3cdc 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 / - /// OnCompleted forwarding lives in . + /// 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; @@ -214,49 +214,49 @@ internal sealed class CombineLatestSubscription : CombineLatestSubscriptionBase< private readonly Func _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; @@ -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..03d4951 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 / - /// OnCompleted forwarding lives in . + /// 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; @@ -223,52 +223,52 @@ internal sealed class CombineLatestSubscription : CombineLatestSubscriptionBase< private readonly Func _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; @@ -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..a863882 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 / - /// OnCompleted forwarding lives in . + /// 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; @@ -97,10 +97,10 @@ internal sealed class CombineLatestSubscription : CombineLatestSubscriptionBase< private readonly Func _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; @@ -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..71afc23 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 / - /// OnCompleted forwarding lives in . + /// 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; @@ -106,13 +106,13 @@ internal sealed class CombineLatestSubscription : CombineLatestSubscriptionBase< private readonly Func _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; @@ -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..2127b4d 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 / - /// OnCompleted forwarding lives in . + /// 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; @@ -115,16 +115,16 @@ internal sealed class CombineLatestSubscription : CombineLatestSubscriptionBase< private readonly Func _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; @@ -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..aabea74 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 / - /// OnCompleted forwarding lives in . + /// 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; @@ -124,19 +124,19 @@ internal sealed class CombineLatestSubscription : CombineLatestSubscriptionBase< private readonly Func _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; @@ -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..d286fd7 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 / - /// OnCompleted forwarding lives in . + /// 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,22 +133,22 @@ internal sealed class CombineLatestSubscription : CombineLatestSubscriptionBase< private readonly Func _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; @@ -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..b922eac 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 / - /// OnCompleted forwarding lives in . + /// 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; @@ -142,25 +142,25 @@ internal sealed class CombineLatestSubscription : CombineLatestSubscriptionBase< private readonly Func _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; @@ -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..cbfb18a 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 / - /// OnCompleted forwarding lives in . + /// 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; @@ -151,28 +151,28 @@ internal sealed class CombineLatestSubscription : CombineLatestSubscriptionBase< private readonly Func _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; @@ -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..3e0fad2 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 / - /// OnCompleted forwarding lives in . + /// 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; @@ -160,31 +160,31 @@ internal sealed class CombineLatestSubscription : CombineLatestSubscriptionBase< private readonly Func _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; @@ -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..09bbe3f 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/CombineLatestEnumerable.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/CombineLatestEnumerable.cs @@ -91,19 +91,19 @@ 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); } /// - /// 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 subscription. + /// The parent coordinator. /// The source index. - internal sealed class IndexedObserver(Subscription parent, int index) : IObserverAsync + internal sealed class IndexedWitness(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(); @@ -191,13 +191,13 @@ 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); } } /// - 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..2688a04 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 . + /// A witness 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 . + /// A witness 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..c16c234 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); } /// @@ -75,32 +75,32 @@ 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. /// 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) : TaskResultWitnessAsyncBase(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/ObserveOnAsyncSignal.cs b/src/ReactiveUI.Primitives.Async/Operators/ContextSwitchSignalAsync.cs similarity index 83% rename from src/ReactiveUI.Primitives.Async/Operators/ObserveOnAsyncSignal.cs rename to src/ReactiveUI.Primitives.Async/Operators/ContextSwitchSignalAsync.cs index fbd76fb..f4de676 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/ObserveOnAsyncSignal.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 ObserveOnAsyncSignal( +internal sealed class ContextSwitchSignalAsync( 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 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 ObserveOnObserver(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. @@ -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/CountAsync.cs b/src/ReactiveUI.Primitives.Async/Operators/CountAsync.cs index 988ca71..dc4fc68 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); } /// @@ -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 CountAsyncObserver(Func? predicate, CancellationToken cancellationToken) - : TaskObserverAsyncBase(cancellationToken) + internal sealed class CountTaskWitness(Func? predicate, CancellationToken cancellationToken) + : TaskResultWitnessAsyncBase(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/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 28913d4..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) { @@ -108,15 +108,15 @@ 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; } - /// 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) { @@ -162,16 +162,16 @@ 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; } - /// 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 59d045e..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) { @@ -114,15 +114,15 @@ 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; } - /// 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) { @@ -183,16 +183,16 @@ 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; } - /// 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 bd1dd52..fab81ee 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. + /// A witness 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..2cd3198 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,32 +69,32 @@ 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); } /// - /// 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 FirstAsyncObserver(Func? predicate, CancellationToken cancellationToken) - : TaskObserverAsyncBase(cancellationToken) + internal sealed class FirstTaskWitness(Func? predicate, CancellationToken cancellationToken) + : TaskResultWitnessAsyncBase(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..82def38 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,38 +104,38 @@ 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); } /// - /// 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. /// 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) : TaskResultWitnessAsyncBase(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..ff47722 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. + /// 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 ForEachObserver( + internal sealed class ForEachAsyncTaskWitness( Func onNextAsync, - CancellationToken cancellationToken) : TaskObserverAsyncBase(cancellationToken) + CancellationToken cancellationToken) : TaskResultWitnessAsyncBase(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. + /// 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 ForEachObserverSync(Action onNext, CancellationToken cancellationToken) - : TaskObserverAsyncBase(cancellationToken) + internal sealed class ForEachSyncTaskWitness(Action onNext, CancellationToken cancellationToken) + : TaskResultWitnessAsyncBase(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..350aca0 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 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 0b24891..ca98ffb 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) + : TaskResultWitnessAsyncBase(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..b87d929 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,22 +103,22 @@ 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); } /// - /// 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. /// 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) : TaskResultWitnessAsyncBase(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..8103d78 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); } /// @@ -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 LongCountAsyncObserver(Func? predicate, CancellationToken cancellationToken) - : TaskObserverAsyncBase(cancellationToken) + internal sealed class LongCountTaskWitness(Func? predicate, CancellationToken cancellationToken) + : TaskResultWitnessAsyncBase(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..c95a8a7 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 MergeBranchWitness 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)) { @@ -349,12 +349,12 @@ protected internal async ValueTask CompleteAsync(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 InnerAsyncObserver(MergeSubscription 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. @@ -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 = (MergeBranchWitnessWithPermit)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,28 +446,28 @@ protected internal override async ValueTask SubscribeInnerAsync(IObservableAsync } /// - protected internal override InnerAsyncObserver CreateInnerObserver() => - new InnerAsyncObserverWithSemaphore(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 InnerAsyncObserverWithSemaphore(MergeSubscriptionWithMaxConcurrency parent) - : InnerAsyncObserver(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 CompleteAsync path). Without this guard, - /// would be called multiple times per observer, exceeding maxCount and throwing + /// and from the parent's FinishAsync path). Without this guard, + /// 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; /// - 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 MergeBranchWitness(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)) { @@ -777,23 +777,23 @@ internal async ValueTask CompleteAsync(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 InnerAsyncObserver(MergeEnumerableSubscription parent) : ObserverAsync + internal sealed class MergeBranchWitness(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..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) { @@ -58,14 +58,14 @@ 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; } - /// 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 a1e2586..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) { @@ -38,14 +38,14 @@ 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; } - /// 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) { @@ -104,14 +104,14 @@ 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; } /// 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. @@ -170,14 +170,14 @@ 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; } /// 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) { @@ -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; } @@ -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) { @@ -295,15 +295,15 @@ 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; } - /// 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) { @@ -355,14 +355,14 @@ 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; } /// 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) { @@ -399,14 +399,14 @@ 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; } /// 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) { @@ -443,14 +443,14 @@ 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; } /// 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) { @@ -487,14 +487,14 @@ 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; } /// 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 1c7fd24..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) { @@ -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; } @@ -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) { @@ -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; } @@ -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) { @@ -189,16 +189,16 @@ 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; } - /// 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, @@ -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); } } } @@ -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) { @@ -362,15 +362,15 @@ 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; } - /// 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) { @@ -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; } @@ -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, @@ -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); } } } @@ -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) { @@ -900,14 +900,14 @@ 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; } - /// 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/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..44ae873 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; @@ -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. - internal sealed class RefCountObsever(RefCountSignal parent, IObserverAsync observer) + /// 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. @@ -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..41358c9 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/SingleAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/SingleAsync.cs @@ -88,9 +88,9 @@ 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.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..cf63f8d 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/SingleOrDefaultAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/SingleOrDefaultAsync.cs @@ -130,8 +130,8 @@ 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.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..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 AnonymousObserverAsync(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 AnonymousObserverAsync((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 AnonymousObserverAsync( + var observer = new CallbackWitnessAsync( (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 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 8cc0119..e9f61d7 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 . + /// Witness 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,23 +366,23 @@ 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 . + /// Witness 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. + /// Forwards an element from the inner sequence to the downstream witness. /// /// The element to forward. /// 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..2aed53f 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,8 +395,8 @@ public Subscription(TakeUntilCancellationToken parent, IObserverAsync obse /// A task representing the asynchronous subscribe operation. public async ValueTask SubscribeSourcesAsync(CancellationToken cancellationToken) { - _tokenRegistration = _parent._cancellationToken.Register(OnTokenCanceled); - _subscription = await _parent._source.SubscribeAsync(new TakeUntilSourceObserver(_lifecycle), cancellationToken).ConfigureAwait(false); + _tokenRegistration = _parent._cancellationToken.Register(CompleteFromCancellation); + _subscription = await _parent._source.SubscribeAsync(new TakeUntilSourceWitness(_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,8 +500,8 @@ public Subscription(TakeUntilFromRawSignal parent, IObserverAsync observer /// A task representing the asynchronous subscribe operation. public async ValueTask SubscribeSourcesAsync(CancellationToken cancellationToken) { - WaitAndComplete(); - _subscription = await _parent._source.SubscribeAsync(new TakeUntilSourceObserver(_lifecycle), cancellationToken).ConfigureAwait(false); + AwaitStopThenComplete(); + _subscription = await _parent._source.SubscribeAsync(new TakeUntilSourceWitness(_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,8 +644,8 @@ public Subscription(TakeUntilTask parent, IObserverAsync observer) public async ValueTask SubscribeSourcesAsync(CancellationToken cancellationToken) { var task = _parent._task; - WaitAndComplete(task); - _subscription = await _parent._source.SubscribeAsync(new TakeUntilSourceObserver(_lifecycle), cancellationToken).ConfigureAwait(false); + AwaitStopThenComplete(task); + _subscription = await _parent._source.SubscribeAsync(new TakeUntilSourceWitness(_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,11 +760,11 @@ 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 = - 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; @@ -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..63a93a6 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); } /// @@ -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. @@ -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) + : TaskResultWitnessAsyncBase>(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..a25bbb2 100644 --- a/src/ReactiveUI.Primitives.Async/Operators/ToListAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Operators/ToListAsync.cs @@ -35,18 +35,18 @@ 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); } /// - /// 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 ToListAsyncObserver(CancellationToken cancellationToken) - : TaskObserverAsyncBase>(cancellationToken) + internal sealed class ToListTaskWitness(CancellationToken cancellationToken) + : TaskResultWitnessAsyncBase>(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..de928bd 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) + : TaskResultWitnessAsyncBase(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 86% rename from src/ReactiveUI.Primitives.Async/Operators/ObserveOn.cs rename to src/ReactiveUI.Primitives.Async/Operators/WitnessOn.cs index 0a76be3..35df3f8 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 ContextSwitchSignalAsync(@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 ContextSwitchSignalAsync(@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 ContextSwitchSignalAsync(@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 ContextSwitchSignalAsync(@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/Wrap.cs b/src/ReactiveUI.Primitives.Async/Operators/Wrap.cs index 72aff30..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 WrappedObserverAsync(observer); + : new RelayWitnessAsync(observer); } diff --git a/src/ReactiveUI.Primitives.Async/Operators/Yield.cs b/src/ReactiveUI.Primitives.Async/Operators/Yield.cs index ba7737f..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 ObserveOnAsyncSignal.ObserveOnObserver(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 67c678d..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; @@ -24,7 +25,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. @@ -70,24 +71,11 @@ 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 { ImmutableArray> observers; - using (await _gate.LockAsync(token).ConfigureAwait(false)) + using (await _gate.EnterAsync(token).ConfigureAwait(false)) { if (_result is not null) { @@ -123,22 +111,11 @@ 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 { ImmutableArray> observers; - using (await _gate.LockAsync(token).ConfigureAwait(false)) + using (await _gate.EnterAsync(token).ConfigureAwait(false)) { if (_result is not null) { @@ -167,7 +144,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) { @@ -252,41 +229,123 @@ 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.LockAsync(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 ObserverLease(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 => - { - using (await state.signal._gate.LockAsync(state.token).ConfigureAwait(false)) - { - state.signal._observers = state.signal._observers.Remove(state.observer); - } - }); + /// + /// 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. + /// 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) + { + return; + } + + try + { + await RemoveObserverCoreAsync(observer).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // The signal was disposed while removal was waiting to enter the gate. + } + catch (ObjectDisposedException) + { + // 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 observer from a replay signal when disposed. + /// + /// 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 + { + /// + /// 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 5d9df2a..f2b2b75 100644 --- a/src/ReactiveUI.Primitives.Async/Signals/Base/BaseStatelessReplayLatestSignalAsync.cs +++ b/src/ReactiveUI.Primitives.Async/Signals/Base/BaseStatelessReplayLatestSignalAsync.cs @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for full license information. using System.Collections.Immutable; -using ReactiveUI.Primitives.Async.Disposables; +using System.Diagnostics.CodeAnalysis; using ReactiveUI.Primitives.Async.Internals; using ReactiveUI.Primitives.Internal; @@ -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. @@ -73,25 +73,11 @@ 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 { ImmutableArray> observers; - using (await _gate.LockAsync(token).ConfigureAwait(false)) + using (await _gate.EnterAsync(token).ConfigureAwait(false)) { _value = new(value); observers = _observers; @@ -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.LockAsync(token).ConfigureAwait(false)) + await OnErrorResumeAsyncCore(observers, error, token).ConfigureAwait(false); + } + finally { - observers = _observers; + linkedCts?.Dispose(); } - - await OnErrorResumeAsyncCore(observers, error, token).ConfigureAwait(false); } /// @@ -142,7 +132,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 = []; @@ -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.LockAsync(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.LockAsync(token).ConfigureAwait(false)) + return new ObserverLease(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,93 @@ 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. + /// 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) + { + return; + } + + try + { + await RemoveObserverAndResetCoreAsync(observer).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // The signal was disposed while removal was waiting to enter the gate. + } + catch (ObjectDisposedException) + { + // 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 an observer from a stateless replay signal when disposed. + /// + /// 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 observer. + /// + private int _disposed; + + /// + public ValueTask DisposeAsync() + { + return Interlocked.Exchange(ref _disposed, 1) != 0 + ? default + : signal.RemoveObserverAndResetAsync(observer); + } + } } 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..f902eb6 100644 --- a/src/ReactiveUI.Primitives/SignalOperatorParityMixins.cs +++ b/src/ReactiveUI.Primitives/SignalOperatorParityMixins.cs @@ -501,6 +501,40 @@ public static IObservable FlatMap( return new FlatMapResultSignal(source, collectionSelector, resultSelector); } + /// + /// 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 FlatMapValues(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)); + } + /// /// Counts the source values as an . /// diff --git a/src/ReactiveUI.Primitives/Signals/Signal{Collect}.cs b/src/ReactiveUI.Primitives/Signals/Signal{Collect}.cs new file mode 100644 index 0000000..b9c63c8 --- /dev/null +++ b/src/ReactiveUI.Primitives/Signals/Signal{Collect}.cs @@ -0,0 +1,227 @@ +// 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. + [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 { Length: > 0 }) + { + _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/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/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[] { 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/AsyncRenameCoverageTests.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/AsyncRenameCoverageTests.cs new file mode 100644 index 0000000..c74c7c4 --- /dev/null +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/AsyncRenameCoverageTests.cs @@ -0,0 +1,331 @@ +// 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.Async.Internals; +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 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( + () => + { + sameInSequencer = sequencerContext.IsSameAsCurrentAsyncContext(); + 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.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. + /// + /// 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; + } + + /// + /// 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. + /// + 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/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..6d99d16 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/CombineLatestEnumerableInternalsTests.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/CombineLatestEnumerableInternalsTests.cs @@ -19,11 +19,11 @@ 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]); - 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/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..ad9d140 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 CallbackWitnessAsync((_, _) => 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 CallbackWitnessAsync((_, _) => 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 CallbackWitnessAsync( (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 CallbackWitnessAsync( (_, _) => 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 CallbackWitnessAsync((_, _) => 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 CallbackWitnessAsync((_, _) => 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..e95cdb9 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 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 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 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 AnonymousObserverAsync(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 AnonymousObserverAsync(static (_, _) => default, null, MakeCompletedSlow(a)), - new AnonymousObserverAsync(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 AnonymousObserverAsync MakeSync(IntCapture capture) => + private static CallbackWitnessAsync 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 CallbackWitnessAsync 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..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 AnonymousObserverAsync((x, _) => + var observer = new CallbackWitnessAsync((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..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); @@ -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/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 7c43d52..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. @@ -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,33 @@ 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); + } + + /// 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); @@ -92,7 +118,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 +138,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 +156,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 +173,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 ContextSwitchSignalAsync.ContextSwitchWitness(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 ContextSwitchSignalAsync.ContextSwitchWitness(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 ContextSwitchSignalAsync.ContextSwitchWitness(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/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/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. 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..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 OtherObserver.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); @@ -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..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 FirstAsyncObserver.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 2560c6a..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 DoSyncObserver'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() @@ -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.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); } 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..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) { } @@ -1087,17 +1088,22 @@ namespace ReactiveUI.Primitives.Signals 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 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) { } 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) { } 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) { } @@ -1116,6 +1122,9 @@ namespace ReactiveUI.Primitives.Signals 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) { } @@ -1158,6 +1167,7 @@ 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.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..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) { } @@ -1087,17 +1088,22 @@ namespace ReactiveUI.Primitives.Signals 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 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) { } 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) { } 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) { } @@ -1116,6 +1122,9 @@ namespace ReactiveUI.Primitives.Signals 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) { } @@ -1158,6 +1167,7 @@ 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.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..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) { } @@ -1087,17 +1088,22 @@ namespace ReactiveUI.Primitives.Signals 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 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) { } 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) { } 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) { } @@ -1116,6 +1122,9 @@ namespace ReactiveUI.Primitives.Signals 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) { } @@ -1158,6 +1167,7 @@ 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.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/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 c3ba79a..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); } /// @@ -778,7 +789,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!)); @@ -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()); 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)); + } +}