From f7509af1ef450ac527df69721ec89fc324ee1aeb Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Mon, 25 May 2026 17:15:54 +0100 Subject: [PATCH] Improve sequencers, scheduling, and signal APIs Fix generator bridge emission and refactor primitives across concurrency and signal code. Key changes: - Generators: add marker sources and only emit bridge source when corresponding R3/System.Reactive types are referenced. - Sequencers: introduce shared Now via TimeProvider (when available), add thread-static state and helpers to CurrentThreadSequencer, unify Now usage in ImmediateSequencer, add default capacity for SequencerQueue. - Scheduling/runtime: overhaul ScheduledItem (IComparable impl, safer disposal, use RuntimeHelpers.GetHashCode), add ScheduledItem fields, and refactor recursive scheduling in Sequencer.Simple into a RecursiveScheduleState helper with proper null checks. - Threading: add guards and comments to ThreadPoolSequencer, adjust TaskPoolSequencer MultipleDisposable usage. - Virtual time: improve VirtualTimeSequencerBase/VirtualTimeSequencer with clearer control flow, stopwatch backed by virtual clock, and small API fixes. - Connectable signals: add XML-docs, parameter/argument checks, AutoConnect convenience overload, and internal gate types (RefCountGate/AutoConnectGate) to manage connection lifetimes safely. - Broadcaster: make struct equatable and add clearer observer management helpers. - Misc: numerous documentation comments, small API surface improvements, and defensive null/argument checks across tests and primitives. These changes improve correctness, safety, and readability of scheduling and signal primitives while ensuring generators only emit bridge code when appropriate. --- .../R3BridgeGenerator.cs | 12 +- .../SystemReactiveBridgeGenerator.cs | 12 +- .../Concurrency/CurrentThreadSequencer.cs | 49 +- .../Concurrency/DispatcherSequencer.cs | 2 +- .../Concurrency/IScheduledItem.cs | 2 +- .../Concurrency/ImmediateSequencer.cs | 8 +- .../Concurrency/ScheduledItem.cs | 60 +- .../ScheduledItem{TAbsolute,TValue}.cs | 11 + .../Concurrency/Sequencer.Simple.cs | 153 +++- .../Concurrency/Sequencer.cs | 11 +- .../Concurrency/SequencerQueue.cs | 10 +- .../Concurrency/TaskPoolSequencer.cs | 5 +- .../Concurrency/ThreadPoolSequencer.cs | 13 +- ...lTimeSequencerBase{TAbsolute,TRelative}.cs | 74 +- .../VirtualTimeSequencerExtensions.cs | 5 + ...rtualTimeSequencer{TAbsolute,TRelative}.cs | 3 + .../ConnectableSignal{T}.cs | 178 ++++- .../Core/Broadcaster{T}.cs | 48 +- .../Core/DisposedWitness{T}.cs | 13 + .../Core/EmptyWitness{T}.cs | 52 ++ .../Core/IObserver{TValue,TResult}.cs | 6 +- .../Core/IRequireCurrentThread.cs | 2 +- .../Core/ImmutableList{T}.cs | 36 +- .../Core/ListWitness{T}.cs | 27 + src/ReactiveUI.Primitives/Core/Moment{T}.cs | 2 +- .../Core/PriorityQueue.cs | 133 +++- src/ReactiveUI.Primitives/Core/Spark.cs | 30 +- src/ReactiveUI.Primitives/Core/Spark{T}.cs | 94 ++- .../Core/ThrowWitness{T}.cs | 13 + .../Core/TimeInterval{T}.cs | 2 +- src/ReactiveUI.Primitives/Core/Witness.cs | 57 ++ .../Disposables/AssignmentSlot.cs | 20 +- .../Disposables/CancellationDisposable.cs | 22 +- .../Disposables/Disposable.cs | 7 + .../Disposables/MultipleDisposable.cs | 68 +- .../Disposables/SingleDisposable.cs | 36 +- .../SingleReplaceableDisposable.cs | 44 +- src/ReactiveUI.Primitives/Disposables/Slot.cs | 20 +- src/ReactiveUI.Primitives/ExceptionMixins.cs | 7 + src/ReactiveUI.Primitives/Handle.cs | 20 + src/ReactiveUI.Primitives/Handle{T1,T2,T3}.cs | 17 +- src/ReactiveUI.Primitives/Handle{T1,T2}.cs | 16 +- src/ReactiveUI.Primitives/Handle{T}.cs | 19 +- .../LinqMixins.OperatorGate.cs | 22 + src/ReactiveUI.Primitives/LinqMixins.cs | 10 +- .../Signal/AsyncSignal{T}.cs | 150 +++- .../Signal/BehaviourSignal{T}.cs | 83 +- .../Signal/BufferSignal{T,TResult}.cs | 74 +- .../Signal/CommandSignal{TResult}.cs | 83 +- .../Signal/ISignal{T}.cs | 4 +- .../Signal/KeepSignal{T}.cs | 79 +- .../Signal/MapSignal{TSource,TResult}.cs | 74 +- .../Signal/ReadOnlyState{T}.cs | 27 +- .../Signal/ReplaySignal{T}.cs | 136 +++- src/ReactiveUI.Primitives/Signal/Signal{T}.cs | 188 ++++- .../Signal/TaskSignal.cs | 40 +- .../Signal/TaskSignal{T}.cs | 48 +- .../SignalOperatorMixins.Coordinators.cs | 687 +++++++++++++++++ .../SignalOperatorMixins.cs | 418 ++++------- .../SignalOperatorParityMixins.cs | 707 +++++++++++++----- .../Signals/Core/CatchSignal{T,TException}.cs | 89 ++- .../Signals/Core/CatchSignal{T}.cs | 102 ++- .../Signals/Core/CreateSafeSignal{T}.cs | 45 +- .../Signals/Core/CreateSignal{T,TState}.cs | 52 +- .../Signals/Core/CreateSignal{T}.cs | 45 +- .../Signals/Core/DeferSignal{T}.cs | 40 +- .../Signals/Core/EmptySignal{T}.cs | 40 +- .../Signals/Core/FinallySignal{T}.cs | 55 +- .../Signals/Core/IInlineSignal{T}.cs | 11 + .../Signals/Core/ImmediateReturnSignal{T}.cs | 29 +- .../Signals/Core/ImmutableEmptySignal{T}.cs | 28 + .../Signals/Core/ImmutableNeverSignal{T}.cs | 20 +- .../Core/ImmutableReturnFalseSignal.cs | 29 +- .../Core/ImmutableReturnInt32Signal.cs | 79 +- .../Core/ImmutableReturnRxVoidSignal.cs | 29 +- .../Signals/Core/ImmutableReturnTrueSignal.cs | 31 +- .../Signals/Core/RangeSignal.cs | 33 +- .../Signals/Core/RepeatSignal{T}.cs | 34 +- .../Signals/Core/ReturnSignal{T}.cs | 45 +- .../Signals/Core/SignalsBase{T}.cs | 28 +- .../Signals/Core/ThrowSignal{T}.cs | 45 +- .../Core/WitnessBase{TSource,TResult}.cs | 63 +- .../Signals/Core/WitnessOnSignal{T}.cs | 231 +++++- .../Signals/Signal{Catch}.cs | 4 +- .../Signals/Signal{Empty}.cs | 2 + .../Signals/Signal{Factories}.cs | 436 +++++++---- .../Signals/Signal{FromTask}.cs | 400 ++++++---- .../Signals/Signal{GetAwaiter}.cs | 22 + .../Signals/Signal{Never}.cs | 2 + .../Signals/Signal{Throw}.cs | 2 + src/ReactiveUI.Primitives/SubscribeMixins.cs | 27 +- .../Program.cs | 10 +- .../ReactiveUI.Primitives.Tests/Assert.cs | 62 +- .../AsyncSignalTests.cs | 2 +- .../BehaviourSignalTests.cs | 2 +- .../ConcurencyTests.cs | 2 +- .../CoreRuntimeContractTests.cs | 2 +- .../CoverageCompletionTests.cs | 67 +- .../DisposableTests.cs | 2 +- .../DummyDisposable.cs | 2 +- .../FactoryOperatorContractTests.cs | 52 +- .../ReplaySignalTests.cs | 2 +- .../SignalCreateTests.cs | 12 +- .../SignalFromTaskTest.cs | 10 +- .../SignalTests.cs | 9 +- .../StatefulSharingAndBridgeContractTests.cs | 16 +- .../TestClasses/EmptySequencer.cs | 2 +- 107 files changed, 5291 insertions(+), 1310 deletions(-) create mode 100644 src/ReactiveUI.Primitives/LinqMixins.OperatorGate.cs create mode 100644 src/ReactiveUI.Primitives/SignalOperatorMixins.Coordinators.cs diff --git a/src/ReactiveUI.Primitives.R3Bridge.Generator/R3BridgeGenerator.cs b/src/ReactiveUI.Primitives.R3Bridge.Generator/R3BridgeGenerator.cs index 350f9f8..9a09610 100644 --- a/src/ReactiveUI.Primitives.R3Bridge.Generator/R3BridgeGenerator.cs +++ b/src/ReactiveUI.Primitives.R3Bridge.Generator/R3BridgeGenerator.cs @@ -14,6 +14,9 @@ namespace ReactiveUI.Primitives.R3Bridge.Generator; [Generator(LanguageNames.CSharp)] public sealed class R3BridgeGenerator : IIncrementalGenerator { + /// + /// Attribute source emitted during post initialization so consumers can identify generated bridge output. + /// private const string MarkerSource = """ // namespace ReactiveUI.Primitives.R3Bridge.Generated; @@ -27,6 +30,9 @@ internal sealed class PrimitivesR3BridgeGeneratedAttribute : global::System.Attr } """; + /// + /// Bridge extension source emitted when the consumer compilation references R3. + /// private const string BridgeSource = """ // #nullable enable @@ -75,10 +81,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.RegisterSourceOutput(context.CompilationProvider, static (output, compilation) => { - if (compilation.GetTypeByMetadataName("R3.Observable`1") != null) + if (compilation.GetTypeByMetadataName("R3.Observable`1") == null) { - output.AddSource("R3SignalBridge.g.cs", SourceText.From(BridgeSource, Encoding.UTF8)); + return; } + + output.AddSource("R3SignalBridge.g.cs", SourceText.From(BridgeSource, Encoding.UTF8)); }); } } diff --git a/src/ReactiveUI.Primitives.SystemReactiveBridge.Generator/SystemReactiveBridgeGenerator.cs b/src/ReactiveUI.Primitives.SystemReactiveBridge.Generator/SystemReactiveBridgeGenerator.cs index b2d002c..3ac235c 100644 --- a/src/ReactiveUI.Primitives.SystemReactiveBridge.Generator/SystemReactiveBridgeGenerator.cs +++ b/src/ReactiveUI.Primitives.SystemReactiveBridge.Generator/SystemReactiveBridgeGenerator.cs @@ -14,6 +14,9 @@ namespace ReactiveUI.Primitives.SystemReactiveBridge.Generator; [Generator(LanguageNames.CSharp)] public sealed class SystemReactiveBridgeGenerator : IIncrementalGenerator { + /// + /// Attribute source emitted during post initialization so consumers can identify generated bridge output. + /// private const string MarkerSource = """ // namespace ReactiveUI.Primitives.SystemReactiveBridge.Generated; @@ -27,6 +30,9 @@ internal sealed class PrimitivesSystemReactiveBridgeGeneratedAttribute : global: } """; + /// + /// Bridge extension source emitted when the consumer compilation references System.Reactive. + /// private const string BridgeSource = """ // #nullable enable @@ -75,10 +81,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.RegisterSourceOutput(context.CompilationProvider, static (output, compilation) => { - if (compilation.GetTypeByMetadataName("System.Reactive.Linq.Observable") != null) + if (compilation.GetTypeByMetadataName("System.Reactive.Linq.Observable") == null) { - output.AddSource("SystemReactiveSignalBridge.g.cs", SourceText.From(BridgeSource, Encoding.UTF8)); + return; } + + output.AddSource("SystemReactiveSignalBridge.g.cs", SourceText.From(BridgeSource, Encoding.UTF8)); }); } } diff --git a/src/ReactiveUI.Primitives/Concurrency/CurrentThreadSequencer.cs b/src/ReactiveUI.Primitives/Concurrency/CurrentThreadSequencer.cs index 917efa0..2bb0ef9 100644 --- a/src/ReactiveUI.Primitives/Concurrency/CurrentThreadSequencer.cs +++ b/src/ReactiveUI.Primitives/Concurrency/CurrentThreadSequencer.cs @@ -14,17 +14,32 @@ namespace ReactiveUI.Primitives.Concurrency; [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public sealed class CurrentThreadSequencer : ISequencer { + /// + /// Singleton holder for the current-thread sequencer. + /// private static readonly Lazy StaticInstance = new(() => new CurrentThreadSequencer()); + /// + /// Tracks whether the current thread is running scheduled work. + /// [ThreadStatic] private static bool _running; + /// + /// Holds recursive work queued for the current thread. + /// [ThreadStatic] private static SequencerQueue? _threadLocalQueue; + /// + /// Measures relative due times for the current thread. + /// [ThreadStatic] private static Stopwatch? clock; + /// + /// Initializes a new instance of the class. + /// private CurrentThreadSequencer() { } @@ -45,8 +60,11 @@ private CurrentThreadSequencer() /// /// Gets the scheduler's notion of current time. /// - public DateTimeOffset Now => DateTimeOffset.UtcNow; + public DateTimeOffset Now => Sequencer.Now; + /// + /// Gets elapsed time on the current thread. + /// private static TimeSpan Time { get @@ -100,7 +118,7 @@ public IDisposable Schedule(TState state, TimeSpan dueTime, Func TimeSpan.Zero) { @@ -116,7 +134,7 @@ public IDisposable Schedule(TState state, TimeSpan dueTime, Func(TState state, TimeSpan dueTime, Func(TState state, DateTimeOffset dueTime, Func + /// Gets the queued recursive work for the current thread. + /// + /// The current thread queue, if one exists. private static SequencerQueue? GetQueue() => _threadLocalQueue; + /// + /// Sets the queued recursive work for the current thread. + /// + /// The queue to assign. private static void SetQueue(SequencerQueue? newQueue) => _threadLocalQueue = newQueue; + /// + /// Sets the current-thread running marker. + /// + /// Value indicating whether work is running. + private static void SetRunning(bool running) => _running = running; + + /// + /// Runs queued current-thread work. + /// private static class Trampoline { + /// + /// Runs all work currently in the queue. + /// + /// Queue to drain. public static void Run(SequencerQueue queue) { while (queue.Count > 0) diff --git a/src/ReactiveUI.Primitives/Concurrency/DispatcherSequencer.cs b/src/ReactiveUI.Primitives/Concurrency/DispatcherSequencer.cs index 483e447..e26e8b1 100644 --- a/src/ReactiveUI.Primitives/Concurrency/DispatcherSequencer.cs +++ b/src/ReactiveUI.Primitives/Concurrency/DispatcherSequencer.cs @@ -89,7 +89,7 @@ public IDisposable Schedule(TState state, TimeSpan dueTime, Func + timer.Tick += (_, _) => { timer?.Stop(); timer = null; diff --git a/src/ReactiveUI.Primitives/Concurrency/IScheduledItem.cs b/src/ReactiveUI.Primitives/Concurrency/IScheduledItem.cs index 4e6c862..3a359f8 100644 --- a/src/ReactiveUI.Primitives/Concurrency/IScheduledItem.cs +++ b/src/ReactiveUI.Primitives/Concurrency/IScheduledItem.cs @@ -8,7 +8,7 @@ namespace ReactiveUI.Primitives.Concurrency; /// Represents a work item that has been scheduled. /// /// Absolute time representation type. -public interface IScheduledItem +public interface IScheduledItem { /// /// Gets the absolute time at which the item is due for invocation. diff --git a/src/ReactiveUI.Primitives/Concurrency/ImmediateSequencer.cs b/src/ReactiveUI.Primitives/Concurrency/ImmediateSequencer.cs index fb00ce3..38435ea 100644 --- a/src/ReactiveUI.Primitives/Concurrency/ImmediateSequencer.cs +++ b/src/ReactiveUI.Primitives/Concurrency/ImmediateSequencer.cs @@ -11,8 +11,14 @@ namespace ReactiveUI.Primitives.Concurrency; [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public sealed class ImmediateSequencer : ISequencer { + /// + /// Singleton holder for the immediate sequencer. + /// private static readonly Lazy StaticInstance = new(static () => new ImmediateSequencer()); + /// + /// Initializes a new instance of the class. + /// private ImmediateSequencer() { } @@ -25,7 +31,7 @@ private ImmediateSequencer() /// /// Gets the scheduler's notion of current time. /// - public DateTimeOffset Now => DateTimeOffset.UtcNow; + public DateTimeOffset Now => Sequencer.Now; /// /// Schedules the specified state. diff --git a/src/ReactiveUI.Primitives/Concurrency/ScheduledItem.cs b/src/ReactiveUI.Primitives/Concurrency/ScheduledItem.cs index e89bde5..4499e45 100644 --- a/src/ReactiveUI.Primitives/Concurrency/ScheduledItem.cs +++ b/src/ReactiveUI.Primitives/Concurrency/ScheduledItem.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.Runtime.CompilerServices; using ReactiveUI.Primitives.Disposables; namespace ReactiveUI.Primitives.Concurrency; @@ -11,11 +12,22 @@ namespace ReactiveUI.Primitives.Concurrency; /// /// Absolute time representation type. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] -public abstract class ScheduledItem : IScheduledItem, IComparable>, IsDisposed +public abstract class ScheduledItem : IScheduledItem, IComparable>, IsDisposed, IComparable where TAbsolute : IComparable { + /// + /// Compares scheduled items by due time. + /// private readonly IComparer _comparer; + + /// + /// Holds the disposable returned by the scheduled action. + /// private IDisposable? _disposable; + + /// + /// Tracks cancellation without taking a lock. + /// private int _isDisposed; /// @@ -106,7 +118,11 @@ protected ScheduledItem(TAbsolute dueTime, IComparer comparer) /// /// Work item to compare the current work item to. /// Relative ordering between this and the specified work item. - /// The inequality operators are overloaded to provide results consistent with the implementation. Equality operators implement traditional reference equality semantics. + /// + /// The inequality operators are overloaded to provide results consistent with the + /// implementation. Equality operators implement traditional + /// reference equality semantics. + /// public int CompareTo(ScheduledItem? other) { // MSDN: By definition, any object compares greater than null, and two null references compare equal to each other. @@ -118,6 +134,26 @@ public int CompareTo(ScheduledItem? other) return _comparer.Compare(DueTime, other.DueTime); } + /// + /// Compares the current instance with another object of the same type and returns an integer that indicates relative ordering. + /// + /// An object to compare with this instance. + /// A value that indicates the relative order of the objects being compared. + public int CompareTo(object? obj) + { + if (obj == null) + { + return 1; + } + + if (obj is ScheduledItem x) + { + return CompareTo(x); + } + + throw new ArgumentException("Object must be a compatible scheduled item.", nameof(obj)); + } + /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// @@ -131,14 +167,18 @@ public void Dispose() /// Determines whether a object is equal to the specified object. /// /// The object to compare to the current object. - /// true if the obj parameter is a object and is equal to the current object; otherwise, false. + /// + /// true if the obj parameter is a + /// object and is equal to the current + /// object; otherwise, false. + /// public override bool Equals(object? obj) => ReferenceEquals(this, obj); /// /// Returns the hash code for the current object. /// /// A 32-bit signed integer hash code. - public override int GetHashCode() => base.GetHashCode(); + public override int GetHashCode() => RuntimeHelpers.GetHashCode(this); /// /// Invokes the work item. @@ -158,10 +198,12 @@ public void Invoke() return; } - if (IsDisposed) + if (!IsDisposed) { - disposable.Dispose(); + return; } + + disposable.Dispose(); } /// @@ -170,10 +212,12 @@ public void Invoke() /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool disposing) { - if (disposing && Interlocked.Exchange(ref _isDisposed, 1) == 0) + if (!disposing || Interlocked.Exchange(ref _isDisposed, 1) != 0) { - Interlocked.Exchange(ref _disposable, Disposable.Empty)?.Dispose(); + return; } + + Interlocked.Exchange(ref _disposable, Disposable.Empty)?.Dispose(); } /// diff --git a/src/ReactiveUI.Primitives/Concurrency/ScheduledItem{TAbsolute,TValue}.cs b/src/ReactiveUI.Primitives/Concurrency/ScheduledItem{TAbsolute,TValue}.cs index 2979d17..f8c1633 100644 --- a/src/ReactiveUI.Primitives/Concurrency/ScheduledItem{TAbsolute,TValue}.cs +++ b/src/ReactiveUI.Primitives/Concurrency/ScheduledItem{TAbsolute,TValue}.cs @@ -12,8 +12,19 @@ namespace ReactiveUI.Primitives.Concurrency; public sealed class ScheduledItem : ScheduledItem where TAbsolute : IComparable { + /// + /// Sequencer passed to the scheduled action. + /// private readonly ISequencer _scheduler; + + /// + /// State passed to the scheduled action. + /// private readonly TValue _state; + + /// + /// Action invoked when the scheduled item runs. + /// private readonly Func _action; /// diff --git a/src/ReactiveUI.Primitives/Concurrency/Sequencer.Simple.cs b/src/ReactiveUI.Primitives/Concurrency/Sequencer.Simple.cs index ae97f23..eb58b87 100644 --- a/src/ReactiveUI.Primitives/Concurrency/Sequencer.Simple.cs +++ b/src/ReactiveUI.Primitives/Concurrency/Sequencer.Simple.cs @@ -102,48 +102,17 @@ public static IDisposable Schedule(this ISequencer scheduler, DateTimeOffset due /// The disposable object used to cancel the scheduled action (best effort). public static IDisposable Schedule(this ISequencer scheduler, Action action) { - // InvokeRec1 - var group = new MultipleDisposable(); - var gate = new object(); - -#pragma warning disable IDE0039 // Use local function - Action? recursiveAction = null; -#pragma warning restore IDE0039 // Use local function - recursiveAction = () => action(() => + if (scheduler == null) { - var isAdded = false; - var isDone = false; - var d = default(IDisposable); - d = scheduler.Schedule(() => - { - lock (gate) - { - if (isAdded) - { - group.Remove(d); - } - else - { - isDone = true; - } - } - - recursiveAction!(); - }); - - lock (gate) - { - if (!isDone) - { - group.Add(d); - isAdded = true; - } - } - }); + throw new ArgumentNullException(nameof(scheduler)); + } - group.Add(scheduler.Schedule(recursiveAction)); + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } - return group; + return new RecursiveScheduleState(scheduler, action).Start(); } /// @@ -187,6 +156,7 @@ public static IDisposable ScheduleAction(this ISequencer scheduler, TSta /// /// Schedules an action to be executed. /// + /// The type of the state. /// Sequencer to execute the action on. /// A state object to be passed to . /// Action to execute. @@ -360,18 +330,123 @@ internal static IDisposable ScheduleAction(this ISequencer scheduler, TS //// return scheduler.ScheduleLongRunning(action, static (a, c) => a(c)); ////} + /// + /// Invokes an action and returns an empty disposable. + /// + /// Action to invoke. + /// An empty disposable. private static IDisposable Invoke(Action action) { action(); return Disposable.Empty; } + /// + /// Invokes a stateful action and returns an empty disposable. + /// + /// The type of the state. + /// Tuple containing the state and action. + /// An empty disposable. private static IDisposable Invoke((TState state, Action action) tuple) { tuple.action(tuple.state); return Disposable.Empty; } + /// + /// Invokes a stateful disposable-returning action. + /// + /// The type of the state. + /// Tuple containing the state and action. + /// The disposable returned by the action. private static IDisposable Invoke((TState state, Func action) tuple) => tuple.action(tuple.state); + + /// + /// Holds state for recursive action scheduling. + /// + private sealed class RecursiveScheduleState : MultipleDisposable + { + /// + /// Sequencer used for recursive scheduling. + /// + private readonly ISequencer _scheduler; + + /// + /// Recursive action supplied by the caller. + /// + private readonly Action _action; + + /// + /// Guards handoff between scheduling and execution. + /// + private readonly object _gate = new(); + + /// + /// Cached delegate used to avoid recreating the recursive action. + /// + private readonly Action _recursiveAction; + + /// + /// Initializes a new instance of the class. + /// + /// Sequencer used for recursive scheduling. + /// Recursive action supplied by the caller. + public RecursiveScheduleState(ISequencer scheduler, Action action) + { + _scheduler = scheduler; + _action = action; + _recursiveAction = RunRecursiveAction; + } + + /// + /// Starts recursive scheduling. + /// + /// The disposable object used to cancel recursive work. + public RecursiveScheduleState Start() + { + Add(_scheduler.Schedule(_recursiveAction)); + return this; + } + + /// + /// Invokes the caller-provided recursive action. + /// + private void RunRecursiveAction() => _action(Reschedule); + + /// + /// Schedules the next recursive action invocation. + /// + private void Reschedule() + { + var isAdded = false; + var isDone = false; + IDisposable? disposable = null; + disposable = _scheduler.Schedule(() => + { + lock (_gate) + { + if (isAdded) + { + Remove(disposable!); + } + else + { + isDone = true; + } + } + + RunRecursiveAction(); + }); + + lock (_gate) + { + if (!isDone) + { + Add(disposable); + isAdded = true; + } + } + } + } } diff --git a/src/ReactiveUI.Primitives/Concurrency/Sequencer.cs b/src/ReactiveUI.Primitives/Concurrency/Sequencer.cs index c608ed5..484eca1 100644 --- a/src/ReactiveUI.Primitives/Concurrency/Sequencer.cs +++ b/src/ReactiveUI.Primitives/Concurrency/Sequencer.cs @@ -24,7 +24,16 @@ public static partial class Sequencer /// public static ISequencer Default => TaskPoolSequencer.Default; - internal static DateTimeOffset Now => DateTime.UtcNow; + /// + /// Gets the shared wall-clock time used by real-time sequencers. + /// +#if NET8_0_OR_GREATER + internal static DateTimeOffset Now => TimeProvider.System.GetUtcNow(); +#else +#pragma warning disable S6354 // TimeProvider is not available on supported .NET Framework target frameworks. + internal static DateTimeOffset Now => DateTimeOffset.UtcNow; +#pragma warning restore S6354 +#endif /// /// Normalizes the specified value to a positive value. diff --git a/src/ReactiveUI.Primitives/Concurrency/SequencerQueue.cs b/src/ReactiveUI.Primitives/Concurrency/SequencerQueue.cs index c7d8c51..e1552af 100644 --- a/src/ReactiveUI.Primitives/Concurrency/SequencerQueue.cs +++ b/src/ReactiveUI.Primitives/Concurrency/SequencerQueue.cs @@ -15,6 +15,14 @@ namespace ReactiveUI.Primitives.Concurrency; public class SequencerQueue where TAbsolute : IComparable { + /// + /// Default initial capacity for scheduler queues. + /// + private const int DefaultCapacity = 1024; + + /// + /// Priority queue storing scheduled work. + /// private readonly PriorityQueue> _queue; /// @@ -22,7 +30,7 @@ public class SequencerQueue /// Creates a new scheduler queue with a default initial capacity. /// public SequencerQueue() - : this(1024) + : this(DefaultCapacity) { } diff --git a/src/ReactiveUI.Primitives/Concurrency/TaskPoolSequencer.cs b/src/ReactiveUI.Primitives/Concurrency/TaskPoolSequencer.cs index 05486de..45e71c4 100644 --- a/src/ReactiveUI.Primitives/Concurrency/TaskPoolSequencer.cs +++ b/src/ReactiveUI.Primitives/Concurrency/TaskPoolSequencer.cs @@ -13,6 +13,9 @@ namespace ReactiveUI.Primitives.Concurrency; [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public sealed class TaskPoolSequencer : ISequencer { + /// + /// Task factory used to schedule asynchronous work. + /// private readonly TaskFactory _taskFactory; /// @@ -95,7 +98,7 @@ public IDisposable Schedule(TState state, TimeSpan dueTime, Func public static readonly ThreadPoolSequencer Instance = new(); + + /// + /// Guards access to outstanding timers. + /// internal static readonly object Gate = new(); - internal static readonly Dictionary Timers = new(); + /// + /// Keeps timers rooted until they fire or are cancelled. + /// + internal static readonly Dictionary Timers = []; + + /// + /// Initializes a new instance of the class. + /// private ThreadPoolSequencer() { } diff --git a/src/ReactiveUI.Primitives/Concurrency/VirtualTimeSequencerBase{TAbsolute,TRelative}.cs b/src/ReactiveUI.Primitives/Concurrency/VirtualTimeSequencerBase{TAbsolute,TRelative}.cs index 1ae7a66..18b7960 100644 --- a/src/ReactiveUI.Primitives/Concurrency/VirtualTimeSequencerBase{TAbsolute,TRelative}.cs +++ b/src/ReactiveUI.Primitives/Concurrency/VirtualTimeSequencerBase{TAbsolute,TRelative}.cs @@ -71,7 +71,11 @@ public TAbsolute Clock /// /// Relative time to advance the scheduler's clock by. /// is negative. - /// The scheduler is already running. VirtualTimeSequencer doesn't support running nested work dispatch loops. To simulate time slippage while running work on the scheduler, use . + /// + /// The scheduler is already running. VirtualTimeSequencer doesn't support running nested + /// work dispatch loops. To simulate time slippage while running work on the scheduler, + /// use . + /// public void AdvanceBy(TRelative time) { var dt = Add(Clock, time); @@ -102,7 +106,11 @@ public void AdvanceBy(TRelative time) /// /// Absolute time to advance the scheduler's clock to. /// is in the past. - /// The scheduler is already running. VirtualTimeSequencer doesn't support running nested work dispatch loops. To simulate time slippage while running work on the scheduler, use . + /// + /// The scheduler is already running. VirtualTimeSequencer doesn't support running nested + /// work dispatch loops. To simulate time slippage while running work on the scheduler, + /// use . + /// public void AdvanceTo(TAbsolute time) { var dueToClock = Comparer.Compare(time, Clock); @@ -266,28 +274,30 @@ public void Sleep(TRelative time) /// public void Start() { - if (!IsEnabled) + if (IsEnabled) { - IsEnabled = true; - do - { - var next = GetNext(); - if (next != null) - { - if (Comparer.Compare(next.DueTime, Clock) > 0) - { - Clock = next.DueTime; - } + return; + } - next.Invoke(); - } - else + IsEnabled = true; + do + { + var next = GetNext(); + if (next != null) + { + if (Comparer.Compare(next.DueTime, Clock) > 0) { - IsEnabled = false; + Clock = next.DueTime; } + + next.Invoke(); + } + else + { + IsEnabled = false; } - while (IsEnabled); } + while (IsEnabled); } /// @@ -331,12 +341,12 @@ public void Stop() /// Object implementing the requested service, if available; null otherwise. protected virtual object? GetService(Type serviceType) { - if (serviceType == typeof(IStopwatchProvider)) + if (serviceType != typeof(IStopwatchProvider)) { - return this; + return null; } - return null; + return this; } /// @@ -353,19 +363,41 @@ public void Stop() /// The corresponding relative time value. protected abstract TRelative ToRelative(TimeSpan timeSpan); + /// + /// Converts the current clock value to a . + /// + /// The current virtual clock as a date-time offset. private DateTimeOffset ClockToDateTimeOffset() => ToDateTimeOffset(Clock); + /// + /// Stopwatch backed by virtual time. + /// private sealed class VirtualTimeStopwatch : IStopwatch { + /// + /// Parent sequencer that owns the virtual clock. + /// private readonly VirtualTimeSequencerBase _parent; + + /// + /// Start time captured when the stopwatch was created. + /// private readonly DateTimeOffset _start; + /// + /// Initializes a new instance of the class. + /// + /// Parent virtual-time sequencer. + /// Start time for elapsed calculations. public VirtualTimeStopwatch(VirtualTimeSequencerBase parent, DateTimeOffset start) { _parent = parent; _start = start; } + /// + /// Gets the elapsed virtual time. + /// public TimeSpan Elapsed => _parent.ClockToDateTimeOffset() - _start; } } diff --git a/src/ReactiveUI.Primitives/Concurrency/VirtualTimeSequencerExtensions.cs b/src/ReactiveUI.Primitives/Concurrency/VirtualTimeSequencerExtensions.cs index 00b32a6..2099765 100644 --- a/src/ReactiveUI.Primitives/Concurrency/VirtualTimeSequencerExtensions.cs +++ b/src/ReactiveUI.Primitives/Concurrency/VirtualTimeSequencerExtensions.cs @@ -68,6 +68,11 @@ public static IDisposable ScheduleAbsolute(this VirtualTim return scheduler.ScheduleAbsolute(action, dueTime, static (_, a) => Invoke(a)); } + /// + /// Invokes an action and returns an empty disposable. + /// + /// Action to invoke. + /// An empty disposable. private static IDisposable Invoke(Action action) { action(); diff --git a/src/ReactiveUI.Primitives/Concurrency/VirtualTimeSequencer{TAbsolute,TRelative}.cs b/src/ReactiveUI.Primitives/Concurrency/VirtualTimeSequencer{TAbsolute,TRelative}.cs index 0644df4..7a3fd79 100644 --- a/src/ReactiveUI.Primitives/Concurrency/VirtualTimeSequencer{TAbsolute,TRelative}.cs +++ b/src/ReactiveUI.Primitives/Concurrency/VirtualTimeSequencer{TAbsolute,TRelative}.cs @@ -13,6 +13,9 @@ namespace ReactiveUI.Primitives.Concurrency; public abstract class VirtualTimeSequencer : VirtualTimeSequencerBase where TAbsolute : IComparable { + /// + /// Queue of scheduled virtual-time work. + /// private readonly SequencerQueue _queue = new(); /// diff --git a/src/ReactiveUI.Primitives/ConnectableSignal{T}.cs b/src/ReactiveUI.Primitives/ConnectableSignal{T}.cs index 8d92b1b..97b2b32 100644 --- a/src/ReactiveUI.Primitives/ConnectableSignal{T}.cs +++ b/src/ReactiveUI.Primitives/ConnectableSignal{T}.cs @@ -16,9 +16,24 @@ namespace ReactiveUI.Primitives; [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public sealed class ConnectableSignal : IObservable { + /// + /// Synchronizes connection state. + /// private readonly object _gate = new(); + + /// + /// Source sequence to connect. + /// private readonly IObservable _source; + + /// + /// Multicast hub that receives source values. + /// private readonly ISignal _hub; + + /// + /// Active source connection. + /// private IDisposable? _connection; /// @@ -70,6 +85,10 @@ public static class ConnectableSignalMixins /// /// Multicasts source values through the supplied hub. /// + /// The value type. + /// Source sequence to multicast. + /// Hub that receives source values. + /// A connectable signal. public static ConnectableSignal Multicast(this IObservable source, ISignal hub) { if (source == null) @@ -88,29 +107,47 @@ public static ConnectableSignal Multicast(this IObservable source, ISig /// /// Publishes source values through a live signal hub. /// + /// The value type. + /// Source sequence to publish. + /// A connectable live signal. public static ConnectableSignal PublishLive(this IObservable source) => source.Multicast(new Signal()); /// /// Replays source values through a bounded replay hub. /// + /// The value type. + /// Source sequence to replay. + /// Maximum number of values to replay. + /// A connectable replay signal. public static ConnectableSignal ReplayLive(this IObservable source, int bufferSize) => source.Multicast(new ReplaySignal(bufferSize)); /// /// Replays source values through a replay hub constrained by count and time. /// + /// The value type. + /// Source sequence to replay. + /// Maximum number of values to replay. + /// Maximum replay window. + /// A connectable replay signal. public static ConnectableSignal ReplayLive(this IObservable source, int bufferSize, TimeSpan window) => source.Multicast(new ReplaySignal(bufferSize, window)); /// /// Shares one live source subscription while at least one observer is subscribed. /// + /// The value type. + /// Source sequence to share. + /// A reference-counted live sequence. public static IObservable ShareLive(this IObservable source) => source.PublishLive().RefCount(); /// /// Connects on first subscriber and disconnects when the last subscriber disposes. /// + /// The value type. + /// Connectable signal to reference count. + /// A reference-counted sequence. public static IObservable RefCount(this ConnectableSignal source) { if (source == null) @@ -122,10 +159,23 @@ public static IObservable RefCount(this ConnectableSignal source) return ReactiveUI.Primitives.Signals.Signal.Create(gate.Subscribe); } + /// + /// Connects on the first observer subscription. + /// + /// The value type. + /// Connectable signal to connect. + /// A sequence that connects after the first subscription. + public static IObservable AutoConnect(this ConnectableSignal source) => + AutoConnect(source, 1); + /// /// Connects after observers have subscribed. /// - public static IObservable AutoConnect(this ConnectableSignal source, int subscriberCount = 1) + /// The value type. + /// Connectable signal to connect. + /// Number of observers required before connecting. + /// A sequence that connects after the requested number of subscriptions. + public static IObservable AutoConnect(this ConnectableSignal source, int subscriberCount) { if (source == null) { @@ -137,37 +187,54 @@ public static IObservable AutoConnect(this ConnectableSignal source, in throw new ArgumentOutOfRangeException(nameof(subscriberCount)); } - var gate = new object(); - var count = 0; - var connected = false; - return ReactiveUI.Primitives.Signals.Signal.Create(observer => - { - var subscription = source.Subscribe(observer); - lock (gate) - { - count++; - if (!connected && count >= subscriberCount) - { - connected = true; - source.Connect(); - } - } - - return subscription; - }); + var gate = AutoConnectGate.For(source, subscriberCount); + return ReactiveUI.Primitives.Signals.Signal.Create(gate.Subscribe); } + /// + /// Tracks reference-counted connection state. + /// + /// The value type. private sealed class RefCountGate { + /// + /// Synchronizes reference-count state. + /// private readonly object _gate = new(); + + /// + /// Connectable signal being reference-counted. + /// private readonly ConnectableSignal _source; + + /// + /// Active subscriber count. + /// private int _count; + + /// + /// Active source connection. + /// private IDisposable? _connection; + /// + /// Initializes a new instance of the class. + /// + /// Connectable signal being reference-counted. private RefCountGate(ConnectableSignal source) => _source = source; + /// + /// Creates a reference-count gate for a connectable signal. + /// + /// Connectable signal being reference-counted. + /// A reference-count gate. public static RefCountGate For(ConnectableSignal source) => new(source); + /// + /// Subscribes an observer and manages the shared connection lifetime. + /// + /// Observer to subscribe. + /// A disposable that removes the observer and may disconnect the source. public IDisposable Subscribe(IObserver observer) { IDisposable subscription; @@ -193,4 +260,77 @@ public IDisposable Subscribe(IObserver observer) }); } } + + /// + /// Tracks auto-connect subscription state. + /// + /// The value type. + private sealed class AutoConnectGate + { + /// + /// Synchronizes auto-connect state. + /// + private readonly object _gate = new(); + + /// + /// Connectable signal being auto-connected. + /// + private readonly ConnectableSignal _source; + + /// + /// Number of observers required before connecting. + /// + private readonly int _subscriberCount; + + /// + /// Current subscriber count. + /// + private int _count; + + /// + /// Value indicating whether the source has connected. + /// + private bool _connected; + + /// + /// Initializes a new instance of the class. + /// + /// Connectable signal being auto-connected. + /// Number of observers required before connecting. + private AutoConnectGate(ConnectableSignal source, int subscriberCount) + { + _source = source; + _subscriberCount = subscriberCount; + } + + /// + /// Creates an auto-connect gate for a connectable signal. + /// + /// Connectable signal being auto-connected. + /// Number of observers required before connecting. + /// An auto-connect gate. + public static AutoConnectGate For(ConnectableSignal source, int subscriberCount) => + new(source, subscriberCount); + + /// + /// Subscribes an observer and connects when the threshold is reached. + /// + /// Observer to subscribe. + /// A disposable that removes the observer subscription. + public IDisposable Subscribe(IObserver observer) + { + var subscription = _source.Subscribe(observer); + lock (_gate) + { + _count++; + if (!_connected && _count >= _subscriberCount) + { + _connected = true; + _source.Connect(); + } + } + + return subscription; + } + } } diff --git a/src/ReactiveUI.Primitives/Core/Broadcaster{T}.cs b/src/ReactiveUI.Primitives/Core/Broadcaster{T}.cs index fcd6afb..3ef0062 100644 --- a/src/ReactiveUI.Primitives/Core/Broadcaster{T}.cs +++ b/src/ReactiveUI.Primitives/Core/Broadcaster{T}.cs @@ -8,12 +8,22 @@ namespace ReactiveUI.Primitives.Core; /// Copy-on-write observer broadcaster optimized for zero-allocation single-subscriber delivery. /// /// The value type. -internal struct Broadcaster +internal struct Broadcaster : IEquatable> { + /// + /// Stores either a single observer, an observer array, or . + /// private object? _observers; + /// + /// Gets a value indicating whether at least one observer is registered. + /// public bool HasObservers => Volatile.Read(ref _observers) is not null; + /// + /// Adds an observer to the broadcaster. + /// + /// Observer to add. public void Add(IObserver observer) { if (_observers is IObserver[] many) @@ -34,8 +44,15 @@ public void Add(IObserver observer) Volatile.Write(ref _observers, observer); } + /// + /// Removes all observers from the broadcaster. + /// public void Clear() => Volatile.Write(ref _observers, null); + /// + /// Removes an observer from the broadcaster. + /// + /// Observer to remove. public void Remove(IObserver observer) { if (ReferenceEquals(_observers, observer)) @@ -75,6 +92,10 @@ public void Remove(IObserver observer) Volatile.Write(ref _observers, copy); } + /// + /// Broadcasts a value to the current observers. + /// + /// Value to broadcast. public void Next(T value) { var snapshot = Volatile.Read(ref _observers); @@ -95,12 +116,16 @@ public void Next(T value) } } - public void Error(Exception error) + /// + /// Broadcasts an error to the current observers. + /// + /// Error to broadcast. + public void Error(Exception exception) { var snapshot = Volatile.Read(ref _observers); if (snapshot is IObserver single) { - single.OnError(error); + single.OnError(exception); return; } @@ -111,10 +136,13 @@ public void Error(Exception error) for (var i = 0; i < many.Length; i++) { - many[i].OnError(error); + many[i].OnError(exception); } } + /// + /// Broadcasts completion to the current observers. + /// public void Completed() { var snapshot = Volatile.Read(ref _observers); @@ -134,4 +162,16 @@ public void Completed() many[i].OnCompleted(); } } + + /// + public readonly bool Equals(Broadcaster other) => + ReferenceEquals(_observers, other._observers); + + /// + public override readonly bool Equals(object? obj) => + obj is Broadcaster other && Equals(other); + + /// + public override readonly int GetHashCode() => + _observers is null ? 0 : System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(_observers); } diff --git a/src/ReactiveUI.Primitives/Core/DisposedWitness{T}.cs b/src/ReactiveUI.Primitives/Core/DisposedWitness{T}.cs index 1d8497a..4dcf9c4 100644 --- a/src/ReactiveUI.Primitives/Core/DisposedWitness{T}.cs +++ b/src/ReactiveUI.Primitives/Core/DisposedWitness{T}.cs @@ -4,18 +4,31 @@ namespace ReactiveUI.Primitives.Core; +/// +/// Observer that rejects every notification because the subscription has already been disposed. +/// +/// The observed value type. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal sealed class DisposedWitness : IObserver { + /// + /// Gets the shared disposed witness instance. + /// public static readonly DisposedWitness Instance = new(); + /// + /// Initializes a new instance of the class. + /// private DisposedWitness() { } + /// public void OnCompleted() => throw new ObjectDisposedException(string.Empty); + /// public void OnError(Exception error) => throw new ObjectDisposedException(string.Empty, error); + /// public void OnNext(T value) => throw new ObjectDisposedException(string.Empty); } diff --git a/src/ReactiveUI.Primitives/Core/EmptyWitness{T}.cs b/src/ReactiveUI.Primitives/Core/EmptyWitness{T}.cs index c5fbbcb..f21da00 100644 --- a/src/ReactiveUI.Primitives/Core/EmptyWitness{T}.cs +++ b/src/ReactiveUI.Primitives/Core/EmptyWitness{T}.cs @@ -6,33 +6,83 @@ namespace ReactiveUI.Primitives.Core; +/// +/// Delegate-backed observer that defaults missing handlers to no-op behavior. +/// +/// The observed value type. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal sealed class EmptyWitness : IObserver { + /// + /// Gets the shared no-op witness instance. + /// public static readonly EmptyWitness Instance = new(_ => { }); + + /// + /// Rethrows observer errors with their original stack information. + /// private static readonly Action rethrow = e => ExceptionDispatchInfo.Capture(e).Throw(); + + /// + /// Completion callback that does nothing. + /// private static readonly Action nop = () => { }; + + /// + /// Error callback that does nothing. + /// private static readonly Action nope = _ => { }; + /// + /// Callback invoked for each value. + /// private readonly Action _onNext; + + /// + /// Callback invoked for an error. + /// private readonly Action _onError; + + /// + /// Callback invoked for completion. + /// private readonly Action _onCompleted; + /// + /// Initializes a new instance of the class. + /// + /// Callback invoked for each value. public EmptyWitness(Action onNext) : this(onNext, rethrow, nop) { } + /// + /// Initializes a new instance of the class. + /// + /// Callback invoked for each value. + /// Callback invoked for an error. public EmptyWitness(Action onNext, Action onError) : this(onNext, onError, nop) { } + /// + /// Initializes a new instance of the class. + /// + /// Callback invoked for each value. + /// Callback invoked for completion. public EmptyWitness(Action onNext, Action onCompleted) : this(onNext, rethrow, onCompleted) { } + /// + /// Initializes a new instance of the class. + /// + /// Callback invoked for each value. + /// Callback invoked for an error. + /// Callback invoked for completion. public EmptyWitness(Action onNext, Action onError, Action onCompleted) { _onNext = onNext; @@ -48,10 +98,12 @@ public EmptyWitness(Action onNext, Action onError, Action onComple /// /// Calls the action implementing . /// + /// Error notification. public void OnError(Exception error) => (_onError ?? nope)(error); /// /// Calls the action implementing . /// + /// Value notification. public void OnNext(T value) => _onNext(value); } diff --git a/src/ReactiveUI.Primitives/Core/IObserver{TValue,TResult}.cs b/src/ReactiveUI.Primitives/Core/IObserver{TValue,TResult}.cs index 9fc9d29..905ba96 100644 --- a/src/ReactiveUI.Primitives/Core/IObserver{TValue,TResult}.cs +++ b/src/ReactiveUI.Primitives/Core/IObserver{TValue,TResult}.cs @@ -9,13 +9,13 @@ namespace ReactiveUI.Primitives.Core; /// /// /// The type of the elements received by the observer. -/// This type parameter is contravariant. That is, you can use either the type you specified or any type that is less derived. For more information about covariance and contravariance, see Covariance and Contravariance in Generics. +/// This type parameter is contravariant. That is, you can use either the type you specified or any type that is less derived. /// /// /// The type of the result returned from the observer's notification handlers. -/// This type parameter is covariant. That is, you can use either the type you specified or any type that is more derived. For more information about covariance and contravariance, see Covariance and Contravariance in Generics. +/// This type parameter is covariant. That is, you can use either the type you specified or any type that is more derived. /// -public interface IObserver +public interface IObserver { /// /// Notifies the observer of a new element in the sequence. diff --git a/src/ReactiveUI.Primitives/Core/IRequireCurrentThread.cs b/src/ReactiveUI.Primitives/Core/IRequireCurrentThread.cs index 6617fb1..1cbc399 100644 --- a/src/ReactiveUI.Primitives/Core/IRequireCurrentThread.cs +++ b/src/ReactiveUI.Primitives/Core/IRequireCurrentThread.cs @@ -8,7 +8,7 @@ namespace ReactiveUI.Primitives.Core; /// IRequireCurrentThread. /// /// The Type. -public interface IRequireCurrentThread : IObservable +public interface IRequireCurrentThread : IObservable { /// /// Determines whether [is required subscribe on current thread]. diff --git a/src/ReactiveUI.Primitives/Core/ImmutableList{T}.cs b/src/ReactiveUI.Primitives/Core/ImmutableList{T}.cs index b4b70b0..64f6575 100644 --- a/src/ReactiveUI.Primitives/Core/ImmutableList{T}.cs +++ b/src/ReactiveUI.Primitives/Core/ImmutableList{T}.cs @@ -4,17 +4,39 @@ namespace ReactiveUI.Primitives.Core; +/// +/// Immutable array-backed list optimized for copy-on-write observer storage. +/// +/// The item type. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] -internal class ImmutableList +internal sealed class ImmutableList { + /// + /// Gets the shared empty list. + /// public static readonly ImmutableList Empty = new(); + /// + /// Initializes a new instance of the class. + /// + /// Items owned by the immutable list. public ImmutableList(T[] data) => Items = data; - private ImmutableList() => Items = new T[0]; + /// + /// Initializes a new instance of the class. + /// + private ImmutableList() => Items = []; + /// + /// Gets the immutable list items. + /// public T[] Items { get; } + /// + /// Returns a new list with the value appended. + /// + /// Value to append. + /// A new immutable list containing the added value. public ImmutableList Add(T value) { var newData = new T[Items.Length + 1]; @@ -23,6 +45,11 @@ public ImmutableList Add(T value) return new ImmutableList(newData); } + /// + /// Returns a new list with the first matching value removed. + /// + /// Value to remove. + /// A new immutable list without the value, or the current list when the value is absent. public ImmutableList Remove(T value) { var i = IndexOf(value); @@ -45,6 +72,11 @@ public ImmutableList Remove(T value) return new ImmutableList(newData); } + /// + /// Finds the first matching value. + /// + /// Value to find. + /// The value index, or -1 when the value is absent. public int IndexOf(T value) { for (var i = 0; i < Items.Length; ++i) diff --git a/src/ReactiveUI.Primitives/Core/ListWitness{T}.cs b/src/ReactiveUI.Primitives/Core/ListWitness{T}.cs index e2b8579..612f848 100644 --- a/src/ReactiveUI.Primitives/Core/ListWitness{T}.cs +++ b/src/ReactiveUI.Primitives/Core/ListWitness{T}.cs @@ -4,14 +4,29 @@ namespace ReactiveUI.Primitives.Core; +/// +/// Observer that forwards notifications to an immutable observer list. +/// +/// The observed value type. internal sealed class ListWitness : IObserver { + /// + /// Immutable observer snapshot. + /// private readonly ImmutableList> _observers; + /// + /// Initializes a new instance of the class. + /// + /// Observers that receive forwarded notifications. public ListWitness(ImmutableList> observers) => _observers = observers; + /// + /// Gets a value indicating whether the list contains observers. + /// public bool HasObservers => _observers.Items.Length > 0; + /// public void OnCompleted() { var targetObservers = _observers.Items; @@ -21,6 +36,7 @@ public void OnCompleted() } } + /// public void OnError(Exception error) { var targetObservers = _observers.Items; @@ -30,6 +46,7 @@ public void OnError(Exception error) } } + /// public void OnNext(T value) { var targetObservers = _observers.Items; @@ -39,8 +56,18 @@ public void OnNext(T value) } } + /// + /// Returns a witness with the observer added. + /// + /// Observer to add. + /// The updated observer list witness. internal IObserver Add(IObserver observer) => new ListWitness(_observers.Add(observer)); + /// + /// Returns a witness with the observer removed. + /// + /// Observer to remove. + /// The updated observer list witness. internal IObserver Remove(IObserver observer) { var i = Array.IndexOf(_observers.Items, observer); diff --git a/src/ReactiveUI.Primitives/Core/Moment{T}.cs b/src/ReactiveUI.Primitives/Core/Moment{T}.cs index e18fc63..cb0224d 100644 --- a/src/ReactiveUI.Primitives/Core/Moment{T}.cs +++ b/src/ReactiveUI.Primitives/Core/Moment{T}.cs @@ -59,7 +59,7 @@ public Moment(T value, DateTimeOffset timestamp) /// public override int GetHashCode() { - var valueHashCode = Value == null ? 1963 : Value.GetHashCode(); + var valueHashCode = Value is null ? 1963 : EqualityComparer.Default.GetHashCode(Value); return Timestamp.GetHashCode() ^ valueHashCode; } diff --git a/src/ReactiveUI.Primitives/Core/PriorityQueue.cs b/src/ReactiveUI.Primitives/Core/PriorityQueue.cs index 74b5020..5b94e95 100644 --- a/src/ReactiveUI.Primitives/Core/PriorityQueue.cs +++ b/src/ReactiveUI.Primitives/Core/PriorityQueue.cs @@ -4,26 +4,76 @@ namespace ReactiveUI.Primitives.Core; +/// +/// Binary heap priority queue that preserves insertion order for equal-priority items. +/// +/// The queued item type. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal sealed class PriorityQueue where T : IComparable { + /// + /// Default queue capacity. + /// + private const int DefaultCapacity = 16; + + /// + /// Number of children per heap node. + /// + private const int HeapBranchingFactor = 2; + + /// + /// Offset from a node's doubled index to its left child. + /// + private const int LeftChildOffset = 1; + + /// + /// Offset from a node's doubled index to its right child. + /// + private const int RightChildOffset = 2; + + /// + /// Capacity divisor used to shrink sparse queues. + /// + private const int ShrinkDivisor = 4; + + /// + /// Monotonic tie-breaker for equal-priority items. + /// private long _count = long.MinValue; + + /// + /// Heap storage. + /// private IndexedItem[] _items; + /// + /// Initializes a new instance of the class. + /// public PriorityQueue() - : this(16) + : this(DefaultCapacity) { } + /// + /// Initializes a new instance of the class. + /// + /// Initial queue capacity. public PriorityQueue(int capacity) { _items = new IndexedItem[capacity]; Count = 0; } + /// + /// Gets the number of queued items. + /// public int Count { get; private set; } + /// + /// Removes and returns the highest-priority item. + /// + /// The highest-priority item. public T Dequeue() { var result = Peek(); @@ -31,12 +81,16 @@ public T Dequeue() return result; } + /// + /// Adds an item to the queue. + /// + /// Item to enqueue. public void Enqueue(T item) { if (Count >= _items.Length) { var temp = _items; - _items = new IndexedItem[_items.Length * 2]; + _items = new IndexedItem[_items.Length * HeapBranchingFactor]; Array.Copy(temp, _items, temp.Length); } @@ -45,6 +99,10 @@ public void Enqueue(T item) Percolate(index); } + /// + /// Returns the highest-priority item without removing it. + /// + /// The highest-priority item. public T Peek() { if (Count == 0) @@ -55,6 +113,11 @@ public T Peek() return _items[0].Value; } + /// + /// Removes a matching item from the queue. + /// + /// Item to remove. + /// when the item was found and removed; otherwise, . public bool Remove(T item) { for (var i = 0; i < Count; ++i) @@ -69,6 +132,10 @@ public bool Remove(T item) return false; } + /// + /// Restores heap order from the supplied index downward. + /// + /// Index to heapify. private void Heapify(int index) { if (index >= Count || index < 0) @@ -78,8 +145,8 @@ private void Heapify(int index) while (true) { - var left = (2 * index) + 1; - var right = (2 * index) + 2; + var left = (HeapBranchingFactor * index) + LeftChildOffset; + var right = (HeapBranchingFactor * index) + RightChildOffset; var first = index; if (left < Count && IsHigherPriority(left, first)) @@ -103,8 +170,19 @@ private void Heapify(int index) } } + /// + /// Determines whether the left index has higher priority than the right index. + /// + /// Candidate item index. + /// Current item index. + /// when the left item should be ordered before the right item. private bool IsHigherPriority(int left, int right) => _items[left].CompareTo(_items[right]) < 0; + /// + /// Restores heap order from the supplied index upward. + /// + /// Index to percolate. + /// The final index of the percolated item. private int Percolate(int index) { if (index >= Count || index < 0) @@ -112,18 +190,22 @@ private int Percolate(int index) return index; } - var parent = (index - 1) / 2; + var parent = (index - 1) / HeapBranchingFactor; while (parent >= 0 && parent != index && IsHigherPriority(index, parent)) { // swap index and parent (_items[parent], _items[index]) = (_items[index], _items[parent]); index = parent; - parent = (index - 1) / 2; + parent = (index - 1) / HeapBranchingFactor; } return index; } + /// + /// Removes the item at the supplied index. + /// + /// Index to remove. private void RemoveAt(int index) { _items[index] = _items[--Count]; @@ -134,19 +216,32 @@ private void RemoveAt(int index) Heapify(index); } - if (Count < _items.Length / 4) + if (Count >= _items.Length / ShrinkDivisor) { - var temp = _items; - _items = new IndexedItem[_items.Length / 2]; - Array.Copy(temp, 0, _items, 0, Count); + return; } + + var temp = _items; + _items = new IndexedItem[_items.Length / HeapBranchingFactor]; + Array.Copy(temp, 0, _items, 0, Count); } - private struct IndexedItem : IComparable + /// + /// Heap item with an insertion-order tie-breaker. + /// + private struct IndexedItem : IComparable, IEquatable { + /// + /// Insertion order id. + /// public long Id; + + /// + /// Queued value. + /// public T Value; + /// public int CompareTo(IndexedItem other) { var c = Value.CompareTo(other.Value); @@ -157,5 +252,21 @@ public int CompareTo(IndexedItem other) return c; } + + /// + public readonly bool Equals(IndexedItem other) => + Id == other.Id && EqualityComparer.Default.Equals(Value, other.Value); + + /// + public override readonly bool Equals(object? obj) => + obj is IndexedItem other && Equals(other); + + /// + public override readonly int GetHashCode() + { + var valueHash = Value is null ? 0 : EqualityComparer.Default.GetHashCode(Value); + + return unchecked((Id.GetHashCode() * 397) ^ valueHash); + } } } diff --git a/src/ReactiveUI.Primitives/Core/Spark.cs b/src/ReactiveUI.Primitives/Core/Spark.cs index 6bb2357..36a611b 100644 --- a/src/ReactiveUI.Primitives/Core/Spark.cs +++ b/src/ReactiveUI.Primitives/Core/Spark.cs @@ -12,7 +12,10 @@ public static class Spark /// /// Creates an object that represents an OnNext spark to an observer. /// - /// The type of the elements received by the observer. Upon dematerialization of the spark into an observable sequence, this type is used as the element type for the sequence. + /// + /// The type of the elements received by the observer. + /// Upon dematerialization of the spark into an observable sequence, this type is used as the element type for the sequence. + /// /// The value contained in the spark. /// The OnNext spark containing the value. public static Spark CreateOnNext(T value) => new Spark.OnNextSpark(value); @@ -20,10 +23,17 @@ public static class Spark /// /// Creates an object that represents an OnError spark to an observer. /// - /// The type of the elements received by the observer. Upon dematerialization of the spark into an observable sequence, this type is used as the element type for the sequence. + /// + /// The type of the elements received by the observer. + /// Upon dematerialization of the spark into an observable sequence, this type is used as the element type for the sequence. + /// /// The exception contained in the spark. /// The OnError spark containing the exception. /// is null. + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Sonar Code Smell", + "S4018:Generic methods should provide type parameters", + Justification = "The type parameter determines the returned spark value type.")] public static Spark CreateOnError(Exception error) { if (error == null) @@ -37,12 +47,26 @@ public static Spark CreateOnError(Exception error) /// /// Creates an object that represents an OnCompleted spark to an observer. /// - /// The type of the elements received by the observer. Upon dematerialization of the spark into an observable sequence, this type is used as the element type for the sequence. + /// + /// The type of the elements received by the observer. + /// Upon dematerialization of the spark into an observable sequence, this type is used as the element type for the sequence. + /// /// The OnCompleted spark. + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Sonar Code Smell", + "S4018:Generic methods should provide type parameters", + Justification = "The type parameter determines the cached completed spark value type.")] public static Spark CreateOnCompleted() => CompletedSparkCache.Instance; + /// + /// Holds the cached completed spark for a value type. + /// + /// The cached spark value type. private static class CompletedSparkCache { + /// + /// Gets the cached completed spark. + /// public static readonly Spark Instance = new Spark.OnCompletedSpark(); } } diff --git a/src/ReactiveUI.Primitives/Core/Spark{T}.cs b/src/ReactiveUI.Primitives/Core/Spark{T}.cs index 76d2bd3..e70018f 100644 --- a/src/ReactiveUI.Primitives/Core/Spark{T}.cs +++ b/src/ReactiveUI.Primitives/Core/Spark{T}.cs @@ -14,18 +14,14 @@ namespace ReactiveUI.Primitives.Core /// /// The type of the elements received by the observer. [Serializable] -#pragma warning disable CS0659 // Type overrides Object.Equals(object o) but does not override Object.GetHashCode() -#pragma warning disable CS0661 // Type defines operator == or operator != but does not override Object.GetHashCode() [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public abstract class Spark : IEquatable> -#pragma warning restore CS0661 // Type defines operator == or operator != but does not override Object.GetHashCode() -#pragma warning restore CS0659 // Type overrides Object.Equals(object o) but does not override Object.GetHashCode() { /// /// Initializes a new instance of the class. /// Default constructor used by derived types. /// - protected internal Spark() + private protected Spark() { } @@ -54,27 +50,35 @@ protected internal Spark() /// /// The first Spark<T> to compare, or null. /// The second Spark<T> to compare, or null. - /// true if the first Spark<T> value has a different observer message payload as the second Spark<T> value; otherwise, false. + /// + /// if the first Spark<T> value has a different observer message payload as the second Spark<T> value; + /// otherwise, . + /// /// - /// Equality of Spark<T> objects is based on the equality of the observer message payload they represent, including the Spark Kind and the Value or Exception (if any). - /// This means two Spark<T> objects can be equal even though they don't represent the same observer method call, but have the same Kind and have equal parameters passed to the observer method. - /// In case one wants to determine whether two Spark<T> objects represent a different observer method call, use Object.ReferenceEquals identity equality instead. + /// Equality of Spark<T> objects is based on the equality of the observer message payload they represent, + /// including the Spark Kind and the Value or Exception (if any). This means two Spark<T> objects can be equal even though + /// they don't represent the same observer method call, but have the same Kind and have equal parameters passed to the observer method. + /// Use Object.ReferenceEquals identity equality to determine whether two Spark<T> objects represent a different observer method call. /// - public static bool operator !=(Spark left, Spark right) => !(left == right); + public static bool operator !=(Spark? left, Spark? right) => !(left == right); /// /// Determines whether the two specified Spark<T> objects have the same observer message payload. /// /// The first Spark<T> to compare, or null. /// The second Spark<T> to compare, or null. - /// true if the first Spark<T> value has the same observer message payload as the second Spark<T> value; otherwise, false. + /// + /// if the first Spark<T> value has the same observer message payload as the second Spark<T> value; + /// otherwise, . + /// /// - /// Equality of Spark<T> objects is based on the equality of the observer message payload they represent, including the Spark Kind and the Value or Exception (if any). - /// This means two Spark<T> objects can be equal even though they don't represent the same observer method call, but have the same Kind and have equal parameters passed to the observer method. - /// In case one wants to determine whether two Spark<T> objects represent a different observer method call, use Object.ReferenceEquals identity equality instead. + /// Equality of Spark<T> objects is based on the equality of the observer message payload they represent, + /// including the Spark Kind and the Value or Exception (if any). This means two Spark<T> objects can be equal even though + /// they don't represent the same observer method call, but have the same Kind and have equal parameters passed to the observer method. + /// Use Object.ReferenceEquals identity equality to determine whether two Spark<T> objects represent a different observer method call. /// public static bool operator ==(Spark? left, Spark? right) => - ReferenceEquals(left, right) || (left is not null && left.Equals(right)); + ReferenceEquals(left, right) || (left?.Equals(right) == true); /// /// Determines whether the current Spark<T> object has the same observer message payload as a specified Spark<T> value. @@ -82,9 +86,10 @@ protected internal Spark() /// An object to compare to the current Spark<T> object. /// true if both Spark<T> objects have the same observer message payload; otherwise, false. /// - /// Equality of Spark<T> objects is based on the equality of the observer message payload they represent, including the Spark Kind and the Value or Exception (if any). - /// This means two Spark<T> objects can be equal even though they don't represent the same observer method call, but have the same Kind and have equal parameters passed to the observer method. - /// In case one wants to determine whether two Spark<T> objects represent the same observer method call, use Object.ReferenceEquals identity equality instead. + /// Equality of Spark<T> objects is based on the equality of the observer message payload they represent, + /// including the Spark Kind and the Value or Exception (if any). This means two Spark<T> objects can be equal even though + /// they don't represent the same observer method call, but have the same Kind and have equal parameters passed to the observer method. + /// Use Object.ReferenceEquals identity equality to determine whether two Spark<T> objects represent the same observer method call. /// public abstract bool Equals(Spark? other); @@ -94,12 +99,19 @@ protected internal Spark() /// The System.Object to compare with the current Spark<T>. /// true if the specified System.Object is equal to the current Spark<T>; otherwise, false. /// - /// Equality of Spark<T> objects is based on the equality of the observer message payload they represent, including the Spark Kind and the Value or Exception (if any). - /// This means two Spark<T> objects can be equal even though they don't represent the same observer method call, but have the same Kind and have equal parameters passed to the observer method. - /// In case one wants to determine whether two Spark<T> objects represent the same observer method call, use Object.ReferenceEquals identity equality instead. + /// Equality of Spark<T> objects is based on the equality of the observer message payload they represent, + /// including the Spark Kind and the Value or Exception (if any). This means two Spark<T> objects can be equal even though + /// they don't represent the same observer method call, but have the same Kind and have equal parameters passed to the observer method. + /// Use Object.ReferenceEquals identity equality to determine whether two Spark<T> objects represent the same observer method call. /// public override bool Equals(object? obj) => Equals(obj as Spark); + /// + /// Returns the hash code for this spark. + /// + /// A hash code for this spark. + public abstract override int GetHashCode(); + /// /// Invokes the observer's method corresponding to the Spark. /// @@ -153,10 +165,12 @@ public IObservable ToObservable(ISequencer scheduler) return Signal.Create(observer => scheduler.Schedule(() => { Accept(observer); - if (Kind == SparkKind.OnNext) + if (Kind != SparkKind.OnNext) { - observer.OnCompleted(); + return; } + + observer.OnCompleted(); })); } @@ -171,6 +185,7 @@ internal sealed class OnNextSpark : Spark /// Initializes a new instance of the class. /// Constructs a Spark of a new value. /// + /// The value carried by the spark. public OnNextSpark(T value) => Value = value; /// @@ -196,6 +211,7 @@ internal sealed class OnNextSpark : Spark /// /// Returns the hash code for this instance. /// + /// A hash code for this instance. public override int GetHashCode() => EqualityComparer.Default.GetHashCode(Value!); /// @@ -226,6 +242,7 @@ public override bool Equals(Spark? other) /// /// Returns a string representation of this instance. /// + /// A string representation of this instance. public override string ToString() => string.Format(CultureInfo.CurrentCulture, "OnNext({0})", Value); /// @@ -245,6 +262,7 @@ public override void Accept(IObserver observer) /// /// Invokes the observer's method corresponding to the Spark and returns the produced result. /// + /// The result type. /// Observer to invoke the Spark on. /// Result produced by the observation. public override TResult Accept(IObserver observer) @@ -286,6 +304,7 @@ public override void Accept(Action onNext, Action onError, Action /// /// Invokes the delegate corresponding to the Spark and returns the produced result. /// + /// The result type. /// Delegate to invoke for an OnNext Spark. /// Delegate to invoke for an OnError Spark. /// Delegate to invoke for an OnCompleted Spark. @@ -322,11 +341,16 @@ internal sealed class OnErrorSpark : Spark /// Initializes a new instance of the class. /// Constructs a Spark of an exception. /// + /// The exception carried by the spark. public OnErrorSpark(Exception exception) => Exception = exception; /// /// Gets throws the exception. /// + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Sonar Code Smell", + "S2372:Exceptions should not be thrown from property getters", + Justification = "Non-OnNext sparks intentionally throw when their Value is requested.")] public override T Value { get @@ -354,11 +378,14 @@ public override T Value /// /// Returns the hash code for this instance. /// + /// A hash code for this instance. public override int GetHashCode() => Exception.GetHashCode(); /// /// Indicates whether this instance and other are equal. /// + /// The other spark. + /// when the sparks are equal; otherwise, . public override bool Equals(Spark? other) { if (ReferenceEquals(this, other)) @@ -382,6 +409,7 @@ public override bool Equals(Spark? other) /// /// Returns a string representation of this instance. /// + /// A string representation of this instance. public override string ToString() => string.Format(CultureInfo.CurrentCulture, "OnError({0})", Exception.GetType().FullName); /// @@ -401,6 +429,7 @@ public override void Accept(IObserver observer) /// /// Invokes the observer's method corresponding to the Spark and returns the produced result. /// + /// The result type. /// Observer to invoke the Spark on. /// Result produced by the observation. public override TResult Accept(IObserver observer) @@ -442,6 +471,7 @@ public override void Accept(Action onNext, Action onError, Action /// /// Invokes the delegate corresponding to the Spark and returns the produced result. /// + /// The result type. /// Delegate to invoke for an OnNext Spark. /// Delegate to invoke for an OnError Spark. /// Delegate to invoke for an OnCompleted Spark. @@ -474,17 +504,13 @@ public override TResult Accept(Func onNext, Func { - /// - /// Initializes a new instance of the class. - /// Constructs a Spark of the end of a sequence. - /// - public OnCompletedSpark() - { - } - /// /// Gets throws an InvalidOperationException. /// + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Sonar Code Smell", + "S2372:Exceptions should not be thrown from property getters", + Justification = "Non-OnNext sparks intentionally throw when their Value is requested.")] public override T Value => throw new InvalidOperationException("No Value"); /// @@ -505,11 +531,14 @@ public OnCompletedSpark() /// /// Returns the hash code for this instance. /// + /// A hash code for this instance. public override int GetHashCode() => typeof(T).GetHashCode() ^ 8510; /// /// Indicates whether this instance and other are equal. /// + /// The other spark. + /// when the sparks are equal; otherwise, . public override bool Equals(Spark? other) { if (ReferenceEquals(this, other)) @@ -528,6 +557,7 @@ public override bool Equals(Spark? other) /// /// Returns a string representation of this instance. /// + /// A string representation of this instance. public override string ToString() => "OnCompleted()"; /// @@ -547,6 +577,7 @@ public override void Accept(IObserver observer) /// /// Invokes the observer's method corresponding to the Spark and returns the produced result. /// + /// The result type. /// Observer to invoke the Spark on. /// Result produced by the observation. public override TResult Accept(IObserver observer) @@ -588,6 +619,7 @@ public override void Accept(Action onNext, Action onError, Action /// /// Invokes the delegate corresponding to the Spark and returns the produced result. /// + /// The result type. /// Delegate to invoke for an OnNext Spark. /// Delegate to invoke for an OnError Spark. /// Delegate to invoke for an OnCompleted Spark. diff --git a/src/ReactiveUI.Primitives/Core/ThrowWitness{T}.cs b/src/ReactiveUI.Primitives/Core/ThrowWitness{T}.cs index e6dd09c..a9f9f83 100644 --- a/src/ReactiveUI.Primitives/Core/ThrowWitness{T}.cs +++ b/src/ReactiveUI.Primitives/Core/ThrowWitness{T}.cs @@ -4,21 +4,34 @@ namespace ReactiveUI.Primitives.Core; +/// +/// Observer that ignores values and completion and rethrows errors. +/// +/// The observed value type. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal sealed class ThrowWitness : IObserver { + /// + /// Gets the shared throw witness instance. + /// public static readonly ThrowWitness Instance = new(); + /// + /// Initializes a new instance of the class. + /// private ThrowWitness() { } + /// public void OnCompleted() { } + /// public void OnError(Exception error) => error.Rethrow(); + /// public void OnNext(T value) { } diff --git a/src/ReactiveUI.Primitives/Core/TimeInterval{T}.cs b/src/ReactiveUI.Primitives/Core/TimeInterval{T}.cs index cefc75f..d5bdb0f 100644 --- a/src/ReactiveUI.Primitives/Core/TimeInterval{T}.cs +++ b/src/ReactiveUI.Primitives/Core/TimeInterval{T}.cs @@ -83,7 +83,7 @@ public override bool Equals(object? obj) /// A hash code for the current TimeInterval value. public override int GetHashCode() { - var valueHashCode = Value == null ? 1963 : Value.GetHashCode(); + var valueHashCode = Value is null ? 1963 : EqualityComparer.Default.GetHashCode(Value); return Interval.GetHashCode() ^ valueHashCode; } diff --git a/src/ReactiveUI.Primitives/Core/Witness.cs b/src/ReactiveUI.Primitives/Core/Witness.cs index d731983..7e5684b 100644 --- a/src/ReactiveUI.Primitives/Core/Witness.cs +++ b/src/ReactiveUI.Primitives/Core/Witness.cs @@ -13,7 +13,14 @@ namespace ReactiveUI.Primitives.Core; [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public static class Witness { + /// + /// Completion callback that does nothing. + /// private static readonly Action Nop = static () => { }; + + /// + /// Error callback that rethrows with preserved exception details. + /// private static readonly Action Rethrow = static error => ExceptionDispatchInfo.Capture(error).Throw(); /// @@ -106,12 +113,33 @@ public static IObserver Safe(IObserver observer, IDisposable cancel) return new SafeWitness(observer, cancel); } + /// + /// Delegate-backed observer implementation. + /// + /// The observed value type. private sealed class DelegateWitness : IObserver { + /// + /// Callback invoked for each value. + /// private readonly Action _onNext; + + /// + /// Callback invoked for an error. + /// private readonly Action _onError; + + /// + /// Callback invoked for completion. + /// private readonly Action _onCompleted; + /// + /// Initializes a new instance of the class. + /// + /// Callback invoked for each value. + /// Callback invoked for an error. + /// Callback invoked for completion. public DelegateWitness(Action onNext, Action onError, Action onCompleted) { _onNext = onNext; @@ -119,25 +147,49 @@ public DelegateWitness(Action onNext, Action onError, Action onCom _onCompleted = onCompleted; } + /// public void OnCompleted() => _onCompleted(); + /// public void OnError(Exception error) => _onError(error ?? throw new ArgumentNullException(nameof(error))); + /// public void OnNext(T value) => _onNext(value); } + /// + /// Observer wrapper that prevents notifications after termination. + /// + /// The observed value type. private sealed class SafeWitness : IObserver { + /// + /// Wrapped observer. + /// private readonly IObserver _observer; + + /// + /// Cancellation resource disposed on terminal notifications. + /// private IDisposable? _cancel; + + /// + /// Non-zero after the observer has stopped. + /// private int _stopped; + /// + /// Initializes a new instance of the class. + /// + /// Wrapped observer. + /// Cancellation resource disposed on terminal notifications. public SafeWitness(IObserver observer, IDisposable cancel) { _observer = observer; _cancel = cancel; } + /// public void OnCompleted() { if (Interlocked.Exchange(ref _stopped, 1) != 0) @@ -155,6 +207,7 @@ public void OnCompleted() } } + /// public void OnError(Exception error) { if (error == null) @@ -177,6 +230,7 @@ public void OnError(Exception error) } } + /// public void OnNext(T value) { if (Volatile.Read(ref _stopped) != 0) @@ -196,6 +250,9 @@ public void OnNext(T value) } } + /// + /// Disposes the cancellation resource exactly once. + /// private void DisposeCancel() => Interlocked.Exchange(ref _cancel, null)?.Dispose(); } } diff --git a/src/ReactiveUI.Primitives/Disposables/AssignmentSlot.cs b/src/ReactiveUI.Primitives/Disposables/AssignmentSlot.cs index 97857d4..b3bc7c9 100644 --- a/src/ReactiveUI.Primitives/Disposables/AssignmentSlot.cs +++ b/src/ReactiveUI.Primitives/Disposables/AssignmentSlot.cs @@ -10,21 +10,37 @@ namespace ReactiveUI.Primitives.Disposables; [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public sealed class AssignmentSlot : SingleDisposable { + /// + /// Initializes a new instance of the class. + /// + public AssignmentSlot() + { + } + /// /// Initializes a new instance of the class. /// /// Action to invoke before the assigned disposable is disposed. - public AssignmentSlot(Action? action = null) + public AssignmentSlot(Action? action) : base(action) { } + /// + /// Initializes a new instance of the class. + /// + /// Initial assignment. + public AssignmentSlot(IDisposable disposable) + : base(disposable) + { + } + /// /// Initializes a new instance of the class. /// /// Initial assignment. /// Action to invoke before the assigned disposable is disposed. - public AssignmentSlot(IDisposable disposable, Action? action = null) + public AssignmentSlot(IDisposable disposable, Action? action) : base(disposable, action) { } diff --git a/src/ReactiveUI.Primitives/Disposables/CancellationDisposable.cs b/src/ReactiveUI.Primitives/Disposables/CancellationDisposable.cs index ef2b8f0..81e1968 100644 --- a/src/ReactiveUI.Primitives/Disposables/CancellationDisposable.cs +++ b/src/ReactiveUI.Primitives/Disposables/CancellationDisposable.cs @@ -10,6 +10,9 @@ namespace ReactiveUI.Primitives.Disposables; /// public sealed class CancellationDisposable : IsDisposed { + /// + /// Cancellation source owned by this disposable. + /// private readonly CancellationTokenSource _cts; /// @@ -50,19 +53,24 @@ public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: true); - GC.SuppressFinalize(this); } + /// + /// Releases resources used by this disposable. + /// + /// when called from . private void Dispose(bool disposing) { - if (!IsDisposed) + if (IsDisposed) { - if (disposing) - { - _cts.Cancel(); - } + return; + } - IsDisposed = true; + if (disposing) + { + _cts.Cancel(); } + + IsDisposed = true; } } diff --git a/src/ReactiveUI.Primitives/Disposables/Disposable.cs b/src/ReactiveUI.Primitives/Disposables/Disposable.cs index e65e014..300ea0b 100644 --- a/src/ReactiveUI.Primitives/Disposables/Disposable.cs +++ b/src/ReactiveUI.Primitives/Disposables/Disposable.cs @@ -23,8 +23,12 @@ public static class Disposable public static IDisposable Create(Action dispose) => dispose == null ? Empty : new AnonymousDisposable(dispose); + /// + /// Disposable that performs no action. + /// internal sealed class EmptyDisposable : IDisposable { + /// public void Dispose() { } @@ -35,6 +39,9 @@ public void Dispose() /// internal sealed class AnonymousDisposable : IDisposable { + /// + /// Disposal action, cleared after the first dispose call. + /// private volatile Action? _dispose; /// diff --git a/src/ReactiveUI.Primitives/Disposables/MultipleDisposable.cs b/src/ReactiveUI.Primitives/Disposables/MultipleDisposable.cs index 1765715..19827c3 100644 --- a/src/ReactiveUI.Primitives/Disposables/MultipleDisposable.cs +++ b/src/ReactiveUI.Primitives/Disposables/MultipleDisposable.cs @@ -10,14 +10,44 @@ namespace ReactiveUI.Primitives.Disposables; [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public class MultipleDisposable : IsDisposed { + /// + /// Initial capacity for overflow disposable storage. + /// private const int OverflowInitialCapacity = 2; + + /// + /// Growth factor for overflow disposable storage. + /// private const int OverflowGrowthFactor = 2; + /// + /// Synchronizes mutations to the disposable set. + /// private readonly object _gate = new(); + + /// + /// First inline disposable slot. + /// private IDisposable? _slot0; + + /// + /// Second inline disposable slot. + /// private IDisposable? _slot1; + + /// + /// Overflow disposable slots used after the inline slots are occupied. + /// private IDisposable[]? _overflow; + + /// + /// Number of active overflow disposable slots. + /// private int _overflowCount; + + /// + /// Value indicating whether this group is disposed. + /// private bool _disposed; /// @@ -117,10 +147,12 @@ public void Add(IDisposable disposable) } } - if (shouldDispose) + if (!shouldDispose) { - disposable.Dispose(); + return; } + + disposable.Dispose(); } /// @@ -207,6 +239,10 @@ protected virtual void Dispose(bool disposing) } } + /// + /// Adds a disposable while the caller holds the gate. + /// + /// Disposable to add. private void AddCore(IDisposable disposable) { if (_slot0 == null) @@ -235,6 +271,11 @@ private void AddCore(IDisposable disposable) _overflow[_overflowCount++] = disposable; } + /// + /// Removes a disposable while the caller holds the gate. + /// + /// Disposable to remove. + /// when the item was removed; otherwise, . private bool RemoveCore(IDisposable item) { if (_slot0 != null && EqualityComparer.Default.Equals(_slot0, item)) @@ -275,22 +316,35 @@ private bool RemoveCore(IDisposable item) return false; } + /// + /// Array-backed disposable group returned by the static factory. + /// private sealed class MultipleDisposableBase : IDisposable { + /// + /// Disposables to release, or after disposal. + /// private IDisposable[]? _disposables; + /// + /// Initializes a new instance of the class. + /// + /// Disposables owned by the group. public MultipleDisposableBase(IDisposable[] disposables) => Volatile.Write(ref _disposables, disposables ?? throw new ArgumentNullException(nameof(disposables))); + /// public void Dispose() { var disposables = Interlocked.Exchange(ref _disposables, null); - if (disposables != null) + if (disposables == null) + { + return; + } + + foreach (var disposable in disposables) { - foreach (var disposable in disposables) - { - disposable?.Dispose(); - } + disposable?.Dispose(); } } } diff --git a/src/ReactiveUI.Primitives/Disposables/SingleDisposable.cs b/src/ReactiveUI.Primitives/Disposables/SingleDisposable.cs index 86f6403..2d6a349 100644 --- a/src/ReactiveUI.Primitives/Disposables/SingleDisposable.cs +++ b/src/ReactiveUI.Primitives/Disposables/SingleDisposable.cs @@ -10,23 +10,49 @@ namespace ReactiveUI.Primitives.Disposables; [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public class SingleDisposable : IsDisposed { + /// + /// Marker used once the slot has been disposed. + /// private static readonly IDisposable DisposedSentinel = new DisposedMarker(); + /// + /// Action invoked before disposal. + /// private readonly Action? _action; + + /// + /// Assigned disposable or the disposed marker. + /// private IDisposable? _disposable; + /// + /// Initializes a new instance of the class. + /// + public SingleDisposable() + { + } + /// /// Initializes a new instance of the class. /// /// Action to invoke before the assigned disposable is disposed. - public SingleDisposable(Action? action = null) => _action = action; + public SingleDisposable(Action? action) => _action = action; + + /// + /// Initializes a new instance of the class. + /// + /// The disposable. + public SingleDisposable(IDisposable disposable) + : this(disposable, null) + { + } /// /// Initializes a new instance of the class. /// /// The disposable. /// Action to invoke before the assigned disposable is disposed. - public SingleDisposable(IDisposable disposable, Action? action = null) + public SingleDisposable(IDisposable disposable, Action? action) : this(action) => Create(disposable); /// @@ -66,7 +92,7 @@ public void Create(IDisposable disposable) return; } - throw new InvalidOperationException("The disposable slot has already been assigned."); + throw new InvalidOperationException($"The {nameof(disposable)} slot has already been assigned."); } /// @@ -99,8 +125,12 @@ protected virtual void Dispose(bool disposing) disposable.Dispose(); } + /// + /// Disposable marker for disposed slots. + /// private sealed class DisposedMarker : IDisposable { + /// public void Dispose() { } diff --git a/src/ReactiveUI.Primitives/Disposables/SingleReplaceableDisposable.cs b/src/ReactiveUI.Primitives/Disposables/SingleReplaceableDisposable.cs index 075d1b3..5af90c4 100644 --- a/src/ReactiveUI.Primitives/Disposables/SingleReplaceableDisposable.cs +++ b/src/ReactiveUI.Primitives/Disposables/SingleReplaceableDisposable.cs @@ -10,27 +10,53 @@ namespace ReactiveUI.Primitives.Disposables; [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public class SingleReplaceableDisposable : IsDisposed { + /// + /// Marker used once the slot has been disposed. + /// private static readonly IDisposable DisposedSentinel = new DisposedMarker(); + /// + /// Action invoked before disposal. + /// private readonly Action? _action; + + /// + /// Current disposable or the disposed marker. + /// private IDisposable? _disposable; + /// + /// Initializes a new instance of the class. + /// + public SingleReplaceableDisposable() + { + } + /// /// Initializes a new instance of the class. /// /// The action. - public SingleReplaceableDisposable(Action? action = null) => + public SingleReplaceableDisposable(Action? action) => _action = action; + /// + /// Initializes a new instance of the class. + /// + /// The disposable. + public SingleReplaceableDisposable(IDisposable disposable) + : this(disposable, null) + { + } + /// /// Initializes a new instance of the class. /// /// The disposable. /// The action to call before disposal. - public SingleReplaceableDisposable(IDisposable disposable, Action? action = null) + public SingleReplaceableDisposable(IDisposable disposable, Action? action) { - Create(disposable); _action = action; + Create(disposable); } /// @@ -51,14 +77,20 @@ public bool IsDisposed /// Creates the specified disposable. /// /// The disposable. + /// is . public void Create(IDisposable disposable) { + if (disposable == null) + { + throw new ArgumentNullException(nameof(disposable)); + } + while (true) { var current = Volatile.Read(ref _disposable); if (ReferenceEquals(current, DisposedSentinel)) { - disposable?.Dispose(); + disposable.Dispose(); _action?.Invoke(); return; } @@ -99,8 +131,12 @@ protected virtual void Dispose(bool disposing) _action?.Invoke(); } + /// + /// Disposable marker for disposed slots. + /// private sealed class DisposedMarker : IDisposable { + /// public void Dispose() { } diff --git a/src/ReactiveUI.Primitives/Disposables/Slot.cs b/src/ReactiveUI.Primitives/Disposables/Slot.cs index bea4d38..7dce65b 100644 --- a/src/ReactiveUI.Primitives/Disposables/Slot.cs +++ b/src/ReactiveUI.Primitives/Disposables/Slot.cs @@ -10,21 +10,37 @@ namespace ReactiveUI.Primitives.Disposables; [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public sealed class Slot : SingleReplaceableDisposable { + /// + /// Initializes a new instance of the class. + /// + public Slot() + { + } + /// /// Initializes a new instance of the class. /// /// Action to call when the slot is disposed. - public Slot(Action? action = null) + public Slot(Action? action) : base(action) { } + /// + /// Initializes a new instance of the class. + /// + /// Initial disposable. + public Slot(IDisposable disposable) + : base(disposable) + { + } + /// /// Initializes a new instance of the class. /// /// Initial disposable. /// Action to call when the slot is disposed. - public Slot(IDisposable disposable, Action? action = null) + public Slot(IDisposable disposable, Action? action) : base(disposable, action) { } diff --git a/src/ReactiveUI.Primitives/ExceptionMixins.cs b/src/ReactiveUI.Primitives/ExceptionMixins.cs index 0b0cee2..2847271 100644 --- a/src/ReactiveUI.Primitives/ExceptionMixins.cs +++ b/src/ReactiveUI.Primitives/ExceptionMixins.cs @@ -4,8 +4,15 @@ namespace ReactiveUI.Primitives; +/// +/// Exception helper methods. +/// internal static class ExceptionMixins { + /// + /// Throws the exception while preserving stack trace where required by the target framework. + /// + /// Exception to throw. public static void Throw(this Exception exception) { #if NET472 || NETSTANDARD2_0 diff --git a/src/ReactiveUI.Primitives/Handle.cs b/src/ReactiveUI.Primitives/Handle.cs index 23decf1..d669a9b 100644 --- a/src/ReactiveUI.Primitives/Handle.cs +++ b/src/ReactiveUI.Primitives/Handle.cs @@ -6,11 +6,31 @@ namespace ReactiveUI.Primitives; +/// +/// Shared delegate handlers. +/// internal static class Handle { + /// + /// Action that does nothing. + /// public static readonly Action Nop = () => { }; + + /// + /// Error handler that throws the supplied exception. + /// public static readonly Action Throw = ex => ex.Throw(); + /// + /// Converts an error into an empty observable sequence. + /// + /// The source value type. + /// Ignored exception. + /// An empty sequence. + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Sonar Code Smell", + "S4018:Generic methods should provide type parameters", + Justification = "The type parameter determines the empty sequence value type.")] public static IObservable CatchIgnore(Exception ex) => Signal.Empty(); } diff --git a/src/ReactiveUI.Primitives/Handle{T1,T2,T3}.cs b/src/ReactiveUI.Primitives/Handle{T1,T2,T3}.cs index b60d981..cc86e28 100644 --- a/src/ReactiveUI.Primitives/Handle{T1,T2,T3}.cs +++ b/src/ReactiveUI.Primitives/Handle{T1,T2,T3}.cs @@ -4,10 +4,23 @@ namespace ReactiveUI.Primitives; +/// +/// Shared delegate handlers for three-argument callbacks. +/// +/// The first value type. +/// The second value type. +/// The third value type. internal static class Handle { #pragma warning disable SA1313 // Parameter names should begin with lower-case letter - public static readonly Action Ignore = (_, __, ___) => { }; - public static readonly Action Throw = (ex, _, __, ___) => ex.Throw(); + /// + /// Callback that ignores all values. + /// + public static readonly Action Ignore = (_, _, _) => { }; + + /// + /// Error callback that throws the supplied exception. + /// + public static readonly Action Throw = (ex, _, _, _) => ex.Throw(); #pragma warning restore SA1313 // Parameter names should begin with lower-case letter } diff --git a/src/ReactiveUI.Primitives/Handle{T1,T2}.cs b/src/ReactiveUI.Primitives/Handle{T1,T2}.cs index b59cc2e..a7219eb 100644 --- a/src/ReactiveUI.Primitives/Handle{T1,T2}.cs +++ b/src/ReactiveUI.Primitives/Handle{T1,T2}.cs @@ -4,8 +4,20 @@ namespace ReactiveUI.Primitives; +/// +/// Shared delegate handlers for two-argument callbacks. +/// +/// The first value type. +/// The second value type. internal static class Handle { - public static readonly Action Ignore = (_, __) => { }; - public static readonly Action Throw = (ex, _, __) => ex.Throw(); + /// + /// Callback that ignores both values. + /// + public static readonly Action Ignore = (_, _) => { }; + + /// + /// Error callback that throws the supplied exception. + /// + public static readonly Action Throw = (ex, _, _) => ex.Throw(); } diff --git a/src/ReactiveUI.Primitives/Handle{T}.cs b/src/ReactiveUI.Primitives/Handle{T}.cs index be86e40..cc3fa77 100644 --- a/src/ReactiveUI.Primitives/Handle{T}.cs +++ b/src/ReactiveUI.Primitives/Handle{T}.cs @@ -4,9 +4,24 @@ namespace ReactiveUI.Primitives; +/// +/// Shared delegate handlers for one-argument callbacks. +/// +/// The value type. internal static class Handle { - public static readonly Action Ignore = (T _) => { }; - public static readonly Func Identity = (T t) => t; + /// + /// Callback that ignores its value. + /// + public static readonly Action Ignore = (_) => { }; + + /// + /// Function that returns its input. + /// + public static readonly Func Identity = (t) => t; + + /// + /// Error callback that throws the supplied exception. + /// public static readonly Action Throw = (ex, _) => ex.Throw(); } diff --git a/src/ReactiveUI.Primitives/LinqMixins.OperatorGate.cs b/src/ReactiveUI.Primitives/LinqMixins.OperatorGate.cs new file mode 100644 index 0000000..f2800e5 --- /dev/null +++ b/src/ReactiveUI.Primitives/LinqMixins.OperatorGate.cs @@ -0,0 +1,22 @@ +// 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; + +/// +/// SelectMixins. +/// +public static partial class LinqMixins +{ + /// + /// Per-subscription synchronization gate for operators that coordinate callbacks from multiple sources. + /// + private sealed class OperatorGate + { + /// + /// Gets the stable synchronization object for the subscription. + /// + internal object SyncRoot => this; + } +} diff --git a/src/ReactiveUI.Primitives/LinqMixins.cs b/src/ReactiveUI.Primitives/LinqMixins.cs index e1d41a3..23ef249 100644 --- a/src/ReactiveUI.Primitives/LinqMixins.cs +++ b/src/ReactiveUI.Primitives/LinqMixins.cs @@ -99,13 +99,21 @@ public static IDisposable DisposeWith(this IDisposable disposable, MultipleDispo return disposable; } + /// + /// Disposes the with. + /// + /// The disposable. + /// A SingleDisposable. + public static SingleDisposable DisposeWith(this IDisposable disposable) => + new(disposable); + /// /// Disposes the with. /// /// The disposable. /// The action. /// A SingleDisposable. - public static SingleDisposable DisposeWith(this IDisposable disposable, Action? action = null) => + public static SingleDisposable DisposeWith(this IDisposable disposable, Action? action) => new(disposable, action); /// diff --git a/src/ReactiveUI.Primitives/Signal/AsyncSignal{T}.cs b/src/ReactiveUI.Primitives/Signal/AsyncSignal{T}.cs index edc0890..a6853d7 100644 --- a/src/ReactiveUI.Primitives/Signal/AsyncSignal{T}.cs +++ b/src/ReactiveUI.Primitives/Signal/AsyncSignal{T}.cs @@ -15,10 +15,30 @@ namespace ReactiveUI.Primitives.Signals; [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public class AsyncSignal : IAwaitSignal { + /// + /// Executes the new operation. + /// + /// The result. private readonly object _observerLock = new(); + + /// + /// Stores state for the signal implementation. + /// private T? _lastValue; + + /// + /// Stores state for the signal implementation. + /// private bool _hasValue; + + /// + /// Stores state for the signal implementation. + /// private Exception? _lastError; + + /// + /// Stores state for the signal implementation. + /// private IObserver _outObserver = EmptyWitness.Instance; /// @@ -102,6 +122,21 @@ public void OnCompleted() } } + /// + /// Specifies a callback action that will be invoked when the subject completes. + /// + /// Callback action that will be invoked when the subject completes. + /// is null. + public void OnCompleted(Action continuation) + { + if (continuation == null) + { + throw new ArgumentNullException(nameof(continuation)); + } + + SubscribeCompletion(continuation, true); + } + /// /// Called when [error]. /// @@ -182,11 +217,11 @@ public IDisposable Subscribe(IObserver observer) var current = _outObserver; if (current is EmptyWitness) { - _outObserver = new ListWitness(new ImmutableList>(new[] { observer })); + _outObserver = new ListWitness(new ImmutableList>([observer])); } else { - _outObserver = new ListWitness(new ImmutableList>(new[] { current, observer })); + _outObserver = new ListWitness(new ImmutableList>([current, observer])); } } @@ -231,21 +266,6 @@ public void Dispose() /// Object that can be awaited. public IAwaitSignal GetAwaiter() => this; - /// - /// Specifies a callback action that will be invoked when the subject completes. - /// - /// Callback action that will be invoked when the subject completes. - /// is null. - public void OnCompleted(Action continuation) - { - if (continuation == null) - { - throw new ArgumentNullException(nameof(continuation)); - } - - OnCompleted(continuation, true); - } - /// /// Gets the last element of the subject, potentially blocking until the subject completes successfully or exceptionally. /// @@ -256,7 +276,7 @@ public T GetResult() if (!IsCompleted) { var e = new ManualResetEvent(false); - OnCompleted(() => e.Set(), false); + SubscribeCompletion(() => e.Set(), false); e.WaitOne(); } @@ -276,38 +296,65 @@ public T GetResult() /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool disposing) { - if (!IsDisposed) + if (IsDisposed) + { + return; + } + + if (disposing) { - if (disposing) + lock (_observerLock) { - lock (_observerLock) - { - _outObserver = DisposedWitness.Instance; - _lastError = null; - _lastValue = default; - } + _outObserver = DisposedWitness.Instance; + _lastError = null; + _lastValue = default; } - - IsDisposed = true; } + + IsDisposed = true; } + /// + /// Executes the ThrowIfDisposed operation. + /// private void ThrowIfDisposed() { - if (IsDisposed) + if (!IsDisposed) { - throw new ObjectDisposedException(string.Empty); + return; } + + throw new ObjectDisposedException(string.Empty); } - private void OnCompleted(Action continuation, bool originalContext) => + /// + /// Executes the SubscribeCompletion operation. + /// + /// The continuation value. + /// The originalContext value. + private void SubscribeCompletion(Action continuation, bool originalContext) => Subscribe(new AwaitObserver(continuation, originalContext)); - private class AwaitObserver : IObserver + /// + /// Represents the AwaitObserver class. + /// + private sealed class AwaitObserver : IObserver { + /// + /// Stores state for the signal implementation. + /// private readonly SynchronizationContext? _context; + + /// + /// Stores state for the signal implementation. + /// private readonly Action _callback; + /// + /// Initializes a new instance of the class. + /// + /// The callback value. + /// The originalContext value. public AwaitObserver(Action callback, bool originalContext) { if (originalContext) @@ -318,14 +365,28 @@ public AwaitObserver(Action callback, bool originalContext) _callback = callback; } + /// + /// Executes the OnCompleted operation. + /// public void OnCompleted() => InvokeOnOriginalContext(); + /// + /// Executes the OnError operation. + /// + /// The error value. public void OnError(Exception error) => InvokeOnOriginalContext(); + /// + /// Executes the OnNext operation. + /// + /// The value. public void OnNext(T value) { } + /// + /// Executes the InvokeOnOriginalContext operation. + /// private void InvokeOnOriginalContext() { if (_context != null) @@ -339,18 +400,41 @@ private void InvokeOnOriginalContext() } } - private class ObserverHandler : IDisposable + /// + /// Represents the ObserverHandler class. + /// + private sealed class ObserverHandler : IDisposable { + /// + /// Executes the new operation. + /// + /// The result. private readonly object _gate = new(); + + /// + /// Stores state for the signal implementation. + /// private AsyncSignal? _subject; + + /// + /// Stores state for the signal implementation. + /// private IObserver? _observer; + /// + /// Initializes a new instance of the class. + /// + /// The subject value. + /// The observer value. public ObserverHandler(AsyncSignal subject, IObserver observer) { _subject = subject; _observer = observer; } + /// + /// Executes the Dispose operation. + /// public void Dispose() { lock (_gate) diff --git a/src/ReactiveUI.Primitives/Signal/BehaviourSignal{T}.cs b/src/ReactiveUI.Primitives/Signal/BehaviourSignal{T}.cs index 5b7c8c6..77374a0 100644 --- a/src/ReactiveUI.Primitives/Signal/BehaviourSignal{T}.cs +++ b/src/ReactiveUI.Primitives/Signal/BehaviourSignal{T}.cs @@ -14,17 +14,42 @@ namespace ReactiveUI.Primitives.Signals; [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public class BehaviourSignal : ISignal { + /// + /// Executes the new operation. + /// + /// The result. private readonly object _observerLock = new(); +#pragma warning disable S3459 // Broadcaster is a mutable struct whose default value is the empty broadcaster. + + /// + /// Stores state for the signal implementation. + /// private Broadcaster _broadcaster; +#pragma warning restore S3459 + + /// + /// Stores state for the signal implementation. + /// private bool _isStopped; + + /// + /// Stores state for the signal implementation. + /// private T? _lastValue; + + /// + /// Stores state for the signal implementation. + /// private Exception? _lastError; /// /// Initializes a new instance of the class. /// /// The default value. - public BehaviourSignal(T defaultValue) => _lastValue = defaultValue; + public BehaviourSignal(T defaultValue) + { + _lastValue = defaultValue; + } /// /// Gets the current value or throws an exception. @@ -228,42 +253,72 @@ public void Dispose() /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool disposing) { - if (!IsDisposed) + if (IsDisposed) { - if (disposing) + return; + } + + if (disposing) + { + lock (_observerLock) { - lock (_observerLock) - { - _broadcaster.Clear(); - _lastError = null; - _lastValue = default; - } + _broadcaster.Clear(); + _lastError = null; + _lastValue = default; } - - IsDisposed = true; } + + IsDisposed = true; } + /// + /// Executes the ThrowIfDisposed operation. + /// private void ThrowIfDisposed() { - if (IsDisposed) + if (!IsDisposed) { - throw new ObjectDisposedException(string.Empty); + return; } + + throw new ObjectDisposedException(string.Empty); } - private class ObserverHandler : IDisposable + /// + /// Represents the ObserverHandler class. + /// + private sealed class ObserverHandler : IDisposable { + /// + /// Executes the new operation. + /// + /// The result. private readonly object _lock = new(); + + /// + /// Stores state for the signal implementation. + /// private BehaviourSignal? _subject; + + /// + /// Stores state for the signal implementation. + /// private IObserver? _observer; + /// + /// Initializes a new instance of the class. + /// + /// The subject value. + /// The observer value. public ObserverHandler(BehaviourSignal subject, IObserver observer) { _subject = subject; _observer = observer; } + /// + /// Executes the Dispose operation. + /// public void Dispose() { lock (_lock) diff --git a/src/ReactiveUI.Primitives/Signal/BufferSignal{T,TResult}.cs b/src/ReactiveUI.Primitives/Signal/BufferSignal{T,TResult}.cs index f441cc4..e209b3f 100644 --- a/src/ReactiveUI.Primitives/Signal/BufferSignal{T,TResult}.cs +++ b/src/ReactiveUI.Primitives/Signal/BufferSignal{T,TResult}.cs @@ -4,16 +4,46 @@ namespace ReactiveUI.Primitives.Signals; +/// +/// Represents the BufferSignal class. +/// +/// The T type. +/// The TResult type. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] -internal class BufferSignal : Signal> - where TResult : IList? +internal sealed class BufferSignal : Signal + where TResult : class, IList { + /// + /// Stores state for the signal implementation. + /// private readonly int _skip; + + /// + /// Stores state for the signal implementation. + /// private readonly int _count; - private IList? _buffer; + + /// + /// Stores state for the signal implementation. + /// + private TResult? _buffer; + + /// + /// Stores state for the signal implementation. + /// private int _index; + + /// + /// Stores state for the signal implementation. + /// private IDisposable? _subscription; + /// + /// Initializes a new instance of the class. + /// + /// The source value. + /// The count value. + /// The skip value. public BufferSignal(IObservable source, int count, int skip) { _skip = skip; @@ -31,7 +61,7 @@ public BufferSignal(IObservable source, int count, int skip) if (idx == 0) { // Reset buffer. - buffer = new List(); + buffer = CreateBuffer(); _buffer = buffer; } @@ -71,26 +101,38 @@ public BufferSignal(IObservable source, int count, int skip) }); } + /// + /// Executes the Dispose operation. + /// + /// The disposing value. protected override void Dispose(bool disposing) { - if (IsDisposed) + if (IsDisposed || !disposing) { + base.Dispose(disposing); return; } - Dispose(disposing); - if (disposing) + var buffer = _buffer; + _buffer = null; + + if (buffer != null) { - var buffer = _buffer; - _buffer = null; + OnNext(buffer); + } - if (buffer != null) - { - OnNext(buffer); - } + _subscription?.Dispose(); + _subscription = null; + base.Dispose(disposing); + } - _subscription?.Dispose(); - _subscription = null; - } + /// + /// Executes the CreateBuffer operation. + /// + /// The result. + private TResult CreateBuffer() + { + var buffer = new List(_count); + return (TResult)(IList)buffer; } } diff --git a/src/ReactiveUI.Primitives/Signal/CommandSignal{TResult}.cs b/src/ReactiveUI.Primitives/Signal/CommandSignal{TResult}.cs index 378218a..503479d 100644 --- a/src/ReactiveUI.Primitives/Signal/CommandSignal{TResult}.cs +++ b/src/ReactiveUI.Primitives/Signal/CommandSignal{TResult}.cs @@ -13,37 +13,87 @@ namespace ReactiveUI.Primitives.Signals; [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public sealed class CommandSignal : IDisposable { + /// + /// Stores state for the signal implementation. + /// private readonly Func> _execute; + + /// + /// Executes the new operation. + /// + /// The result. private readonly object _gate = new(); + + /// + /// Executes the new operation. + /// + /// The result. private readonly Signal _results = new(); + + /// + /// Executes the new operation. + /// + /// The result. private readonly Signal _faults = new(); + + /// + /// Stores state for the signal implementation. + /// private readonly IDisposable? _canRunSubscription; + + /// + /// Stores state for the signal implementation. + /// private bool _canRun; + + /// + /// Stores state for the signal implementation. + /// private bool _disposed; /// /// Initializes a new instance of the class. /// /// The async operation to execute. - /// Optional gating signal. When omitted, execution is always allowed. - public CommandSignal(Func> execute, IObservable? canRun = null) + public CommandSignal(Func> execute) + : this(execute, null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The async operation to execute. + /// Gating signal. When null, execution is always allowed. + public CommandSignal(Func> execute, IObservable? canRun) { _execute = execute ?? throw new ArgumentNullException(nameof(execute)); _canRun = canRun == null; IsRunning = new StateSignal(false); - if (canRun != null) + if (canRun == null) { - _canRunSubscription = canRun.Subscribe(value => _canRun = value, _faults.OnNext); + return; } + + _canRunSubscription = canRun.Subscribe(value => _canRun = value, _faults.OnNext); + } + + /// + /// Initializes a new instance of the class. + /// + /// The synchronous operation to execute. + public CommandSignal(Func execute) + : this(execute, null) + { } /// /// Initializes a new instance of the class. /// /// The synchronous operation to execute. - /// Optional gating signal. When omitted, execution is always allowed. - public CommandSignal(Func execute, IObservable? canRun = null) + /// Gating signal. When null, execution is always allowed. + public CommandSignal(Func execute, IObservable? canRun) : this(_ => Task.FromResult((execute ?? throw new ArgumentNullException(nameof(execute)))()), canRun) { } @@ -68,12 +118,18 @@ public CommandSignal(Func execute, IObservable? canRun = null) /// public bool CanRun => Volatile.Read(ref _canRun); + /// + /// Executes the command if allowed and publishes the result or fault. + /// + /// The command result. + public Task ExecuteAsync() => ExecuteAsync(CancellationToken.None); + /// /// Executes the command if allowed and publishes the result or fault. /// /// Cancellation token for the operation. /// The command result. - public async Task ExecuteAsync(CancellationToken cancellationToken = default) + public async Task ExecuteAsync(CancellationToken cancellationToken) { ThrowIfDisposed(); lock (_gate) @@ -103,7 +159,9 @@ public async Task ExecuteAsync(CancellationToken cancellationToken = de } } - /// + /// + /// Executes the Dispose operation. + /// public void Dispose() { if (_disposed) @@ -118,11 +176,16 @@ public void Dispose() IsRunning.Dispose(); } + /// + /// Executes the ThrowIfDisposed operation. + /// private void ThrowIfDisposed() { - if (_disposed) + if (!_disposed) { - throw new ObjectDisposedException(nameof(CommandSignal)); + return; } + + throw new ObjectDisposedException(nameof(CommandSignal)); } } diff --git a/src/ReactiveUI.Primitives/Signal/ISignal{T}.cs b/src/ReactiveUI.Primitives/Signal/ISignal{T}.cs index 1b6633c..baacb69 100644 --- a/src/ReactiveUI.Primitives/Signal/ISignal{T}.cs +++ b/src/ReactiveUI.Primitives/Signal/ISignal{T}.cs @@ -8,6 +8,4 @@ namespace ReactiveUI.Primitives.Signals; /// ISubject. /// /// The Type. -public interface ISignal : ISignal -{ -} +public interface ISignal : ISignal; diff --git a/src/ReactiveUI.Primitives/Signal/KeepSignal{T}.cs b/src/ReactiveUI.Primitives/Signal/KeepSignal{T}.cs index 7abc0f5..046ff44 100644 --- a/src/ReactiveUI.Primitives/Signal/KeepSignal{T}.cs +++ b/src/ReactiveUI.Primitives/Signal/KeepSignal{T}.cs @@ -6,21 +6,46 @@ namespace ReactiveUI.Primitives.Signals; +/// +/// Represents the KeepSignal class. +/// +/// The T type. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] -internal sealed class KeepSignal : IObservable, IRequireCurrentThread +internal sealed class KeepSignal : IRequireCurrentThread { + /// + /// Stores state for the signal implementation. + /// private readonly IObservable _source; + + /// + /// Stores state for the signal implementation. + /// private readonly Func _predicate; + /// + /// Initializes a new instance of the class. + /// + /// The source value. + /// The predicate value. public KeepSignal(IObservable source, Func predicate) { _source = source; _predicate = predicate; } + /// + /// Executes the IsRequiredSubscribeOnCurrentThread operation. + /// + /// The result. public bool IsRequiredSubscribeOnCurrentThread() => _source is IRequireCurrentThread currentThread && currentThread.IsRequiredSubscribeOnCurrentThread(); + /// + /// Executes the Subscribe operation. + /// + /// The observer value. + /// The result. public IDisposable Subscribe(IObserver observer) { if (observer == null) @@ -31,36 +56,70 @@ public IDisposable Subscribe(IObserver observer) return _source.Subscribe(new KeepObserver(observer, _predicate)); } + /// + /// Represents the KeepObserver class. + /// private sealed class KeepObserver : IObserver { + /// + /// Stores state for the signal implementation. + /// private readonly IObserver _observer; + + /// + /// Stores state for the signal implementation. + /// private readonly Func _predicate; + + /// + /// Stores state for the signal implementation. + /// private bool _stopped; + /// + /// Initializes a new instance of the class. + /// + /// The observer value. + /// The predicate value. public KeepObserver(IObserver observer, Func predicate) { _observer = observer; _predicate = predicate; } + /// + /// Executes the OnCompleted operation. + /// public void OnCompleted() { - if (!_stopped) + if (_stopped) { - _stopped = true; - _observer.OnCompleted(); + return; } + + _stopped = true; + _observer.OnCompleted(); } + /// + /// Executes the OnError operation. + /// + /// The error value. public void OnError(Exception error) { - if (!_stopped) + if (_stopped) { - _stopped = true; - _observer.OnError(error); + return; } + + _stopped = true; + _observer.OnError(error); } + /// + /// Executes the OnNext operation. + /// + /// The value. public void OnNext(T value) { if (_stopped) @@ -79,10 +138,12 @@ public void OnNext(T value) return; } - if (keep) + if (!keep) { - _observer.OnNext(value); + return; } + + _observer.OnNext(value); } } } diff --git a/src/ReactiveUI.Primitives/Signal/MapSignal{TSource,TResult}.cs b/src/ReactiveUI.Primitives/Signal/MapSignal{TSource,TResult}.cs index cea9d72..7601281 100644 --- a/src/ReactiveUI.Primitives/Signal/MapSignal{TSource,TResult}.cs +++ b/src/ReactiveUI.Primitives/Signal/MapSignal{TSource,TResult}.cs @@ -6,21 +6,47 @@ namespace ReactiveUI.Primitives.Signals; +/// +/// Represents the MapSignal class. +/// +/// The TSource type. +/// The TResult type. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] -internal sealed class MapSignal : IObservable, IRequireCurrentThread +internal sealed class MapSignal : IRequireCurrentThread { + /// + /// Stores state for the signal implementation. + /// private readonly IObservable _source; + + /// + /// Stores state for the signal implementation. + /// private readonly Func _selector; + /// + /// Initializes a new instance of the class. + /// + /// The source value. + /// The selector value. public MapSignal(IObservable source, Func selector) { _source = source; _selector = selector; } + /// + /// Executes the IsRequiredSubscribeOnCurrentThread operation. + /// + /// The result. public bool IsRequiredSubscribeOnCurrentThread() => _source is IRequireCurrentThread currentThread && currentThread.IsRequiredSubscribeOnCurrentThread(); + /// + /// Executes the Subscribe operation. + /// + /// The observer value. + /// The result. public IDisposable Subscribe(IObserver observer) { if (observer == null) @@ -31,36 +57,70 @@ public IDisposable Subscribe(IObserver observer) return _source.Subscribe(new MapObserver(observer, _selector)); } + /// + /// Represents the MapObserver class. + /// private sealed class MapObserver : IObserver { + /// + /// Stores state for the signal implementation. + /// private readonly IObserver _observer; + + /// + /// Stores state for the signal implementation. + /// private readonly Func _selector; + + /// + /// Stores state for the signal implementation. + /// private bool _stopped; + /// + /// Initializes a new instance of the class. + /// + /// The observer value. + /// The selector value. public MapObserver(IObserver observer, Func selector) { _observer = observer; _selector = selector; } + /// + /// Executes the OnCompleted operation. + /// public void OnCompleted() { - if (!_stopped) + if (_stopped) { - _stopped = true; - _observer.OnCompleted(); + return; } + + _stopped = true; + _observer.OnCompleted(); } + /// + /// Executes the OnError operation. + /// + /// The error value. public void OnError(Exception error) { - if (!_stopped) + if (_stopped) { - _stopped = true; - _observer.OnError(error); + return; } + + _stopped = true; + _observer.OnError(error); } + /// + /// Executes the OnNext operation. + /// + /// The value. public void OnNext(TSource value) { if (_stopped) diff --git a/src/ReactiveUI.Primitives/Signal/ReadOnlyState{T}.cs b/src/ReactiveUI.Primitives/Signal/ReadOnlyState{T}.cs index 5f44475..83c3cde 100644 --- a/src/ReactiveUI.Primitives/Signal/ReadOnlyState{T}.cs +++ b/src/ReactiveUI.Primitives/Signal/ReadOnlyState{T}.cs @@ -13,7 +13,14 @@ namespace ReactiveUI.Primitives.Signals; [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public sealed class ReadOnlyState : IObservable, IDisposable { + /// + /// Stores state for the signal implementation. + /// private readonly StateSignal _inner; + + /// + /// Stores state for the signal implementation. + /// private readonly IDisposable _subscription; /// @@ -42,10 +49,15 @@ public ReadOnlyState(IObservable source, T initialValue) /// public IObservable Changed => _inner; - /// + /// + /// Executes the Subscribe operation. + /// + /// The result. public IDisposable Subscribe(IObserver observer) => _inner.Subscribe(observer); - /// + /// + /// Executes the Dispose operation. + /// public void Dispose() { _subscription.Dispose(); @@ -83,9 +95,12 @@ public static ReadOnlyState ToReadOnlyState( throw new ArgumentNullException(nameof(selector)); } - return new ReadOnlyState(ReactiveUI.Primitives.Signals.Signal.CreateSafe(observer => source.Subscribe( - value => observer.OnNext(selector(value)), - observer.OnError, - observer.OnCompleted)), initialValue); + return new ReadOnlyState( + ReactiveUI.Primitives.Signals.Signal.CreateSafe( + observer => source.Subscribe( + value => observer.OnNext(selector(value)), + observer.OnError, + observer.OnCompleted)), + initialValue); } } diff --git a/src/ReactiveUI.Primitives/Signal/ReplaySignal{T}.cs b/src/ReactiveUI.Primitives/Signal/ReplaySignal{T}.cs index b806150..30e522f 100644 --- a/src/ReactiveUI.Primitives/Signal/ReplaySignal{T}.cs +++ b/src/ReactiveUI.Primitives/Signal/ReplaySignal{T}.cs @@ -15,18 +15,72 @@ namespace ReactiveUI.Primitives.Signals; [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public class ReplaySignal : ISignal { + /// + /// Stores state for the signal implementation. + /// private readonly int _bufferSize; + + /// + /// Stores state for the signal implementation. + /// private readonly TimeSpan _window; + + /// + /// Stores state for the signal implementation. + /// private readonly DateTimeOffset _startTime; + + /// + /// Stores state for the signal implementation. + /// private readonly ISequencer _scheduler; + + /// + /// Stores state for the signal implementation. + /// private readonly bool _usesWindow; + + /// + /// Executes the new operation. + /// + /// The result. private readonly object _observerLock = new(); +#pragma warning disable S3459 // Broadcaster is a mutable struct whose default value is the empty broadcaster. + + /// + /// Stores state for the signal implementation. + /// private Broadcaster _broadcaster; +#pragma warning restore S3459 + + /// + /// Stores state for the signal implementation. + /// private bool _isStopped; + + /// + /// Stores state for the signal implementation. + /// private Exception? _lastError; + + /// + /// Stores state for the signal implementation. + /// private Queue>? _queue; + + /// + /// Stores state for the signal implementation. + /// private T[]? _ring; + + /// + /// Stores state for the signal implementation. + /// private int _ringCount; + + /// + /// Stores state for the signal implementation. + /// private int _ringNext; /// @@ -64,7 +118,7 @@ public ReplaySignal(int bufferSize, TimeSpan window, ISequencer scheduler) } else { - _ring = bufferSize == 0 ? Array.Empty() : new T[bufferSize]; + _ring = bufferSize == 0 ? [] : new T[bufferSize]; } } @@ -237,7 +291,6 @@ public void OnNext(T value) _queue!.Enqueue(new TimeInterval(value, interval)); Trim(); } - } _broadcaster.Next(value); @@ -305,33 +358,43 @@ public IDisposable Subscribe(IObserver observer) /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool disposing) { - if (!IsDisposed) + if (IsDisposed) { - if (disposing) + return; + } + + if (disposing) + { + lock (_observerLock) { - lock (_observerLock) - { - _broadcaster.Clear(); - _lastError = null; - _queue = null; - _ring = null; - _ringCount = 0; - _ringNext = 0; - } + _broadcaster.Clear(); + _lastError = null; + _queue = null; + _ring = null; + _ringCount = 0; + _ringNext = 0; } - - IsDisposed = true; } + + IsDisposed = true; } + /// + /// Executes the ThrowIfDisposed operation. + /// private void ThrowIfDisposed() { - if (IsDisposed) + if (!IsDisposed) { - throw new ObjectDisposedException(string.Empty); + return; } + + throw new ObjectDisposedException(string.Empty); } + /// + /// Executes the Trim operation. + /// private void Trim() { while (_queue!.Count > _bufferSize) @@ -352,6 +415,10 @@ private void Trim() } } + /// + /// Executes the AppendToRing operation. + /// + /// The value. private void AppendToRing(T value) { var ring = _ring!; @@ -367,12 +434,18 @@ private void AppendToRing(T value) _ringNext = 0; } - if (_ringCount < ring.Length) + if (_ringCount >= ring.Length) { - _ringCount++; + return; } + + _ringCount++; } + /// + /// Executes the ReplayRing operation. + /// + /// The observer value. private void ReplayRing(IObserver observer) { var ring = _ring!; @@ -398,18 +471,41 @@ private void ReplayRing(IObserver observer) } } - private class ObserverHandler : IDisposable + /// + /// Represents the ObserverHandler class. + /// + private sealed class ObserverHandler : IDisposable { + /// + /// Executes the new operation. + /// + /// The result. private readonly object _lock = new(); + + /// + /// Stores state for the signal implementation. + /// private ReplaySignal? _subject; + + /// + /// Stores state for the signal implementation. + /// private IObserver? _observer; + /// + /// Initializes a new instance of the class. + /// + /// The subject value. + /// The observer value. public ObserverHandler(ReplaySignal subject, IObserver observer) { _subject = subject; _observer = observer; } + /// + /// Executes the Dispose operation. + /// public void Dispose() { lock (_lock) diff --git a/src/ReactiveUI.Primitives/Signal/Signal{T}.cs b/src/ReactiveUI.Primitives/Signal/Signal{T}.cs index b1c75b4..d303ea5 100644 --- a/src/ReactiveUI.Primitives/Signal/Signal{T}.cs +++ b/src/ReactiveUI.Primitives/Signal/Signal{T}.cs @@ -14,17 +14,66 @@ namespace ReactiveUI.Primitives.Signals; [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public class Signal : ISignal { + /// + /// Stores state for the signal implementation. + /// + private const int InitialSubscriptionCapacity = 4; + + /// + /// Stores state for the signal implementation. + /// private static readonly Action NoopOnNext = static _ => { }; + + /// + /// Executes the ThrowDisposed operation. + /// + /// The result. private static readonly Action ThrowDisposedOnNext = static _ => ThrowDisposed(); + /// + /// Executes the new operation. + /// + /// The result. private readonly object _observerLock = new(); + + /// + /// Stores state for the signal implementation. + /// private Exception? _exception; + + /// + /// Stores state for the signal implementation. + /// private SignalSubscription? _singleActionSubscription; + + /// + /// Stores state for the signal implementation. + /// private SignalSubscription?[]? _subscriptions; + + /// + /// Stores state for the signal implementation. + /// private int _subscriptionCount; + + /// + /// Stores state for the signal implementation. + /// private int _subscriptionTail; + + /// + /// Stores state for the signal implementation. + /// private Action _onNext = NoopOnNext; + + /// + /// Stores state for the signal implementation. + /// private bool _isDisposed; + + /// + /// Stores state for the signal implementation. + /// private bool _isStopped; /// @@ -97,10 +146,12 @@ public void OnError(Exception error) } Error(subscriptions, error); - if (hasActionSubscribers) + if (!hasActionSubscribers) { - ExceptionDispatchInfo.Capture(error).Throw(); + return; } + + ExceptionDispatchInfo.Capture(error).Throw(); } /// @@ -158,6 +209,11 @@ public IDisposable Subscribe(IObserver observer) return Disposable.Empty; } + /// + /// Executes the SubscribeAction operation. + /// + /// The onNext value. + /// The result. internal IDisposable SubscribeAction(Action onNext) { if (onNext == null) @@ -210,23 +266,40 @@ internal IDisposable SubscribeAction(Action onNext) /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool disposing) { - if (!IsDisposed) + if (IsDisposed) { - if (disposing) - { - lock (_observerLock) - { - ClearObserversLocked(); - _exception = null; - _onNext = ThrowDisposedOnNext; - _isDisposed = true; - } - } + return; + } + + if (!disposing) + { + return; + } + + SignalSubscription? singleActionSubscription; + SignalSubscription?[]? subscriptions; + lock (_observerLock) + { + singleActionSubscription = _singleActionSubscription; + subscriptions = ClearObserversLocked(); + _exception = null; + _onNext = ThrowDisposedOnNext; + _isDisposed = true; } + + singleActionSubscription?.Dispose(); + DisposeSubscriptions(subscriptions); } + /// + /// Executes the ThrowDisposed operation. + /// private static void ThrowDisposed() => throw new ObjectDisposedException(string.Empty); + /// + /// Executes the Completed operation. + /// + /// The subscriptions value. private static void Completed(SignalSubscription?[]? subscriptions) { if (subscriptions == null) @@ -240,7 +313,12 @@ private static void Completed(SignalSubscription?[]? subscriptions) } } - private static void Error(SignalSubscription?[]? subscriptions, Exception error) + /// + /// Executes the Error operation. + /// + /// The subscriptions value. + /// The exception value. + private static void Error(SignalSubscription?[]? subscriptions, Exception exception) { if (subscriptions == null) { @@ -249,10 +327,15 @@ private static void Error(SignalSubscription?[]? subscriptions, Exception error) for (var i = 0; i < subscriptions.Length; i++) { - subscriptions[i]?.Observer?.OnError(error); + subscriptions[i]?.Observer?.OnError(exception); } } + /// + /// Executes the HasActionSubscribers operation. + /// + /// The subscriptions value. + /// The result. private static bool HasActionSubscribers(SignalSubscription?[]? subscriptions) { if (subscriptions == null) @@ -271,20 +354,46 @@ private static bool HasActionSubscribers(SignalSubscription?[]? subscriptions) return false; } + /// + /// Executes the DisposeSubscriptions operation. + /// + /// The subscriptions value. + private static void DisposeSubscriptions(SignalSubscription?[]? subscriptions) + { + if (subscriptions == null) + { + return; + } + + for (var i = 0; i < subscriptions.Length; i++) + { + subscriptions[i]?.Dispose(); + } + } + + /// + /// Executes the ThrowIfDisposed operation. + /// private void ThrowIfDisposed() { - if (IsDisposed) + if (!IsDisposed) { - ThrowDisposed(); + return; } + + ThrowDisposed(); } + /// + /// Executes the AddSubscriptionLocked operation. + /// + /// The subscription value. private void AddSubscriptionLocked(SignalSubscription subscription) { var subscriptions = _subscriptions; if (subscriptions == null) { - subscriptions = new SignalSubscription[4]; + subscriptions = new SignalSubscription[InitialSubscriptionCapacity]; Volatile.Write(ref _subscriptions, subscriptions); } @@ -315,6 +424,10 @@ private void AddSubscriptionLocked(SignalSubscription subscription) _subscriptionCount++; } + /// + /// Executes the ClearObserversLocked operation. + /// + /// The result. private SignalSubscription?[]? ClearObserversLocked() { _singleActionSubscription = null; @@ -326,6 +439,9 @@ private void AddSubscriptionLocked(SignalSubscription subscription) return subscriptions; } + /// + /// Executes the PromoteSingleActionObserverLocked operation. + /// private void PromoteSingleActionObserverLocked() { var single = _singleActionSubscription; @@ -338,6 +454,10 @@ private void PromoteSingleActionObserverLocked() AddSubscriptionLocked(single); } + /// + /// Executes the Remove operation. + /// + /// The subscription value. private void Remove(SignalSubscription subscription) { lock (_observerLock) @@ -368,6 +488,10 @@ private void Remove(SignalSubscription subscription) } } + /// + /// Executes the DispatchSubscriptions operation. + /// + /// The value. private void DispatchSubscriptions(T value) { var subscriptions = Volatile.Read(ref _subscriptions); @@ -396,10 +520,21 @@ private void DispatchSubscriptions(T value) } } + /// + /// Represents the SignalSubscription class. + /// private sealed class SignalSubscription : IDisposable { + /// + /// Stores state for the signal implementation. + /// private Signal? _subject; + /// + /// Initializes a new instance of the class. + /// + /// The subject value. + /// The observer value. public SignalSubscription(Signal subject, IObserver observer) { _subject = subject; @@ -407,6 +542,11 @@ public SignalSubscription(Signal subject, IObserver observer) Index = -1; } + /// + /// Initializes a new instance of the class. + /// + /// The subject value. + /// The onNext value. public SignalSubscription(Signal subject, Action onNext) { _subject = subject; @@ -414,12 +554,24 @@ public SignalSubscription(Signal subject, Action onNext) Index = -1; } + /// + /// Gets or sets the value. + /// public int Index { get; set; } + /// + /// Gets the value. + /// public IObserver? Observer { get; } + /// + /// Gets the value. + /// public Action? OnNext { get; } + /// + /// Executes the Dispose operation. + /// public void Dispose() { var subject = Interlocked.Exchange(ref _subject, null); diff --git a/src/ReactiveUI.Primitives/Signal/TaskSignal.cs b/src/ReactiveUI.Primitives/Signal/TaskSignal.cs index c43ab9b..c29e4e0 100644 --- a/src/ReactiveUI.Primitives/Signal/TaskSignal.cs +++ b/src/ReactiveUI.Primitives/Signal/TaskSignal.cs @@ -12,6 +12,31 @@ namespace ReactiveUI.Primitives.Signals; [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public static class TaskSignal { + /// + /// Creates the specified source. + /// + /// The type of the result. + /// The observable factory. + /// + /// An AsyncObservable. + /// + /// observableFactory. + public static ITaskSignal Create(Func, IObservable> observableFactory) => + Instance(observableFactory, null, null); + + /// + /// Creates the specified source. + /// + /// The type of the result. + /// The observable factory. + /// The scheduler. + /// + /// An AsyncObservable. + /// + /// observableFactory. + public static ITaskSignal Create(Func, IObservable> observableFactory, ISequencer? scheduler) => + Instance(observableFactory, scheduler, null); + /// /// Creates the specified source. /// @@ -23,10 +48,21 @@ public static class TaskSignal /// An AsyncObservable. /// /// observableFactory. - public static ITaskSignal Create(Func, IObservable> observableFactory, ISequencer? scheduler = null, CancellationTokenSource? cancellationTokenSource = null) => + public static ITaskSignal Create( + Func, IObservable> observableFactory, + ISequencer? scheduler, + CancellationTokenSource? cancellationTokenSource) => Instance(observableFactory, scheduler, cancellationTokenSource); - private static ITaskSignal Instance(Func, IObservable> observableFactory, ISequencer? scheduler, CancellationTokenSource? cancellationTokenSource) + /// + /// Executes the Instance operation. + /// + /// The TResult type. + /// The observableFactory value. + /// The scheduler value. + /// The cancellationTokenSource value. + /// The result. + private static TaskSignal Instance(Func, IObservable> observableFactory, ISequencer? scheduler, CancellationTokenSource? cancellationTokenSource) { if (observableFactory is null) { diff --git a/src/ReactiveUI.Primitives/Signal/TaskSignal{T}.cs b/src/ReactiveUI.Primitives/Signal/TaskSignal{T}.cs index 3315dd7..cb6f9c9 100644 --- a/src/ReactiveUI.Primitives/Signal/TaskSignal{T}.cs +++ b/src/ReactiveUI.Primitives/Signal/TaskSignal{T}.cs @@ -12,18 +12,26 @@ namespace ReactiveUI.Primitives.Signals; /// /// The object that provides notification information. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] -internal class TaskSignal : ITaskSignal +internal sealed class TaskSignal : ITaskSignal { - private readonly ISequencer _scheduler; + /// + /// Stores state for the signal implementation. + /// + private readonly ISequencer _sequencer; + + /// + /// Executes the new operation. + /// + /// The result. private readonly MultipleDisposable? _cleanUp = new(); /// /// Initializes a new instance of the class. /// /// The observable factory. - /// The scheduler. + /// The sequencer. /// The cancellation token source. - public TaskSignal(Func, IObservable> observableFactory, ISequencer? scheduler = null, CancellationTokenSource? cancellationTokenSource = null) + public TaskSignal(Func, IObservable> observableFactory, ISequencer? sequencer = null, CancellationTokenSource? cancellationTokenSource = null) { if (observableFactory is null) { @@ -31,7 +39,7 @@ public TaskSignal(Func, IObservable> observableFactory, ISeque } CancellationTokenSource = cancellationTokenSource ?? new(); - _scheduler = scheduler ?? CurrentThreadSequencer.Instance; + _sequencer = sequencer ?? CurrentThreadSequencer.Instance; Source = observableFactory(this); } @@ -77,7 +85,7 @@ public void GetOperationCanceled(IObserver observer) => /// The observer. /// A Disposable. public IDisposable Subscribe(IObserver observer) => - Source!.WitnessOn(_scheduler).Subscribe(observer).DisposeWith(_cleanUp!); + Source!.WitnessOn(_sequencer).Subscribe(observer).DisposeWith(_cleanUp!); /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. @@ -85,27 +93,29 @@ public IDisposable Subscribe(IObserver observer) => public void Dispose() { Dispose(true); - GC.SuppressFinalize(this); } /// /// Releases unmanaged and - optionally - managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected virtual void Dispose(bool disposing) + private void Dispose(bool disposing) { - if (_cleanUp?.IsDisposed == false && disposing) + if (_cleanUp?.IsDisposed != false || !disposing) { - try - { - CancellationTokenSource?.Cancel(); - } - catch (ObjectDisposedException) - { - } - - _cleanUp?.Dispose(); - CancellationTokenSource?.Dispose(); + return; } + + try + { + CancellationTokenSource?.Cancel(); + } + catch (ObjectDisposedException) + { + // The token source can be disposed by the task completion path. + } + + _cleanUp?.Dispose(); + CancellationTokenSource?.Dispose(); } } diff --git a/src/ReactiveUI.Primitives/SignalOperatorMixins.Coordinators.cs b/src/ReactiveUI.Primitives/SignalOperatorMixins.Coordinators.cs new file mode 100644 index 0000000..97e051a --- /dev/null +++ b/src/ReactiveUI.Primitives/SignalOperatorMixins.Coordinators.cs @@ -0,0 +1,687 @@ +// 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.Disposables; + +namespace ReactiveUI.Primitives; + +/// +/// Coordinator helpers for multi-source signal operators. +/// +public static partial class LinqMixins +{ + /// + /// Coordinates race subscriptions and forwards only the winning source. + /// + /// The source value type. + private sealed class RaceCoordinator : IDisposable + { + /// + /// The downstream observer. + /// + private readonly IObserver _observer; + + /// + /// The active subscriptions. + /// + private readonly MultipleDisposable _subscriptions = new(); + + /// + /// The winning source index. + /// + private int _winner = -1; + + /// + /// The next source index. + /// + private int _index; + + /// + /// Initializes a new instance of the class. + /// + /// The downstream observer. + internal RaceCoordinator(IObserver observer) => _observer = observer; + + /// + /// Releases the active subscriptions. + /// + public void Dispose() => _subscriptions.Dispose(); + + /// + /// Starts observing the candidate source streams. + /// + /// The candidate source streams. + /// The coordinator that owns the subscription cleanup. + internal RaceCoordinator Run(IObservable> sources) + { + _subscriptions.Add(sources.Subscribe(OnSource, _observer.OnError, OnOuterCompleted)); + return this; + } + + /// + /// Forwards a value from a candidate source. + /// + /// The candidate source index. + /// The value to forward. + private void OnNext(int candidate, T value) + { + if (!Win(candidate)) + { + return; + } + + _observer.OnNext(value); + } + + /// + /// Forwards an error from a candidate source. + /// + /// The candidate source index. + /// The error to forward. + private void OnError(int candidate, Exception error) + { + if (!Win(candidate)) + { + return; + } + + _observer.OnError(error); + } + + /// + /// Forwards completion from a candidate source. + /// + /// The candidate source index. + private void OnCompleted(int candidate) + { + if (!Win(candidate)) + { + return; + } + + _observer.OnCompleted(); + } + + /// + /// Handles completion of the outer sequence. + /// + private void OnOuterCompleted() + { + // Race completion is controlled by the first inner source to win. + } + + /// + /// Subscribes to a candidate source. + /// + /// The source to observe. + private void OnSource(IObservable source) + { + var current = Interlocked.Increment(ref _index) - 1; + _subscriptions.Add(source.Subscribe( + value => OnNext(current, value), + error => OnError(current, error), + () => OnCompleted(current))); + } + + /// + /// Attempts to make a candidate source the winner. + /// + /// The candidate source index. + /// true when the candidate is the winning source; otherwise, false. + private bool Win(int candidate) + { + var current = Volatile.Read(ref _winner); + if (current == candidate) + { + return true; + } + + if (current >= 0) + { + return false; + } + + return Interlocked.CompareExchange(ref _winner, candidate, -1) == -1; + } + } + + /// + /// Coordinates a two-source zip operation. + /// + /// The left value type. + /// The right value type. + /// The result value type. + private sealed class ZipCoordinator + { + /// + /// The synchronization gate. + /// + private readonly OperatorGate _gate = new(); + + /// + /// The downstream observer. + /// + private readonly IObserver _observer; + + /// + /// The projection function. + /// + private readonly Func _selector; + + /// + /// The queued left values. + /// + private readonly Queue _leftQueue = new(); + + /// + /// The queued right values. + /// + private readonly Queue _rightQueue = new(); + + /// + /// A value indicating whether the left source completed. + /// + private bool _leftCompleted; + + /// + /// A value indicating whether the right source completed. + /// + private bool _rightCompleted; + + /// + /// Initializes a new instance of the class. + /// + /// The downstream observer. + /// The projection function. + internal ZipCoordinator(IObserver observer, Func selector) + { + _observer = observer; + _selector = selector; + } + + /// + /// Subscribes to both zip sources. + /// + /// The left source. + /// The right source. + /// The subscription cleanup. + internal MultipleDisposable Run(IObservable left, IObservable right) => + new( + left.Subscribe(OnLeftNext, _observer.OnError, OnLeftCompleted), + right.Subscribe(OnRightNext, _observer.OnError, OnRightCompleted)); + + /// + /// Queues a left value. + /// + /// The value to queue. + private void OnLeftNext(TLeft value) + { + lock (_gate.SyncRoot) + { + _leftQueue.Enqueue(value); + } + + Drain(); + } + + /// + /// Queues a right value. + /// + /// The value to queue. + private void OnRightNext(TRight value) + { + lock (_gate.SyncRoot) + { + _rightQueue.Enqueue(value); + } + + Drain(); + } + + /// + /// Marks the left source as complete. + /// + private void OnLeftCompleted() + { + lock (_gate.SyncRoot) + { + _leftCompleted = true; + } + + Drain(); + } + + /// + /// Marks the right source as complete. + /// + private void OnRightCompleted() + { + lock (_gate.SyncRoot) + { + _rightCompleted = true; + } + + Drain(); + } + + /// + /// Emits all currently available pairs. + /// + private void Drain() + { + while (TryTake(out var left, out var right)) + { + _observer.OnNext(_selector(left, right)); + } + } + + /// + /// Attempts to remove the next available pair from the queues. + /// + /// The left value. + /// The right value. + /// true when a pair was available; otherwise, false. + private bool TryTake(out TLeft left, out TRight right) + { + lock (_gate.SyncRoot) + { + if (_leftQueue.Count != 0 && _rightQueue.Count != 0) + { + left = _leftQueue.Dequeue(); + right = _rightQueue.Dequeue(); + return true; + } + + if ((_leftCompleted && _leftQueue.Count == 0) || (_rightCompleted && _rightQueue.Count == 0)) + { + _observer.OnCompleted(); + } + + left = default!; + right = default!; + return false; + } + } + } + + /// + /// Coordinates a two-source combine-latest operation. + /// + /// The left value type. + /// The right value type. + /// The result value type. + private sealed class CombineLatestCoordinator + { + /// + /// The synchronization gate. + /// + private readonly OperatorGate _gate = new(); + + /// + /// The downstream observer. + /// + private readonly IObserver _observer; + + /// + /// The projection function. + /// + private readonly Func _selector; + + /// + /// A value indicating whether the left source has produced a value. + /// + private bool _hasLeft; + + /// + /// A value indicating whether the right source has produced a value. + /// + private bool _hasRight; + + /// + /// A value indicating whether the left source completed. + /// + private bool _leftDone; + + /// + /// A value indicating whether the right source completed. + /// + private bool _rightDone; + + /// + /// The latest left value. + /// + private TLeft? _latestLeft; + + /// + /// The latest right value. + /// + private TRight? _latestRight; + + /// + /// Initializes a new instance of the class. + /// + /// The downstream observer. + /// The projection function. + internal CombineLatestCoordinator(IObserver observer, Func selector) + { + _observer = observer; + _selector = selector; + } + + /// + /// Subscribes to both combine-latest sources. + /// + /// The left source. + /// The right source. + /// The subscription cleanup. + internal MultipleDisposable Run(IObservable left, IObservable right) => + new( + left.Subscribe(OnLeftNext, _observer.OnError, OnLeftCompleted), + right.Subscribe(OnRightNext, _observer.OnError, OnRightCompleted)); + + /// + /// Handles a left value. + /// + /// The left value. + private void OnLeftNext(TLeft value) + { + if (!TryUpdateLeft(value, out var projected)) + { + return; + } + + _observer.OnNext(projected); + } + + /// + /// Handles a right value. + /// + /// The right value. + private void OnRightNext(TRight value) + { + if (!TryUpdateRight(value, out var projected)) + { + return; + } + + _observer.OnNext(projected); + } + + /// + /// Marks the left source as complete. + /// + private void OnLeftCompleted() + { + if (!CompleteLeft()) + { + return; + } + + _observer.OnCompleted(); + } + + /// + /// Marks the right source as complete. + /// + private void OnRightCompleted() + { + if (!CompleteRight()) + { + return; + } + + _observer.OnCompleted(); + } + + /// + /// Updates the latest left value. + /// + /// The new value. + /// The projected result. + /// true when a result is available; otherwise, false. + private bool TryUpdateLeft(TLeft value, out TResult result) + { + lock (_gate.SyncRoot) + { + _latestLeft = value; + _hasLeft = true; + return TryProject(out result); + } + } + + /// + /// Updates the latest right value. + /// + /// The new value. + /// The projected result. + /// true when a result is available; otherwise, false. + private bool TryUpdateRight(TRight value, out TResult result) + { + lock (_gate.SyncRoot) + { + _latestRight = value; + _hasRight = true; + return TryProject(out result); + } + } + + /// + /// Marks the left source as complete. + /// + /// true when both sources are complete; otherwise, false. + private bool CompleteLeft() + { + lock (_gate.SyncRoot) + { + _leftDone = true; + return _rightDone; + } + } + + /// + /// Marks the right source as complete. + /// + /// true when both sources are complete; otherwise, false. + private bool CompleteRight() + { + lock (_gate.SyncRoot) + { + _rightDone = true; + return _leftDone; + } + } + + /// + /// Projects the current latest values. + /// + /// The projected value. + /// true when both sources have values; otherwise, false. + private bool TryProject(out TResult result) + { + if (!_hasLeft || !_hasRight) + { + result = default!; + return false; + } + + result = _selector(_latestLeft!, _latestRight!); + return true; + } + } + + /// + /// Coordinates a switch operation. + /// + /// The source value type. + private sealed class SwitchCoordinator : IDisposable + { + /// + /// The synchronization gate. + /// + private readonly OperatorGate _gate = new(); + + /// + /// The downstream observer. + /// + private readonly IObserver _observer; + + /// + /// The active subscriptions. + /// + private readonly MultipleDisposable _subscriptions = new(); + + /// + /// The active inner subscription. + /// + private readonly SingleReplaceableDisposable _innerSlot = new(); + + /// + /// A value indicating whether the outer source completed. + /// + private bool _outerCompleted; + + /// + /// A value indicating whether an inner source is active. + /// + private bool _innerActive; + + /// + /// The current inner source version. + /// + private int _version; + + /// + /// Initializes a new instance of the class. + /// + /// The downstream observer. + internal SwitchCoordinator(IObserver observer) => _observer = observer; + + /// + /// Releases the active subscriptions. + /// + public void Dispose() + { + _innerSlot.Dispose(); + _subscriptions.Dispose(); + } + + /// + /// Subscribes to the outer source. + /// + /// The outer source. + /// The coordinator that owns the subscription cleanup. + internal SwitchCoordinator Run(IObservable> sources) + { + _subscriptions.Add(_innerSlot); + _subscriptions.Add(sources.Subscribe(OnSource, _observer.OnError, OnOuterCompleted)); + return this; + } + + /// + /// Switches to a new inner source. + /// + /// The new inner source. + private void OnSource(IObservable source) + { + int current; + lock (_gate.SyncRoot) + { + current = ++_version; + _innerActive = true; + } + + _innerSlot.Create(source.Subscribe( + value => OnNext(current, value), + error => OnError(current, error), + () => OnCompleted(current))); + } + + /// + /// Marks the outer source as complete. + /// + private void OnOuterCompleted() + { + lock (_gate.SyncRoot) + { + _outerCompleted = true; + } + + TryComplete(); + } + + /// + /// Forwards an inner value when it belongs to the current source. + /// + /// The inner version. + /// The value to forward. + private void OnNext(int version, T value) + { + if (!IsCurrent(version)) + { + return; + } + + _observer.OnNext(value); + } + + /// + /// Forwards an inner error when it belongs to the current source. + /// + /// The inner version. + /// The error to forward. + private void OnError(int version, Exception error) + { + if (!IsCurrent(version)) + { + return; + } + + _observer.OnError(error); + } + + /// + /// Completes an inner source when it belongs to the current source. + /// + /// The inner version. + private void OnCompleted(int version) + { + lock (_gate.SyncRoot) + { + if (version == _version) + { + _innerActive = false; + } + } + + TryComplete(); + } + + /// + /// Determines whether a version is the current inner source. + /// + /// The candidate version. + /// true if the version is current; otherwise, false. + private bool IsCurrent(int version) + { + lock (_gate.SyncRoot) + { + return version == _version; + } + } + + /// + /// Completes the observer when both outer and inner sources are complete. + /// + private void TryComplete() + { + lock (_gate.SyncRoot) + { + if (_outerCompleted && !_innerActive) + { + _observer.OnCompleted(); + } + } + } + } +} diff --git a/src/ReactiveUI.Primitives/SignalOperatorMixins.cs b/src/ReactiveUI.Primitives/SignalOperatorMixins.cs index 2dac042..1af73ae 100644 --- a/src/ReactiveUI.Primitives/SignalOperatorMixins.cs +++ b/src/ReactiveUI.Primitives/SignalOperatorMixins.cs @@ -2,7 +2,6 @@ // ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Collections.Concurrent; using ReactiveUI.Primitives.Concurrency; using ReactiveUI.Primitives.Core; using ReactiveUI.Primitives.Disposables; @@ -94,10 +93,12 @@ public static IObservable KeepNotNull(this IObservable source) return Signal.CreateSafe(observer => source.Subscribe( value => { - if (value != null) + if (value == null) { - observer.OnNext(value); + return; } + + observer.OnNext(value); }, observer.OnError, observer.OnCompleted)); @@ -106,6 +107,10 @@ public static IObservable KeepNotNull(this IObservable source) /// /// Projects only values assignable to . /// + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Major Code Smell", + "S4018:Generic methods should provide type parameters", + Justification = "LINQ-style OfType requires the caller to provide the result type.")] public static IObservable OfType(this IObservable source) { if (source == null) @@ -116,10 +121,12 @@ public static IObservable OfType(this IObservable sou return Signal.CreateSafe(observer => source.Subscribe( value => { - if (value is TResult result) + if (value is not TResult result) { - observer.OnNext(result); + return; } + + observer.OnNext(result); }, observer.OnError, observer.OnCompleted)); @@ -128,6 +135,10 @@ public static IObservable OfType(this IObservable sou /// /// Casts every value to . /// + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Major Code Smell", + "S4018:Generic methods should provide type parameters", + Justification = "LINQ-style Cast requires the caller to provide the result type.")] public static IObservable Cast(this IObservable source) { if (source == null) @@ -260,10 +271,12 @@ public static IObservable Take(this IObservable source, int count) observer.OnNext(value); remaining--; - if (remaining == 0) + if (remaining != 0) { - observer.OnCompleted(); + return; } + + observer.OnCompleted(); }, observer.OnError, observer.OnCompleted); @@ -307,7 +320,13 @@ public static IObservable Skip(this IObservable source, int count) /// /// Suppresses duplicate values according to the comparer. /// - public static IObservable Distinct(this IObservable source, IEqualityComparer? comparer = null) + public static IObservable Distinct(this IObservable source) => + source.Distinct(null); + + /// + /// Suppresses duplicate values according to the comparer. + /// + public static IObservable Distinct(this IObservable source, IEqualityComparer? comparer) { if (source == null) { @@ -320,10 +339,12 @@ public static IObservable Distinct(this IObservable source, IEqualityCo return source.Subscribe( value => { - if (seen.Add(value)) + if (!seen.Add(value)) { - observer.OnNext(value); + return; } + + observer.OnNext(value); }, observer.OnError, observer.OnCompleted); @@ -333,7 +354,13 @@ public static IObservable Distinct(this IObservable source, IEqualityCo /// /// Suppresses adjacent duplicate values according to the comparer. /// - public static IObservable DistinctUntilChanged(this IObservable source, IEqualityComparer? comparer = null) + public static IObservable DistinctUntilChanged(this IObservable source) => + source.DistinctUntilChanged(null); + + /// + /// Suppresses adjacent duplicate values according to the comparer. + /// + public static IObservable DistinctUntilChanged(this IObservable source, IEqualityComparer? comparer) { if (source == null) { @@ -348,12 +375,14 @@ public static IObservable DistinctUntilChanged(this IObservable source, return source.Subscribe( value => { - if (!hasLast || !comparer.Equals(last!, value)) + if (hasLast && comparer.Equals(last!, value)) { - hasLast = true; - last = value; - observer.OnNext(value); + return; } + + hasLast = true; + last = value; + observer.OnNext(value); }, observer.OnError, observer.OnCompleted); @@ -412,7 +441,7 @@ public static IObservable Concat(this IObservable> sources) return Signal.Create(observer => { - var gate = new object(); + var gate = new OperatorGate(); var queue = new Queue>(); var pocket = new MultipleDisposable(); var active = false; @@ -421,7 +450,7 @@ public static IObservable Concat(this IObservable> sources) void Drain() { IObservable? next = null; - lock (gate) + lock (gate.SyncRoot) { if (active) { @@ -440,21 +469,23 @@ void Drain() } } - if (next != null) + if (next == null) { - pocket.Add(next.Subscribe( - observer.OnNext, - observer.OnError, - () => + return; + } + + pocket.Add(next.Subscribe( + observer.OnNext, + observer.OnError, + () => + { + lock (gate.SyncRoot) { - lock (gate) - { - active = false; - } + active = false; + } - Drain(); - })); - } + Drain(); + })); } pocket.Add(sources.Subscribe( @@ -466,7 +497,7 @@ void Drain() return; } - lock (gate) + lock (gate.SyncRoot) { queue.Enqueue(source); } @@ -476,7 +507,7 @@ void Drain() observer.OnError, () => { - lock (gate) + lock (gate.SyncRoot) { outerCompleted = true; } @@ -506,14 +537,14 @@ public static IObservable Merge(this IObservable> sources) return Signal.Create(observer => { - var gate = new object(); + var gate = new OperatorGate(); var pocket = new MultipleDisposable(); var outerCompleted = false; var active = 0; void TryComplete() { - lock (gate) + lock (gate.SyncRoot) { if (outerCompleted && active == 0) { @@ -531,7 +562,7 @@ void TryComplete() return; } - lock (gate) + lock (gate.SyncRoot) { active++; } @@ -541,7 +572,7 @@ void TryComplete() observer.OnError, () => { - lock (gate) + lock (gate.SyncRoot) { active--; } @@ -552,7 +583,7 @@ void TryComplete() observer.OnError, () => { - lock (gate) + lock (gate.SyncRoot) { outerCompleted = true; } @@ -574,55 +605,7 @@ public static IObservable Race(this IObservable> sources) throw new ArgumentNullException(nameof(sources)); } - return Signal.Create(observer => - { - var gate = new object(); - var pocket = new MultipleDisposable(); - var winner = -1; - var index = 0; - - pocket.Add(sources.Subscribe(source => - { - var current = index++; - pocket.Add(source.Subscribe( - value => - { - if (Win(current)) - { - observer.OnNext(value); - } - }, - error => - { - if (Win(current)) - { - observer.OnError(error); - } - }, - () => - { - if (Win(current)) - { - observer.OnCompleted(); - } - })); - }, observer.OnError, () => { })); - - return pocket; - - bool Win(int candidate) - { - lock (gate) - { - if (winner < 0) - { - winner = candidate; - } - - return winner == candidate; - } - } - }); + return Signal.Create(observer => new RaceCoordinator(observer).Run(sources)); } /// @@ -645,44 +628,7 @@ public static IObservable Zip(this IObservable< throw new ArgumentNullException(nameof(selector)); } - return Signal.CreateSafe(observer => - { - var gate = new object(); - var leftQueue = new Queue(); - var rightQueue = new Queue(); - var leftCompleted = false; - var rightCompleted = false; - - void Drain() - { - while (true) - { - TLeft l; - TRight r; - lock (gate) - { - if (leftQueue.Count == 0 || rightQueue.Count == 0) - { - if ((leftCompleted && leftQueue.Count == 0) || (rightCompleted && rightQueue.Count == 0)) - { - observer.OnCompleted(); - } - - return; - } - - l = leftQueue.Dequeue(); - r = rightQueue.Dequeue(); - } - - observer.OnNext(selector(l, r)); - } - } - - return MultipleDisposable.Create( - left.Subscribe(value => { lock (gate) { leftQueue.Enqueue(value); } Drain(); }, observer.OnError, () => { lock (gate) { leftCompleted = true; } Drain(); }), - right.Subscribe(value => { lock (gate) { rightQueue.Enqueue(value); } Drain(); }, observer.OnError, () => { lock (gate) { rightCompleted = true; } Drain(); })); - }); + return Signal.CreateSafe(observer => new ZipCoordinator(observer, selector).Run(left, right)); } /// @@ -705,66 +651,7 @@ public static IObservable CombineLatest(this IO throw new ArgumentNullException(nameof(selector)); } - return Signal.CreateSafe(observer => - { - var gate = new object(); - var hasLeft = false; - var hasRight = false; - var leftDone = false; - var rightDone = false; - var latestLeft = default(TLeft); - var latestRight = default(TRight); - - void CompleteIfBothDone() - { - if (leftDone && rightDone) - { - observer.OnCompleted(); - } - } - - return MultipleDisposable.Create( - left.Subscribe(value => - { - TResult? projected = default; - var emit = false; - lock (gate) - { - latestLeft = value; - hasLeft = true; - if (hasRight) - { - projected = selector(latestLeft!, latestRight!); - emit = true; - } - } - - if (emit) - { - observer.OnNext(projected!); - } - }, observer.OnError, () => { lock (gate) { leftDone = true; CompleteIfBothDone(); } }), - right.Subscribe(value => - { - TResult? projected = default; - var emit = false; - lock (gate) - { - latestRight = value; - hasRight = true; - if (hasLeft) - { - projected = selector(latestLeft!, latestRight!); - emit = true; - } - } - - if (emit) - { - observer.OnNext(projected!); - } - }, observer.OnError, () => { lock (gate) { rightDone = true; CompleteIfBothDone(); } })); - }); + return Signal.CreateSafe(observer => new CombineLatestCoordinator(observer, selector).Run(left, right)); } /// @@ -789,26 +676,39 @@ public static IObservable WithLatest(this IObse return Signal.CreateSafe(observer => { - var gate = new object(); + var gate = new OperatorGate(); var hasRight = false; var latestRight = default(TRight); return MultipleDisposable.Create( - right.Subscribe(value => { lock (gate) { hasRight = true; latestRight = value; } }, observer.OnError, () => { }), - left.Subscribe(value => - { - TRight rightValue; - lock (gate) + right.Subscribe( + value => { - if (!hasRight) + lock (gate.SyncRoot) { - return; + hasRight = true; + latestRight = value; } + }, + observer.OnError, + () => { }), + left.Subscribe( + value => + { + TRight rightValue; + lock (gate.SyncRoot) + { + if (!hasRight) + { + return; + } - rightValue = latestRight!; - } + rightValue = latestRight!; + } - observer.OnNext(selector(value, rightValue)); - }, observer.OnError, observer.OnCompleted)); + observer.OnNext(selector(value, rightValue)); + }, + observer.OnError, + observer.OnCompleted)); }); } @@ -822,55 +722,7 @@ public static IObservable Switch(this IObservable> sources) throw new ArgumentNullException(nameof(sources)); } - return Signal.Create(observer => - { - var gate = new object(); - var pocket = new MultipleDisposable(); - var innerSlot = new SingleReplaceableDisposable(); - var outerCompleted = false; - var innerActive = false; - var version = 0; - pocket.Add(innerSlot); - - void TryComplete() - { - lock (gate) - { - if (outerCompleted && !innerActive) - { - observer.OnCompleted(); - } - } - } - - pocket.Add(sources.Subscribe(source => - { - int current; - lock (gate) - { - current = ++version; - innerActive = true; - } - - innerSlot.Create(source.Subscribe( - value => { lock (gate) { if (current == version) { observer.OnNext(value); } } }, - error => { lock (gate) { if (current == version) { observer.OnError(error); } } }, - () => - { - lock (gate) - { - if (current == version) - { - innerActive = false; - } - } - - TryComplete(); - })); - }, observer.OnError, () => { lock (gate) { outerCompleted = true; } TryComplete(); })); - - return pocket; - }); + return Signal.Create(observer => new SwitchCoordinator(observer).Run(sources)); } /// @@ -921,7 +773,7 @@ void SubscribeNext() /// Recovers from errors by switching to a handler-provided signal. /// public static IObservable Rescue(this IObservable source, Func> handler) => - Signal.Catch(source, handler); + source.Catch(handler); /// /// Continues with a fallback signal after an error. @@ -933,13 +785,19 @@ public static IObservable Resume(this IObservable source, IObservable(source, _ => fallback); + return source.Catch(_ => fallback); } /// /// Delays notifications by . /// - public static IObservable Delay(this IObservable source, TimeSpan dueTime, ISequencer? scheduler = null) + public static IObservable Delay(this IObservable source, TimeSpan dueTime) => + source.Delay(dueTime, null); + + /// + /// Delays notifications by . + /// + public static IObservable Delay(this IObservable source, TimeSpan dueTime, ISequencer? scheduler) { if (source == null) { @@ -947,21 +805,29 @@ public static IObservable Delay(this IObservable source, TimeSpan dueTi } scheduler ??= ThreadPoolSequencer.Instance; - return Signal.CreateSafe(observer => - { - var pocket = new MultipleDisposable(); - pocket.Add(source.Subscribe( - value => pocket.Add(scheduler.Schedule(dueTime, () => observer.OnNext(value))), - error => pocket.Add(scheduler.Schedule(dueTime, () => observer.OnError(error))), - () => pocket.Add(scheduler.Schedule(dueTime, observer.OnCompleted)))); - return pocket; - }, scheduler == Sequencer.CurrentThread); + return Signal.CreateSafe( + observer => + { + var pocket = new MultipleDisposable(); + pocket.Add(source.Subscribe( + value => pocket.Add(scheduler.Schedule(dueTime, () => observer.OnNext(value))), + error => pocket.Add(scheduler.Schedule(dueTime, () => observer.OnError(error))), + () => pocket.Add(scheduler.Schedule(dueTime, observer.OnCompleted)))); + return pocket; + }, + scheduler == Sequencer.CurrentThread); } /// /// Fails the signal if no terminal signal arrives before the timeout. /// - public static IObservable Timeout(this IObservable source, TimeSpan dueTime, ISequencer? scheduler = null) + public static IObservable Timeout(this IObservable source, TimeSpan dueTime) => + source.Timeout(dueTime, null); + + /// + /// Fails the signal if no terminal signal arrives before the timeout. + /// + public static IObservable Timeout(this IObservable source, TimeSpan dueTime, ISequencer? scheduler) { if (source == null) { @@ -975,16 +841,42 @@ public static IObservable Timeout(this IObservable source, TimeSpan due var done = 0; pocket.Add(scheduler.Schedule(dueTime, () => { - if (Interlocked.Exchange(ref done, 1) == 0) + if (Interlocked.Exchange(ref done, 1) != 0) { - observer.OnError(new TimeoutException()); - pocket.Dispose(); + return; } + + observer.OnError(new TimeoutException()); + pocket.Dispose(); })); pocket.Add(source.Subscribe( - value => { if (Volatile.Read(ref done) == 0) { observer.OnNext(value); } }, - error => { if (Interlocked.Exchange(ref done, 1) == 0) { observer.OnError(error); } }, - () => { if (Interlocked.Exchange(ref done, 1) == 0) { observer.OnCompleted(); } })); + value => + { + if (Volatile.Read(ref done) != 0) + { + return; + } + + observer.OnNext(value); + }, + error => + { + if (Interlocked.Exchange(ref done, 1) != 0) + { + return; + } + + observer.OnError(error); + }, + () => + { + if (Interlocked.Exchange(ref done, 1) != 0) + { + return; + } + + observer.OnCompleted(); + })); return pocket; }); } diff --git a/src/ReactiveUI.Primitives/SignalOperatorParityMixins.cs b/src/ReactiveUI.Primitives/SignalOperatorParityMixins.cs index 305acc9..63b4be9 100644 --- a/src/ReactiveUI.Primitives/SignalOperatorParityMixins.cs +++ b/src/ReactiveUI.Primitives/SignalOperatorParityMixins.cs @@ -113,13 +113,19 @@ public static IObservable ObserveOn(this IObservable source, ISequencer throw new ArgumentNullException(nameof(scheduler)); } - return Signal.WitnessOn(source, scheduler); + return source.WitnessOn(scheduler); } /// /// Alias for using the System.Reactive operator name. /// - public static IObservable DelaySubscription(this IObservable source, TimeSpan dueTime, ISequencer? scheduler = null) => + public static IObservable DelaySubscription(this IObservable source, TimeSpan dueTime) => + source.DelayStart(dueTime, null); + + /// + /// Alias for using the System.Reactive operator name. + /// + public static IObservable DelaySubscription(this IObservable source, TimeSpan dueTime, ISequencer? scheduler) => source.DelayStart(dueTime, scheduler); /// @@ -192,7 +198,13 @@ public static IObservable IgnoreValues(this IObservable source) /// /// Emits the supplied value if the source completes without values. /// - public static IObservable DefaultIfEmpty(this IObservable source, T defaultValue = default!) + public static IObservable DefaultIfEmpty(this IObservable source) => + source.DefaultIfEmpty(default!); + + /// + /// Emits the supplied value if the source completes without values. + /// + public static IObservable DefaultIfEmpty(this IObservable source, T defaultValue) { if (source == null) { @@ -224,7 +236,13 @@ public static IObservable DefaultIfEmpty(this IObservable source, T def /// /// Suppresses duplicate keys according to the comparer. /// - public static IObservable DistinctBy(this IObservable source, Func keySelector, IEqualityComparer? comparer = null) + public static IObservable DistinctBy(this IObservable source, Func keySelector) => + source.DistinctBy(keySelector, null); + + /// + /// Suppresses duplicate keys according to the comparer. + /// + public static IObservable DistinctBy(this IObservable source, Func keySelector, IEqualityComparer? comparer) { if (source == null) { @@ -242,10 +260,12 @@ public static IObservable DistinctBy(this IObservable source, Fun return source.Subscribe( value => { - if (seen.Add(keySelector(value))) + if (!seen.Add(keySelector(value))) { - observer.OnNext(value); + return; } + + observer.OnNext(value); }, observer.OnError, observer.OnCompleted); @@ -255,7 +275,13 @@ public static IObservable DistinctBy(this IObservable source, Fun /// /// Suppresses adjacent duplicate keys according to the comparer. /// - public static IObservable DistinctUntilChangedBy(this IObservable source, Func keySelector, IEqualityComparer? comparer = null) + public static IObservable DistinctUntilChangedBy(this IObservable source, Func keySelector) => + source.DistinctUntilChangedBy(keySelector, null); + + /// + /// Suppresses adjacent duplicate keys according to the comparer. + /// + public static IObservable DistinctUntilChangedBy(this IObservable source, Func keySelector, IEqualityComparer? comparer) { if (source == null) { @@ -276,12 +302,14 @@ public static IObservable DistinctUntilChangedBy(this IObservable value => { var key = keySelector(value); - if (!hasLast || !comparer.Equals(last!, key)) + if (hasLast && comparer.Equals(last!, key)) { - hasLast = true; - last = key; - observer.OnNext(value); + return; } + + hasLast = true; + last = key; + observer.OnNext(value); }, observer.OnError, observer.OnCompleted); @@ -473,21 +501,25 @@ public static IObservable Any(this IObservable source, Func return source.Subscribe( value => { - if (!matched && predicate(value)) + if (matched || !predicate(value)) { - matched = true; - observer.OnNext(true); - observer.OnCompleted(); + return; } + + matched = true; + observer.OnNext(true); + observer.OnCompleted(); }, observer.OnError, () => { - if (!matched) + if (matched) { - observer.OnNext(false); - observer.OnCompleted(); + return; } + + observer.OnNext(false); + observer.OnCompleted(); }); }); } @@ -513,21 +545,25 @@ public static IObservable All(this IObservable source, Func return source.Subscribe( value => { - if (!failed && !predicate(value)) + if (failed || predicate(value)) { - failed = true; - observer.OnNext(false); - observer.OnCompleted(); + return; } + + failed = true; + observer.OnNext(false); + observer.OnCompleted(); }, observer.OnError, () => { - if (!failed) + if (failed) { - observer.OnNext(true); - observer.OnCompleted(); + return; } + + observer.OnNext(true); + observer.OnCompleted(); }); }); } @@ -535,7 +571,13 @@ public static IObservable All(this IObservable source, Func /// /// Emits true when the source contains the requested value. /// - public static IObservable Contains(this IObservable source, T value, IEqualityComparer? comparer = null) + public static IObservable Contains(this IObservable source, T value) => + source.Contains(value, null); + + /// + /// Emits true when the source contains the requested value. + /// + public static IObservable Contains(this IObservable source, T value, IEqualityComparer? comparer) { comparer ??= EqualityComparer.Default; return source.Any(candidate => comparer.Equals(candidate, value)); @@ -549,7 +591,13 @@ public static IObservable Contains(this IObservable source, T value, /// /// Emits values from source after delaying subscription by the due time. /// - public static IObservable DelayStart(this IObservable source, TimeSpan dueTime, ISequencer? scheduler = null) + public static IObservable DelayStart(this IObservable source, TimeSpan dueTime) => + source.DelayStart(dueTime, null); + + /// + /// Emits values from source after delaying subscription by the due time. + /// + public static IObservable DelayStart(this IObservable source, TimeSpan dueTime, ISequencer? scheduler) { if (source == null) { @@ -568,7 +616,13 @@ public static IObservable DelayStart(this IObservable source, TimeSpan /// /// Emits only the most recent value after the quiet period elapses. /// - public static IObservable Throttle(this IObservable source, TimeSpan dueTime, ISequencer? scheduler = null) + public static IObservable Throttle(this IObservable source, TimeSpan dueTime) => + source.Throttle(dueTime, null); + + /// + /// Emits only the most recent value after the quiet period elapses. + /// + public static IObservable Throttle(this IObservable source, TimeSpan dueTime, ISequencer? scheduler) { if (source == null) { @@ -576,43 +630,51 @@ public static IObservable Throttle(this IObservable source, TimeSpan du } scheduler ??= ThreadPoolSequencer.Instance; - return Signal.CreateSafe(observer => - { - var gate = new object(); - var pocket = new MultipleDisposable(); - var slot = new SingleReplaceableDisposable(); - var version = 0; - pocket.Add(slot); - pocket.Add(source.Subscribe( - value => - { - int current; - lock (gate) + return Signal.CreateSafe( + observer => + { + var gate = new OperatorGate(); + var pocket = new MultipleDisposable(); + var slot = new SingleReplaceableDisposable(); + var version = 0; + pocket.Add(slot); + pocket.Add(source.Subscribe( + value => { - current = ++version; - } + int current; + lock (gate.SyncRoot) + { + current = ++version; + } - slot.Create(scheduler.Schedule(Sequencer.Normalize(dueTime), () => - { - lock (gate) + slot.Create(scheduler.Schedule(Sequencer.Normalize(dueTime), () => { - if (current == version) + lock (gate.SyncRoot) { - observer.OnNext(value); + if (current == version) + { + observer.OnNext(value); + } } - } - })); - }, - observer.OnError, - observer.OnCompleted)); - return pocket; - }, scheduler == Sequencer.CurrentThread); + })); + }, + observer.OnError, + observer.OnCompleted)); + return pocket; + }, + scheduler == Sequencer.CurrentThread); } /// /// Emits the latest source value whenever the sampling period ticks. /// - public static IObservable Sample(this IObservable source, TimeSpan period, ISequencer? scheduler = null) + public static IObservable Sample(this IObservable source, TimeSpan period) => + source.Sample(period, null); + + /// + /// Emits the latest source value whenever the sampling period ticks. + /// + public static IObservable Sample(this IObservable source, TimeSpan period, ISequencer? scheduler) { if (source == null) { @@ -625,79 +687,21 @@ public static IObservable Sample(this IObservable source, TimeSpan peri } scheduler ??= ThreadPoolSequencer.Instance; - return Signal.CreateSafe(observer => - { - var gate = new object(); - var pocket = new MultipleDisposable(); - var timer = new SingleReplaceableDisposable(); - var hasLatest = false; - var latest = default(T); - var done = false; - pocket.Add(timer); - pocket.Add(source.Subscribe( - value => - { - lock (gate) - { - hasLatest = true; - latest = value; - } - }, - observer.OnError, - () => - { - lock (gate) - { - done = true; - } - - observer.OnCompleted(); - })); - - Action? tick = null; - tick = () => timer.Create(scheduler.Schedule(period, () => - { - T value; - var emit = false; - lock (gate) - { - if (hasLatest) - { - value = latest!; - hasLatest = false; - emit = true; - } - else - { - value = default!; - } - - if (done) - { - return; - } - } - - if (emit) - { - observer.OnNext(value); - } - - if (!timer.IsDisposed) - { - tick!(); - } - })); - - tick(); - return pocket; - }, scheduler == Sequencer.CurrentThread); + return Signal.CreateSafe( + observer => new SampleCoordinator(source, period, scheduler).Run(observer), + scheduler == Sequencer.CurrentThread); } /// /// Annotates values with their scheduler timestamp. /// - public static IObservable> Timestamp(this IObservable source, ISequencer? scheduler = null) + public static IObservable> Timestamp(this IObservable source) => + source.Timestamp(null); + + /// + /// Annotates values with their scheduler timestamp. + /// + public static IObservable> Timestamp(this IObservable source, ISequencer? scheduler) { if (source == null) { @@ -711,7 +715,13 @@ public static IObservable> Timestamp(this IObservable source, IS /// /// Annotates each value with the elapsed scheduler time since the previous value. /// - public static IObservable> TimeInterval(this IObservable source, ISequencer? scheduler = null) + public static IObservable> TimeInterval(this IObservable source) => + source.TimeInterval(null); + + /// + /// Annotates each value with the elapsed scheduler time since the previous value. + /// + public static IObservable> TimeInterval(this IObservable source, ISequencer? scheduler) { if (source == null) { @@ -769,50 +779,7 @@ public static IObservable ForkJoin(this IObserv throw new ArgumentNullException(nameof(selector)); } - return Signal.CreateSafe(observer => - { - var gate = new object(); - var hasLeft = false; - var hasRight = false; - var leftDone = false; - var rightDone = false; - var latestLeft = default(TLeft); - var latestRight = default(TRight); - - void FinishIfReady() - { - TResult result; - var emit = false; - lock (gate) - { - if (!leftDone || !rightDone) - { - return; - } - - if (hasLeft && hasRight) - { - result = selector(latestLeft!, latestRight!); - emit = true; - } - else - { - result = default!; - } - } - - if (emit) - { - observer.OnNext(result); - } - - observer.OnCompleted(); - } - - return MultipleDisposable.Create( - left.Subscribe(value => { lock (gate) { hasLeft = true; latestLeft = value; } }, observer.OnError, () => { lock (gate) { leftDone = true; } FinishIfReady(); }), - right.Subscribe(value => { lock (gate) { hasRight = true; latestRight = value; } }, observer.OnError, () => { lock (gate) { rightDone = true; } FinishIfReady(); })); - }); + return Signal.CreateSafe(observer => new ForkJoinCoordinator(observer, selector).Run(left, right)); } /// @@ -823,7 +790,13 @@ void FinishIfReady() /// /// Awaits the first source value, returning a default value when the source is empty. /// - public static Task FirstOrDefaultAsync(this IObservable source, T defaultValue = default!) => source.FirstOrDefaultCoreAsync(true, defaultValue); + public static Task FirstOrDefaultAsync(this IObservable source) => + source.FirstOrDefaultCoreAsync(true, default!); + + /// + /// Awaits the first source value, returning a default value when the source is empty. + /// + public static Task FirstOrDefaultAsync(this IObservable source, T defaultValue) => source.FirstOrDefaultCoreAsync(true, defaultValue); /// /// Awaits source completion and returns the last value produced by the source. @@ -905,7 +878,7 @@ public static Task CollectArrayAsync(this IObservable source) var completion = new TaskCompletionSource(); var values = new List(); - source.Subscribe(values.Add, error => completion.TrySetException(error), () => completion.TrySetResult(values.ToArray())); + source.Subscribe(values.Add, error => completion.TrySetException(error), () => completion.TrySetResult([.. values])); return completion.Task; } @@ -925,6 +898,14 @@ public static Task> CollectListAsync(this IObservable source) return completion.Task; } + /// + /// Awaits the first source value and applies the configured empty-source behavior. + /// + /// The source value type. + /// The source observable. + /// A value indicating whether to use when the source is empty. + /// The fallback value to use when the source is empty. + /// A representing the result of the asynchronous operation. private static Task FirstOrDefaultCoreAsync(this IObservable source, bool hasDefault, T defaultValue) { if (source == null) @@ -937,27 +918,389 @@ private static Task FirstOrDefaultCoreAsync(this IObservable source, bo source.Subscribe( value => { - if (!seen) + if (seen) { - seen = true; - completion.TrySetResult(value); + return; } + + seen = true; + completion.TrySetResult(value); }, error => completion.TrySetException(error), () => { - if (!seen) + if (seen) { - if (hasDefault) - { - completion.TrySetResult(defaultValue); - } - else - { - completion.TrySetException(new InvalidOperationException("The source completed without producing a value.")); - } + return; + } + + if (hasDefault) + { + completion.TrySetResult(defaultValue); + } + else + { + completion.TrySetException(new InvalidOperationException("The source completed without producing a value.")); } }); return completion.Task; } + + /// + /// Coordinates a sampled observable sequence. + /// + /// The source value type. + private sealed class SampleCoordinator : IDisposable + { + /// + /// The source observable. + /// + private readonly IObservable _source; + + /// + /// The sample period. + /// + private readonly TimeSpan _period; + + /// + /// The sequencer used to schedule ticks. + /// + private readonly ISequencer _sequencer; + + /// + /// The synchronization gate. + /// + private readonly OperatorGate _gate = new(); + + /// + /// The active subscriptions. + /// + private readonly MultipleDisposable _subscriptions = new(); + + /// + /// The timer slot. + /// + private readonly SingleReplaceableDisposable _timer = new(); + + /// + /// The downstream observer. + /// + private IObserver? _observer; + + /// + /// A value indicating whether a latest value is available. + /// + private bool _hasLatest; + + /// + /// The latest value. + /// + private T? _latest; + + /// + /// A value indicating whether the source has completed. + /// + private bool _done; + + /// + /// Initializes a new instance of the class. + /// + /// The source observable. + /// The sample period. + /// The sequencer used to schedule ticks. + internal SampleCoordinator(IObservable source, TimeSpan period, ISequencer sequencer) + { + _source = source; + _period = period; + _sequencer = sequencer; + } + + /// + /// Releases the active subscriptions. + /// + public void Dispose() + { + _timer.Dispose(); + _subscriptions.Dispose(); + } + + /// + /// Starts sampling the source. + /// + /// The downstream observer. + /// The coordinator that owns the subscription cleanup. + internal SampleCoordinator Run(IObserver observer) + { + _observer = observer; + _subscriptions.Add(_timer); + _subscriptions.Add(_source.Subscribe(OnNext, observer.OnError, OnCompleted)); + ScheduleNext(); + return this; + } + + /// + /// Records the latest source value. + /// + /// The source value. + private void OnNext(T value) + { + lock (_gate.SyncRoot) + { + _hasLatest = true; + _latest = value; + } + } + + /// + /// Marks the source as completed. + /// + private void OnCompleted() + { + lock (_gate.SyncRoot) + { + _done = true; + } + + _observer!.OnCompleted(); + } + + /// + /// Schedules the next sample tick. + /// + private void ScheduleNext() => + _timer.Create(_sequencer.Schedule(_period, Tick)); + + /// + /// Handles a sample tick. + /// + private void Tick() + { + if (!TryTake(out var value)) + { + return; + } + + _observer!.OnNext(value); + if (_timer.IsDisposed) + { + return; + } + + ScheduleNext(); + } + + /// + /// Attempts to take the latest value. + /// + /// The latest value. + /// true when a value should be emitted; otherwise, false. + private bool TryTake(out T value) + { + lock (_gate.SyncRoot) + { + if (_done || !_hasLatest) + { + value = default!; + return false; + } + + value = _latest!; + _hasLatest = false; + return true; + } + } + } + + /// + /// Coordinates a two-source fork-join operation. + /// + /// The left value type. + /// The right value type. + /// The result value type. + private sealed class ForkJoinCoordinator + { + /// + /// The synchronization gate. + /// + private readonly OperatorGate _gate = new(); + + /// + /// The downstream observer. + /// + private readonly IObserver _observer; + + /// + /// The projection function. + /// + private readonly Func _selector; + + /// + /// A value indicating whether the left source produced a value. + /// + private bool _hasLeft; + + /// + /// A value indicating whether the right source produced a value. + /// + private bool _hasRight; + + /// + /// A value indicating whether the left source completed. + /// + private bool _leftDone; + + /// + /// A value indicating whether the right source completed. + /// + private bool _rightDone; + + /// + /// The latest left value. + /// + private TLeft? _latestLeft; + + /// + /// The latest right value. + /// + private TRight? _latestRight; + + /// + /// Initializes a new instance of the class. + /// + /// The downstream observer. + /// The projection function. + internal ForkJoinCoordinator(IObserver observer, Func selector) + { + _observer = observer; + _selector = selector; + } + + /// + /// Subscribes to both fork-join sources. + /// + /// The left source. + /// The right source. + /// The subscription cleanup. + internal MultipleDisposable Run(IObservable left, IObservable right) => + new( + left.Subscribe(OnLeftNext, _observer.OnError, OnLeftCompleted), + right.Subscribe(OnRightNext, _observer.OnError, OnRightCompleted)); + + /// + /// Records a left value. + /// + /// The left value. + private void OnLeftNext(TLeft value) + { + lock (_gate.SyncRoot) + { + _hasLeft = true; + _latestLeft = value; + } + } + + /// + /// Records a right value. + /// + /// The right value. + private void OnRightNext(TRight value) + { + lock (_gate.SyncRoot) + { + _hasRight = true; + _latestRight = value; + } + } + + /// + /// Marks the left source as complete. + /// + private void OnLeftCompleted() + { + if (!CompleteLeft(out var result, out var emit)) + { + return; + } + + Finish(result, emit); + } + + /// + /// Marks the right source as complete. + /// + private void OnRightCompleted() + { + if (!CompleteRight(out var result, out var emit)) + { + return; + } + + Finish(result, emit); + } + + /// + /// Marks the left source complete and computes the result if both sources are complete. + /// + /// The result to emit. + /// A value indicating whether a result should be emitted. + /// true when fork-join is ready to finish; otherwise, false. + private bool CompleteLeft(out TResult result, out bool emit) + { + lock (_gate.SyncRoot) + { + _leftDone = true; + return TryFinish(out result, out emit); + } + } + + /// + /// Marks the right source complete and computes the result if both sources are complete. + /// + /// The result to emit. + /// A value indicating whether a result should be emitted. + /// true when fork-join is ready to finish; otherwise, false. + private bool CompleteRight(out TResult result, out bool emit) + { + lock (_gate.SyncRoot) + { + _rightDone = true; + return TryFinish(out result, out emit); + } + } + + /// + /// Computes the final result when both sources are complete. + /// + /// The result to emit. + /// A value indicating whether a result should be emitted. + /// true when both sources are complete; otherwise, false. + private bool TryFinish(out TResult result, out bool emit) + { + if (!_leftDone || !_rightDone) + { + result = default!; + emit = false; + return false; + } + + emit = _hasLeft && _hasRight; + result = emit ? _selector(_latestLeft!, _latestRight!) : default!; + return true; + } + + /// + /// Emits the final result and completes. + /// + /// The result to emit. + /// A value indicating whether a result should be emitted. + private void Finish(TResult result, bool emit) + { + if (emit) + { + _observer.OnNext(result); + } + + _observer.OnCompleted(); + } + } } diff --git a/src/ReactiveUI.Primitives/Signals/Core/CatchSignal{T,TException}.cs b/src/ReactiveUI.Primitives/Signals/Core/CatchSignal{T,TException}.cs index 7e27f92..ef0c4b5 100644 --- a/src/ReactiveUI.Primitives/Signals/Core/CatchSignal{T,TException}.cs +++ b/src/ReactiveUI.Primitives/Signals/Core/CatchSignal{T,TException}.cs @@ -6,13 +6,30 @@ namespace ReactiveUI.Primitives.Signals.Core; +/// +/// Represents the CatchSignal class. +/// +/// The T type. +/// The TException type. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] -internal class CatchSignal : SignalsBase +internal sealed class CatchSignal : SignalsBase where TException : Exception { + /// + /// Stores state for the signal implementation. + /// private readonly IObservable _source; + + /// + /// Stores state for the signal implementation. + /// private readonly Func> _errorHandler; + /// + /// Initializes a new instance of the class. + /// + /// The source value. + /// The errorHandler value. public CatchSignal(IObservable source, Func> errorHandler) : base(true) { @@ -20,28 +37,61 @@ public CatchSignal(IObservable source, Func> error _errorHandler = errorHandler; } + /// + /// Executes the SubscribeCore operation. + /// + /// The observer value. + /// The cancel value. + /// The result. protected override IDisposable SubscribeCore(IObserver observer, IDisposable cancel) => new Catch(this, observer, cancel).Run(); - private class Catch : WitnessBase + /// + /// Represents the Catch class. + /// + private sealed class Catch : WitnessBase { + /// + /// Stores state for the signal implementation. + /// private readonly CatchSignal _parent; - private SingleDisposable? _sourceSubscription; + + /// + /// Stores state for the signal implementation. + /// private SingleDisposable? _exceptionSubscription; + /// + /// Initializes a new instance of the class. + /// + /// The parent value. + /// The observer value. + /// The cancel value. public Catch(CatchSignal parent, IObserver observer, IDisposable cancel) : base(observer, cancel) => _parent = parent; - public IDisposable Run() + /// + /// Executes the Run operation. + /// + /// The result. + public MultipleDisposable Run() { _exceptionSubscription = new SingleDisposable(); - _sourceSubscription = new SingleDisposable(_parent._source.Subscribe(this)); + var sourceSubscription = new SingleDisposable(_parent._source.Subscribe(this)); - return new MultipleDisposable(_sourceSubscription, _exceptionSubscription); + return new MultipleDisposable(sourceSubscription, _exceptionSubscription); } + /// + /// Executes the OnNext operation. + /// + /// The value. public override void OnNext(T value) => Observer.OnNext(value); + /// + /// Executes the OnError operation. + /// + /// The error value. public override void OnError(Exception error) { if (error is TException e) @@ -49,14 +99,7 @@ public override void OnError(Exception error) IObservable next; try { - if (_parent._errorHandler == Handle.CatchIgnore) - { - next = Signal.Empty(); - } - else - { - next = _parent._errorHandler(e); - } + next = _parent._errorHandler == Handle.CatchIgnore ? Signal.Empty() : _parent._errorHandler(e); } catch (Exception ex) { @@ -87,6 +130,9 @@ public override void OnError(Exception error) } } + /// + /// Executes the OnCompleted operation. + /// public override void OnCompleted() { try @@ -98,5 +144,20 @@ public override void OnCompleted() Dispose(); } } + + /// + /// Executes the Dispose operation. + /// + /// The disposing value. + protected override void Dispose(bool disposing) + { + if (disposing) + { + _exceptionSubscription?.Dispose(); + _exceptionSubscription = null; + } + + base.Dispose(disposing); + } } } diff --git a/src/ReactiveUI.Primitives/Signals/Core/CatchSignal{T}.cs b/src/ReactiveUI.Primitives/Signals/Core/CatchSignal{T}.cs index b16e148..bd9e70c 100644 --- a/src/ReactiveUI.Primitives/Signals/Core/CatchSignal{T}.cs +++ b/src/ReactiveUI.Primitives/Signals/Core/CatchSignal{T}.cs @@ -7,31 +7,89 @@ namespace ReactiveUI.Primitives.Signals.Core; +/// +/// Represents the CatchSignal class. +/// +/// The T type. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] -internal class CatchSignal : SignalsBase +internal sealed class CatchSignal : SignalsBase { + /// + /// Stores state for the signal implementation. + /// private readonly IEnumerable> _sources; + /// + /// Initializes a new instance of the class. + /// + /// The sources value. public CatchSignal(IEnumerable> sources) : base(true) => _sources = sources; + /// + /// Executes the SubscribeCore operation. + /// + /// The observer value. + /// The cancel value. + /// The result. protected override IDisposable SubscribeCore(IObserver observer, IDisposable cancel) => new Catch(this, observer, cancel).Run(); - private class Catch : WitnessBase + /// + /// Represents the Catch class. + /// + private sealed class Catch : WitnessBase { + /// + /// Stores state for the signal implementation. + /// private readonly CatchSignal _parent; + + /// + /// Executes the new operation. + /// + /// The result. private readonly object _gate = new(); + + /// + /// Stores state for the signal implementation. + /// private bool _isDisposed; + + /// + /// Stores state for the signal implementation. + /// private IEnumerator>? _e; + + /// + /// Stores state for the signal implementation. + /// private SingleReplaceableDisposable? _subscription; + + /// + /// Stores state for the signal implementation. + /// private Exception? _lastException; + + /// + /// Stores state for the signal implementation. + /// private Action? _nextSelf; + /// + /// Initializes a new instance of the class. + /// + /// The parent value. + /// The observer value. + /// The cancel value. public Catch(CatchSignal parent, IObserver observer, IDisposable cancel) : base(observer, cancel) => _parent = parent; - public IDisposable Run() + /// + /// Executes the Run operation. + /// + /// The result. + public MultipleDisposable Run() { _isDisposed = false; _e = _parent._sources.GetEnumerator(); @@ -49,14 +107,25 @@ public IDisposable Run() })); } + /// + /// Executes the OnNext operation. + /// + /// The value. public override void OnNext(T value) => Observer.OnNext(value); + /// + /// Executes the OnError operation. + /// + /// The error value. public override void OnError(Exception error) { _lastException = error; _nextSelf!(); } + /// + /// Executes the OnCompleted operation. + /// public override void OnCompleted() { try @@ -69,6 +138,27 @@ public override void OnCompleted() } } + /// + /// Executes the Dispose operation. + /// + /// The disposing value. + protected override void Dispose(bool disposing) + { + if (disposing) + { + _e?.Dispose(); + _e = null; + _subscription?.Dispose(); + _subscription = null; + } + + base.Dispose(disposing); + } + + /// + /// Executes the RecursiveRun operation. + /// + /// The self value. private void RecursiveRun(Action self) { lock (_gate) @@ -88,11 +178,7 @@ private void RecursiveRun(Action self) hasNext = _e!.MoveNext(); if (hasNext) { - current = _e.Current; - if (current == null) - { - throw new InvalidOperationException("sequence is null."); - } + current = _e.Current ?? throw new InvalidOperationException("sequence is null."); } else { diff --git a/src/ReactiveUI.Primitives/Signals/Core/CreateSafeSignal{T}.cs b/src/ReactiveUI.Primitives/Signals/Core/CreateSafeSignal{T}.cs index 9f06cab..1528705 100644 --- a/src/ReactiveUI.Primitives/Signals/Core/CreateSafeSignal{T}.cs +++ b/src/ReactiveUI.Primitives/Signals/Core/CreateSafeSignal{T}.cs @@ -6,30 +6,64 @@ namespace ReactiveUI.Primitives.Signals.Core; +/// +/// Represents the CreateSafeSignal class. +/// +/// The T type. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] -internal class CreateSafeSignal : SignalsBase +internal sealed class CreateSafeSignal : SignalsBase { + /// + /// Stores state for the signal implementation. + /// private readonly Func, IDisposable> _subscribe; + /// + /// Initializes a new instance of the class. + /// + /// The subscribe value. public CreateSafeSignal(Func, IDisposable> subscribe) : base(true) => _subscribe = subscribe; // fail safe + /// + /// Initializes a new instance of the class. + /// + /// The subscribe value. + /// The isRequiredSubscribeOnCurrentThread value. public CreateSafeSignal(Func, IDisposable> subscribe, bool isRequiredSubscribeOnCurrentThread) : base(isRequiredSubscribeOnCurrentThread) => _subscribe = subscribe; + /// + /// Executes the SubscribeCore operation. + /// + /// The observer value. + /// The cancel value. + /// The result. protected override IDisposable SubscribeCore(IObserver observer, IDisposable cancel) { observer = new CreateSafe(observer, cancel); return _subscribe(observer) ?? Disposable.Empty; } - private class CreateSafe : WitnessBase + /// + /// Represents the CreateSafe class. + /// + private sealed class CreateSafe : WitnessBase { + /// + /// Initializes a new instance of the class. + /// + /// The observer value. + /// The cancel value. public CreateSafe(IObserver observer, IDisposable cancel) : base(observer, cancel) { } + /// + /// Executes the OnNext operation. + /// + /// The value. public override void OnNext(T value) { try @@ -43,6 +77,10 @@ public override void OnNext(T value) } } + /// + /// Executes the OnError operation. + /// + /// The error value. public override void OnError(Exception error) { try @@ -55,6 +93,9 @@ public override void OnError(Exception error) } } + /// + /// Executes the OnCompleted operation. + /// public override void OnCompleted() { try diff --git a/src/ReactiveUI.Primitives/Signals/Core/CreateSignal{T,TState}.cs b/src/ReactiveUI.Primitives/Signals/Core/CreateSignal{T,TState}.cs index 7dc2e2b..eb419bf 100644 --- a/src/ReactiveUI.Primitives/Signals/Core/CreateSignal{T,TState}.cs +++ b/src/ReactiveUI.Primitives/Signals/Core/CreateSignal{T,TState}.cs @@ -6,12 +6,29 @@ namespace ReactiveUI.Primitives.Signals.Core; +/// +/// Represents the CreateSignal class. +/// +/// The T type. +/// The TState type. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] -internal class CreateSignal : SignalsBase +internal sealed class CreateSignal : SignalsBase { + /// + /// Stores state for the signal implementation. + /// private readonly TState _state; + + /// + /// Stores state for the signal implementation. + /// private readonly Func, IDisposable> _subscribe; + /// + /// Initializes a new instance of the class. + /// + /// The state value. + /// The subscribe value. public CreateSignal(TState state, Func, IDisposable> subscribe) : base(true) // fail safe { @@ -19,6 +36,12 @@ public CreateSignal(TState state, Func, IDisposable> subscr _subscribe = subscribe; } + /// + /// Initializes a new instance of the class. + /// + /// The state value. + /// The subscribe value. + /// The isRequiredSubscribeOnCurrentThread value. public CreateSignal(TState state, Func, IDisposable> subscribe, bool isRequiredSubscribeOnCurrentThread) : base(isRequiredSubscribeOnCurrentThread) { @@ -26,21 +49,43 @@ public CreateSignal(TState state, Func, IDisposable> subscr _subscribe = subscribe; } + /// + /// Executes the SubscribeCore operation. + /// + /// The observer value. + /// The cancel value. + /// The result. protected override IDisposable SubscribeCore(IObserver observer, IDisposable cancel) { observer = new Create(observer, cancel); return _subscribe(_state, observer) ?? Disposable.Empty; } - private class Create : WitnessBase + /// + /// Represents the Create class. + /// + private sealed class Create : WitnessBase { + /// + /// Initializes a new instance of the class. + /// + /// The observer value. + /// The cancel value. public Create(IObserver observer, IDisposable cancel) : base(observer, cancel) { } + /// + /// Executes the OnNext operation. + /// + /// The value. public override void OnNext(T value) => Observer.OnNext(value); + /// + /// Executes the OnError operation. + /// + /// The error value. public override void OnError(Exception error) { try @@ -53,6 +98,9 @@ public override void OnError(Exception error) } } + /// + /// Executes the OnCompleted operation. + /// public override void OnCompleted() { try diff --git a/src/ReactiveUI.Primitives/Signals/Core/CreateSignal{T}.cs b/src/ReactiveUI.Primitives/Signals/Core/CreateSignal{T}.cs index ed400e2..32e3c60 100644 --- a/src/ReactiveUI.Primitives/Signals/Core/CreateSignal{T}.cs +++ b/src/ReactiveUI.Primitives/Signals/Core/CreateSignal{T}.cs @@ -6,32 +6,70 @@ namespace ReactiveUI.Primitives.Signals.Core; +/// +/// Represents the CreateSignal class. +/// +/// The T type. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] -internal class CreateSignal : SignalsBase +internal sealed class CreateSignal : SignalsBase { + /// + /// Stores state for the signal implementation. + /// private readonly Func, IDisposable> _subscribe; + /// + /// Initializes a new instance of the class. + /// + /// The subscribe value. public CreateSignal(Func, IDisposable> subscribe) : base(true) => _subscribe = subscribe; // fail safe + /// + /// Initializes a new instance of the class. + /// + /// The subscribe value. + /// The isRequiredSubscribeOnCurrentThread value. public CreateSignal(Func, IDisposable> subscribe, bool isRequiredSubscribeOnCurrentThread) : base(isRequiredSubscribeOnCurrentThread) => _subscribe = subscribe; + /// + /// Executes the SubscribeCore operation. + /// + /// The observer value. + /// The cancel value. + /// The result. protected override IDisposable SubscribeCore(IObserver observer, IDisposable cancel) { observer = new Create(observer, cancel); return _subscribe(observer) ?? Disposable.Empty; } - private class Create : WitnessBase + /// + /// Represents the Create class. + /// + private sealed class Create : WitnessBase { + /// + /// Initializes a new instance of the class. + /// + /// The observer value. + /// The cancel value. public Create(IObserver observer, IDisposable cancel) : base(observer, cancel) { } + /// + /// Executes the OnNext operation. + /// + /// The value. public override void OnNext(T value) => Observer.OnNext(value); + /// + /// Executes the OnError operation. + /// + /// The error value. public override void OnError(Exception error) { try @@ -44,6 +82,9 @@ public override void OnError(Exception error) } } + /// + /// Executes the OnCompleted operation. + /// public override void OnCompleted() { try diff --git a/src/ReactiveUI.Primitives/Signals/Core/DeferSignal{T}.cs b/src/ReactiveUI.Primitives/Signals/Core/DeferSignal{T}.cs index f87c270..56924c4 100644 --- a/src/ReactiveUI.Primitives/Signals/Core/DeferSignal{T}.cs +++ b/src/ReactiveUI.Primitives/Signals/Core/DeferSignal{T}.cs @@ -4,14 +4,31 @@ namespace ReactiveUI.Primitives.Signals.Core; +/// +/// Represents the DeferSignal class. +/// +/// The T type. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] -internal class DeferSignal : SignalsBase +internal sealed class DeferSignal : SignalsBase { + /// + /// Stores state for the signal implementation. + /// private readonly Func> _observableFactory; + /// + /// Initializes a new instance of the class. + /// + /// The observableFactory value. public DeferSignal(Func> observableFactory) : base(false) => _observableFactory = observableFactory; + /// + /// Executes the SubscribeCore operation. + /// + /// The observer value. + /// The cancel value. + /// The result. protected override IDisposable SubscribeCore(IObserver observer, IDisposable cancel) { observer = new Defer(observer, cancel); @@ -29,13 +46,25 @@ protected override IDisposable SubscribeCore(IObserver observer, IDisposable return source.Subscribe(observer); } - private class Defer : WitnessBase + /// + /// Represents the Defer class. + /// + private sealed class Defer : WitnessBase { + /// + /// Initializes a new instance of the class. + /// + /// The observer value. + /// The cancel value. public Defer(IObserver observer, IDisposable cancel) : base(observer, cancel) { } + /// + /// Executes the OnNext operation. + /// + /// The value. public override void OnNext(T value) { try @@ -49,6 +78,10 @@ public override void OnNext(T value) } } + /// + /// Executes the OnError operation. + /// + /// The error value. public override void OnError(Exception error) { try @@ -61,6 +94,9 @@ public override void OnError(Exception error) } } + /// + /// Executes the OnCompleted operation. + /// public override void OnCompleted() { try diff --git a/src/ReactiveUI.Primitives/Signals/Core/EmptySignal{T}.cs b/src/ReactiveUI.Primitives/Signals/Core/EmptySignal{T}.cs index 1609188..c3b5632 100644 --- a/src/ReactiveUI.Primitives/Signals/Core/EmptySignal{T}.cs +++ b/src/ReactiveUI.Primitives/Signals/Core/EmptySignal{T}.cs @@ -7,14 +7,31 @@ namespace ReactiveUI.Primitives.Signals.Core; +/// +/// Represents the EmptySignal class. +/// +/// The T type. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] -internal class EmptySignal : SignalsBase +internal sealed class EmptySignal : SignalsBase { + /// + /// Stores state for the signal implementation. + /// private readonly ISequencer _scheduler; + /// + /// Initializes a new instance of the class. + /// + /// The scheduler value. public EmptySignal(ISequencer scheduler) : base(false) => _scheduler = scheduler; + /// + /// Executes the SubscribeCore operation. + /// + /// The observer value. + /// The cancel value. + /// The result. protected override IDisposable SubscribeCore(IObserver observer, IDisposable cancel) { observer = new Empty(observer, cancel); @@ -28,13 +45,25 @@ protected override IDisposable SubscribeCore(IObserver observer, IDisposable return _scheduler.Schedule(observer.OnCompleted); } - private class Empty : WitnessBase + /// + /// Represents the Empty class. + /// + private sealed class Empty : WitnessBase { + /// + /// Initializes a new instance of the class. + /// + /// The observer value. + /// The cancel value. public Empty(IObserver observer, IDisposable cancel) : base(observer, cancel) { } + /// + /// Executes the OnNext operation. + /// + /// The value. public override void OnNext(T value) { try @@ -48,6 +77,10 @@ public override void OnNext(T value) } } + /// + /// Executes the OnError operation. + /// + /// The error value. public override void OnError(Exception error) { try @@ -60,6 +93,9 @@ public override void OnError(Exception error) } } + /// + /// Executes the OnCompleted operation. + /// public override void OnCompleted() { try diff --git a/src/ReactiveUI.Primitives/Signals/Core/FinallySignal{T}.cs b/src/ReactiveUI.Primitives/Signals/Core/FinallySignal{T}.cs index fee0d87..d688fe9 100644 --- a/src/ReactiveUI.Primitives/Signals/Core/FinallySignal{T}.cs +++ b/src/ReactiveUI.Primitives/Signals/Core/FinallySignal{T}.cs @@ -6,12 +6,28 @@ namespace ReactiveUI.Primitives.Signals.Core; +/// +/// Represents the FinallySignal class. +/// +/// The T type. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] -internal class FinallySignal : SignalsBase +internal sealed class FinallySignal : SignalsBase { + /// + /// Stores state for the signal implementation. + /// private readonly IObservable _source; + + /// + /// Stores state for the signal implementation. + /// private readonly Action _finallyAction; + /// + /// Initializes a new instance of the class. + /// + /// The source value. + /// The finallyAction value. public FinallySignal(IObservable source, Action finallyAction) : base(true) { @@ -19,17 +35,39 @@ public FinallySignal(IObservable source, Action finallyAction) _finallyAction = finallyAction; } + /// + /// Executes the SubscribeCore operation. + /// + /// The observer value. + /// The cancel value. + /// The result. protected override IDisposable SubscribeCore(IObserver observer, IDisposable cancel) => new Finally(this, observer, cancel).Run(); - private class Finally : WitnessBase + /// + /// Represents the Finally class. + /// + private sealed class Finally : WitnessBase { + /// + /// Stores state for the signal implementation. + /// private readonly FinallySignal _parent; + /// + /// Initializes a new instance of the class. + /// + /// The parent value. + /// The observer value. + /// The cancel value. public Finally(FinallySignal parent, IObserver observer, IDisposable cancel) : base(observer, cancel) => _parent = parent; - public IDisposable Run() + /// + /// Executes the Run operation. + /// + /// The result. + public MultipleDisposable Run() { IDisposable subscription; try @@ -45,8 +83,16 @@ public IDisposable Run() return new MultipleDisposable(subscription, Disposable.Create(() => _parent._finallyAction())); } + /// + /// Executes the OnNext operation. + /// + /// The value. public override void OnNext(T value) => Observer.OnNext(value); + /// + /// Executes the OnError operation. + /// + /// The error value. public override void OnError(Exception error) { try @@ -59,6 +105,9 @@ public override void OnError(Exception error) } } + /// + /// Executes the OnCompleted operation. + /// public override void OnCompleted() { try diff --git a/src/ReactiveUI.Primitives/Signals/Core/IInlineSignal{T}.cs b/src/ReactiveUI.Primitives/Signals/Core/IInlineSignal{T}.cs index 224ebc7..72e4d79 100644 --- a/src/ReactiveUI.Primitives/Signals/Core/IInlineSignal{T}.cs +++ b/src/ReactiveUI.Primitives/Signals/Core/IInlineSignal{T}.cs @@ -4,7 +4,18 @@ namespace ReactiveUI.Primitives.Signals.Core; +/// +/// Represents the IInlineSignal interface. +/// +/// The T type. internal interface IInlineSignal : IObservable { + /// + /// Executes the Subscribe operation. + /// + /// The onNext value. + /// The onError value. + /// The onCompleted value. + /// The result. IDisposable Subscribe(Action onNext, Action onError, Action onCompleted); } diff --git a/src/ReactiveUI.Primitives/Signals/Core/ImmediateReturnSignal{T}.cs b/src/ReactiveUI.Primitives/Signals/Core/ImmediateReturnSignal{T}.cs index e897a10..8647a84 100644 --- a/src/ReactiveUI.Primitives/Signals/Core/ImmediateReturnSignal{T}.cs +++ b/src/ReactiveUI.Primitives/Signals/Core/ImmediateReturnSignal{T}.cs @@ -7,15 +7,35 @@ namespace ReactiveUI.Primitives.Signals.Core; +/// +/// Represents the ImmediateReturnSignal class. +/// +/// The T type. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] -internal class ImmediateReturnSignal : IObservable, IRequireCurrentThread, IInlineSignal +internal sealed class ImmediateReturnSignal : IRequireCurrentThread, IInlineSignal { + /// + /// Stores state for the signal implementation. + /// private readonly T _value; + /// + /// Initializes a new instance of the class. + /// + /// The value. public ImmediateReturnSignal(T value) => _value = value; + /// + /// Executes the IsRequiredSubscribeOnCurrentThread operation. + /// + /// The result. public bool IsRequiredSubscribeOnCurrentThread() => false; + /// + /// Executes the Subscribe operation. + /// + /// The observer value. + /// The result. public IDisposable Subscribe(IObserver observer) { observer.OnNext(_value); @@ -23,6 +43,13 @@ public IDisposable Subscribe(IObserver observer) return Disposable.Empty; } + /// + /// Executes the Subscribe operation. + /// + /// The onNext value. + /// The onError value. + /// The onCompleted value. + /// The result. public IDisposable Subscribe(Action onNext, Action onError, Action onCompleted) { onNext(_value); diff --git a/src/ReactiveUI.Primitives/Signals/Core/ImmutableEmptySignal{T}.cs b/src/ReactiveUI.Primitives/Signals/Core/ImmutableEmptySignal{T}.cs index 11f5746..f2d6a6b 100644 --- a/src/ReactiveUI.Primitives/Signals/Core/ImmutableEmptySignal{T}.cs +++ b/src/ReactiveUI.Primitives/Signals/Core/ImmutableEmptySignal{T}.cs @@ -7,25 +7,53 @@ namespace ReactiveUI.Primitives.Signals.Core; +/// +/// Represents the ImmutableEmptySignal class. +/// +/// The T type. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal sealed class ImmutableEmptySignal : IRequireCurrentThread, IInlineSignal { #pragma warning disable SA1401 // Fields should be private + + /// + /// Executes the new operation. + /// + /// The result. internal static ImmutableEmptySignal Instance = new(); #pragma warning restore SA1401 // Fields should be private + /// + /// Initializes a new instance of the class. + /// private ImmutableEmptySignal() { } + /// + /// Executes the IsRequiredSubscribeOnCurrentThread operation. + /// + /// The result. public bool IsRequiredSubscribeOnCurrentThread() => false; + /// + /// Executes the Subscribe operation. + /// + /// The observer value. + /// The result. public IDisposable Subscribe(IObserver observer) { observer.OnCompleted(); return Disposable.Empty; } + /// + /// Executes the Subscribe operation. + /// + /// The onNext value. + /// The onError value. + /// The onCompleted value. + /// The result. public IDisposable Subscribe(Action onNext, Action onError, Action onCompleted) { onCompleted(); diff --git a/src/ReactiveUI.Primitives/Signals/Core/ImmutableNeverSignal{T}.cs b/src/ReactiveUI.Primitives/Signals/Core/ImmutableNeverSignal{T}.cs index f4cbcc2..2508e12 100644 --- a/src/ReactiveUI.Primitives/Signals/Core/ImmutableNeverSignal{T}.cs +++ b/src/ReactiveUI.Primitives/Signals/Core/ImmutableNeverSignal{T}.cs @@ -7,15 +7,33 @@ namespace ReactiveUI.Primitives.Signals.Core; +/// +/// Represents the ImmutableNeverSignal class. +/// +/// The T type. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] -internal class ImmutableNeverSignal : IRequireCurrentThread +internal sealed class ImmutableNeverSignal : IRequireCurrentThread { #pragma warning disable SA1401 // Fields should be private + + /// + /// Executes the new operation. + /// + /// The result. internal static ImmutableNeverSignal Instance = new(); #pragma warning restore SA1401 // Fields should be private + /// + /// Executes the IsRequiredSubscribeOnCurrentThread operation. + /// + /// The result. public bool IsRequiredSubscribeOnCurrentThread() => false; + /// + /// Executes the Subscribe operation. + /// + /// The observer value. + /// The result. public IDisposable Subscribe(IObserver observer) => Disposable.Empty; } diff --git a/src/ReactiveUI.Primitives/Signals/Core/ImmutableReturnFalseSignal.cs b/src/ReactiveUI.Primitives/Signals/Core/ImmutableReturnFalseSignal.cs index 991cf0a..89349bb 100644 --- a/src/ReactiveUI.Primitives/Signals/Core/ImmutableReturnFalseSignal.cs +++ b/src/ReactiveUI.Primitives/Signals/Core/ImmutableReturnFalseSignal.cs @@ -7,19 +7,39 @@ namespace ReactiveUI.Primitives.Signals.Core; +/// +/// Represents the ImmutableReturnFalseSignal class. +/// [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] -internal class ImmutableReturnFalseSignal : IObservable, IRequireCurrentThread, IInlineSignal +internal sealed class ImmutableReturnFalseSignal : IRequireCurrentThread, IInlineSignal { #pragma warning disable SA1401 // Fields should be private + + /// + /// Executes the new operation. + /// + /// The result. internal static ImmutableReturnFalseSignal Instance = new(); #pragma warning restore SA1401 // Fields should be private + /// + /// Initializes a new instance of the class. + /// private ImmutableReturnFalseSignal() { } + /// + /// Executes the IsRequiredSubscribeOnCurrentThread operation. + /// + /// The result. public bool IsRequiredSubscribeOnCurrentThread() => false; + /// + /// Executes the Subscribe operation. + /// + /// The observer value. + /// The result. public IDisposable Subscribe(IObserver observer) { observer.OnNext(false); @@ -27,6 +47,13 @@ public IDisposable Subscribe(IObserver observer) return Disposable.Empty; } + /// + /// Executes the Subscribe operation. + /// + /// The onNext value. + /// The onError value. + /// The onCompleted value. + /// The result. public IDisposable Subscribe(Action onNext, Action onError, Action onCompleted) { onNext(false); diff --git a/src/ReactiveUI.Primitives/Signals/Core/ImmutableReturnInt32Signal.cs b/src/ReactiveUI.Primitives/Signals/Core/ImmutableReturnInt32Signal.cs index 5e8d880..0ddff0c 100644 --- a/src/ReactiveUI.Primitives/Signals/Core/ImmutableReturnInt32Signal.cs +++ b/src/ReactiveUI.Primitives/Signals/Core/ImmutableReturnInt32Signal.cs @@ -7,39 +7,77 @@ namespace ReactiveUI.Primitives.Signals.Core; -internal class ImmutableReturnInt32Signal : IObservable, IRequireCurrentThread, IInlineSignal +/// +/// Represents the ImmutableReturnInt32Signal class. +/// +internal sealed class ImmutableReturnInt32Signal : IRequireCurrentThread, IInlineSignal { - private static readonly ImmutableReturnInt32Signal[] Caches = new ImmutableReturnInt32Signal[] - { - new ImmutableReturnInt32Signal(-1), - new ImmutableReturnInt32Signal(0), - new ImmutableReturnInt32Signal(1), - new ImmutableReturnInt32Signal(2), - new ImmutableReturnInt32Signal(3), - new ImmutableReturnInt32Signal(4), - new ImmutableReturnInt32Signal(5), - new ImmutableReturnInt32Signal(6), - new ImmutableReturnInt32Signal(7), - new ImmutableReturnInt32Signal(8), - new ImmutableReturnInt32Signal(9), - }; + /// + /// Stores state for the signal implementation. + /// + private const int MinCachedValue = -1; + + /// + /// Stores state for the signal implementation. + /// + private const int MaxCachedValue = 9; + + /// + /// Executes the new operation. + /// + /// The result. + private static readonly ImmutableReturnInt32Signal[] Caches = + [ + new(-1), + new(0), + new(1), + new(2), + new(3), + new(4), + new(5), + new(6), + new(7), + new(8), + new(9), + ]; + /// + /// Stores state for the signal implementation. + /// private readonly int _x; + /// + /// Initializes a new instance of the class. + /// + /// The x value. internal ImmutableReturnInt32Signal(int x) => _x = x; + /// + /// Executes the GetInt32Signals operation. + /// + /// The x value. + /// The result. public static IObservable GetInt32Signals(int x) { - if (x >= -1 && x <= 9) + if (x is >= MinCachedValue and <= MaxCachedValue) { - return Caches[x + 1]; + return Caches[x - MinCachedValue]; } return new ImmediateReturnSignal(x); } + /// + /// Executes the IsRequiredSubscribeOnCurrentThread operation. + /// + /// The result. public bool IsRequiredSubscribeOnCurrentThread() => false; + /// + /// Executes the Subscribe operation. + /// + /// The observer value. + /// The result. public IDisposable Subscribe(IObserver observer) { observer.OnNext(_x); @@ -47,6 +85,13 @@ public IDisposable Subscribe(IObserver observer) return Disposable.Empty; } + /// + /// Executes the Subscribe operation. + /// + /// The onNext value. + /// The onError value. + /// The onCompleted value. + /// The result. public IDisposable Subscribe(Action onNext, Action onError, Action onCompleted) { onNext(_x); diff --git a/src/ReactiveUI.Primitives/Signals/Core/ImmutableReturnRxVoidSignal.cs b/src/ReactiveUI.Primitives/Signals/Core/ImmutableReturnRxVoidSignal.cs index a2ddfc1..be4babb 100644 --- a/src/ReactiveUI.Primitives/Signals/Core/ImmutableReturnRxVoidSignal.cs +++ b/src/ReactiveUI.Primitives/Signals/Core/ImmutableReturnRxVoidSignal.cs @@ -7,19 +7,39 @@ namespace ReactiveUI.Primitives.Signals.Core; +/// +/// Represents the ImmutableReturnRxVoidSignal class. +/// [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] -internal sealed class ImmutableReturnRxVoidSignal : IObservable, IRequireCurrentThread, IInlineSignal +internal sealed class ImmutableReturnRxVoidSignal : IRequireCurrentThread, IInlineSignal { #pragma warning disable SA1401 // Fields should be private + + /// + /// Executes the new operation. + /// + /// The result. internal static ImmutableReturnRxVoidSignal Instance = new(); #pragma warning restore SA1401 // Fields should be private + /// + /// Initializes a new instance of the class. + /// private ImmutableReturnRxVoidSignal() { } + /// + /// Executes the IsRequiredSubscribeOnCurrentThread operation. + /// + /// The result. public bool IsRequiredSubscribeOnCurrentThread() => false; + /// + /// Executes the Subscribe operation. + /// + /// The observer value. + /// The result. public IDisposable Subscribe(IObserver observer) { observer.OnNext(RxVoid.Default); @@ -27,6 +47,13 @@ public IDisposable Subscribe(IObserver observer) return Disposable.Empty; } + /// + /// Executes the Subscribe operation. + /// + /// The onNext value. + /// The onError value. + /// The onCompleted value. + /// The result. public IDisposable Subscribe(Action onNext, Action onError, Action onCompleted) { onNext(RxVoid.Default); diff --git a/src/ReactiveUI.Primitives/Signals/Core/ImmutableReturnTrueSignal.cs b/src/ReactiveUI.Primitives/Signals/Core/ImmutableReturnTrueSignal.cs index 849017b..5def093 100644 --- a/src/ReactiveUI.Primitives/Signals/Core/ImmutableReturnTrueSignal.cs +++ b/src/ReactiveUI.Primitives/Signals/Core/ImmutableReturnTrueSignal.cs @@ -7,19 +7,39 @@ namespace ReactiveUI.Primitives.Signals.Core; +/// +/// Represents the ImmutableReturnTrueSignal class. +/// [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] -internal sealed class ImmutableReturnTrueSignal : IObservable, IRequireCurrentThread, IInlineSignal +internal sealed class ImmutableReturnTrueSignal : IRequireCurrentThread, IInlineSignal { #pragma warning disable SA1401 // Fields should be private - internal static ImmutableReturnTrueSignal Instance = new ImmutableReturnTrueSignal(); + + /// + /// Executes the new operation. + /// + /// The result. + internal static ImmutableReturnTrueSignal Instance = new(); #pragma warning restore SA1401 // Fields should be private + /// + /// Initializes a new instance of the class. + /// private ImmutableReturnTrueSignal() { } + /// + /// Executes the IsRequiredSubscribeOnCurrentThread operation. + /// + /// The result. public bool IsRequiredSubscribeOnCurrentThread() => false; + /// + /// Executes the Subscribe operation. + /// + /// The observer value. + /// The result. public IDisposable Subscribe(IObserver observer) { observer.OnNext(true); @@ -27,6 +47,13 @@ public IDisposable Subscribe(IObserver observer) return Disposable.Empty; } + /// + /// Executes the Subscribe operation. + /// + /// The onNext value. + /// The onError value. + /// The onCompleted value. + /// The result. public IDisposable Subscribe(Action onNext, Action onError, Action onCompleted) { onNext(true); diff --git a/src/ReactiveUI.Primitives/Signals/Core/RangeSignal.cs b/src/ReactiveUI.Primitives/Signals/Core/RangeSignal.cs index 916d125..83b1f4c 100644 --- a/src/ReactiveUI.Primitives/Signals/Core/RangeSignal.cs +++ b/src/ReactiveUI.Primitives/Signals/Core/RangeSignal.cs @@ -7,20 +7,44 @@ namespace ReactiveUI.Primitives.Signals.Core; +/// +/// Represents the RangeSignal class. +/// [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] -internal sealed class RangeSignal : IObservable, IRequireCurrentThread, IInlineSignal +internal sealed class RangeSignal : IRequireCurrentThread, IInlineSignal { + /// + /// Stores state for the signal implementation. + /// private readonly int _start; + + /// + /// Stores state for the signal implementation. + /// private readonly int _count; + /// + /// Initializes a new instance of the class. + /// + /// The start value. + /// The count value. public RangeSignal(int start, int count) { _start = start; _count = count; } + /// + /// Executes the IsRequiredSubscribeOnCurrentThread operation. + /// + /// The result. public bool IsRequiredSubscribeOnCurrentThread() => false; + /// + /// Executes the Subscribe operation. + /// + /// The observer value. + /// The result. public IDisposable Subscribe(IObserver observer) { if (observer == null) @@ -37,6 +61,13 @@ public IDisposable Subscribe(IObserver observer) return Disposable.Empty; } + /// + /// Executes the Subscribe operation. + /// + /// The onNext value. + /// The onError value. + /// The onCompleted value. + /// The result. public IDisposable Subscribe(Action onNext, Action onError, Action onCompleted) { if (onNext == null) diff --git a/src/ReactiveUI.Primitives/Signals/Core/RepeatSignal{T}.cs b/src/ReactiveUI.Primitives/Signals/Core/RepeatSignal{T}.cs index c59cf6b..63d4e30 100644 --- a/src/ReactiveUI.Primitives/Signals/Core/RepeatSignal{T}.cs +++ b/src/ReactiveUI.Primitives/Signals/Core/RepeatSignal{T}.cs @@ -7,20 +7,45 @@ namespace ReactiveUI.Primitives.Signals.Core; +/// +/// Represents the RepeatSignal class. +/// +/// The T type. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] -internal sealed class RepeatSignal : IObservable, IRequireCurrentThread, IInlineSignal +internal sealed class RepeatSignal : IRequireCurrentThread, IInlineSignal { + /// + /// Stores state for the signal implementation. + /// private readonly T _value; + + /// + /// Stores state for the signal implementation. + /// private readonly int _count; + /// + /// Initializes a new instance of the class. + /// + /// The value. + /// The count value. public RepeatSignal(T value, int count) { _value = value; _count = count; } + /// + /// Executes the IsRequiredSubscribeOnCurrentThread operation. + /// + /// The result. public bool IsRequiredSubscribeOnCurrentThread() => false; + /// + /// Executes the Subscribe operation. + /// + /// The observer value. + /// The result. public IDisposable Subscribe(IObserver observer) { if (observer == null) @@ -37,6 +62,13 @@ public IDisposable Subscribe(IObserver observer) return Disposable.Empty; } + /// + /// Executes the Subscribe operation. + /// + /// The onNext value. + /// The onError value. + /// The onCompleted value. + /// The result. public IDisposable Subscribe(Action onNext, Action onError, Action onCompleted) { if (onNext == null) diff --git a/src/ReactiveUI.Primitives/Signals/Core/ReturnSignal{T}.cs b/src/ReactiveUI.Primitives/Signals/Core/ReturnSignal{T}.cs index 2df529f..67db66b 100644 --- a/src/ReactiveUI.Primitives/Signals/Core/ReturnSignal{T}.cs +++ b/src/ReactiveUI.Primitives/Signals/Core/ReturnSignal{T}.cs @@ -7,12 +7,28 @@ namespace ReactiveUI.Primitives.Signals.Core; +/// +/// Represents the ReturnSignal class. +/// +/// The T type. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] -internal class ReturnSignal : SignalsBase +internal sealed class ReturnSignal : SignalsBase { + /// + /// Stores state for the signal implementation. + /// private readonly T _value; + + /// + /// Stores state for the signal implementation. + /// private readonly ISequencer _scheduler; + /// + /// Initializes a new instance of the class. + /// + /// The value. + /// The scheduler value. public ReturnSignal(T value, ISequencer scheduler) : base(scheduler == Sequencer.CurrentThread) { @@ -20,6 +36,12 @@ public ReturnSignal(T value, ISequencer scheduler) _scheduler = scheduler; } + /// + /// Executes the SubscribeCore operation. + /// + /// The observer value. + /// The cancel value. + /// The result. protected override IDisposable SubscribeCore(IObserver observer, IDisposable cancel) { observer = new Return(observer, cancel); @@ -38,13 +60,25 @@ protected override IDisposable SubscribeCore(IObserver observer, IDisposable }); } - private class Return : WitnessBase + /// + /// Represents the Return class. + /// + private sealed class Return : WitnessBase { + /// + /// Initializes a new instance of the class. + /// + /// The observer value. + /// The cancel value. public Return(IObserver observer, IDisposable cancel) : base(observer, cancel) { } + /// + /// Executes the OnNext operation. + /// + /// The value. public override void OnNext(T value) { try @@ -58,6 +92,10 @@ public override void OnNext(T value) } } + /// + /// Executes the OnError operation. + /// + /// The error value. public override void OnError(Exception error) { try @@ -70,6 +108,9 @@ public override void OnError(Exception error) } } + /// + /// Executes the OnCompleted operation. + /// public override void OnCompleted() { try diff --git a/src/ReactiveUI.Primitives/Signals/Core/SignalsBase{T}.cs b/src/ReactiveUI.Primitives/Signals/Core/SignalsBase{T}.cs index 0dbba1b..3ff53bb 100644 --- a/src/ReactiveUI.Primitives/Signals/Core/SignalsBase{T}.cs +++ b/src/ReactiveUI.Primitives/Signals/Core/SignalsBase{T}.cs @@ -8,16 +8,36 @@ namespace ReactiveUI.Primitives.Signals.Core; +/// +/// Represents the SignalsBase class. +/// +/// The T type. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal abstract class SignalsBase : IRequireCurrentThread { + /// + /// Stores state for the signal implementation. + /// private readonly bool _isRequiredSubscribeOnCurrentThread; - internal SignalsBase(bool isRequiredSubscribeOnCurrentThread) => + /// + /// Initializes a new instance of the class. + /// + /// The isRequiredSubscribeOnCurrentThread value. + private protected SignalsBase(bool isRequiredSubscribeOnCurrentThread) => _isRequiredSubscribeOnCurrentThread = isRequiredSubscribeOnCurrentThread; + /// + /// Executes the IsRequiredSubscribeOnCurrentThread operation. + /// + /// The result. public bool IsRequiredSubscribeOnCurrentThread() => _isRequiredSubscribeOnCurrentThread; + /// + /// Executes the Subscribe operation. + /// + /// The observer value. + /// The result. public IDisposable Subscribe(IObserver observer) { if (observer == null) @@ -39,5 +59,11 @@ public IDisposable Subscribe(IObserver observer) return subscription; } + /// + /// Executes the SubscribeCore operation. + /// + /// The observer value. + /// The cancel value. + /// The result. protected abstract IDisposable SubscribeCore(IObserver observer, IDisposable cancel); } diff --git a/src/ReactiveUI.Primitives/Signals/Core/ThrowSignal{T}.cs b/src/ReactiveUI.Primitives/Signals/Core/ThrowSignal{T}.cs index 896a6f4..f66d6e4 100644 --- a/src/ReactiveUI.Primitives/Signals/Core/ThrowSignal{T}.cs +++ b/src/ReactiveUI.Primitives/Signals/Core/ThrowSignal{T}.cs @@ -7,12 +7,28 @@ namespace ReactiveUI.Primitives.Signals.Core; +/// +/// Represents the ThrowSignal class. +/// +/// The T type. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] -internal class ThrowSignal : SignalsBase +internal sealed class ThrowSignal : SignalsBase { + /// + /// Stores state for the signal implementation. + /// private readonly Exception _error; + + /// + /// Stores state for the signal implementation. + /// private readonly ISequencer _scheduler; + /// + /// Initializes a new instance of the class. + /// + /// The error value. + /// The scheduler value. public ThrowSignal(Exception error, ISequencer scheduler) : base(scheduler == Sequencer.CurrentThread) { @@ -20,6 +36,12 @@ public ThrowSignal(Exception error, ISequencer scheduler) _scheduler = scheduler; } + /// + /// Executes the SubscribeCore operation. + /// + /// The observer value. + /// The cancel value. + /// The result. protected override IDisposable SubscribeCore(IObserver observer, IDisposable cancel) { observer = new Throw(observer, cancel); @@ -37,13 +59,25 @@ protected override IDisposable SubscribeCore(IObserver observer, IDisposable }); } - private class Throw : WitnessBase + /// + /// Represents the Throw class. + /// + private sealed class Throw : WitnessBase { + /// + /// Initializes a new instance of the class. + /// + /// The observer value. + /// The cancel value. public Throw(IObserver observer, IDisposable cancel) : base(observer, cancel) { } + /// + /// Executes the OnNext operation. + /// + /// The value. public override void OnNext(T value) { try @@ -57,6 +91,10 @@ public override void OnNext(T value) } } + /// + /// Executes the OnError operation. + /// + /// The error value. public override void OnError(Exception error) { try @@ -69,6 +107,9 @@ public override void OnError(Exception error) } } + /// + /// Executes the OnCompleted operation. + /// public override void OnCompleted() { try diff --git a/src/ReactiveUI.Primitives/Signals/Core/WitnessBase{TSource,TResult}.cs b/src/ReactiveUI.Primitives/Signals/Core/WitnessBase{TSource,TResult}.cs index 5cb210c..f7e959c 100644 --- a/src/ReactiveUI.Primitives/Signals/Core/WitnessBase{TSource,TResult}.cs +++ b/src/ReactiveUI.Primitives/Signals/Core/WitnessBase{TSource,TResult}.cs @@ -6,30 +6,87 @@ namespace ReactiveUI.Primitives.Signals.Core { + /// + /// Represents the WitnessBase class. + /// + /// The TSource type. + /// The TResult type. internal abstract class WitnessBase : IDisposable, IObserver { #pragma warning disable SA1401 // Fields should be private + + /// + /// Stores state for the signal implementation. + /// protected internal volatile IObserver Observer; #pragma warning restore SA1401 // Fields should be private + + /// + /// Stores state for the signal implementation. + /// private IDisposable? _cancel; - internal WitnessBase(IObserver observer, IDisposable cancel) + /// + /// Stores state for the signal implementation. + /// + private bool _disposed; + + /// + /// Initializes a new instance of the class. + /// + /// The observer value. + /// The cancel value. + private protected WitnessBase(IObserver observer, IDisposable cancel) { _cancel = cancel ?? throw new ArgumentNullException(nameof(cancel)); Observer = observer; } + /// + /// Executes the OnNext operation. + /// + /// The value. public abstract void OnNext(TSource value); + /// + /// Executes the OnError operation. + /// + /// The error value. public abstract void OnError(Exception error); + /// + /// Executes the OnCompleted operation. + /// public abstract void OnCompleted(); + /// + /// Executes the Dispose operation. + /// public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Executes the Dispose operation. + /// + /// The disposing value. + protected virtual void Dispose(bool disposing) { Observer = EmptyWitness.Instance; - var target = Interlocked.Exchange(ref _cancel, null); - target?.Dispose(); + if (_disposed) + { + return; + } + + if (disposing) + { + var target = Interlocked.Exchange(ref _cancel, null); + target?.Dispose(); + } + + _disposed = true; } } } diff --git a/src/ReactiveUI.Primitives/Signals/Core/WitnessOnSignal{T}.cs b/src/ReactiveUI.Primitives/Signals/Core/WitnessOnSignal{T}.cs index 4b7ba80..77749a5 100644 --- a/src/ReactiveUI.Primitives/Signals/Core/WitnessOnSignal{T}.cs +++ b/src/ReactiveUI.Primitives/Signals/Core/WitnessOnSignal{T}.cs @@ -8,12 +8,28 @@ namespace ReactiveUI.Primitives.Signals.Core; +/// +/// Represents the WitnessOnSignal class. +/// +/// The T type. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] -internal class WitnessOnSignal : SignalsBase +internal sealed class WitnessOnSignal : SignalsBase { + /// + /// Stores state for the signal implementation. + /// private readonly IObservable _source; + + /// + /// Stores state for the signal implementation. + /// private readonly ISequencer _scheduler; + /// + /// Initializes a new instance of the class. + /// + /// The source value. + /// The scheduler value. public WitnessOnSignal(IObservable source, ISequencer scheduler) : base(true) { @@ -21,6 +37,12 @@ public WitnessOnSignal(IObservable source, ISequencer scheduler) _scheduler = scheduler; } + /// + /// Executes the SubscribeCore operation. + /// + /// The observer value. + /// The cancel value. + /// The result. protected override IDisposable SubscribeCore(IObserver observer, IDisposable cancel) { if (_scheduler is not ThreadPoolSequencer queueing) @@ -28,19 +50,44 @@ protected override IDisposable SubscribeCore(IObserver observer, IDisposable return new WitnessOn(this, observer, cancel).Run(); } - return new WitnessOn_(this, queueing, observer, cancel).Run(); + return new ThreadPoolWitnessOn(this, queueing, observer, cancel).Run(); } - private class WitnessOn : WitnessBase + /// + /// Represents the WitnessOn class. + /// + private sealed class WitnessOn : WitnessBase { + /// + /// Stores state for the signal implementation. + /// private readonly WitnessOnSignal _parent; + + /// + /// Executes the new operation. + /// + /// The result. private readonly LinkedList _actions = new(); + + /// + /// Stores state for the signal implementation. + /// private bool _isDisposed; + /// + /// Initializes a new instance of the class. + /// + /// The parent value. + /// The observer value. + /// The cancel value. public WitnessOn(WitnessOnSignal parent, IObserver observer, IDisposable cancel) : base(observer, cancel) => _parent = parent; - public IDisposable Run() + /// + /// Executes the Run operation. + /// + /// The result. + public MultipleDisposable Run() { _isDisposed = false; @@ -62,12 +109,27 @@ public IDisposable Run() })); } + /// + /// Executes the OnNext operation. + /// + /// The value. public override void OnNext(T value) => QueueAction(new Spark.OnNextSpark(value)); + /// + /// Executes the OnError operation. + /// + /// The error value. public override void OnError(Exception error) => QueueAction(new Spark.OnErrorSpark(error)); + /// + /// Executes the OnCompleted operation. + /// public override void OnCompleted() => QueueAction(new Spark.OnCompletedSpark()); + /// + /// Executes the QueueAction operation. + /// + /// The data value. private void QueueAction(Spark data) { var action = new SchedulableAction(data); @@ -83,6 +145,9 @@ private void QueueAction(Spark data) } } + /// + /// Executes the ProcessNext operation. + /// private void ProcessNext() { lock (_actions) @@ -103,18 +168,7 @@ private void ProcessNext() { try { - switch (action.Data?.Kind) - { - case SparkKind.OnNext: - Observer.OnNext(action.Data.Value); - break; - case SparkKind.OnError: - Observer.OnError(action.Data.Exception); - break; - case SparkKind.OnCompleted: - Observer.OnCompleted(); - break; - } + Dispatch(action); } finally { @@ -136,77 +190,191 @@ private void ProcessNext() } } - private class SchedulableAction : IDisposable + /// + /// Executes the Dispatch operation. + /// + /// The action value. + private void Dispatch(SchedulableAction action) + { + switch (action.Data.Kind) + { + case SparkKind.OnNext: + { + Observer.OnNext(action.Data.Value); + break; + } + + case SparkKind.OnError: + { + Observer.OnError(action.Data.Exception); + break; + } + + case SparkKind.OnCompleted: + { + Observer.OnCompleted(); + break; + } + } + } + + /// + /// Represents the SchedulableAction class. + /// + private sealed class SchedulableAction : IDisposable { + /// + /// Initializes a new instance of the class. + /// + /// The data value. public SchedulableAction(Spark data) { Data = data; } + /// + /// Gets the value. + /// public Spark Data { get; } + /// + /// Gets or sets the value. + /// public LinkedListNode? Node { get; set; } + /// + /// Gets or sets the value. + /// public IDisposable? Schedule { get; set; } + /// + /// Gets a value indicating whether the condition is met. + /// public bool IsScheduled => Schedule != null; + /// + /// Executes the Dispose operation. + /// public void Dispose() { Schedule?.Dispose(); Schedule = null; - if (Node?.List != null) + if (Node?.List == null) { - Node.List.Remove(Node); + return; } + + Node.List.Remove(Node); } } } - private class WitnessOn_ : WitnessBase + /// + /// Represents the ThreadPoolWitnessOn class. + /// + private sealed class ThreadPoolWitnessOn : WitnessBase { + /// + /// Stores state for the signal implementation. + /// private readonly WitnessOnSignal _parent; + + /// + /// Stores state for the signal implementation. + /// private readonly ThreadPoolSequencer _scheduler; + + /// + /// Stores state for the signal implementation. + /// private readonly BooleanDisposable _isDisposed; + + /// + /// Stores state for the signal implementation. + /// private readonly Action _onNext; - public WitnessOn_(WitnessOnSignal parent, ThreadPoolSequencer scheduler, IObserver observer, IDisposable cancel) + /// + /// Initializes a new instance of the class. + /// + /// The parent value. + /// The scheduler value. + /// The observer value. + /// The cancel value. + public ThreadPoolWitnessOn(WitnessOnSignal parent, ThreadPoolSequencer scheduler, IObserver observer, IDisposable cancel) : base(observer, cancel) { _parent = parent; _scheduler = scheduler; _isDisposed = new BooleanDisposable(); - _onNext = new Action(OnNext_); + _onNext = OnNextCore; } - public IDisposable Run() + /// + /// Executes the Run operation. + /// + /// The result. + public MultipleDisposable Run() { var sourceDisposable = _parent._source.Subscribe(this); return new MultipleDisposable(sourceDisposable, _isDisposed); } + /// + /// Executes the OnNext operation. + /// + /// The value. public override void OnNext(T value) => - _scheduler.Schedule(value, (s, v) => + _scheduler.Schedule(value, (_, v) => { _onNext(v); return _isDisposed; }); + /// + /// Executes the OnError operation. + /// + /// The error value. public override void OnError(Exception error) => - _scheduler.Schedule(error, (s, v) => + _scheduler.Schedule(error, (_, v) => { - OnError_(v); + OnErrorCore(v); return _isDisposed; }); + /// + /// Executes the OnCompleted operation. + /// public override void OnCompleted() => - _scheduler.Schedule(() => OnCompleted_(RxVoid.Default)); + _scheduler.Schedule(OnCompletedCore); - private void OnNext_(T value) => Observer.OnNext(value); + /// + /// Executes the Dispose operation. + /// + /// The disposing value. + protected override void Dispose(bool disposing) + { + if (disposing) + { + _isDisposed.Dispose(); + } + + base.Dispose(disposing); + } - private void OnError_(Exception error) + /// + /// Executes the OnNextCore operation. + /// + /// The value. + private void OnNextCore(T value) => Observer.OnNext(value); + + /// + /// Executes the OnErrorCore operation. + /// + /// The error value. + private void OnErrorCore(Exception error) { try { @@ -218,7 +386,10 @@ private void OnError_(Exception error) } } - private void OnCompleted_(RxVoid v) + /// + /// Executes the OnCompletedCore operation. + /// + private void OnCompletedCore() { try { diff --git a/src/ReactiveUI.Primitives/Signals/Signal{Catch}.cs b/src/ReactiveUI.Primitives/Signals/Signal{Catch}.cs index 97c2eb6..55ff3bb 100644 --- a/src/ReactiveUI.Primitives/Signals/Signal{Catch}.cs +++ b/src/ReactiveUI.Primitives/Signals/Signal{Catch}.cs @@ -19,7 +19,9 @@ public static partial class Signal /// The type of the exception to catch and handle. Needs to derive from . /// Source sequence. /// Exception handler function, producing another observable sequence. - /// An observable sequence containing the source sequence's elements, followed by the elements produced by the handler's resulting observable sequence in case an exception occurred. + /// + /// An observable sequence containing the source sequence's elements, followed by the handler sequence's elements when an exception occurs. + /// /// or is null. public static IObservable Catch(this IObservable source, Func> handler) where TException : Exception diff --git a/src/ReactiveUI.Primitives/Signals/Signal{Empty}.cs b/src/ReactiveUI.Primitives/Signals/Signal{Empty}.cs index de223f4..1f20a89 100644 --- a/src/ReactiveUI.Primitives/Signals/Signal{Empty}.cs +++ b/src/ReactiveUI.Primitives/Signals/Signal{Empty}.cs @@ -18,6 +18,7 @@ public static partial class Signal /// The Type. /// The scheduler. /// An Signals. +#pragma warning disable S4018 // Result type is intentionally explicit for Rx-style factory APIs. public static IObservable Empty(ISequencer scheduler) { if (scheduler == Sequencer.Immediate) @@ -47,6 +48,7 @@ public static IObservable Empty(ISequencer scheduler, T witness) => /// An Signals. public static IObservable Empty() => Empty(Sequencer.Immediate); +#pragma warning restore S4018 /// /// Empty Signals. Returns only OnCompleted. witness is for type inference. diff --git a/src/ReactiveUI.Primitives/Signals/Signal{Factories}.cs b/src/ReactiveUI.Primitives/Signals/Signal{Factories}.cs index b29e7ad..7d8f1ca 100644 --- a/src/ReactiveUI.Primitives/Signals/Signal{Factories}.cs +++ b/src/ReactiveUI.Primitives/Signals/Signal{Factories}.cs @@ -46,26 +46,30 @@ public static IObservable Range(int start, int count, ISequencer scheduler) return new RangeSignal(start, count); } - return CreateSafe(observer => scheduler.Schedule(() => - { - for (var i = 0; i < count; i++) + return CreateSafe( + observer => scheduler.Schedule(() => { - observer.OnNext(start + i); - } + for (var i = 0; i < count; i++) + { + observer.OnNext(start + i); + } - observer.OnCompleted(); - }), scheduler == Sequencer.CurrentThread); + observer.OnCompleted(); + }), + scheduler == Sequencer.CurrentThread); } /// /// Creates a signal that repeats a value forever. /// public static IObservable Repeat(T value) => - Create(observer => Sequencer.CurrentThread.Schedule(self => - { - observer.OnNext(value); - self(); - }), true); + Create( + observer => Sequencer.CurrentThread.Schedule(self => + { + observer.OnNext(value); + self(); + }), + true); /// /// Creates a signal that repeats a value times. @@ -109,17 +113,19 @@ public static IObservable Unfold( throw new ArgumentNullException(nameof(resultSelector)); } - return CreateSafe(observer => Sequencer.CurrentThread.Schedule(() => - { - var state = initialState; - while (condition(state)) + return CreateSafe( + observer => Sequencer.CurrentThread.Schedule(() => { - observer.OnNext(resultSelector(state)); - state = iterate(state); - } + var state = initialState; + while (condition(state)) + { + observer.OnNext(resultSelector(state)); + state = iterate(state); + } - observer.OnCompleted(); - }), true); + observer.OnCompleted(); + }), + true); } /// @@ -170,12 +176,9 @@ public static IObservable FromEnumerable(IEnumerable values) return CreateSafe(observer => { - using (var enumerator = values.GetEnumerator()) + foreach (var value in values) { - while (enumerator.MoveNext()) - { - observer.OnNext(enumerator.Current); - } + observer.OnNext(value); } observer.OnCompleted(); @@ -195,7 +198,9 @@ public static IObservable FromTask(Task task) if (task.Status == TaskStatus.RanToCompletion) { - return Return(task.Result); +#pragma warning disable S4462 // Completed-task fast path avoids async state machine allocation. + return Return(task.GetAwaiter().GetResult()); +#pragma warning restore S4462 } if (task.IsCanceled) @@ -211,27 +216,29 @@ public static IObservable FromTask(Task task) return CreateSafe(observer => { var disposed = 0; - task.ContinueWith(completed => - { - if (Volatile.Read(ref disposed) != 0) + task.ContinueWith( + completed => { - return; - } + if (Volatile.Read(ref disposed) != 0) + { + return; + } - if (completed.IsCanceled) - { - observer.OnError(new TaskCanceledException(completed)); - } - else if (completed.IsFaulted) - { - observer.OnError(completed.Exception!.InnerException ?? completed.Exception); - } - else - { - observer.OnNext(completed.Result); - observer.OnCompleted(); - } - }, TaskScheduler.Default); + if (completed.IsCanceled) + { + observer.OnError(new TaskCanceledException(completed)); + } + else if (completed.IsFaulted) + { + observer.OnError(completed.Exception!.InnerException ?? completed.Exception); + } + else + { + observer.OnNext(completed.Result); + observer.OnCompleted(); + } + }, + TaskScheduler.Default); return Disposable.Create(() => Volatile.Write(ref disposed, 1)); }); @@ -240,32 +247,50 @@ public static IObservable FromTask(Task task) /// /// Runs a function on the supplied scheduler and emits its result. /// - public static IObservable Start(Func function, ISequencer? scheduler = null) + public static IObservable Start(Func function) => + Start(function, Sequencer.Default); + + /// + /// Runs a function on the supplied scheduler and emits its result. + /// + public static IObservable Start(Func function, ISequencer scheduler) { if (function == null) { throw new ArgumentNullException(nameof(function)); } - scheduler ??= Sequencer.Default; - return CreateSafe(observer => scheduler.Schedule(() => + if (scheduler == null) { - try - { - observer.OnNext(function()); - observer.OnCompleted(); - } - catch (Exception error) + throw new ArgumentNullException(nameof(scheduler)); + } + + return CreateSafe( + observer => scheduler.Schedule(() => { - observer.OnError(error); - } - }), scheduler == Sequencer.CurrentThread); + try + { + observer.OnNext(function()); + observer.OnCompleted(); + } + catch (Exception error) + { + observer.OnError(error); + } + }), + scheduler == Sequencer.CurrentThread); } /// /// Runs an action on the supplied scheduler and emits when it completes. /// - public static IObservable Start(Action action, ISequencer? scheduler = null) + public static IObservable Start(Action action) => + Start(action, Sequencer.Default); + + /// + /// Runs an action on the supplied scheduler and emits when it completes. + /// + public static IObservable Start(Action action, ISequencer scheduler) { if (action == null) { @@ -285,186 +310,287 @@ public static IObservable Start(Action action, ISequencer? scheduler = n /// /// Creates a signal from an async enumerable sequence and cancels enumeration when disposed. /// - public static IObservable FromAsyncEnumerable(IAsyncEnumerable values, CancellationToken cancellationToken = default) + public static IObservable FromAsyncEnumerable(IAsyncEnumerable values) => + FromAsyncEnumerable(values, CancellationToken.None); + + /// + /// Creates a signal from an async enumerable sequence and cancels enumeration when disposed. + /// + public static IObservable FromAsyncEnumerable(IAsyncEnumerable values, CancellationToken cancellationToken) { if (values == null) { throw new ArgumentNullException(nameof(values)); } - return CreateSafe(observer => - { - var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - IAsyncEnumerator? enumerator = null; - _ = Task.Run(async () => - { - try - { - enumerator = values.GetAsyncEnumerator(cts.Token); - while (!cts.IsCancellationRequested && await enumerator.MoveNextAsync().ConfigureAwait(false)) - { - observer.OnNext(enumerator.Current); - } - - if (!cts.IsCancellationRequested) - { - observer.OnCompleted(); - } - } - catch (OperationCanceledException) when (cts.IsCancellationRequested) - { - } - catch (Exception error) when (!cts.IsCancellationRequested) - { - observer.OnError(error); - } - finally - { - if (enumerator != null) - { - await enumerator.DisposeAsync().ConfigureAwait(false); - } - - cts.Dispose(); - } - }, CancellationToken.None); - - return Disposable.Create(() => - { - cts.Cancel(); - var current = Volatile.Read(ref enumerator); - if (current != null) - { - try - { - _ = current.DisposeAsync().AsTask(); - } - catch (NotSupportedException) - { - } - } - }); - }); + return CreateSafe(observer => SubscribeAsyncEnumerable(values, observer, cancellationToken)); } + #endif /// /// Emits a single zero tick after the due time. /// - public static IObservable After(TimeSpan dueTime, ISequencer? scheduler = null) + public static IObservable After(TimeSpan dueTime) => + After(dueTime, ThreadPoolSequencer.Instance); + + /// + /// Emits a single zero tick after the due time. + /// + public static IObservable After(TimeSpan dueTime, ISequencer scheduler) { - scheduler ??= ThreadPoolSequencer.Instance; - return CreateSafe(observer => scheduler.Schedule(Sequencer.Normalize(dueTime), () => + if (scheduler == null) { - observer.OnNext(0L); - observer.OnCompleted(); - }), scheduler == Sequencer.CurrentThread); + throw new ArgumentNullException(nameof(scheduler)); + } + + return CreateSafe( + observer => scheduler.Schedule( + Sequencer.Normalize(dueTime), + () => + { + observer.OnNext(0L); + observer.OnCompleted(); + }), + scheduler == Sequencer.CurrentThread); } /// /// Emits monotonically increasing ticks at the specified period. /// - public static IObservable Every(TimeSpan period, ISequencer? scheduler = null) + public static IObservable Every(TimeSpan period) => + Every(period, ThreadPoolSequencer.Instance); + + /// + /// Emits monotonically increasing ticks at the specified period. + /// + public static IObservable Every(TimeSpan period, ISequencer scheduler) { if (period < TimeSpan.Zero) { throw new ArgumentOutOfRangeException(nameof(period)); } - scheduler ??= ThreadPoolSequencer.Instance; - return CreateSafe(observer => + if (scheduler == null) { - var slot = new SingleReplaceableDisposable(); - var tick = 0L; - Action? scheduleNext = null; - scheduleNext = () => slot.Create(scheduler.Schedule(period, () => + throw new ArgumentNullException(nameof(scheduler)); + } + + return CreateSafe( + observer => { - observer.OnNext(tick++); - if (!slot.IsDisposed) + var slot = new SingleReplaceableDisposable(); + var tick = 0L; + Action? scheduleNext = null; + scheduleNext = () => slot.Create(scheduler.Schedule(period, () => { + observer.OnNext(tick++); + if (slot.IsDisposed) + { + return; + } + scheduleNext!(); - } - })); + })); - scheduleNext(); - return slot; - }, scheduler == Sequencer.CurrentThread); + scheduleNext(); + return slot; + }, + scheduler == Sequencer.CurrentThread); } /// /// Alias for . /// - public static IObservable Pulse(TimeSpan period, ISequencer? scheduler = null) => Every(period, scheduler); + public static IObservable Pulse(TimeSpan period) => Every(period); + + /// + /// Alias for . + /// + public static IObservable Pulse(TimeSpan period, ISequencer scheduler) => Every(period, scheduler); /// /// Alias for . /// - public static IObservable Interval(TimeSpan period, ISequencer? scheduler = null) => Every(period, scheduler); + public static IObservable Interval(TimeSpan period) => Every(period); + + /// + /// Alias for . + /// + public static IObservable Interval(TimeSpan period, ISequencer scheduler) => Every(period, scheduler); + + /// + /// Alias for . + /// + public static IObservable Timer(TimeSpan dueTime) => After(dueTime); /// /// Alias for . /// - public static IObservable Timer(TimeSpan dueTime, ISequencer? scheduler = null) => After(dueTime, scheduler); + public static IObservable Timer(TimeSpan dueTime, ISequencer scheduler) => After(dueTime, scheduler); /// /// Creates a timer that emits first after and then at . /// - public static IObservable Timer(TimeSpan dueTime, TimeSpan period, ISequencer? scheduler = null) + public static IObservable Timer(TimeSpan dueTime, TimeSpan period) => + Timer(dueTime, period, ThreadPoolSequencer.Instance); + + /// + /// Creates a timer that emits first after and then at . + /// + public static IObservable Timer(TimeSpan dueTime, TimeSpan period, ISequencer scheduler) { - scheduler ??= ThreadPoolSequencer.Instance; - return CreateSafe(observer => + if (scheduler == null) { - var pocket = new MultipleDisposable(); - var current = 0L; - pocket.Add(scheduler.Schedule(Sequencer.Normalize(dueTime), () => - { - observer.OnNext(current++); - pocket.Add(Every(period, scheduler).Subscribe(value => observer.OnNext(current + value), observer.OnError, observer.OnCompleted)); - })); + throw new ArgumentNullException(nameof(scheduler)); + } - return pocket; - }, scheduler == Sequencer.CurrentThread); + return CreateSafe( + observer => + { + var pocket = new MultipleDisposable(); + var current = 0L; + pocket.Add( + scheduler.Schedule( + Sequencer.Normalize(dueTime), + () => + { + observer.OnNext(current++); + pocket.Add(Every(period, scheduler).Subscribe(value => observer.OnNext(current + value), observer.OnError, observer.OnCompleted)); + })); + + return pocket; + }, + scheduler == Sequencer.CurrentThread); } /// /// Concatenates the supplied signals. /// public static IObservable Concat(params IObservable[] sources) => - ReactiveUI.Primitives.LinqMixins.Concat(FromEnumerable(sources)); + FromEnumerable(sources).Concat(); /// /// Merges the supplied signals. /// public static IObservable Merge(params IObservable[] sources) => - ReactiveUI.Primitives.LinqMixins.Merge(FromEnumerable(sources)); + FromEnumerable(sources).Merge(); /// /// Races the supplied signals and mirrors the first one to produce a value or terminal signal. /// public static IObservable Race(params IObservable[] sources) => - ReactiveUI.Primitives.LinqMixins.Race(FromEnumerable(sources)); + FromEnumerable(sources).Race(); /// /// Zips two signals with a result selector. /// public static IObservable Zip(IObservable left, IObservable right, Func selector) => - ReactiveUI.Primitives.LinqMixins.Zip(left, right, selector); + left.Zip(right, selector); /// /// Combines the latest values from two signals. /// public static IObservable CombineLatest(IObservable left, IObservable right, Func selector) => - ReactiveUI.Primitives.LinqMixins.CombineLatest(left, right, selector); + left.CombineLatest(right, selector); /// /// Combines latest values from two signals using latest-fusion semantics. /// public static IObservable ZipLatest(IObservable left, IObservable right, Func selector) => - ReactiveUI.Primitives.LinqMixins.ZipLatest(left, right, selector); + left.ZipLatest(right, selector); /// /// Waits for both signals to complete and emits one result from their last values. /// public static IObservable ForkJoin(IObservable left, IObservable right, Func selector) => - ReactiveUI.Primitives.LinqMixins.ForkJoin(left, right, selector); + left.ForkJoin(right, selector); + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER || NET5_0_OR_GREATER + + /// + /// Executes the SubscribeAsyncEnumerable operation. + /// + /// The T type. + /// The values value. + /// The observer value. + /// The cancellationToken value. + /// The result. + private static IDisposable SubscribeAsyncEnumerable(IAsyncEnumerable values, IObserver observer, CancellationToken cancellationToken) + { + var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + IAsyncEnumerator? enumerator = null; + _ = Task.Run( + async () => await PumpAsyncEnumerable(values, observer, cts, enumeratorReference => enumerator = enumeratorReference).ConfigureAwait(false), + CancellationToken.None); + + return Disposable.Create(() => + { + cts.Cancel(); + var current = Volatile.Read(ref enumerator); + if (current == null) + { + return; + } + + try + { + _ = current.DisposeAsync().AsTask(); + } + catch (NotSupportedException) + { + // Some enumerators only support disposal from the enumeration path. + } + }); + } + + /// + /// Executes the PumpAsyncEnumerable operation. + /// + /// The T type. + /// The values value. + /// The observer value. + /// The cts value. + /// The setEnumerator value. + /// The result. + private static async Task PumpAsyncEnumerable( + IAsyncEnumerable values, + IObserver observer, + CancellationTokenSource cts, + Action> setEnumerator) + { + IAsyncEnumerator? enumerator = null; + try + { + enumerator = values.GetAsyncEnumerator(cts.Token); + setEnumerator(enumerator); + while (!cts.IsCancellationRequested && await enumerator.MoveNextAsync().ConfigureAwait(false)) + { + observer.OnNext(enumerator.Current); + } + + if (!cts.IsCancellationRequested) + { + observer.OnCompleted(); + } + } + catch (OperationCanceledException) when (cts.IsCancellationRequested) + { + // Disposal requested cancellation; observers should not receive a terminal signal. + } + catch (Exception error) when (!cts.IsCancellationRequested) + { + observer.OnError(error); + } + finally + { + if (enumerator != null) + { + await enumerator.DisposeAsync().ConfigureAwait(false); + } + + cts.Dispose(); + } + } +#endif } diff --git a/src/ReactiveUI.Primitives/Signals/Signal{FromTask}.cs b/src/ReactiveUI.Primitives/Signals/Signal{FromTask}.cs index c4568d0..c57d816 100644 --- a/src/ReactiveUI.Primitives/Signals/Signal{FromTask}.cs +++ b/src/ReactiveUI.Primitives/Signals/Signal{FromTask}.cs @@ -12,6 +12,37 @@ namespace ReactiveUI.Primitives.Signals; /// public static partial class Signal { + /// + /// Stores state for the signal implementation. + /// + private const int TaskCompleted = 1; + + /// + /// Stores state for the signal implementation. + /// + private const int TaskFaulted = 2; + + /// + /// Handles Asnyc Tasks with cancellation. + /// + /// The function to execute. + /// + /// An ITaskSignal of T. + /// + public static ITaskSignal FromTask(Func> execution) => + FromTask(execution, null, null); + + /// + /// Handles Asnyc Tasks with cancellation. + /// + /// The function to execute. + /// The scheduler. + /// + /// An ITaskSignal of T. + /// + public static ITaskSignal FromTask(Func> execution, ISequencer? scheduler) => + FromTask(execution, scheduler, null); + /// /// Handles Asnyc Tasks with cancellation. /// @@ -21,71 +52,34 @@ public static partial class Signal /// /// An ITaskSignal of T. /// - public static ITaskSignal FromTask(Func> execution, ISequencer? scheduler = null, CancellationTokenSource? cancellationTokenSource = null) => - TaskSignal.Create( - ao => Defer(() => Create( - obs => - { - // CancelationToken - var src = ao.CancellationTokenSource!; - var ct = src.Token; - ct.ThrowIfCancellationRequested(); - var hasError = false; - var hasCompleted = false; - var cancellableTask = Task.Factory.StartNew(() => execution(src), ct, TaskCreationOptions.None, TaskScheduler.Current).WhenCancelled(ct); - - Task.Run(async () => - { - try - { -#pragma warning disable IDE0042 // Deconstruct variable declaration - var cancellableTaskHandler = await cancellableTask; -#pragma warning restore IDE0042 // Deconstruct variable declaration - var result = await cancellableTaskHandler.Result; - if (!cancellableTaskHandler.IsCanceled && !src.IsCancellationRequested) - { - obs.OnNext(result); - hasCompleted = !src.IsCancellationRequested; - obs.OnCompleted(); - } - else - { - obs.OnError(new OperationCanceledException()); - } - } - catch (Exception ex) - { - hasError = true; - - // Catch the exception and pass it to the observer if not user handled. - obs.OnError(ex); - await Task.Delay(1); - } - }); - return Disposable.Create(() => - { - if (hasError) - { - Task.Delay(2).Wait(); - } - - if (hasError || !hasCompleted) - { - try - { - src.Cancel(); - } - catch (ObjectDisposedException) - { - throw new OperationCanceledException(); - } - } - - src.Dispose(); - }); - })), - scheduler, - cancellationTokenSource); + public static ITaskSignal FromTask( + Func> execution, + ISequencer? scheduler, + CancellationTokenSource? cancellationTokenSource) => + CreateTaskSignal(execution, static _ => true, scheduler, cancellationTokenSource); + + /// + /// Froms the asynchronous. + /// + /// The type of the return value. + /// The action asynchronous. + /// + /// An TaskSignal of T. + /// + public static ITaskSignal FromTask(Func> actionAsync) => + FromTask(actionAsync, null, null); + + /// + /// Froms the asynchronous. + /// + /// The type of the return value. + /// The action asynchronous. + /// The scheduler. + /// + /// An TaskSignal of T. + /// + public static ITaskSignal FromTask(Func> actionAsync, ISequencer? scheduler) => + FromTask(actionAsync, scheduler, null); /// /// Froms the asynchronous. @@ -97,71 +91,18 @@ public static ITaskSignal FromTask(Func /// An TaskSignal of T. /// - public static ITaskSignal FromTask(Func> actionAsync, ISequencer? scheduler = null, CancellationTokenSource? cancellationTokenSource = null) => - TaskSignal.Create( - ao => Defer(() => Create( - obs => - { - // CancelationToken - var src = ao.CancellationTokenSource!; - var ct = src.Token; - ct.ThrowIfCancellationRequested(); - var hasError = false; - var hasCompleted = false; - var cancellableTask = Task.Factory.StartNew(() => actionAsync(src), ct, TaskCreationOptions.None, TaskScheduler.Current).WhenCancelled(ct); - - Task.Run(async () => - { - try - { -#pragma warning disable IDE0042 // Deconstruct variable declaration - var cancellableTaskHandler = await cancellableTask; -#pragma warning restore IDE0042 // Deconstruct variable declaration - var result = await cancellableTaskHandler.Result; - if (result != null && !src.IsCancellationRequested) - { - obs.OnNext(result); - hasCompleted = !src.IsCancellationRequested; - obs.OnCompleted(); - } - else - { - obs.OnError(new OperationCanceledException()); - } - } - catch (Exception ex) - { - hasError = true; - - // Catch the exception and pass it to the observer if not user handled. - obs.OnError(ex); - await Task.Delay(1); - } - }); - return Disposable.Create(() => - { - if (hasError) - { - Task.Delay(2).Wait(); - } - - if (hasError || !hasCompleted) - { - try - { - src.Cancel(); - } - catch (ObjectDisposedException) - { - throw new OperationCanceledException(); - } - } - - src.Dispose(); - }); - })), - scheduler, - cancellationTokenSource); + public static ITaskSignal FromTask( + Func> actionAsync, + ISequencer? scheduler, + CancellationTokenSource? cancellationTokenSource) => + CreateTaskSignal(actionAsync, static _ => true, scheduler, cancellationTokenSource); + + /// + /// Handles the cancellation. + /// + /// The asynchronous task. + /// A Task. + public static Task HandleCancellation(this Task asyncTask) => HandleCancellation(asyncTask, null); /// /// Handles the cancellation. @@ -169,11 +110,11 @@ public static ITaskSignal FromTask(FuncThe asynchronous task. /// The action. /// A Task. - public static async Task HandleCancellation(this Task asyncTask, Action? action = null) + public static async Task HandleCancellation(this Task asyncTask, Action? action) { try { - await asyncTask; + await asyncTask.ConfigureAwait(false); } catch (OperationCanceledException) { @@ -181,6 +122,14 @@ public static async Task HandleCancellation(this Task asyncTask, Action? action } } + /// + /// Handles the cancellation. + /// + /// The type of the result. + /// The asynchronous task. + /// A Task of TResult. + public static Task HandleCancellation(this Task asyncTask) => HandleCancellation(asyncTask, null); + /// /// Handles the cancellation. /// @@ -188,11 +137,11 @@ public static async Task HandleCancellation(this Task asyncTask, Action? action /// The asynchronous task. /// The action. /// A Task of TResult. - public static async Task HandleCancellation(this Task asyncTask, Action? action = null) + public static async Task HandleCancellation(this Task asyncTask, Action? action) { try { - return await asyncTask; + return await asyncTask.ConfigureAwait(false); } catch (OperationCanceledException) { @@ -208,16 +157,28 @@ public static async Task HandleCancellation(this Task asyncTask, Action? action /// The type. /// The asynchronous task. /// The token. + /// + /// A Task. + /// + public static Task HandleCancellation(this IObservable asyncTask, CancellationToken token) => + HandleCancellation(asyncTask, null, token); + + /// + /// Handles the cancellation. + /// + /// The type. + /// The asynchronous task. /// The action. + /// The token. /// /// A Task. /// - public static async Task HandleCancellation(this IObservable asyncTask, CancellationToken token, Action? action = null) + public static async Task HandleCancellation(this IObservable asyncTask, Action? action, CancellationToken token) { try { token.ThrowIfCancellationRequested(); - return await Task.Run(async () => await asyncTask, token); + return await Task.Run(async () => await asyncTask, token).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -227,23 +188,166 @@ public static async Task HandleCancellation(this Task asyncTask, Action? action return default; } - private static async Task<(TResult Result, bool IsCanceled)> WhenCancelled(this Task asyncTask, CancellationToken cancellationToken) + /// + /// Executes the CreateTaskSignal operation. + /// + /// The TResult type. + /// The execution value. + /// The shouldEmit value. + /// The scheduler value. + /// The cancellationTokenSource value. + /// The result. + private static ITaskSignal CreateTaskSignal( + Func> execution, + Func shouldEmit, + ISequencer? scheduler, + CancellationTokenSource? cancellationTokenSource) => + TaskSignal.Create( + ao => Defer(() => Create(observer => SubscribeTask(ao, execution, shouldEmit, observer))), + scheduler, + cancellationTokenSource); + + /// + /// Executes the SubscribeTask operation. + /// + /// The TResult type. + /// The signal value. + /// The execution value. + /// The shouldEmit value. + /// The observer value. + /// The result. + private static IDisposable SubscribeTask( + ITaskSignal signal, + Func> execution, + Func shouldEmit, + IObserver observer) { - var tcs = new TaskCompletionSource(); - cancellationToken.Register(() => tcs.TrySetCanceled(), useSynchronizationContext: false); - var cancellationTask = tcs.Task; + var source = signal.CancellationTokenSource!; + var token = source.Token; + token.ThrowIfCancellationRequested(); + var completionState = 0; + var cancellableTask = Task.Factory + .StartNew(() => execution(source), token, TaskCreationOptions.None, TaskScheduler.Current) + .WhenCancelled(token); - // Create a task that completes when either the async operation completes, - // or cancellation is requested. - var readyTask = await Task.WhenAny(asyncTask, cancellationTask); + _ = ObserveTask( + cancellableTask, + shouldEmit, + observer, + () => Volatile.Write(ref completionState, TaskCompleted), + () => Volatile.Write(ref completionState, TaskFaulted), + token); - // In case of cancellation, register a continuation to observe any unhandled. - // exceptions from the asynchronous operation (once it completes). - if (readyTask == cancellationTask) + return Disposable.Create(() => { - await asyncTask.ContinueWith(_ => asyncTask.Exception, cancellationToken, TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Current); + if (Volatile.Read(ref completionState) == TaskCompleted) + { + return; + } + + Cancel(source); + }); + } + + /// + /// Executes the ObserveTask operation. + /// + /// The TResult type. + /// The cancellableTask value. + /// The shouldEmit value. + /// The observer value. + /// The setCompleted value. + /// The setFaulted value. + /// The token value. + /// The result. + private static async Task ObserveTask( + Task<(Task Task, bool IsCanceled)> cancellableTask, + Func shouldEmit, + IObserver observer, + Action setCompleted, + Action setFaulted, + CancellationToken token) + { + try + { + var (task, isCanceled) = await cancellableTask.ConfigureAwait(false); + var result = await task.ConfigureAwait(false); + if (!isCanceled && !token.IsCancellationRequested && shouldEmit(result)) + { + observer.OnNext(result); + setCompleted(); + observer.OnCompleted(); + return; + } + + setFaulted(); + observer.OnError(new OperationCanceledException()); + } + catch (Exception error) + { + setFaulted(); + observer.OnError(error); } + } - return (await readyTask, tcs.Task.IsCanceled || readyTask.IsCanceled); + /// + /// Executes the Cancel operation. + /// + /// The source value. + private static void Cancel(CancellationTokenSource source) + { + try + { + source.Cancel(); + } + catch (ObjectDisposedException) + { + // Another completion path already released the token source. + } + } + + /// + /// Executes the WhenCancelled operation. + /// + /// The TResult type. + /// The asyncTask value. + /// The cancellationToken value. + /// The result. + private static async Task<(TResult Value, bool IsCanceled)> WhenCancelled(this Task asyncTask, CancellationToken cancellationToken) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var registration = cancellationToken.Register( + static state => ((TaskCompletionSource)state!).TrySetCanceled(), + tcs, + useSynchronizationContext: false); + var cancellationTask = tcs.Task; + + try + { + // Create a task that completes when either the async operation completes, + // or cancellation is requested. + var readyTask = await Task.WhenAny(asyncTask, cancellationTask).ConfigureAwait(false); + + // In case of cancellation, register a continuation to observe any unhandled. + // exceptions from the asynchronous operation (once it completes). + if (readyTask == cancellationTask) + { + await asyncTask.ContinueWith( + _ => asyncTask.Exception, + cancellationToken, + TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Current).ConfigureAwait(false); + } + + return (await readyTask.ConfigureAwait(false), tcs.Task.IsCanceled || readyTask.IsCanceled); + } + finally + { +#if NET8_0_OR_GREATER + await registration.DisposeAsync().ConfigureAwait(false); +#else + registration.Dispose(); +#endif + } } } diff --git a/src/ReactiveUI.Primitives/Signals/Signal{GetAwaiter}.cs b/src/ReactiveUI.Primitives/Signals/Signal{GetAwaiter}.cs index 1022688..33be3af 100644 --- a/src/ReactiveUI.Primitives/Signals/Signal{GetAwaiter}.cs +++ b/src/ReactiveUI.Primitives/Signals/Signal{GetAwaiter}.cs @@ -49,6 +49,14 @@ public static IAwaitSignal GetAwaiter(this IObservable + /// Executes the RunAsync operation. + /// + /// The TSource type. + /// The source value. + /// The cancellationToken value. + /// The result. private static IAwaitSignal RunAsync(IObservable source, CancellationToken cancellationToken) #pragma warning restore RCS1047 // Non-asynchronous method name should not end with 'Async'. { @@ -69,12 +77,26 @@ private static IAwaitSignal RunAsync(IObservable sour return s; } + /// + /// Executes the Cancel operation. + /// + /// The T type. + /// The subject value. + /// The cancellationToken value. + /// The result. private static IAwaitSignal Cancel(IAwaitSignal subject, CancellationToken cancellationToken) { subject.OnError(new OperationCanceledException(cancellationToken)); return subject; } + /// + /// Executes the RegisterCancelation operation. + /// + /// The T type. + /// The subject value. + /// The subscription value. + /// The token value. private static void RegisterCancelation(IAwaitSignal subject, IDisposable subscription, CancellationToken token) { var ctr = token.Register(() => diff --git a/src/ReactiveUI.Primitives/Signals/Signal{Never}.cs b/src/ReactiveUI.Primitives/Signals/Signal{Never}.cs index 33e1f7f..4af9221 100644 --- a/src/ReactiveUI.Primitives/Signals/Signal{Never}.cs +++ b/src/ReactiveUI.Primitives/Signals/Signal{Never}.cs @@ -16,7 +16,9 @@ public static partial class Signal /// /// The type. /// An Signals. +#pragma warning disable S4018 // Result type is intentionally explicit for Rx-style factory APIs. public static IObservable Never() => ImmutableNeverSignal.Instance; +#pragma warning restore S4018 /// /// Non-Terminating Signals. It's no returns, never finish. witness is for type inference. diff --git a/src/ReactiveUI.Primitives/Signals/Signal{Throw}.cs b/src/ReactiveUI.Primitives/Signals/Signal{Throw}.cs index 73961b7..6cf73c5 100644 --- a/src/ReactiveUI.Primitives/Signals/Signal{Throw}.cs +++ b/src/ReactiveUI.Primitives/Signals/Signal{Throw}.cs @@ -19,6 +19,7 @@ public static partial class Signal /// The error. /// The scheduler. /// An Signals. +#pragma warning disable S4018 // Result type is intentionally explicit for Rx-style factory APIs. public static IObservable Throw(Exception error, ISequencer scheduler) => new ThrowSignal(error, scheduler); @@ -30,6 +31,7 @@ public static IObservable Throw(Exception error, ISequencer scheduler) => /// An Signals. public static IObservable Throw(Exception error) => Throw(error, Sequencer.Immediate); +#pragma warning restore S4018 /// /// Empty Signals. Returns only onError. witness if for Type inference. diff --git a/src/ReactiveUI.Primitives/SubscribeMixins.cs b/src/ReactiveUI.Primitives/SubscribeMixins.cs index 253cb0a..b72f0b3 100644 --- a/src/ReactiveUI.Primitives/SubscribeMixins.cs +++ b/src/ReactiveUI.Primitives/SubscribeMixins.cs @@ -14,7 +14,14 @@ namespace ReactiveUI.Primitives; [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public static class SubscribeMixins { + /// + /// Error callback that rethrows with the original exception dispatch information. + /// private static readonly Action rethrow = e => ExceptionDispatchInfo.Capture(e).Throw(); + + /// + /// Completion callback that does nothing. + /// private static readonly Action nop = () => { }; /// @@ -32,7 +39,7 @@ public static IDisposable Subscribe(this IObservable source) throw new ArgumentNullException(nameof(source)); } - return Subscribe(source, OnNextNoOp(), nop); + return Subscribe(source, OnNextNoOpCache.Instance, nop); } /// @@ -107,11 +114,23 @@ public static IDisposable Subscribe(this IObservable source, Action onN /// The exception. public static void Rethrow(this Exception? exception) { - if (exception != null) + if (exception == null) { - throw exception; + return; } + + throw exception; } - private static Action OnNextNoOp() => _ => { }; + /// + /// Holds cached no-op value callbacks by value type. + /// + /// The value type. + private static class OnNextNoOpCache + { + /// + /// Gets the cached no-op value callback. + /// + public static readonly Action Instance = _ => { }; + } } diff --git a/src/benchmarks/ReactiveUI.Primitives.Benchmarks/Program.cs b/src/benchmarks/ReactiveUI.Primitives.Benchmarks/Program.cs index a62e968..6873b33 100644 --- a/src/benchmarks/ReactiveUI.Primitives.Benchmarks/Program.cs +++ b/src/benchmarks/ReactiveUI.Primitives.Benchmarks/Program.cs @@ -1,7 +1,8 @@ -// Copyright (c) 2019-2023 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. +using System.Reactive.Concurrency; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Running; using ReactiveUI.Primitives; @@ -9,7 +10,6 @@ using ReactiveUI.Primitives.Core; using ReactiveUI.Primitives.Disposables; using ReactiveUI.Primitives.Signals; -using System.Reactive.Concurrency; using RxBehaviorSubject = System.Reactive.Subjects.BehaviorSubject; using RxCompositeDisposable = System.Reactive.Disposables.CompositeDisposable; using RxCurrentThreadScheduler = System.Reactive.Concurrency.CurrentThreadScheduler; @@ -646,10 +646,12 @@ internal sealed class R3CountingObserver : R3.Observer protected override void OnNextCore(T value) { Count++; - if (value is int intValue) + if (value is not int intValue) { - Total += intValue; + return; } + + Total += intValue; } protected override void OnErrorResumeCore(Exception error) => throw error; diff --git a/src/tests/ReactiveUI.Primitives.Tests/Assert.cs b/src/tests/ReactiveUI.Primitives.Tests/Assert.cs index eeabed1..d8141b7 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/Assert.cs +++ b/src/tests/ReactiveUI.Primitives.Tests/Assert.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2023 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. @@ -13,87 +13,107 @@ internal static class Assert { public static void True(bool condition) { - if (!condition) + if (condition) { - throw new InvalidOperationException("Expected condition to be true."); + return; } + + throw new InvalidOperationException("Expected condition to be true."); } public static void True(bool? condition) => True(condition == true); public static void False(bool condition) { - if (condition) + if (!condition) { - throw new InvalidOperationException("Expected condition to be false."); + return; } + + throw new InvalidOperationException("Expected condition to be false."); } public static void False(bool? condition) => False(condition == true); public static void Equal(IEnumerable expected, IEnumerable actual) { - if (!expected.SequenceEqual(actual)) + if (expected.SequenceEqual(actual)) { - throw new InvalidOperationException($"Expected {Format(expected)}, actual {Format(actual)}."); + return; } + + throw new InvalidOperationException($"Expected {Format(expected)}, actual {Format(actual)}."); } public static void Equal(object? expected, object? actual) { - if (!Equals(expected, actual)) + if (Equals(expected, actual)) { - throw new InvalidOperationException($"Expected {Format(expected)}, actual {Format(actual)}."); + return; } + + throw new InvalidOperationException($"Expected {Format(expected)}, actual {Format(actual)}."); } public static void Equal(T expected, T actual) { - if (!EqualityComparer.Default.Equals(expected, actual)) + if (EqualityComparer.Default.Equals(expected, actual)) { - throw new InvalidOperationException($"Expected {Format(expected)}, actual {Format(actual)}."); + return; } + + throw new InvalidOperationException($"Expected {Format(expected)}, actual {Format(actual)}."); } public static void NotEqual(T notExpected, T actual) { - if (EqualityComparer.Default.Equals(notExpected, actual)) + if (!EqualityComparer.Default.Equals(notExpected, actual)) { - throw new InvalidOperationException($"Did not expect {Format(actual)}."); + return; } + + throw new InvalidOperationException($"Did not expect {Format(actual)}."); } public static void Same(T expected, T actual) where T : class { - if (!ReferenceEquals(expected, actual)) + if (ReferenceEquals(expected, actual)) { - throw new InvalidOperationException("Expected both references to point to the same instance."); + return; } + + throw new InvalidOperationException("Expected both references to point to the same instance."); } public static void NotNull(object? value) { - if (value is null) + if (value is not null) { - throw new InvalidOperationException("Expected value not to be null."); + return; } + + throw new InvalidOperationException("Expected value not to be null."); } public static void Contains(T expected, IEnumerable collection) { - if (!collection.Contains(expected)) + if (collection.Contains(expected)) { - throw new InvalidOperationException($"Expected collection to contain {Format(expected)}."); + return; } + + throw new InvalidOperationException($"Expected collection to contain {Format(expected)}."); } public static void DoesNotContain(T expected, IEnumerable collection) { - if (collection.Contains(expected)) + if (!collection.Contains(expected)) { - throw new InvalidOperationException($"Expected collection not to contain {Format(expected)}."); + return; } + + throw new InvalidOperationException($"Expected collection not to contain {Format(expected)}."); } public static TException Throws(Action action) diff --git a/src/tests/ReactiveUI.Primitives.Tests/AsyncSignalTests.cs b/src/tests/ReactiveUI.Primitives.Tests/AsyncSignalTests.cs index 3b14745..9828c89 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/AsyncSignalTests.cs +++ b/src/tests/ReactiveUI.Primitives.Tests/AsyncSignalTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2023 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. diff --git a/src/tests/ReactiveUI.Primitives.Tests/BehaviourSignalTests.cs b/src/tests/ReactiveUI.Primitives.Tests/BehaviourSignalTests.cs index db82d5a..5cdbf76 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/BehaviourSignalTests.cs +++ b/src/tests/ReactiveUI.Primitives.Tests/BehaviourSignalTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2023 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. diff --git a/src/tests/ReactiveUI.Primitives.Tests/ConcurencyTests.cs b/src/tests/ReactiveUI.Primitives.Tests/ConcurencyTests.cs index 72f2735..fab23ea 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/ConcurencyTests.cs +++ b/src/tests/ReactiveUI.Primitives.Tests/ConcurencyTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2023 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. diff --git a/src/tests/ReactiveUI.Primitives.Tests/CoreRuntimeContractTests.cs b/src/tests/ReactiveUI.Primitives.Tests/CoreRuntimeContractTests.cs index 341edfc..95d06c9 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/CoreRuntimeContractTests.cs +++ b/src/tests/ReactiveUI.Primitives.Tests/CoreRuntimeContractTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2023 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. diff --git a/src/tests/ReactiveUI.Primitives.Tests/CoverageCompletionTests.cs b/src/tests/ReactiveUI.Primitives.Tests/CoverageCompletionTests.cs index 585e860..f8d79fd 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/CoverageCompletionTests.cs +++ b/src/tests/ReactiveUI.Primitives.Tests/CoverageCompletionTests.cs @@ -1,9 +1,10 @@ -// Copyright (c) 2019-2023 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. using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Threading; @@ -114,7 +115,7 @@ public void NullGuardsCoverPublicFactoryOperatorAndObserverContracts() Assert.Throws(() => Signal.Use(null!, _ => Signal.Return(1))); Assert.Throws(() => Signal.Use(() => Disposable.Empty, null!)); Assert.Throws(() => Signal.FromEnumerable(null!)); - Assert.Throws(() => Signal.FromTask(null!)); + Assert.Throws(() => Signal.FromTask((Task)null!)); Assert.Throws(() => Signal.FromAsyncEnumerable(null!)); Assert.Throws(() => Signal.Every(TimeSpan.FromTicks(-1))); @@ -152,7 +153,7 @@ public void OperatorSurfaceCoversSuccessErrorAndEarlyTerminationBranches() Assert.Equal(1, terminal); var keepNotNull = new List(); - Signal.FromEnumerable(new string?[] { null, "x", null, "y" }).KeepNotNull().Subscribe(keepNotNull.Add); + Signal.FromEnumerable([null, "x", null, "y"]).KeepNotNull().Subscribe(keepNotNull.Add); Assert.Equal(new[] { "x", "y" }, keepNotNull); var emptyTake = new List(); @@ -169,18 +170,18 @@ public void OperatorSurfaceCoversSuccessErrorAndEarlyTerminationBranches() var allFalse = new List(); var containsFalse = new List(); var longCount = new List(); - Signal.FromEnumerable(new[] { 1, 2, 3 }).Any(value => value > 9).Subscribe(anyFalse.Add); - Signal.FromEnumerable(new[] { 2, 4, 5 }).All(value => value % 2 == 0).Subscribe(allFalse.Add); - Signal.FromEnumerable(new[] { 2, 4, 6 }).Contains(7).Subscribe(containsFalse.Add); - Signal.FromEnumerable(new[] { 1, 2, 3, 4 }).LongCount(value => value % 2 == 0).Subscribe(longCount.Add); + Signal.FromEnumerable([1, 2, 3]).Any(value => value > 9).Subscribe(anyFalse.Add); + Signal.FromEnumerable([2, 4, 5]).All(value => value % 2 == 0).Subscribe(allFalse.Add); + Signal.FromEnumerable([2, 4, 6]).Contains(7).Subscribe(containsFalse.Add); + Signal.FromEnumerable([1, 2, 3, 4]).LongCount(value => value % 2 == 0).Subscribe(longCount.Add); Assert.Equal(new[] { false }, anyFalse); Assert.Equal(new[] { false }, allFalse); Assert.Equal(new[] { false }, containsFalse); Assert.Equal(new[] { 2L }, longCount); var selectMany = new List(); - Signal.FromEnumerable(new[] { 1, 2 }) - .SelectMany(value => Signal.FromEnumerable(new[] { value, value + 10 }), (outer, inner) => outer + ":" + inner) + Signal.FromEnumerable([1, 2]) + .SelectMany(value => Signal.FromEnumerable([value, value + 10]), (outer, inner) => outer + ":" + inner) .Subscribe(selectMany.Add); Assert.Equal(new[] { "1:1", "1:11", "2:2", "2:12" }, selectMany); } @@ -201,18 +202,20 @@ public void ErrorOperatorsMaterializeRecoverAndResumeDeterministically() .Subscribe(spark => { sparkKinds.Add(spark.Kind); - if (spark.Exception != null) + if (spark.Exception == null) { - sparkErrors.Add(spark.Exception.Message); + return; } + + sparkErrors.Add(spark.Exception.Message); }); - Signal.FromEnumerable(new[] - { + Signal.FromEnumerable( + [ Spark.CreateOnNext(1), Spark.CreateOnError(new InvalidOperationException("unspark")), Spark.CreateOnCompleted(), - }) + ]) .Unspark() .Subscribe(unsparkValues.Add, ex => unsparkErrors.Add(ex.Message)); @@ -221,7 +224,7 @@ public void ErrorOperatorsMaterializeRecoverAndResumeDeterministically() .Subscribe(rescueValues.Add); Signal.Throw(new InvalidOperationException("resume")) - .Resume(Signal.FromEnumerable(new[] { 4, 5 })) + .Resume(Signal.FromEnumerable([4, 5])) .Subscribe(resumeValues.Add); Signal.Defer(() => Signal.Throw(new InvalidOperationException("stop"))) @@ -263,7 +266,7 @@ public void HigherOrderOperatorsHandleAsyncOrderingRacesSwitchingAndLatestValues second.OnCompleted(); outer.OnCompleted(); - Signal.Merge(Signal.FromEnumerable(new[] { 1, 2 }), Signal.FromEnumerable(new[] { 3 })).Subscribe(mergeValues.Add, ex => throw ex, () => completed["merge"] = 1); + Signal.Merge(Signal.FromEnumerable([1, 2]), Signal.FromEnumerable([3])).Subscribe(mergeValues.Add, ex => throw ex, () => completed["merge"] = 1); var raceLoser = new Signal(); var raceWinner = new Signal(); @@ -294,11 +297,11 @@ public void HigherOrderOperatorsHandleAsyncOrderingRacesSwitchingAndLatestValues left.OnNext(3); left.OnCompleted(); - Signal.FromEnumerable(new[] { 1, 2, 3 }).Zip(Signal.Return(10), (l, r) => l + r).Subscribe(zipShortValues.Add, ex => throw ex, () => completed["zip"] = 1); + Signal.FromEnumerable([1, 2, 3]).Zip(Signal.Return(10), (l, r) => l + r).Subscribe(zipShortValues.Add, ex => throw ex, () => completed["zip"] = 1); Signal.Empty().ForkJoin(Signal.Return(1), (l, r) => l + r).Subscribe(forkJoinEmpty.Add, ex => throw ex, () => completed["forkJoinEmpty"] = 1); Assert.Equal(new[] { 1, 2, 21 }, concatValues); - Assert.Equal(new[] { 1, 2, 3 }, mergeValues.OrderBy(value => value)); + Assert.Equal([1, 2, 3], mergeValues.Order()); Assert.Equal(new[] { 7 }, raceValues); Assert.Equal(new[] { 1, 3 }, switchValues); Assert.Equal(new[] { "2a", "3b" }, withLatestValues); @@ -333,7 +336,7 @@ public void VirtualTimeOperatorsCoverDelayTimeoutSampleTimerAndTimestampAliases( manual.OnNext(2); Assert.Equal(new[] { 2 }, delayStartValues); - Signal.FromEnumerable(new[] { 3, 4 }).Delay(TimeSpan.FromTicks(3), clock).Subscribe(delayedValues.Add); + Signal.FromEnumerable([3, 4]).Delay(TimeSpan.FromTicks(3), clock).Subscribe(delayedValues.Add); clock.AdvanceBy(TimeSpan.FromTicks(2)); Assert.Equal(0, delayedValues.Count); clock.AdvanceBy(TimeSpan.FromTicks(1)); @@ -364,8 +367,8 @@ public void VirtualTimeOperatorsCoverDelayTimeoutSampleTimerAndTimestampAliases( timer.Dispose(); Assert.Equal(new[] { 0L, 1L, 2L }, timerValues); - Signal.FromEnumerable(new[] { 8, 9 }).Timestamp(clock).Subscribe(timestamps.Add); - Assert.Equal(new[] { 8, 9 }, timestamps.Select(item => item.Value)); + Signal.FromEnumerable([8, 9]).Timestamp(clock).Subscribe(timestamps.Add); + Assert.Equal([8, 9], timestamps.Select(item => item.Value)); Assert.True(timestamps.All(item => item.Timestamp == clock.Now)); } @@ -383,7 +386,7 @@ public async Task FactoriesTasksAndTerminalTasksCoverCancellationFaultAndEmptyBr await ObserveTaskError(Task.FromCanceled(new CancellationToken(true)), taskErrors); await ObserveTaskError(Task.FromException(new InvalidOperationException("faulted")), taskErrors); - async IAsyncEnumerable ThrowingAsyncEnumerable() + static async IAsyncEnumerable ThrowingAsyncEnumerable() { yield return 1; await Task.Yield(); @@ -407,6 +410,8 @@ async IAsyncEnumerable ThrowingAsyncEnumerable() } [Test] + [RequiresUnreferencedCode("The test exercises reflection and dynamic-code coverage branches.")] + [RequiresDynamicCode("The test exercises reflection and dynamic-code coverage branches.")] public void CoreValueTypesDisposablesAndHandlesCoverEqualityAndLifecycleBranches() { var moment = new Moment(7, new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero)); @@ -415,7 +420,7 @@ public void CoreValueTypesDisposablesAndHandlesCoverEqualityAndLifecycleBranches var interval = new TimeInterval(7, TimeSpan.FromTicks(3)); var sameInterval = new TimeInterval(7, TimeSpan.FromTicks(3)); var differentInterval = new TimeInterval(8, TimeSpan.FromTicks(4)); - var rxVoid = new RxVoid(); + var rxVoid = default(RxVoid); var ignored = 0; var thrown = new InvalidOperationException("throw-me"); @@ -435,10 +440,10 @@ public void CoreValueTypesDisposablesAndHandlesCoverEqualityAndLifecycleBranches Assert.Equal(interval.GetHashCode(), sameInterval.GetHashCode()); Assert.True(interval.ToString().Contains("7", StringComparison.Ordinal)); - Assert.True(rxVoid == new RxVoid()); - Assert.False(rxVoid != new RxVoid()); - Assert.True(rxVoid.Equals(new RxVoid())); - Assert.True(rxVoid.Equals((object)new RxVoid())); + Assert.True(rxVoid == default); + Assert.False(rxVoid != default); + Assert.True(rxVoid.Equals(default)); + Assert.True(rxVoid.Equals((object)default(RxVoid))); Assert.Equal(0, rxVoid.GetHashCode()); Assert.Equal("()", rxVoid.ToString()); @@ -575,6 +580,8 @@ private static async Task ObserveTaskError(Task task, List errors) await SpinUntil(() => errors.Count > 0, TimeSpan.FromSeconds(2)); } + [RequiresDynamicCode("Calls System.Type.MakeGenericType(params Type[])")] + [RequiresUnreferencedCode("Calls System.Reflection.Assembly.GetType(String)")] private static void InvokeInternalHandleMembers(Exception exception) { var assembly = typeof(RxVoid).Assembly; @@ -592,11 +599,13 @@ private static void InvokeInternalHandleMembers(Exception exception) InvokeThrows(assembly.GetType("ReactiveUI.Primitives.Handle`3")!.MakeGenericType(typeof(int), typeof(int), typeof(int)).GetField("Throw", BindingFlags.Public | BindingFlags.Static)!.GetValue(null)!, exception, 1, 2, 3); } + [RequiresUnreferencedCode("Calls System.Reflection.Assembly.GetType(String)")] + [RequiresDynamicCode("Calls System.Reflection.MethodInfo.MakeGenericMethod(params Type[])")] private static IObservable InvokeInternalCatchIgnore(Exception exception) { var handle = typeof(RxVoid).Assembly.GetType("ReactiveUI.Primitives.Handle")!; var method = handle.GetMethod("CatchIgnore", BindingFlags.Public | BindingFlags.Static)!.MakeGenericMethod(typeof(T)); - return (IObservable)method.Invoke(null, new object[] { exception })!; + return (IObservable)method.Invoke(null, [exception])!; } private static void InvokeAction(object action, params object[] args) => ((Delegate)action).DynamicInvoke(args); @@ -646,7 +655,7 @@ private static async Task SpinUntil(Func condition, TimeSpan timeout) private sealed class RecordingResultObserver : IObserver, IObserver { - public List Events { get; } = new(); + public List Events { get; } = []; public void OnCompleted() => Events.Add("completed"); diff --git a/src/tests/ReactiveUI.Primitives.Tests/DisposableTests.cs b/src/tests/ReactiveUI.Primitives.Tests/DisposableTests.cs index c9d2d78..e93e8c4 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/DisposableTests.cs +++ b/src/tests/ReactiveUI.Primitives.Tests/DisposableTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2023 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. diff --git a/src/tests/ReactiveUI.Primitives.Tests/DummyDisposable.cs b/src/tests/ReactiveUI.Primitives.Tests/DummyDisposable.cs index b548757..e3b0db9 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/DummyDisposable.cs +++ b/src/tests/ReactiveUI.Primitives.Tests/DummyDisposable.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2023 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. diff --git a/src/tests/ReactiveUI.Primitives.Tests/FactoryOperatorContractTests.cs b/src/tests/ReactiveUI.Primitives.Tests/FactoryOperatorContractTests.cs index c83118e..dac39bd 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/FactoryOperatorContractTests.cs +++ b/src/tests/ReactiveUI.Primitives.Tests/FactoryOperatorContractTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2023 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. @@ -29,7 +29,7 @@ public void FactoriesEmitExpectedFiniteSequencesAndDisposeResources() .Concat(Signal.Unfold(1, state => state <= 3, state => state + 1, state => state * 10)) .Concat(Signal.Use( () => Disposable.Create(() => disposed++), - _ => Signal.FromEnumerable(new[] { 7, 8 }))) + _ => Signal.FromEnumerable([7, 8]))) .Subscribe(values.Add, ex => throw ex, () => completed++); Assert.Equal(new[] { 2, 3, 4, 9, 9, 10, 20, 30, 7, 8 }, values); @@ -45,7 +45,7 @@ public void UnaryOperatorsTransformFilterAggregateAndMaterialize() var terminal = new List(); var taps = 0; - Signal.FromEnumerable(new[] { 1, 2, 2, 3, 4 }) + Signal.FromEnumerable([1, 2, 2, 3, 4]) .Map(value => value * 2) .Keep(value => value >= 4) .DistinctUntilChanged() @@ -56,7 +56,7 @@ public void UnaryOperatorsTransformFilterAggregateAndMaterialize() .Subscribe(sparks.Add); Signal.FromEnumerable(sparks).Unspark().Subscribe(values.Add); - Signal.FromEnumerable(new[] { 1, 2, 3, 4 }).Fold(0, (sum, value) => sum + value).Subscribe(terminal.Add); + Signal.FromEnumerable([1, 2, 3, 4]).Fold(0, (sum, value) => sum + value).Subscribe(terminal.Add); Assert.Equal(new[] { 4, 10, 18 }, values); Assert.Equal(new[] { 10 }, terminal); @@ -99,10 +99,10 @@ public void CombiningOperatorsPreserveCoreOrderingSemantics() var zipped = new List(); var latest = new List(); - Signal.Merge(Signal.FromEnumerable(new[] { 1, 2 }), Signal.FromEnumerable(new[] { 3, 4 })).Subscribe(merged.Add); - Signal.Concat(Signal.FromEnumerable(new[] { 1, 2 }), Signal.FromEnumerable(new[] { 3, 4 })).Subscribe(concatenated.Add); - Signal.Zip(Signal.FromEnumerable(new[] { 1, 2 }), Signal.FromEnumerable(new[] { 10, 20 }), (left, right) => left + right).Subscribe(zipped.Add); - Signal.CombineLatest(Signal.FromEnumerable(new[] { 1, 2 }), Signal.FromEnumerable(new[] { "a", "b" }), (left, right) => left + right).Subscribe(latest.Add); + Signal.Merge(Signal.FromEnumerable([1, 2]), Signal.FromEnumerable([3, 4])).Subscribe(merged.Add); + Signal.Concat(Signal.FromEnumerable([1, 2]), Signal.FromEnumerable([3, 4])).Subscribe(concatenated.Add); + Signal.Zip(Signal.FromEnumerable([1, 2]), Signal.FromEnumerable([10, 20]), (left, right) => left + right).Subscribe(zipped.Add); + Signal.CombineLatest(Signal.FromEnumerable([1, 2]), Signal.FromEnumerable(["a", "b"]), (left, right) => left + right).Subscribe(latest.Add); Assert.Equal(new[] { 1, 2, 3, 4 }, merged); Assert.Equal(new[] { 1, 2, 3, 4 }, concatenated); @@ -196,18 +196,18 @@ public void AdditionalFactoriesAndUnaryOperatorsCoverCommonParitySurface() var isEmpty = new List(); var selected = new List(); - Signal.FromEnumerable(new[] { 2, 3 }).Lead(1).Append(4).Prepend(0).Subscribe(leadAppend.Add); - Signal.FromEnumerable(new[] { 1, 2, 3 }).IgnoreValues().Subscribe(ignored.Add); - Signal.FromEnumerable(new[] { 11, 12, 21, 22 }).DistinctBy(value => value / 10).Subscribe(distinctBy.Add); - Signal.FromEnumerable(new[] { 1, 2, 3, 1 }).TakeWhile(value => value < 3).Subscribe(takeWhile.Add); - Signal.FromEnumerable(new[] { 1, 2, 3, 1 }).SkipWhile(value => value < 3).Subscribe(skipWhile.Add); + Signal.FromEnumerable([2, 3]).Lead(1).Append(4).Prepend(0).Subscribe(leadAppend.Add); + Signal.FromEnumerable([1, 2, 3]).IgnoreValues().Subscribe(ignored.Add); + Signal.FromEnumerable([11, 12, 21, 22]).DistinctBy(value => value / 10).Subscribe(distinctBy.Add); + Signal.FromEnumerable([1, 2, 3, 1]).TakeWhile(value => value < 3).Subscribe(takeWhile.Add); + Signal.FromEnumerable([1, 2, 3, 1]).SkipWhile(value => value < 3).Subscribe(skipWhile.Add); Signal.Empty().DefaultIfEmpty(42).Subscribe(defaulted.Add); - Signal.FromEnumerable(new[] { 1, 2, 3 }).Count().Subscribe(count.Add); - Signal.FromEnumerable(new[] { 1, 2, 3 }).Any(value => value == 2).Subscribe(any.Add); - Signal.FromEnumerable(new[] { 2, 4, 6 }).All(value => value % 2 == 0).Subscribe(all.Add); - Signal.FromEnumerable(new[] { 2, 4, 6 }).Contains(4).Subscribe(contains.Add); + Signal.FromEnumerable([1, 2, 3]).Count().Subscribe(count.Add); + Signal.FromEnumerable([1, 2, 3]).Any(value => value == 2).Subscribe(any.Add); + Signal.FromEnumerable([2, 4, 6]).All(value => value % 2 == 0).Subscribe(all.Add); + Signal.FromEnumerable([2, 4, 6]).Contains(4).Subscribe(contains.Add); Signal.Empty().IsEmpty().Subscribe(isEmpty.Add); - Signal.FromEnumerable(new[] { 1, 2 }).Bind(value => Signal.Range(value * 10, 2)).Subscribe(selected.Add); + Signal.FromEnumerable([1, 2]).Bind(value => Signal.Range(value * 10, 2)).Subscribe(selected.Add); Assert.Equal(new[] { 0, 1, 2, 3, 4 }, leadAppend); Assert.Equal(0, ignored.Count); @@ -233,8 +233,8 @@ public async Task SystemReactiveNamedAliasesCoverMigrationConvenienceSurface() var clock = new TestClock(); var source = new Signal(); - Signal.FromEnumerable(new[] { 2, 3 }) - .StartWith(new[] { 0, 1 }) + Signal.FromEnumerable([2, 3]) + .StartWith([0, 1]) .Do(sideEffects.Add) .AsObservable() .Subscribe(values.Add); @@ -257,7 +257,7 @@ public async Task SystemReactiveNamedAliasesCoverMigrationConvenienceSurface() var converted = new[] { 4, 5 }.ToObservable(); var last = await converted.ToTask(); - var first = await Signal.FromEnumerable(new[] { 9, 10 }).FirstAsync().ToTask(); + var first = await Signal.FromEnumerable([9, 10]).FirstAsync().ToTask(); var started = await Signal.Start(() => 11, Sequencer.CurrentThread).ToTask(); Assert.Equal(5, last); @@ -289,8 +289,8 @@ public void BoundaryAndLatestOperatorsUseVirtualTimeAndCompletionSemantics() source.OnCompleted(); clock.AdvanceBy(TimeSpan.FromTicks(4)); - Signal.FromEnumerable(new[] { 1, 2 }).ZipLatest(Signal.FromEnumerable(new[] { "a", "b" }), (left, right) => left + right).Subscribe(latest.Add); - Signal.ForkJoin(Signal.FromEnumerable(new[] { 1, 2 }), Signal.FromEnumerable(new[] { 10, 20 }), (left, right) => left + right).Subscribe(forkJoined.Add); + Signal.FromEnumerable([1, 2]).ZipLatest(Signal.FromEnumerable(["a", "b"]), (left, right) => left + right).Subscribe(latest.Add); + Signal.ForkJoin(Signal.FromEnumerable([1, 2]), Signal.FromEnumerable([10, 20]), (left, right) => left + right).Subscribe(forkJoined.Add); Assert.Equal(new[] { 3 }, throttled); Assert.Equal(new[] { 2, 3 }, sampled); @@ -304,12 +304,12 @@ public void BoundaryAndLatestOperatorsUseVirtualTimeAndCompletionSemantics() [Test] public async Task TerminalTaskOperatorsCompleteWithExpectedSemantics() { - var first = await Signal.FromEnumerable(new[] { 3, 4 }).FirstAsync(); - var collected = await Signal.FromEnumerable(new[] { 1, 2, 3 }).CollectArrayAsync(); + var first = await Signal.FromEnumerable([3, 4]).FirstAsync(); + var collected = await Signal.FromEnumerable([1, 2, 3]).CollectArrayAsync(); var none = await Signal.Empty().FirstOrDefaultAsync(42); Assert.Equal(3, first); - Assert.Equal(new[] { 1, 2, 3 }, (IEnumerable)collected); + Assert.Equal([1, 2, 3], (IEnumerable)collected); Assert.Equal(42, none); } } diff --git a/src/tests/ReactiveUI.Primitives.Tests/ReplaySignalTests.cs b/src/tests/ReactiveUI.Primitives.Tests/ReplaySignalTests.cs index e7f5829..d48ca97 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/ReplaySignalTests.cs +++ b/src/tests/ReactiveUI.Primitives.Tests/ReplaySignalTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2023 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. diff --git a/src/tests/ReactiveUI.Primitives.Tests/SignalCreateTests.cs b/src/tests/ReactiveUI.Primitives.Tests/SignalCreateTests.cs index 638e536..a57fd51 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/SignalCreateTests.cs +++ b/src/tests/ReactiveUI.Primitives.Tests/SignalCreateTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2023 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. @@ -43,7 +43,7 @@ public void Create_NullCoalescingAction() var d = xs.Subscribe(lst.Add); d.Dispose(); - Assert.True(lst.SequenceEqual(new[] { 42 })); + Assert.True(lst.SequenceEqual([42])); } /// @@ -65,19 +65,19 @@ public void Create_ObserverThrows() { o.OnNext(1); return Disposable.Empty; - }).Subscribe(x => { throw new InvalidOperationException(); })); + }).Subscribe(x => throw new InvalidOperationException())); Assert.Throws(() => Signal.Create(o => { o.OnError(new Exception()); return Disposable.Empty; - }).Subscribe(x => { }, ex => { throw new InvalidOperationException(); })); + }).Subscribe(x => { }, ex => throw new InvalidOperationException())); Assert.Throws(() => Signal.Create(o => { o.OnCompleted(); return Disposable.Empty; - }).Subscribe(x => { }, ex => { }, () => { throw new InvalidOperationException(); })); + }).Subscribe(x => { }, ex => { }, () => throw new InvalidOperationException())); } /// @@ -111,7 +111,7 @@ public void CreateWithDisposable_NullCoalescingAction() var d = xs.Subscribe(lst.Add); d.Dispose(); - Assert.True(lst.SequenceEqual(new[] { 42 })); + Assert.True(lst.SequenceEqual([42])); } /// diff --git a/src/tests/ReactiveUI.Primitives.Tests/SignalFromTaskTest.cs b/src/tests/ReactiveUI.Primitives.Tests/SignalFromTaskTest.cs index f21e85f..b21c539 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/SignalFromTaskTest.cs +++ b/src/tests/ReactiveUI.Primitives.Tests/SignalFromTaskTest.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2023 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. @@ -242,7 +242,7 @@ public async Task SignalFromTaskHandlesCancellationInBase() await Task.Delay(6000).ConfigureAwait(false); Assert.DoesNotContain("finished command Normally", statusTrail.Select(x => x.Item2)); - Assert.Equal("Should always come here.", statusTrail.Last().Item2); + Assert.Equal("Should always come here.", statusTrail[^1].Item2); //// (0, "started command") //// (1, "Should always come here.") @@ -300,7 +300,7 @@ await Task.Delay(10000, cts.Token).HandleCancellation(async () => Assert.DoesNotContain("starting cancelling command", statusTrail.Select(x => x.Item2)); Assert.DoesNotContain("finished cancelling command", statusTrail.Select(x => x.Item2)); Assert.Contains("finished command Normally", statusTrail.Select(x => x.Item2)); - Assert.Equal("Should always come here.", statusTrail.Last().Item2); + Assert.Equal("Should always come here.", statusTrail[^1].Item2); Assert.True(result); //// (0, "started command") //// (2, "finished command Normally") @@ -532,7 +532,7 @@ public async Task SignalFromTask_T_HandlesCancellationInBase() await Task.Delay(6000).ConfigureAwait(false); Assert.DoesNotContain("finished command Normally", statusTrail.Select(x => x.Item2)); - Assert.Equal("Should always come here.", statusTrail.Last().Item2); + Assert.Equal("Should always come here.", statusTrail[^1].Item2); //// (0, "started command") //// (1, "Should always come here.") @@ -590,7 +590,7 @@ await Task.Delay(10000, cts.Token).HandleCancellation(async () => Assert.DoesNotContain("starting cancelling command", statusTrail.Select(x => x.Item2)); Assert.DoesNotContain("finished cancelling command", statusTrail.Select(x => x.Item2)); Assert.Contains("finished command Normally", statusTrail.Select(x => x.Item2)); - Assert.Equal("Should always come here.", statusTrail.Last().Item2); + Assert.Equal("Should always come here.", statusTrail[^1].Item2); Assert.True(result); //// (0, "started command") //// (2, "finished command Normally") diff --git a/src/tests/ReactiveUI.Primitives.Tests/SignalTests.cs b/src/tests/ReactiveUI.Primitives.Tests/SignalTests.cs index d19cb59..280dd26 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/SignalTests.cs +++ b/src/tests/ReactiveUI.Primitives.Tests/SignalTests.cs @@ -1,10 +1,9 @@ -// Copyright (c) 2019-2023 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. using System; using System.Collections.Generic; -using System.Linq; using ReactiveUI.Primitives.Signals; using TUnit.Core; @@ -126,7 +125,7 @@ public void OnCompletedDisposed() subject.Dispose(); - Assert.Throws(() => subject.OnCompleted()); + Assert.Throws(subject.OnCompleted); } /// @@ -348,7 +347,7 @@ public void SubjectBuffer() { var subject = new Signal(); var result = new List(); - subject.Buffer(2).Subscribe(i => result = i.ToList()); + subject.Buffer(2).Subscribe(i => result = [.. i]); subject.OnNext(1); subject.OnNext(2); Assert.Equal(new[] { 1, 2 }, result); @@ -369,7 +368,7 @@ public void SubjectBufferTake2Skip2() { var subject = new Signal(); var result = new List(); - subject.Buffer(2, 2).Subscribe(i => result = i.ToList()); + subject.Buffer(2, 2).Subscribe(i => result = [.. i]); subject.OnNext(1); subject.OnNext(2); Assert.Equal(new[] { 1, 2 }, result); diff --git a/src/tests/ReactiveUI.Primitives.Tests/StatefulSharingAndBridgeContractTests.cs b/src/tests/ReactiveUI.Primitives.Tests/StatefulSharingAndBridgeContractTests.cs index a249fb0..5825be1 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/StatefulSharingAndBridgeContractTests.cs +++ b/src/tests/ReactiveUI.Primitives.Tests/StatefulSharingAndBridgeContractTests.cs @@ -1,10 +1,11 @@ -// Copyright (c) 2019-2023 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. using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Threading; @@ -86,7 +87,8 @@ public void ConnectableShareAndReplayLiveControlSourceSubscriptions() public async Task CommandSignalPublishesResultsFailuresAndRunningState() { var canRun = new StateSignal(true); - var command = new CommandSignal(async token => + var command = new CommandSignal( + async token => { await Task.Yield(); token.ThrowIfCancellationRequested(); @@ -109,6 +111,7 @@ public async Task CommandSignalPublishesResultsFailuresAndRunningState() } [Test] + [RequiresAssemblyFiles()] public void BridgeGeneratorsEmitOnlyWhenExternalShapesArePresentAndCompileSmokeAdapters() { const string source = """ @@ -160,6 +163,7 @@ public static void Use(IObservable source, R3.Observable r3) } [Test] + [RequiresAssemblyFiles()] public void BridgeGeneratorsDoNotEmitExternalAdaptersWhenExternalPackagesAreAbsent() { const string source = """ @@ -179,6 +183,7 @@ public static class CoreOnlySmoke Assert.False(generatedSources.Any(text => text.Contains("R3SignalBridge"))); } + [RequiresAssemblyFiles("Calls System.Reflection.Assembly.Location")] private static (ImmutableArray Diagnostics, string[] GeneratedSources) RunGenerators(string source) { var parseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview); @@ -197,16 +202,15 @@ private static (ImmutableArray Diagnostics, string[] GeneratedSource var compilation = CSharpCompilation.Create( "BridgeGeneratorSmoke", - new[] { CSharpSyntaxTree.ParseText(source, parseOptions) }, + [CSharpSyntaxTree.ParseText(source, parseOptions)], references, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); var driver = CSharpGeneratorDriver.Create( - new ISourceGenerator[] - { + [ new SystemReactiveBridgeGenerator().AsSourceGenerator(), new R3BridgeGenerator().AsSourceGenerator(), - }, + ], parseOptions: parseOptions); driver = (CSharpGeneratorDriver)driver.RunGeneratorsAndUpdateCompilation(compilation, out var updatedCompilation, out var generatorDiagnostics); diff --git a/src/tests/ReactiveUI.Primitives.Tests/TestClasses/EmptySequencer.cs b/src/tests/ReactiveUI.Primitives.Tests/TestClasses/EmptySequencer.cs index dcfa3df..1b1f241 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/TestClasses/EmptySequencer.cs +++ b/src/tests/ReactiveUI.Primitives.Tests/TestClasses/EmptySequencer.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2023 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.