diff --git a/README.md b/README.md index 884d6426..b72bdbc2 100644 --- a/README.md +++ b/README.md @@ -1142,6 +1142,7 @@ share one subscription instead of starting the work over each time. | `IObserver.FastForEach(source)` | Pushes a whole collection into an observer and indexes arrays and lists directly. | - | | `ObserveOn(sequencer)` | Delivers notifications to subscribers on the sequencer you name. | `ObserveOn` | | `WitnessOn(sequencer)` | Another name for `ObserveOn`. | `ObserveOn` | +| `WitnessLatestOn(sequencer)` | Delivers notifications on the sequencer you name. Keeps only the newest value that is waiting and drops the older ones. | - | | `SubscribeOn(sequencer)` | Runs the subscription itself on the sequencer you name. | `SubscribeOn` | | `Synchronize()`, `Synchronize(object gate)` | Delivers notifications one at a time behind a lock, which you can share with other signals. | `Synchronize` | | `Synchronize(Lock gate)` | The same, taking a `System.Threading.Lock`. Available on net9.0 and later only. | `Synchronize` | @@ -2211,18 +2212,36 @@ conveniences that take an action, with or without a delay. | Sequencer | What it does | |---|---| -| `Sequencer.CurrentThread` | Queues work on the calling thread and runs it in order. Also available as `CurrentThreadSequencer.Instance`. | -| `Sequencer.Immediate` | Runs the work right away on the calling thread. Also available as `ImmediateSequencer.Instance`. | +| `Sequencer.CurrentThread` | Queues work on the calling thread and runs it in order. Also available as `CurrentThreadSequencer.Instance`. `new CurrentThreadSequencer(timeProvider)` waits through a `TimeProvider`. | +| `Sequencer.Immediate` | Runs the work right away on the calling thread. Also available as `ImmediateSequencer.Instance`. `new ImmediateSequencer(timeProvider)` waits through a `TimeProvider`. | | `Sequencer.Default` | The default choice for background work. It is `TaskPoolSequencer.Default`. | -| `TaskPoolSequencer` | Runs work through a `TaskFactory`. Use `TaskPoolSequencer.Instance`, or pass your own factory. | -| `ThreadPoolSequencer` | Runs work on the thread pool. Use `ThreadPoolSequencer.Instance`. | -| `SynchronizationContextSequencer` | Posts work to a `SynchronizationContext`. Use `SynchronizationContextSequencer.Current`, or pass a context. | -| `WasmSequencer` | Runs work on a browser's single-threaded event loop. Use `WasmSequencer.Default`. | -| `VirtualClock` | Controls time in a test, using `DateTimeOffset` and `TimeSpan`. You advance the clock, so nothing sleeps. | +| `TaskPoolSequencer` | Runs work through a `TaskFactory`. Use `TaskPoolSequencer.Instance`, or pass your own factory. `new TaskPoolSequencer(factory, timeProvider)` takes its time from a `TimeProvider`. | +| `ThreadPoolSequencer` | Runs work on the thread pool. Use `ThreadPoolSequencer.Instance`. `new ThreadPoolSequencer(timeProvider)` takes its time and delay timer from a `TimeProvider`. | +| `SynchronizationContextSequencer` | Posts work to a `SynchronizationContext`. Use `SynchronizationContextSequencer.Current`, or pass a context. `new SynchronizationContextSequencer(context, timeProvider)` takes its time from a `TimeProvider`. | +| `WasmSequencer` | Runs work on a browser's single-threaded event loop. Use `WasmSequencer.Default`. `new WasmSequencer(timeProvider)` takes its time and timers from a `TimeProvider`. | +| `VirtualClock` | Controls time in a test, using `DateTimeOffset` and `TimeSpan`. You advance the clock, so nothing sleeps. It is also a `TimeProvider`. | | `VirtualTimeSequencer` | Controls time in a test using your own clock types. | +| `sequencer.IsImmediate` | Returns `true` when the sequencer is the immediate sequencer and `false` for any other sequencer or for `null`. Use it instead of comparing against `Sequencer.Immediate`. It comes from `SequencerImmediacyExtensions`. | Use a virtual sequencer for a time-based test. Do not sleep a real thread. +To run a built-in sequencer from a fake clock, pass a `TimeProvider` to its constructor. `VirtualClock` is a +`TimeProvider`, so `clock.AdvanceBy` fires the sequencer's timers. + +```csharp +var clock = new VirtualClock(); +var sequencer = new ThreadPoolSequencer(clock); +var results = new List(); + +using var subscription = source + .Throttle(TimeSpan.FromSeconds(1), sequencer) + .Subscribe(results.Add); + +source.OnNext(1); +clock.AdvanceBy(TimeSpan.FromSeconds(1)); +// results now holds 1 +``` + ### Platform sequencers Each UI framework gets its own package, so the core package pulls in none of them. @@ -3042,6 +3061,7 @@ the same thing. | `DeferSignal` | `Signal.Lazy` / `Signal.Defer` | | `CatchSignal` | `Catch` | | `WitnessOnSignal` | `WitnessOn` / `ObserveOn` | +| `WitnessLatestOnSignal` | `WitnessLatestOn` | | `SelectManyThenCoordinator` | `SelectManyThen`, both projection stages in one sink | | `CalmCoordinator` | `Calm` / `Throttle` | | `ReattemptCoordinator` | `Reattempt` | diff --git a/src/Primitives.Extensions.Shared/Operators/ObserveOnObservable.cs b/src/Primitives.Extensions.Shared/Operators/ObserveOnObservable.cs index 6045ef6d..9647b7cb 100644 --- a/src/Primitives.Extensions.Shared/Operators/ObserveOnObservable.cs +++ b/src/Primitives.Extensions.Shared/Operators/ObserveOnObservable.cs @@ -23,7 +23,7 @@ public IDisposable Subscribe(IObserver observer) InvalidOperationExceptionHelper.ThrowIfNull(scheduler); ArgumentExceptionHelper.ThrowIfNull(observer); - if (ReferenceEquals(scheduler, Sequencer.Immediate)) + if (scheduler.IsImmediate) { return source.Subscribe(observer); } diff --git a/src/Primitives.Shared/Advanced/EmptySignal{T}.cs b/src/Primitives.Shared/Advanced/EmptySignal{T}.cs index bf749610..8ed6deaf 100644 --- a/src/Primitives.Shared/Advanced/EmptySignal{T}.cs +++ b/src/Primitives.Shared/Advanced/EmptySignal{T}.cs @@ -42,7 +42,7 @@ private IDisposable SubscribeCore(IObserver observer, IDisposable cancel) { observer = new GuardedWitness(observer, cancel); - if (_scheduler == Sequencer.Immediate) + if (_scheduler.IsImmediate) { observer.OnCompleted(); return EmptyDisposable.Instance; diff --git a/src/Primitives.Shared/Advanced/ReturnSignal{T}.cs b/src/Primitives.Shared/Advanced/ReturnSignal{T}.cs index 5ea4ccb1..c79cbeb2 100644 --- a/src/Primitives.Shared/Advanced/ReturnSignal{T}.cs +++ b/src/Primitives.Shared/Advanced/ReturnSignal{T}.cs @@ -54,7 +54,7 @@ private IDisposable SubscribeCore(IObserver observer, IDisposable cancel) { observer = new GuardedWitness(observer, cancel); - if (_scheduler == Sequencer.Immediate) + if (_scheduler.IsImmediate) { observer.OnNext(_value); observer.OnCompleted(); diff --git a/src/Primitives.Shared/Advanced/SubscriptionScheduling.cs b/src/Primitives.Shared/Advanced/SubscriptionScheduling.cs index 98b6c117..99e798fb 100644 --- a/src/Primitives.Shared/Advanced/SubscriptionScheduling.cs +++ b/src/Primitives.Shared/Advanced/SubscriptionScheduling.cs @@ -42,7 +42,7 @@ internal static IDisposable OnCurrentThread(TState state, FuncA disposable that cancels the scheduled work, best effort. internal static IDisposable RunOn(ISequencer sequencer, TState state, Action run) { - if (sequencer == Sequencer.Immediate) + if (sequencer.IsImmediate) { run(state); return EmptyDisposable.Instance; diff --git a/src/Primitives.Shared/Advanced/ThrowSignal{T}.cs b/src/Primitives.Shared/Advanced/ThrowSignal{T}.cs index f1d49f1d..1c5588a8 100644 --- a/src/Primitives.Shared/Advanced/ThrowSignal{T}.cs +++ b/src/Primitives.Shared/Advanced/ThrowSignal{T}.cs @@ -64,7 +64,7 @@ private IDisposable SubscribeCore(IObserver observer, IDisposable cancel) { observer = new GuardedWitness(observer, cancel); - if (_scheduler == Sequencer.Immediate) + if (_scheduler.IsImmediate) { observer.OnError(_error); return EmptyDisposable.Instance; diff --git a/src/Primitives.Shared/Advanced/WitnessLatestOnSignal{T}.cs b/src/Primitives.Shared/Advanced/WitnessLatestOnSignal{T}.cs new file mode 100644 index 00000000..4200f294 --- /dev/null +++ b/src/Primitives.Shared/Advanced/WitnessLatestOnSignal{T}.cs @@ -0,0 +1,244 @@ +// 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.Runtime.CompilerServices; + +#if REACTIVE_SHIM +namespace ReactiveUI.Primitives.Reactive.Advanced; +#else +namespace ReactiveUI.Primitives.Advanced; +#endif + +/// Re-dispatches source notifications onto a sequencer, delivering only the newest value that is waiting. +/// The value type. +/// The source observable. +/// The sequencer that dispatches the notifications. +[System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] +public sealed class WitnessLatestOnSignal(IObservable source, ISequencer scheduler) : IRequireCurrentThread +{ + /// The source observable. + private readonly IObservable _source = source; + + /// The sequencer that dispatches the notifications. + private readonly ISequencer _scheduler = scheduler; + + /// Reports that subscription is dispatched through the current-thread sequencer. + /// Always . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() => true; + + /// Subscribes an observer through the current-thread sequencer. + /// The downstream observer. + /// The subscription. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IDisposable Subscribe(IObserver observer) => + SignalSubscription.Subscribe(observer, true, SubscribeCore); + + /// Builds the dispatching sink and subscribes it to the source. + /// The downstream observer. + /// The subscription handle owned by the subscription helper. + /// The source subscription together with the sink. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private IDisposable SubscribeCore(IObserver observer, IDisposable cancel) => + new WitnessLatestOn(this, observer, cancel).Run(); + + /// Holds the newest source value and delivers it on the sequencer. + /// The owning signal. + /// The downstream observer. + /// The subscription handle released on teardown. + private sealed class WitnessLatestOn(WitnessLatestOnSignal parent, IObserver observer, IDisposable cancel) : IObserver, IWorkItem, IsDisposed + { + /// The owning signal. + private readonly WitnessLatestOnSignal _parent = parent; + + /// The downstream observer. + private readonly IObserver _observer = observer; + + /// Synchronization gate guarding the pending slot and scheduling state. + private readonly Lock _gate = new(); + + /// Upstream subscription disposed on teardown. + private readonly IDisposable _cancel = cancel; + + /// The newest value awaiting delivery. + private T _pending = default!; + + /// The error awaiting delivery. + private Exception? _error; + + /// Whether holds a value. + private bool _hasPending; + + /// Whether completion is awaiting delivery. + private bool _hasCompleted; + + /// Whether the sink has been torn down. + private bool _isDisposed; + + /// Tracks whether a drain is scheduled or running. + private bool _isScheduled; + + /// + public bool IsDisposed + { + get + { + lock (_gate) + { + return _isDisposed; + } + } + } + + /// Subscribes to the source. + /// The source subscription together with this sink. + public MultipleDisposable Run() + { + var sourceDisposable = _parent._source.Subscribe(this); + + return new(sourceDisposable, this); + } + + /// Stores a value as the newest pending value. + /// The value to store. + public void OnNext(T value) + { + lock (_gate) + { + if (_isDisposed || _hasCompleted || _error is not null) + { + return; + } + + _pending = value; + _hasPending = true; + if (_isScheduled) + { + return; + } + + _isScheduled = true; + } + + _parent._scheduler.Schedule(this); + } + + /// Stores the error for delivery after any pending value. + /// The error to store. + public void OnError(Exception error) + { + lock (_gate) + { + if (_isDisposed || _error is not null) + { + return; + } + + _error = error; + if (_isScheduled) + { + return; + } + + _isScheduled = true; + } + + _parent._scheduler.Schedule(this); + } + + /// Stores completion for delivery after any pending value. + public void OnCompleted() + { + lock (_gate) + { + if (_isDisposed || _hasCompleted || _error is not null) + { + return; + } + + _hasCompleted = true; + if (_isScheduled) + { + return; + } + + _isScheduled = true; + } + + _parent._scheduler.Schedule(this); + } + + /// Executes the scheduled drain. + public void Execute() + { + while (true) + { + var value = default(T)!; + Exception? error = null; + var isValue = false; + lock (_gate) + { + if (_isDisposed) + { + _isScheduled = false; + return; + } + + if (_hasPending) + { + value = _pending; + _pending = default!; + _hasPending = false; + isValue = true; + } + else if (_error is not null) + { + error = _error; + } + else if (!_hasCompleted) + { + _isScheduled = false; + return; + } + } + + if (isValue) + { + _observer.OnNext(value); + continue; + } + + if (error is not null) + { + _observer.OnError(error); + } + else + { + _observer.OnCompleted(); + } + + Dispose(); + return; + } + } + + /// Drops the pending value and releases the upstream subscription, once. + public void Dispose() + { + lock (_gate) + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + _pending = default!; + _hasPending = false; + } + + _cancel.Dispose(); + } + } +} diff --git a/src/Primitives.Shared/Concurrency/SequencerImmediacyExtensions.cs b/src/Primitives.Shared/Concurrency/SequencerImmediacyExtensions.cs new file mode 100644 index 00000000..b0f1d5cd --- /dev/null +++ b/src/Primitives.Shared/Concurrency/SequencerImmediacyExtensions.cs @@ -0,0 +1,33 @@ +// 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.Runtime.CompilerServices; + +#if REACTIVE_SHIM +namespace ReactiveUI.Primitives.Reactive.Concurrency; +#else +namespace ReactiveUI.Primitives.Concurrency; +#endif + +/// Reports whether a sequencer runs work inline on the calling thread. +public static class SequencerImmediacyExtensions +{ + /// Immediacy check for a sequencer. + /// The sequencer to test; may be . + extension(ISequencer? sequencer) + { + /// Gets a value indicating whether the sequencer is the immediate sequencer. + /// when the sequencer runs work inline on the calling thread; for any other sequencer or . + public bool IsImmediate + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => +#if REACTIVE_SHIM + sequencer is global::System.Reactive.Concurrency.ImmediateScheduler; +#else + sequencer is ImmediateSequencer; +#endif + } + } +} diff --git a/src/Primitives.Shared/SignalOperatorParityMixins.Helpers.cs b/src/Primitives.Shared/SignalOperatorParityMixins.Helpers.cs index 317412cb..99a7d306 100644 --- a/src/Primitives.Shared/SignalOperatorParityMixins.Helpers.cs +++ b/src/Primitives.Shared/SignalOperatorParityMixins.Helpers.cs @@ -163,7 +163,7 @@ public IDisposable Subscribe(Action> onNext, Action onError /// The next callback. private void Emit(Action> onNext) { - if (_sequencer == Sequencer.Immediate) + if (_sequencer.IsImmediate) { var timestamp = _sequencer.Now; for (var i = 0; i < _range.Count; i++) @@ -184,7 +184,7 @@ private void Emit(Action> onNext) /// The downstream observer. private void Emit(IObserver> observer) { - if (_sequencer == Sequencer.Immediate) + if (_sequencer.IsImmediate) { var timestamp = _sequencer.Now; for (var i = 0; i < _range.Count; i++) @@ -238,7 +238,7 @@ public IDisposable Subscribe(Action> onNext, Action o /// The next callback. private void Emit(Action> onNext) { - if (_sequencer == Sequencer.Immediate) + if (_sequencer.IsImmediate) { for (var i = 0; i < _range.Count; i++) { @@ -262,7 +262,7 @@ private void Emit(Action> onNext) /// The downstream observer. private void Emit(IObserver> observer) { - if (_sequencer == Sequencer.Immediate) + if (_sequencer.IsImmediate) { for (var i = 0; i < _range.Count; i++) { diff --git a/src/Primitives.Shared/SignalOperatorParityMixins.cs b/src/Primitives.Shared/SignalOperatorParityMixins.cs index 90dee19e..362fc1ce 100644 --- a/src/Primitives.Shared/SignalOperatorParityMixins.cs +++ b/src/Primitives.Shared/SignalOperatorParityMixins.cs @@ -36,7 +36,7 @@ public IObservable ToObservable(ISequencer scheduler) ArgumentExceptionHelper.ThrowIfNull(scheduler); - return scheduler == Sequencer.Immediate || scheduler == Sequencer.CurrentThread + return scheduler.IsImmediate || scheduler == Sequencer.CurrentThread ? new FromEnumerableSignal(values) : new ScheduledEnumerableSignal(values, scheduler); } @@ -149,7 +149,7 @@ public IObservable ObserveOn(ISequencer scheduler) ArgumentExceptionHelper.ThrowIfNull(scheduler); - return scheduler == Sequencer.Immediate ? source : new WitnessOnSignal(source, scheduler); + return scheduler.IsImmediate ? source : new WitnessOnSignal(source, scheduler); } /// Schedules source subscription on the supplied sequencer. diff --git a/src/Primitives.Shared/Signals/ScheduledSignal{T}.cs b/src/Primitives.Shared/Signals/ScheduledSignal{T}.cs index 185a6fd7..13022661 100644 --- a/src/Primitives.Shared/Signals/ScheduledSignal{T}.cs +++ b/src/Primitives.Shared/Signals/ScheduledSignal{T}.cs @@ -65,7 +65,7 @@ public ScheduledSignal(ISequencer scheduler, IObserver? defaultObserver, ISig _defaultObserver = defaultObserver; _subject = defaultSubject ?? new Signal(); - _observed = scheduler == Sequencer.Immediate + _observed = scheduler.IsImmediate ? _subject : new WitnessOnSignal(_subject, scheduler); _defaultObserverSub = defaultObserver is null ? null : SubscribeDefaultObserver(defaultObserver); diff --git a/src/Primitives.Shared/Signals/SignalExtensions{Create}.cs b/src/Primitives.Shared/Signals/SignalExtensions{Create}.cs index c95d7065..161ff52a 100644 --- a/src/Primitives.Shared/Signals/SignalExtensions{Create}.cs +++ b/src/Primitives.Shared/Signals/SignalExtensions{Create}.cs @@ -26,6 +26,13 @@ public static partial class SignalExtensions public IObservable WitnessOn(ISequencer scheduler) => new WitnessOnSignal(source, scheduler); + /// Delivers only the newest waiting source value on the supplied sequencer. + /// The sequencer that notifications are delivered on. + /// A signal that forwards the latest source value on . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IObservable WitnessLatestOn(ISequencer scheduler) => + new WitnessLatestOnSignal(source, scheduler); + /// Blocks until the signal completes and returns the observed values. /// The values observed before completion. /// is . diff --git a/src/Primitives.Shared/Signals/Signal{Emit}.cs b/src/Primitives.Shared/Signals/Signal{Emit}.cs index b2af64bd..fe7ce9dd 100644 --- a/src/Primitives.Shared/Signals/Signal{Emit}.cs +++ b/src/Primitives.Shared/Signals/Signal{Emit}.cs @@ -18,7 +18,7 @@ public static partial class Signal /// The value to emit. /// The scheduler the value is emitted on. /// A signal that emits and completes. - public static IObservable Emit(T value, ISequencer scheduler) => scheduler == Sequencer.Immediate + public static IObservable Emit(T value, ISequencer scheduler) => scheduler.IsImmediate ? new ImmediateReturnSignal(value) : new ReturnSignal(value, scheduler); diff --git a/src/Primitives.Shared/Signals/Signal{Factories}.cs b/src/Primitives.Shared/Signals/Signal{Factories}.cs index 4b56892a..b5aff0b2 100644 --- a/src/Primitives.Shared/Signals/Signal{Factories}.cs +++ b/src/Primitives.Shared/Signals/Signal{Factories}.cs @@ -42,7 +42,7 @@ public static IObservable Sequence(int start, int count, ISequencer schedul return ImmutableEmptySignal.Instance; } - return scheduler == Sequencer.Immediate || scheduler == Sequencer.CurrentThread + return scheduler.IsImmediate || scheduler == Sequencer.CurrentThread ? new RangeSignal(start, count) : new SequenceSignal(start, count, scheduler); } diff --git a/src/Primitives.Shared/Signals/Signal{Fail}.cs b/src/Primitives.Shared/Signals/Signal{Fail}.cs index b807ac05..9556080d 100644 --- a/src/Primitives.Shared/Signals/Signal{Fail}.cs +++ b/src/Primitives.Shared/Signals/Signal{Fail}.cs @@ -22,7 +22,7 @@ public static partial class Signal "Design", "SST2307:Generic method type parameters should be inferable from the parameters", Justification = "The element type cannot be inferred from the arguments.")] - public static IObservable Fail(Exception error, ISequencer scheduler) => scheduler == Sequencer.Immediate + public static IObservable Fail(Exception error, ISequencer scheduler) => scheduler.IsImmediate ? new ImmediateThrowSignal(error) : new ThrowSignal(error, scheduler); @@ -62,7 +62,7 @@ public static IObservable Fail(Exception error, T witness) => "SST2318:Members should not have identical bodies", Justification = "The witness parameter only fixes the element type, so this overload builds the same signal.")] public static IObservable Fail(Exception error, ISequencer scheduler, T witness) => - scheduler == Sequencer.Immediate + scheduler.IsImmediate ? new ImmediateThrowSignal(error) : new ThrowSignal(error, scheduler); } diff --git a/src/Primitives.Shared/Signals/Signal{FromTask}.cs b/src/Primitives.Shared/Signals/Signal{FromTask}.cs index 7c02fbf6..623edd2b 100644 --- a/src/Primitives.Shared/Signals/Signal{FromTask}.cs +++ b/src/Primitives.Shared/Signals/Signal{FromTask}.cs @@ -139,7 +139,7 @@ private static ITaskSignal CreateTaskSignal( Func> execution, ISequencer? scheduler, CancellationTokenSource? cancellationTokenSource) => - ReferenceEquals(scheduler, Sequencer.Immediate) + scheduler.IsImmediate ? new ImmediateTaskSignal(execution, cancellationTokenSource) : TaskSignal.Create( ao => Lazy(() => Create(observer => SubscribeTask(ao, execution, observer))), diff --git a/src/Primitives.Shared/Signals/Signal{None}.cs b/src/Primitives.Shared/Signals/Signal{None}.cs index f87bdb8e..72fde571 100644 --- a/src/Primitives.Shared/Signals/Signal{None}.cs +++ b/src/Primitives.Shared/Signals/Signal{None}.cs @@ -21,7 +21,7 @@ public static partial class Signal "Design", "SST2307:Generic method type parameters should be inferable from the parameters", Justification = "The element type cannot be inferred from the arguments.")] - public static IObservable None(ISequencer scheduler) => scheduler == Sequencer.Immediate + public static IObservable None(ISequencer scheduler) => scheduler.IsImmediate ? ImmutableEmptySignal.Instance : new EmptySignal(scheduler); @@ -34,7 +34,7 @@ public static IObservable None(ISequencer scheduler) => scheduler == Seque "Design", "SST2318:Members should not have identical bodies", Justification = "The witness parameter only fixes the element type, so this overload builds the same signal.")] - public static IObservable None(ISequencer scheduler, T witness) => scheduler == Sequencer.Immediate + public static IObservable None(ISequencer scheduler, T witness) => scheduler.IsImmediate ? ImmutableEmptySignal.Instance : new EmptySignal(scheduler); diff --git a/src/Primitives.Shared/Signals/Signal{RxAliases}.cs b/src/Primitives.Shared/Signals/Signal{RxAliases}.cs index b1bfaa7d..846f5284 100644 --- a/src/Primitives.Shared/Signals/Signal{RxAliases}.cs +++ b/src/Primitives.Shared/Signals/Signal{RxAliases}.cs @@ -105,7 +105,7 @@ public static IObservable Return(T value, ISequencer scheduler) { ArgumentExceptionHelper.ThrowIfNull(scheduler); - return scheduler == Sequencer.Immediate + return scheduler.IsImmediate ? new ImmediateReturnSignal(value) : new ReturnSignal(value, scheduler); } @@ -152,7 +152,7 @@ public static IObservable Empty(ISequencer scheduler) { ArgumentExceptionHelper.ThrowIfNull(scheduler); - return scheduler == Sequencer.Immediate ? ImmutableEmptySignal.Instance : new EmptySignal(scheduler); + return scheduler.IsImmediate ? ImmutableEmptySignal.Instance : new EmptySignal(scheduler); } /// Returns a non-terminating observable sequence. @@ -189,7 +189,7 @@ public static IObservable Throw(Exception error, ISequencer scheduler) { ArgumentExceptionHelper.ThrowIfNull(scheduler); - return scheduler == Sequencer.Immediate + return scheduler.IsImmediate ? new ImmediateThrowSignal(error) : new ThrowSignal(error, scheduler); } @@ -221,7 +221,7 @@ public static IObservable Range(int start, int count, ISequencer scheduler) return ImmutableEmptySignal.Instance; } - return scheduler == Sequencer.Immediate || scheduler == Sequencer.CurrentThread + return scheduler.IsImmediate || scheduler == Sequencer.CurrentThread ? new RangeSignal(start, count) : new SequenceSignal(start, count, scheduler); } diff --git a/src/Primitives.Shared/Signals/TaskSignal{T}.cs b/src/Primitives.Shared/Signals/TaskSignal{T}.cs index b5726514..1f3da471 100644 --- a/src/Primitives.Shared/Signals/TaskSignal{T}.cs +++ b/src/Primitives.Shared/Signals/TaskSignal{T}.cs @@ -58,7 +58,7 @@ public void GetOperationCanceled(IObserver observer) => /// A disposable that ends the subscription. public IDisposable Subscribe(IObserver observer) { - var subscription = ReferenceEquals(_sequencer, Sequencer.Immediate) + var subscription = _sequencer.IsImmediate ? Source!.Subscribe(observer) : new WitnessOnSignal(Source!, _sequencer).Subscribe(observer); diff --git a/src/ReactiveUI.Primitives.Reactive/Concurrency/WasmScheduler.cs b/src/ReactiveUI.Primitives.Reactive/Concurrency/WasmScheduler.cs index eda9897e..2fbeaeff 100644 --- a/src/ReactiveUI.Primitives.Reactive/Concurrency/WasmScheduler.cs +++ b/src/ReactiveUI.Primitives.Reactive/Concurrency/WasmScheduler.cs @@ -43,16 +43,19 @@ public sealed class WasmScheduler : LocalScheduler, ISchedulerPeriodic, IDisposa /// Non-zero once has released the drain timer and the ready queue. private int _isDisposed; - /// Initializes a new instance of the class. - /// Timer provider; null selects the system provider. + /// Initializes a new instance of the class that reads time and arms its timers through a . + /// The provider supplying the current time and timers. + /// is . [System.Diagnostics.CodeAnalysis.SuppressMessage( "Correctness", "SST2403:Do not let 'this' escape from a constructor", Justification = "The drain timer is created disarmed, so nothing can call back into it until Schedule arms it after construction.")] - internal WasmScheduler(TimeProvider? timeProvider = null) + public WasmScheduler(TimeProvider timeProvider) { - _timeProvider = timeProvider ?? TimeProvider.System; + ArgumentExceptionHelper.ThrowIfNull(timeProvider); + + _timeProvider = timeProvider; _drainTimer = CreateTimer( _timeProvider, static state => ((WasmScheduler)state!).RunDrain(), @@ -69,7 +72,10 @@ internal interface IReadyWorkItem : IDisposable } /// Gets the shared WebAssembly scheduler. - public static WasmScheduler Default { get; } = new(); + public static WasmScheduler Default { get; } = new(TimeProvider.System); + + /// Gets the scheduler's notion of current time. + public override DateTimeOffset Now => _timeProvider.GetUtcNow(); /// Gets a value indicating whether the scheduler has been disposed. private bool IsDisposed => Volatile.Read(ref _isDisposed) != 0; diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-android/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-android/PublicAPI.txt index 3893e7c0..1e2f8564 100644 --- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-android/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-android/PublicAPI.txt @@ -2002,6 +2002,15 @@ namespace ReactiveUI.Primitives.Reactive.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -2067,9 +2076,18 @@ namespace ReactiveUI.Primitives.Reactive.Concurrency public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime) where TAbsolute : System.IComparable { } public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime, System.Collections.Generic.IComparer comparer) where TAbsolute : System.IComparable { } } + public static class SequencerImmediacyExtensions + { + extension(System.Reactive.Concurrency.IScheduler? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("WasmScheduler: ReadyCount = {_readyCount}, DrainState = {_drainState}, Disposed = {_isDisposed}")] public sealed class WasmScheduler : System.Reactive.Concurrency.LocalScheduler, System.IDisposable, System.Reactive.Concurrency.ISchedulerPeriodic { + public WasmScheduler(System.TimeProvider timeProvider) { } + public override System.DateTimeOffset Now { get; } public static ReactiveUI.Primitives.Reactive.Concurrency.WasmScheduler Default { get; } public void Dispose() { } public override System.IDisposable Schedule(TState state, System.Func action) { } @@ -2376,6 +2394,8 @@ namespace ReactiveUI.Primitives.Reactive.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(System.Reactive.Concurrency.IScheduler scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-ios/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-ios/PublicAPI.txt index b53be531..19c13335 100644 --- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-ios/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-ios/PublicAPI.txt @@ -2002,6 +2002,15 @@ namespace ReactiveUI.Primitives.Reactive.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -2065,9 +2074,18 @@ namespace ReactiveUI.Primitives.Reactive.Concurrency public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime) where TAbsolute : System.IComparable { } public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime, System.Collections.Generic.IComparer comparer) where TAbsolute : System.IComparable { } } + public static class SequencerImmediacyExtensions + { + extension(System.Reactive.Concurrency.IScheduler? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("WasmScheduler: ReadyCount = {_readyCount}, DrainState = {_drainState}, Disposed = {_isDisposed}")] public sealed class WasmScheduler : System.Reactive.Concurrency.LocalScheduler, System.IDisposable, System.Reactive.Concurrency.ISchedulerPeriodic { + public WasmScheduler(System.TimeProvider timeProvider) { } + public override System.DateTimeOffset Now { get; } public static ReactiveUI.Primitives.Reactive.Concurrency.WasmScheduler Default { get; } public void Dispose() { } public override System.IDisposable Schedule(TState state, System.Func action) { } @@ -2374,6 +2392,8 @@ namespace ReactiveUI.Primitives.Reactive.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(System.Reactive.Concurrency.IScheduler scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-maccatalyst/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-maccatalyst/PublicAPI.txt index 5ef2122e..35d93411 100644 --- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-maccatalyst/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-maccatalyst/PublicAPI.txt @@ -2002,6 +2002,15 @@ namespace ReactiveUI.Primitives.Reactive.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -2065,9 +2074,18 @@ namespace ReactiveUI.Primitives.Reactive.Concurrency public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime) where TAbsolute : System.IComparable { } public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime, System.Collections.Generic.IComparer comparer) where TAbsolute : System.IComparable { } } + public static class SequencerImmediacyExtensions + { + extension(System.Reactive.Concurrency.IScheduler? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("WasmScheduler: ReadyCount = {_readyCount}, DrainState = {_drainState}, Disposed = {_isDisposed}")] public sealed class WasmScheduler : System.Reactive.Concurrency.LocalScheduler, System.IDisposable, System.Reactive.Concurrency.ISchedulerPeriodic { + public WasmScheduler(System.TimeProvider timeProvider) { } + public override System.DateTimeOffset Now { get; } public static ReactiveUI.Primitives.Reactive.Concurrency.WasmScheduler Default { get; } public void Dispose() { } public override System.IDisposable Schedule(TState state, System.Func action) { } @@ -2374,6 +2392,8 @@ namespace ReactiveUI.Primitives.Reactive.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(System.Reactive.Concurrency.IScheduler scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-macos/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-macos/PublicAPI.txt index f3bb6650..82d04082 100644 --- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-macos/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-macos/PublicAPI.txt @@ -2002,6 +2002,15 @@ namespace ReactiveUI.Primitives.Reactive.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -2065,9 +2074,18 @@ namespace ReactiveUI.Primitives.Reactive.Concurrency public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime) where TAbsolute : System.IComparable { } public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime, System.Collections.Generic.IComparer comparer) where TAbsolute : System.IComparable { } } + public static class SequencerImmediacyExtensions + { + extension(System.Reactive.Concurrency.IScheduler? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("WasmScheduler: ReadyCount = {_readyCount}, DrainState = {_drainState}, Disposed = {_isDisposed}")] public sealed class WasmScheduler : System.Reactive.Concurrency.LocalScheduler, System.IDisposable, System.Reactive.Concurrency.ISchedulerPeriodic { + public WasmScheduler(System.TimeProvider timeProvider) { } + public override System.DateTimeOffset Now { get; } public static ReactiveUI.Primitives.Reactive.Concurrency.WasmScheduler Default { get; } public void Dispose() { } public override System.IDisposable Schedule(TState state, System.Func action) { } @@ -2374,6 +2392,8 @@ namespace ReactiveUI.Primitives.Reactive.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(System.Reactive.Concurrency.IScheduler scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-tvos/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-tvos/PublicAPI.txt index c635e290..b242d7ce 100644 --- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-tvos/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-tvos/PublicAPI.txt @@ -2002,6 +2002,15 @@ namespace ReactiveUI.Primitives.Reactive.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -2065,9 +2074,18 @@ namespace ReactiveUI.Primitives.Reactive.Concurrency public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime) where TAbsolute : System.IComparable { } public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime, System.Collections.Generic.IComparer comparer) where TAbsolute : System.IComparable { } } + public static class SequencerImmediacyExtensions + { + extension(System.Reactive.Concurrency.IScheduler? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("WasmScheduler: ReadyCount = {_readyCount}, DrainState = {_drainState}, Disposed = {_isDisposed}")] public sealed class WasmScheduler : System.Reactive.Concurrency.LocalScheduler, System.IDisposable, System.Reactive.Concurrency.ISchedulerPeriodic { + public WasmScheduler(System.TimeProvider timeProvider) { } + public override System.DateTimeOffset Now { get; } public static ReactiveUI.Primitives.Reactive.Concurrency.WasmScheduler Default { get; } public void Dispose() { } public override System.IDisposable Schedule(TState state, System.Func action) { } @@ -2374,6 +2392,8 @@ namespace ReactiveUI.Primitives.Reactive.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(System.Reactive.Concurrency.IScheduler scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0/PublicAPI.txt index 4b7339c3..eb28d219 100644 --- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0/PublicAPI.txt @@ -2001,6 +2001,15 @@ namespace ReactiveUI.Primitives.Reactive.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -2054,9 +2063,18 @@ namespace ReactiveUI.Primitives.Reactive.Concurrency public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime) where TAbsolute : System.IComparable { } public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime, System.Collections.Generic.IComparer comparer) where TAbsolute : System.IComparable { } } + public static class SequencerImmediacyExtensions + { + extension(System.Reactive.Concurrency.IScheduler? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("WasmScheduler: ReadyCount = {_readyCount}, DrainState = {_drainState}, Disposed = {_isDisposed}")] public sealed class WasmScheduler : System.Reactive.Concurrency.LocalScheduler, System.IDisposable, System.Reactive.Concurrency.ISchedulerPeriodic { + public WasmScheduler(System.TimeProvider timeProvider) { } + public override System.DateTimeOffset Now { get; } public static ReactiveUI.Primitives.Reactive.Concurrency.WasmScheduler Default { get; } public void Dispose() { } public override System.IDisposable Schedule(TState state, System.Func action) { } @@ -2363,6 +2381,8 @@ namespace ReactiveUI.Primitives.Reactive.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(System.Reactive.Concurrency.IScheduler scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-android/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-android/PublicAPI.txt index 3893e7c0..1e2f8564 100644 --- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-android/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-android/PublicAPI.txt @@ -2002,6 +2002,15 @@ namespace ReactiveUI.Primitives.Reactive.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -2067,9 +2076,18 @@ namespace ReactiveUI.Primitives.Reactive.Concurrency public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime) where TAbsolute : System.IComparable { } public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime, System.Collections.Generic.IComparer comparer) where TAbsolute : System.IComparable { } } + public static class SequencerImmediacyExtensions + { + extension(System.Reactive.Concurrency.IScheduler? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("WasmScheduler: ReadyCount = {_readyCount}, DrainState = {_drainState}, Disposed = {_isDisposed}")] public sealed class WasmScheduler : System.Reactive.Concurrency.LocalScheduler, System.IDisposable, System.Reactive.Concurrency.ISchedulerPeriodic { + public WasmScheduler(System.TimeProvider timeProvider) { } + public override System.DateTimeOffset Now { get; } public static ReactiveUI.Primitives.Reactive.Concurrency.WasmScheduler Default { get; } public void Dispose() { } public override System.IDisposable Schedule(TState state, System.Func action) { } @@ -2376,6 +2394,8 @@ namespace ReactiveUI.Primitives.Reactive.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(System.Reactive.Concurrency.IScheduler scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-ios/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-ios/PublicAPI.txt index b53be531..19c13335 100644 --- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-ios/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-ios/PublicAPI.txt @@ -2002,6 +2002,15 @@ namespace ReactiveUI.Primitives.Reactive.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -2065,9 +2074,18 @@ namespace ReactiveUI.Primitives.Reactive.Concurrency public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime) where TAbsolute : System.IComparable { } public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime, System.Collections.Generic.IComparer comparer) where TAbsolute : System.IComparable { } } + public static class SequencerImmediacyExtensions + { + extension(System.Reactive.Concurrency.IScheduler? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("WasmScheduler: ReadyCount = {_readyCount}, DrainState = {_drainState}, Disposed = {_isDisposed}")] public sealed class WasmScheduler : System.Reactive.Concurrency.LocalScheduler, System.IDisposable, System.Reactive.Concurrency.ISchedulerPeriodic { + public WasmScheduler(System.TimeProvider timeProvider) { } + public override System.DateTimeOffset Now { get; } public static ReactiveUI.Primitives.Reactive.Concurrency.WasmScheduler Default { get; } public void Dispose() { } public override System.IDisposable Schedule(TState state, System.Func action) { } @@ -2374,6 +2392,8 @@ namespace ReactiveUI.Primitives.Reactive.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(System.Reactive.Concurrency.IScheduler scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-maccatalyst/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-maccatalyst/PublicAPI.txt index 5ef2122e..35d93411 100644 --- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-maccatalyst/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-maccatalyst/PublicAPI.txt @@ -2002,6 +2002,15 @@ namespace ReactiveUI.Primitives.Reactive.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -2065,9 +2074,18 @@ namespace ReactiveUI.Primitives.Reactive.Concurrency public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime) where TAbsolute : System.IComparable { } public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime, System.Collections.Generic.IComparer comparer) where TAbsolute : System.IComparable { } } + public static class SequencerImmediacyExtensions + { + extension(System.Reactive.Concurrency.IScheduler? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("WasmScheduler: ReadyCount = {_readyCount}, DrainState = {_drainState}, Disposed = {_isDisposed}")] public sealed class WasmScheduler : System.Reactive.Concurrency.LocalScheduler, System.IDisposable, System.Reactive.Concurrency.ISchedulerPeriodic { + public WasmScheduler(System.TimeProvider timeProvider) { } + public override System.DateTimeOffset Now { get; } public static ReactiveUI.Primitives.Reactive.Concurrency.WasmScheduler Default { get; } public void Dispose() { } public override System.IDisposable Schedule(TState state, System.Func action) { } @@ -2374,6 +2392,8 @@ namespace ReactiveUI.Primitives.Reactive.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(System.Reactive.Concurrency.IScheduler scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-macos/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-macos/PublicAPI.txt index f3bb6650..82d04082 100644 --- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-macos/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-macos/PublicAPI.txt @@ -2002,6 +2002,15 @@ namespace ReactiveUI.Primitives.Reactive.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -2065,9 +2074,18 @@ namespace ReactiveUI.Primitives.Reactive.Concurrency public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime) where TAbsolute : System.IComparable { } public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime, System.Collections.Generic.IComparer comparer) where TAbsolute : System.IComparable { } } + public static class SequencerImmediacyExtensions + { + extension(System.Reactive.Concurrency.IScheduler? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("WasmScheduler: ReadyCount = {_readyCount}, DrainState = {_drainState}, Disposed = {_isDisposed}")] public sealed class WasmScheduler : System.Reactive.Concurrency.LocalScheduler, System.IDisposable, System.Reactive.Concurrency.ISchedulerPeriodic { + public WasmScheduler(System.TimeProvider timeProvider) { } + public override System.DateTimeOffset Now { get; } public static ReactiveUI.Primitives.Reactive.Concurrency.WasmScheduler Default { get; } public void Dispose() { } public override System.IDisposable Schedule(TState state, System.Func action) { } @@ -2374,6 +2392,8 @@ namespace ReactiveUI.Primitives.Reactive.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(System.Reactive.Concurrency.IScheduler scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-tvos/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-tvos/PublicAPI.txt index c635e290..b242d7ce 100644 --- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-tvos/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-tvos/PublicAPI.txt @@ -2002,6 +2002,15 @@ namespace ReactiveUI.Primitives.Reactive.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -2065,9 +2074,18 @@ namespace ReactiveUI.Primitives.Reactive.Concurrency public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime) where TAbsolute : System.IComparable { } public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime, System.Collections.Generic.IComparer comparer) where TAbsolute : System.IComparable { } } + public static class SequencerImmediacyExtensions + { + extension(System.Reactive.Concurrency.IScheduler? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("WasmScheduler: ReadyCount = {_readyCount}, DrainState = {_drainState}, Disposed = {_isDisposed}")] public sealed class WasmScheduler : System.Reactive.Concurrency.LocalScheduler, System.IDisposable, System.Reactive.Concurrency.ISchedulerPeriodic { + public WasmScheduler(System.TimeProvider timeProvider) { } + public override System.DateTimeOffset Now { get; } public static ReactiveUI.Primitives.Reactive.Concurrency.WasmScheduler Default { get; } public void Dispose() { } public override System.IDisposable Schedule(TState state, System.Func action) { } @@ -2374,6 +2392,8 @@ namespace ReactiveUI.Primitives.Reactive.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(System.Reactive.Concurrency.IScheduler scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0/PublicAPI.txt index 4b7339c3..eb28d219 100644 --- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0/PublicAPI.txt @@ -2001,6 +2001,15 @@ namespace ReactiveUI.Primitives.Reactive.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -2054,9 +2063,18 @@ namespace ReactiveUI.Primitives.Reactive.Concurrency public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime) where TAbsolute : System.IComparable { } public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime, System.Collections.Generic.IComparer comparer) where TAbsolute : System.IComparable { } } + public static class SequencerImmediacyExtensions + { + extension(System.Reactive.Concurrency.IScheduler? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("WasmScheduler: ReadyCount = {_readyCount}, DrainState = {_drainState}, Disposed = {_isDisposed}")] public sealed class WasmScheduler : System.Reactive.Concurrency.LocalScheduler, System.IDisposable, System.Reactive.Concurrency.ISchedulerPeriodic { + public WasmScheduler(System.TimeProvider timeProvider) { } + public override System.DateTimeOffset Now { get; } public static ReactiveUI.Primitives.Reactive.Concurrency.WasmScheduler Default { get; } public void Dispose() { } public override System.IDisposable Schedule(TState state, System.Func action) { } @@ -2363,6 +2381,8 @@ namespace ReactiveUI.Primitives.Reactive.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(System.Reactive.Concurrency.IScheduler scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net462/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net462/PublicAPI.txt index 47c86d66..153f70da 100644 --- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net462/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net462/PublicAPI.txt @@ -1967,6 +1967,15 @@ namespace ReactiveUI.Primitives.Reactive.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -2020,9 +2029,18 @@ namespace ReactiveUI.Primitives.Reactive.Concurrency public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime) where TAbsolute : System.IComparable { } public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime, System.Collections.Generic.IComparer comparer) where TAbsolute : System.IComparable { } } + public static class SequencerImmediacyExtensions + { + extension(System.Reactive.Concurrency.IScheduler? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("WasmScheduler: ReadyCount = {_readyCount}, DrainState = {_drainState}, Disposed = {_isDisposed}")] public sealed class WasmScheduler : System.Reactive.Concurrency.LocalScheduler, System.IDisposable, System.Reactive.Concurrency.ISchedulerPeriodic { + public WasmScheduler(System.TimeProvider timeProvider) { } + public override System.DateTimeOffset Now { get; } public static ReactiveUI.Primitives.Reactive.Concurrency.WasmScheduler Default { get; } public void Dispose() { } public override System.IDisposable Schedule(TState state, System.Func action) { } @@ -2327,6 +2345,8 @@ namespace ReactiveUI.Primitives.Reactive.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(System.Reactive.Concurrency.IScheduler scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net472/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net472/PublicAPI.txt index 47c86d66..153f70da 100644 --- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net472/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net472/PublicAPI.txt @@ -1967,6 +1967,15 @@ namespace ReactiveUI.Primitives.Reactive.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -2020,9 +2029,18 @@ namespace ReactiveUI.Primitives.Reactive.Concurrency public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime) where TAbsolute : System.IComparable { } public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime, System.Collections.Generic.IComparer comparer) where TAbsolute : System.IComparable { } } + public static class SequencerImmediacyExtensions + { + extension(System.Reactive.Concurrency.IScheduler? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("WasmScheduler: ReadyCount = {_readyCount}, DrainState = {_drainState}, Disposed = {_isDisposed}")] public sealed class WasmScheduler : System.Reactive.Concurrency.LocalScheduler, System.IDisposable, System.Reactive.Concurrency.ISchedulerPeriodic { + public WasmScheduler(System.TimeProvider timeProvider) { } + public override System.DateTimeOffset Now { get; } public static ReactiveUI.Primitives.Reactive.Concurrency.WasmScheduler Default { get; } public void Dispose() { } public override System.IDisposable Schedule(TState state, System.Func action) { } @@ -2327,6 +2345,8 @@ namespace ReactiveUI.Primitives.Reactive.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(System.Reactive.Concurrency.IScheduler scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net48/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net48/PublicAPI.txt index 47c86d66..153f70da 100644 --- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net48/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net48/PublicAPI.txt @@ -1967,6 +1967,15 @@ namespace ReactiveUI.Primitives.Reactive.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -2020,9 +2029,18 @@ namespace ReactiveUI.Primitives.Reactive.Concurrency public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime) where TAbsolute : System.IComparable { } public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime, System.Collections.Generic.IComparer comparer) where TAbsolute : System.IComparable { } } + public static class SequencerImmediacyExtensions + { + extension(System.Reactive.Concurrency.IScheduler? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("WasmScheduler: ReadyCount = {_readyCount}, DrainState = {_drainState}, Disposed = {_isDisposed}")] public sealed class WasmScheduler : System.Reactive.Concurrency.LocalScheduler, System.IDisposable, System.Reactive.Concurrency.ISchedulerPeriodic { + public WasmScheduler(System.TimeProvider timeProvider) { } + public override System.DateTimeOffset Now { get; } public static ReactiveUI.Primitives.Reactive.Concurrency.WasmScheduler Default { get; } public void Dispose() { } public override System.IDisposable Schedule(TState state, System.Func action) { } @@ -2327,6 +2345,8 @@ namespace ReactiveUI.Primitives.Reactive.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(System.Reactive.Concurrency.IScheduler scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net481/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net481/PublicAPI.txt index 47c86d66..153f70da 100644 --- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net481/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net481/PublicAPI.txt @@ -1967,6 +1967,15 @@ namespace ReactiveUI.Primitives.Reactive.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -2020,9 +2029,18 @@ namespace ReactiveUI.Primitives.Reactive.Concurrency public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime) where TAbsolute : System.IComparable { } public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime, System.Collections.Generic.IComparer comparer) where TAbsolute : System.IComparable { } } + public static class SequencerImmediacyExtensions + { + extension(System.Reactive.Concurrency.IScheduler? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("WasmScheduler: ReadyCount = {_readyCount}, DrainState = {_drainState}, Disposed = {_isDisposed}")] public sealed class WasmScheduler : System.Reactive.Concurrency.LocalScheduler, System.IDisposable, System.Reactive.Concurrency.ISchedulerPeriodic { + public WasmScheduler(System.TimeProvider timeProvider) { } + public override System.DateTimeOffset Now { get; } public static ReactiveUI.Primitives.Reactive.Concurrency.WasmScheduler Default { get; } public void Dispose() { } public override System.IDisposable Schedule(TState state, System.Func action) { } @@ -2327,6 +2345,8 @@ namespace ReactiveUI.Primitives.Reactive.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(System.Reactive.Concurrency.IScheduler scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net8.0/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net8.0/PublicAPI.txt index 7773dd1e..3b1f7004 100644 --- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net8.0/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net8.0/PublicAPI.txt @@ -1967,6 +1967,15 @@ namespace ReactiveUI.Primitives.Reactive.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -2020,9 +2029,18 @@ namespace ReactiveUI.Primitives.Reactive.Concurrency public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime) where TAbsolute : System.IComparable { } public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime, System.Collections.Generic.IComparer comparer) where TAbsolute : System.IComparable { } } + public static class SequencerImmediacyExtensions + { + extension(System.Reactive.Concurrency.IScheduler? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("WasmScheduler: ReadyCount = {_readyCount}, DrainState = {_drainState}, Disposed = {_isDisposed}")] public sealed class WasmScheduler : System.Reactive.Concurrency.LocalScheduler, System.IDisposable, System.Reactive.Concurrency.ISchedulerPeriodic { + public WasmScheduler(System.TimeProvider timeProvider) { } + public override System.DateTimeOffset Now { get; } public static ReactiveUI.Primitives.Reactive.Concurrency.WasmScheduler Default { get; } public void Dispose() { } public override System.IDisposable Schedule(TState state, System.Func action) { } @@ -2329,6 +2347,8 @@ namespace ReactiveUI.Primitives.Reactive.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(System.Reactive.Concurrency.IScheduler scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net9.0/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net9.0/PublicAPI.txt index 4b7339c3..eb28d219 100644 --- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net9.0/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net9.0/PublicAPI.txt @@ -2001,6 +2001,15 @@ namespace ReactiveUI.Primitives.Reactive.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -2054,9 +2063,18 @@ namespace ReactiveUI.Primitives.Reactive.Concurrency public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime) where TAbsolute : System.IComparable { } public static ReactiveUI.Primitives.Concurrency.ScheduledItem Create(System.Reactive.Concurrency.IScheduler scheduler, TValue state, System.Func action, TAbsolute dueTime, System.Collections.Generic.IComparer comparer) where TAbsolute : System.IComparable { } } + public static class SequencerImmediacyExtensions + { + extension(System.Reactive.Concurrency.IScheduler? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("WasmScheduler: ReadyCount = {_readyCount}, DrainState = {_drainState}, Disposed = {_isDisposed}")] public sealed class WasmScheduler : System.Reactive.Concurrency.LocalScheduler, System.IDisposable, System.Reactive.Concurrency.ISchedulerPeriodic { + public WasmScheduler(System.TimeProvider timeProvider) { } + public override System.DateTimeOffset Now { get; } public static ReactiveUI.Primitives.Reactive.Concurrency.WasmScheduler Default { get; } public void Dispose() { } public override System.IDisposable Schedule(TState state, System.Func action) { } @@ -2363,6 +2381,8 @@ namespace ReactiveUI.Primitives.Reactive.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(System.Reactive.Concurrency.IScheduler scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(System.Reactive.Concurrency.IScheduler scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives/Advanced/DispatchSequencerState.cs b/src/ReactiveUI.Primitives/Advanced/DispatchSequencerState.cs index 7cb29a38..d1a0feca 100644 --- a/src/ReactiveUI.Primitives/Advanced/DispatchSequencerState.cs +++ b/src/ReactiveUI.Primitives/Advanced/DispatchSequencerState.cs @@ -36,6 +36,9 @@ public record struct DispatchSequencerState /// Schedules delayed work when the platform provides no override. private readonly ISequencer _sharedTimer; + /// Clock deciding whether a due timestamp has already passed. + private readonly SequencerClock _clock; + /// Approximate number of ready items; snapshots a drain batch. private int _readyCount; @@ -77,6 +80,24 @@ internal DispatchSequencerState( Action drain, Action? scheduleDelayed, ISequencer sharedTimer) + : this(owner, post, drain, scheduleDelayed, sharedTimer, SequencerClock.Default) + { + } + + /// Initializes a new instance of the struct. + /// The owning sequencer. + /// Posts the cached drain. + /// The cached drain callback. + /// The optional platform delay override. + /// Schedules work without a platform delay override. + /// The clock that decides whether a due timestamp has already passed. + internal DispatchSequencerState( + ISequencer owner, + Func post, + Action drain, + Action? scheduleDelayed, + ISequencer sharedTimer, + SequencerClock clock) { _ready = new(); _owner = owner; @@ -84,6 +105,7 @@ internal DispatchSequencerState( _drain = drain; _scheduleDelayed = scheduleDelayed; _sharedTimer = sharedTimer; + _clock = clock; } /// Gets the sequencer's notion of current time. @@ -130,7 +152,7 @@ public void Schedule(IWorkItem item, long dueTimestamp) { ArgumentExceptionHelper.ThrowIfNull(item); - if (dueTimestamp <= Sequencer.Timestamp) + if (dueTimestamp <= _clock.GetTimestamp()) { Schedule(item); return; diff --git a/src/ReactiveUI.Primitives/Concurrency/CurrentThreadSequencer.cs b/src/ReactiveUI.Primitives/Concurrency/CurrentThreadSequencer.cs index 652b2cd8..d77ecac7 100644 --- a/src/ReactiveUI.Primitives/Concurrency/CurrentThreadSequencer.cs +++ b/src/ReactiveUI.Primitives/Concurrency/CurrentThreadSequencer.cs @@ -28,11 +28,43 @@ public sealed class CurrentThreadSequencer : ISequencer [ThreadStatic] private static SequencerQueue? _threadLocalQueue; - /// Initializes a new instance of the class. + /// The clock of the sequencer whose call started the trampoline running on the current thread. + [ThreadStatic] + private static SequencerClock? _runningClock; + + /// The clock supplying time and blocking waits. + private readonly SequencerClock _clock; + + /// Reads the monotonic clock. + private readonly Func _timestamp; + + /// Blocks the scheduling thread for a delay measured by the clock. + private readonly Action _wait; + + /// Initializes a new instance of the class that reads time and waits through a . + /// The provider supplying the current time, timestamps and timers. + /// is . + /// Work scheduled while another sequencer instance is running on the same thread joins that instance's trampoline and waits on its provider. + public CurrentThreadSequencer(TimeProvider timeProvider) + : this(new SequencerClock(timeProvider)) + { + } + + /// Initializes a new instance of the class; callers use . private CurrentThreadSequencer() + : this(SequencerClock.Default) { } + /// Initializes a new instance of the class. + /// The clock supplying time and blocking waits. + private CurrentThreadSequencer(SequencerClock clock) + { + _clock = clock; + _timestamp = clock.GetTimestamp; + _wait = clock.Wait; + } + /// Gets the singleton instance of the current thread scheduler. public static CurrentThreadSequencer Instance => StaticInstance.Value; @@ -41,10 +73,10 @@ private CurrentThreadSequencer() public static bool IsScheduleRequired => !_running; /// Gets the scheduler's notion of current time. - public DateTimeOffset Now => Sequencer.Now; + public DateTimeOffset Now => _clock.GetUtcNow(); /// Gets the scheduler's monotonic timestamp. - public long Timestamp => Sequencer.Timestamp; + public long Timestamp => _clock.GetTimestamp(); /// Gets the debugger display text. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] @@ -61,14 +93,14 @@ public IDisposable Schedule(Action action) if (!_running) { - SetRunning(true); + StartRunning(); try { action(); var queue = GetQueue(); if (queue is not null) { - Trampoline.Run(queue); + Trampoline.Run(queue, _timestamp, _wait); } } finally @@ -108,11 +140,11 @@ public void Schedule(IWorkItem item, long dueTimestamp) // Initial work executes before Schedule returns. if (!_running) { - SetRunning(true); + StartRunning(); try { - WaitIfNeeded(Sequencer.TimeUntil(dueTimestamp), Wait); + WaitIfNeeded(_clock.TimeUntil(dueTimestamp), _wait); if (!Sequencer.IsCancelled(item)) { @@ -132,7 +164,7 @@ public void Schedule(IWorkItem item, long dueTimestamp) { try { - Trampoline.Run(queue); + Trampoline.Run(queue, _timestamp, _wait); } finally { @@ -157,7 +189,7 @@ public void Schedule(IWorkItem item, long dueTimestamp) SetQueue(queue); } - ScheduledItem si = new(dueTimestamp, Comparer.Default, _ => + ScheduledItem si = new(ToRunningTimeline(dueTimestamp), Comparer.Default, _ => { if (!Sequencer.IsCancelled(item)) { @@ -195,20 +227,27 @@ internal static void WaitIfNeeded(TimeSpan dueTime, Action wait) /// Value indicating whether work is running. private static void SetRunning(bool running) => _running = running; - /// Blocks the scheduling thread until delayed work becomes due. - /// The remaining delay. - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Wait(TimeSpan dueTime) => Thread.Sleep(dueTime); + /// Marks the current thread as running scheduled work on behalf of this sequencer. + private void StartRunning() + { + _running = true; + _runningClock = _clock; + } + + /// Expresses a due timestamp on the clock of the sequencer that started the running trampoline. + /// The absolute due timestamp on this sequencer's clock. + /// The equivalent due timestamp on the running trampoline's clock. + private long ToRunningTimeline(long dueTimestamp) + { + var runningClock = _runningClock!; + return ReferenceEquals(runningClock, _clock) + ? dueTimestamp + : Sequencer.AddTimestamp(runningClock.GetTimestamp(), _clock.TimeUntil(dueTimestamp)); + } /// Runs queued current-thread work. internal static class Trampoline { - /// Runs all work currently in the queue. - /// Queue to drain. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static void Run(SequencerQueue queue) => Run(queue, static () => Sequencer.Timestamp, Wait); - /// Drains work using the supplied clock and wait operation. /// The pending work. /// The monotonic clock. diff --git a/src/ReactiveUI.Primitives/Concurrency/ImmediateSequencer.cs b/src/ReactiveUI.Primitives/Concurrency/ImmediateSequencer.cs index 5c6db1eb..85ac7e99 100644 --- a/src/ReactiveUI.Primitives/Concurrency/ImmediateSequencer.cs +++ b/src/ReactiveUI.Primitives/Concurrency/ImmediateSequencer.cs @@ -14,19 +14,42 @@ public sealed class ImmediateSequencer : ISequencer /// Singleton holder for the immediate sequencer. private static readonly Lazy StaticInstance = new(static () => new()); - /// Initializes a new instance of the class. + /// The clock supplying time and blocking waits. + private readonly SequencerClock _clock; + + /// Blocks the scheduling thread for a delay measured by the clock. + private readonly Action _wait; + + /// Initializes a new instance of the class that reads time and waits through a . + /// The provider supplying the current time, timestamps and timers. + /// is . + public ImmediateSequencer(TimeProvider timeProvider) + : this(new SequencerClock(timeProvider)) + { + } + + /// Initializes a new instance of the class; callers use . private ImmediateSequencer() + : this(SequencerClock.Default) { } + /// Initializes a new instance of the class. + /// The clock supplying time and blocking waits. + private ImmediateSequencer(SequencerClock clock) + { + _clock = clock; + _wait = clock.Wait; + } + /// Gets the singleton instance of the immediate scheduler. public static ImmediateSequencer Instance => StaticInstance.Value; /// Gets the scheduler's notion of current time. - public DateTimeOffset Now => Sequencer.Now; + public DateTimeOffset Now => _clock.GetUtcNow(); /// Gets the scheduler's monotonic timestamp. - public long Timestamp => Sequencer.Timestamp; + public long Timestamp => _clock.GetTimestamp(); /// Gets the debugger display text. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] @@ -68,7 +91,7 @@ public void Schedule(IWorkItem item, long dueTimestamp) { ArgumentExceptionHelper.ThrowIfNull(item); - RunScheduled(item, Sequencer.TimeUntil(dueTimestamp), Wait); + RunScheduled(item, _clock.TimeUntil(dueTimestamp), _wait); } /// Waits for a relative delay and executes work that remains active. @@ -89,10 +112,4 @@ internal static void RunScheduled(IWorkItem item, TimeSpan dueTime, ActionBlocks the scheduling thread for the remaining delay. - /// The remaining delay. - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] - private static void Wait(TimeSpan dueTime) => Thread.Sleep(dueTime); } diff --git a/src/ReactiveUI.Primitives/Concurrency/SequencerClock.cs b/src/ReactiveUI.Primitives/Concurrency/SequencerClock.cs new file mode 100644 index 00000000..f90231c5 --- /dev/null +++ b/src/ReactiveUI.Primitives/Concurrency/SequencerClock.cs @@ -0,0 +1,101 @@ +// 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.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace ReactiveUI.Primitives.Concurrency; + +/// Reads time, blocks and creates timers through a while reporting timestamps at . +internal sealed class SequencerClock +{ + /// Timer used by a blocking wait to release its waiter. + private static readonly TimerCallback ReleaseWaiter = static state => ((ManualResetEventSlim)state!).Set(); + + /// The provider supplying the current time, timestamps and timers. + private readonly TimeProvider _provider; + + /// The provider's timestamp ticks per second. + private readonly long _frequency; + + /// The provider timestamp treated as zero when rescaling; zero when the provider already counts at . + private readonly long _origin; + + /// Whether the provider is , which waits on the calling thread. + private readonly bool _isSystem; + + /// Initializes a new instance of the class. + /// The provider supplying time, timestamps and timers. + /// is . + internal SequencerClock(TimeProvider timeProvider) + { + ArgumentExceptionHelper.ThrowIfNull(timeProvider); + + _provider = timeProvider; + _frequency = timeProvider.TimestampFrequency; + _origin = _frequency == Stopwatch.Frequency ? 0 : timeProvider.GetTimestamp(); + _isSystem = ReferenceEquals(timeProvider, TimeProvider.System); + } + + /// Gets the clock backed by . + internal static SequencerClock Default { get; } = new(TimeProvider.System); + + /// Gets the current UTC time. + /// The provider's current UTC time. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal DateTimeOffset GetUtcNow() => _provider.GetUtcNow(); + + /// Gets the monotonic timestamp in ticks per second. + /// The provider's timestamp rescaled to units. + internal long GetTimestamp() + { + var timestamp = _provider.GetTimestamp(); + return _frequency == Stopwatch.Frequency ? timestamp : Rescale(timestamp - _origin, _frequency); + } + + /// Calculates the remaining time until a monotonic timestamp. + /// Absolute timestamp in units. + /// The nonnegative remaining time. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal TimeSpan TimeUntil(long dueTimestamp) => Sequencer.TimeUntil(dueTimestamp, GetTimestamp()); + + /// Blocks the calling thread for a delay measured by the provider. + /// The delay. + internal void Wait(TimeSpan dueTime) + { + if (_isSystem) + { + SleepOnSystemClock(dueTime); + return; + } + + using ManualResetEventSlim released = new(false); + using var timer = _provider.CreateTimer(ReleaseWaiter, released, dueTime, Timeout.InfiniteTimeSpan); + released.Wait(); + } + + /// Creates a timer through the provider. + /// The callback invoked when the timer fires. + /// The callback state. + /// The delay before the first firing. + /// The interval between later firings. + /// The timer. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period) => + _provider.CreateTimer(callback, state, dueTime, period); + + /// Converts a timestamp delta counted at ticks per second to units. + /// The timestamp delta. + /// Ticks per second of . + /// The delta in units. + private static long Rescale(long delta, long frequency) => + (delta / frequency * Stopwatch.Frequency) + (long)((decimal)(delta % frequency) * Stopwatch.Frequency / frequency); + + /// Blocks the calling thread on the system clock. + /// The delay. + [ExcludeFromCodeCoverage] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SleepOnSystemClock(TimeSpan dueTime) => Thread.Sleep(dueTime); +} diff --git a/src/ReactiveUI.Primitives/Concurrency/SynchronizationContextSequencer.cs b/src/ReactiveUI.Primitives/Concurrency/SynchronizationContextSequencer.cs index 91adaf2a..f7ca5886 100644 --- a/src/ReactiveUI.Primitives/Concurrency/SynchronizationContextSequencer.cs +++ b/src/ReactiveUI.Primitives/Concurrency/SynchronizationContextSequencer.cs @@ -20,6 +20,15 @@ public SynchronizationContextSequencer(SynchronizationContext context) { } + /// Initializes a new instance of the class that reads time and arms its delay timer through a . + /// The synchronization context used to schedule work. + /// The provider supplying the current time, timestamps and the delay timer. + /// or is . + public SynchronizationContextSequencer(SynchronizationContext context, TimeProvider timeProvider) + : this(context, new ThreadPoolSequencer(timeProvider)) + { + } + /// Initializes a new instance of the class. /// The context receiving ready work. /// The scheduler delivering delayed callbacks. diff --git a/src/ReactiveUI.Primitives/Concurrency/TaskPoolSequencer.cs b/src/ReactiveUI.Primitives/Concurrency/TaskPoolSequencer.cs index faf6e216..14ae2aca 100644 --- a/src/ReactiveUI.Primitives/Concurrency/TaskPoolSequencer.cs +++ b/src/ReactiveUI.Primitives/Concurrency/TaskPoolSequencer.cs @@ -25,6 +25,15 @@ public TaskPoolSequencer(TaskFactory taskFactory) { } + /// Initializes a new instance of the class that reads time and arms its delay timer through a . + /// The task factory. + /// The provider supplying the current time, timestamps and the delay timer. + /// or is . + public TaskPoolSequencer(TaskFactory taskFactory, TimeProvider timeProvider) + : this(taskFactory, new ThreadPoolSequencer(timeProvider)) + { + } + /// Initializes a new instance of the class. /// The factory dispatching ready work. /// The scheduler delivering delayed callbacks. diff --git a/src/ReactiveUI.Primitives/Concurrency/ThreadPoolSequencer.cs b/src/ReactiveUI.Primitives/Concurrency/ThreadPoolSequencer.cs index a543a2c3..843647d3 100644 --- a/src/ReactiveUI.Primitives/Concurrency/ThreadPoolSequencer.cs +++ b/src/ReactiveUI.Primitives/Concurrency/ThreadPoolSequencer.cs @@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; -using Timer = System.Threading.Timer; namespace ReactiveUI.Primitives.Concurrency; @@ -25,8 +24,8 @@ public sealed class ThreadPoolSequencer : ISequencer, IDisposable /// Pending delayed work, ordered by monotonic due timestamp. private readonly PriorityQueue _queue = new(); - /// Single timer owned by the sequencer for all delayed work. - private readonly Timer? _timer; + /// The clock supplying time and the delay timer. + private readonly SequencerClock _clock; /// Reads the monotonic clock used by the delay queue. private readonly Func _timestamp; @@ -37,21 +36,39 @@ public sealed class ThreadPoolSequencer : ISequencer, IDisposable /// Updates the delay timer. private readonly Action _changeTimer; + /// Single timer owned by the sequencer for all delayed work, created when the first delayed item arrives. + private ITimer? _timer; + /// Non-zero after disposal; writes hold the gate, and immediate scheduling reads without it. private int _isDisposed; + /// Initializes a new instance of the class that reads time and arms its delay timer through a . + /// The provider supplying the current time, timestamps and the delay timer. + /// is . + public ThreadPoolSequencer(TimeProvider timeProvider) + : this(new SequencerClock(timeProvider), QueueOnThreadPool) + { + } + /// Initializes a new instance of the class; callers use . - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Correctness", - "SST2403:Do not let 'this' escape from a constructor", - Justification = - "The timer is created disarmed, so nothing can call back into it until Schedule arms it after construction.")] internal ThreadPoolSequencer() + : this(SequencerClock.Default, QueueOnThreadPool) + { + } + + /// Initializes a new instance of the class. + /// The provider supplying the current time, timestamps and the delay timer. + /// Queues an immediate callback. + internal ThreadPoolSequencer(TimeProvider timeProvider, Action queueImmediate) + : this(new SequencerClock(timeProvider), queueImmediate) + { + } + + /// Initializes a new instance of the class that shares an existing clock. + /// The clock supplying time and the delay timer. + internal ThreadPoolSequencer(SequencerClock clock) + : this(clock, QueueOnThreadPool) { - _timer = CreateTimer(this); - _timestamp = static () => Sequencer.Timestamp; - _queueImmediate = QueueOnThreadPool; - _changeTimer = ChangeTimer; } /// Initializes a new instance of the class. @@ -63,13 +80,25 @@ internal ThreadPoolSequencer( Action queueImmediate, Action changeTimer) { + _clock = SequencerClock.Default; _timestamp = timestamp; _queueImmediate = queueImmediate; _changeTimer = changeTimer; } + /// Initializes a new instance of the class. + /// The clock supplying time and the delay timer. + /// Queues an immediate callback. + internal ThreadPoolSequencer(SequencerClock clock, Action queueImmediate) + { + _clock = clock; + _timestamp = clock.GetTimestamp; + _queueImmediate = queueImmediate; + _changeTimer = ChangeTimer; + } + /// Gets the scheduler's notion of current time. - public DateTimeOffset Now => Sequencer.Now; + public DateTimeOffset Now => _clock.GetUtcNow(); /// Gets the scheduler's monotonic timestamp. public long Timestamp => _timestamp(); @@ -161,13 +190,6 @@ internal void RunDue() private static void QueueOnThreadPool(WaitCallback callback, object state) => ThreadPool.UnsafeQueueUserWorkItem(callback, state); - /// Creates a disarmed timer that drains due work. - /// The sequencer receiving timer callbacks. - /// The disarmed timer. - [ExcludeFromCodeCoverage] - private static Timer CreateTimer(ThreadPoolSequencer owner) => - new(static state => ((ThreadPoolSequencer)state!).RunDue(), owner, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); - /// Executes the work item unless it has been cancelled. /// Work item to execute. private static void ExecuteQueued(IWorkItem item) @@ -242,11 +264,17 @@ private void ArmTimerNoLock() _changeTimer(Sequencer.TimeUntil(_queue.Peek().DueTimestamp, Timestamp)); } - /// Arms the runtime delay timer. + /// Arms the delay timer, creating it disarmed on first use. /// The remaining delay. - [ExcludeFromCodeCoverage] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ChangeTimer(TimeSpan dueTime) => _timer!.Change(dueTime, Timeout.InfiniteTimeSpan); + private void ChangeTimer(TimeSpan dueTime) + { + _timer ??= _clock.CreateTimer( + static state => ((ThreadPoolSequencer)state!).RunDue(), + this, + Timeout.InfiniteTimeSpan, + Timeout.InfiniteTimeSpan); + _ = _timer.Change(dueTime, Timeout.InfiniteTimeSpan); + } /// Delayed thread-pool work item queued in the sequencer heap. internal readonly struct TimedWorkItem : IComparable, IEquatable diff --git a/src/ReactiveUI.Primitives/Concurrency/VirtualClock.cs b/src/ReactiveUI.Primitives/Concurrency/VirtualClock.cs index 0addf589..080e7a53 100644 --- a/src/ReactiveUI.Primitives/Concurrency/VirtualClock.cs +++ b/src/ReactiveUI.Primitives/Concurrency/VirtualClock.cs @@ -2,13 +2,19 @@ // 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.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace ReactiveUI.Primitives.Concurrency; -/// Deterministic virtual scheduler backed by and . +/// Deterministic virtual scheduler backed by and that also serves as a . +/// +/// As a the clock reports as UTC time and counts timestamps in ticks of . +/// Its timers fire when , or reaches them. +/// [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] -public sealed class VirtualClock : ISequencer, IServiceProvider, IStopwatchProvider +public sealed class VirtualClock : TimeProvider, ISequencer, IServiceProvider, IStopwatchProvider { /// Adds a normalized relative time to an absolute time. private static readonly Func Adder = static (absolute, relative) => @@ -46,9 +52,12 @@ public VirtualClock(DateTimeOffset initialClock) => /// public long Timestamp => _state.Timestamp; + /// Gets the number of timestamp ticks per second, which is . + public override long TimestampFrequency => TimeSpan.TicksPerSecond; + /// Gets the debugger display text. - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)] + [ExcludeFromCodeCoverage] + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private string DebuggerDisplay => ToString() ?? string.Empty; /// Advances the scheduler's clock by the specified relative time, running all work scheduled for that timespan. @@ -108,7 +117,7 @@ public IDisposable Schedule(TState state, TimeSpan dueTime, FuncAction to be executed. /// The disposable object used to cancel the scheduled action (best effort). [MethodImpl(MethodImplOptions.AggressiveInlining)] - [System.Diagnostics.CodeAnalysis.SuppressMessage( + [SuppressMessage( "Design", "SST2318:Members should not have identical bodies", Justification = @@ -147,6 +156,25 @@ public IDisposable ScheduleRelative( [MethodImpl(MethodImplOptions.AggressiveInlining)] public IStopwatch StartStopwatch() => new VirtualTimeStopwatch(() => Now, Now); + /// Gets the virtual time as a UTC time. + /// . + public override DateTimeOffset GetUtcNow() => Now; + + /// Gets the virtual time as a timestamp counted at . + /// The ticks of . + public override long GetTimestamp() => Timestamp; + + /// Creates a timer whose firings run when the virtual clock advances to them. + /// The callback invoked at each firing. + /// The state passed to . + /// The virtual delay before the first firing, or to leave the timer stopped. + /// The virtual interval between later firings, or or for a single firing. + /// The timer. + /// is . + /// or is negative and not . + public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period) => + VirtualClockTimer.Start(this, callback, state, dueTime, period); + /// object? IServiceProvider.GetService(Type serviceType) => serviceType == typeof(IStopwatchProvider) ? this : null; diff --git a/src/ReactiveUI.Primitives/Concurrency/VirtualClockTimer.cs b/src/ReactiveUI.Primitives/Concurrency/VirtualClockTimer.cs new file mode 100644 index 00000000..2d748474 --- /dev/null +++ b/src/ReactiveUI.Primitives/Concurrency/VirtualClockTimer.cs @@ -0,0 +1,136 @@ +// 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.Runtime.CompilerServices; +using ReactiveUI.Primitives.Disposables; + +namespace ReactiveUI.Primitives.Concurrency; + +/// A timer whose firings are queued on a and run when the clock advances. +internal sealed class VirtualClockTimer : ITimer +{ + /// Serializes changes to the pending firing. + private readonly Lock _gate = new(); + + /// The clock whose virtual time schedules the firings. + private readonly VirtualClock _clock; + + /// The callback invoked at each firing. + private readonly TimerCallback _callback; + + /// The state passed to the callback. + private readonly object? _state; + + /// The queued firing that has not run yet, or when none is queued. + private IDisposable? _pending; + + /// The interval between firings after the first, or for a one-shot timer. + private TimeSpan _period; + + /// Whether the timer has been disposed. + private bool _isDisposed; + + /// Initializes a new instance of the class. + /// The clock whose virtual time schedules the firings. + /// The callback invoked at each firing. + /// The state passed to the callback. + private VirtualClockTimer(VirtualClock clock, TimerCallback callback, object? state) + { + _clock = clock; + _callback = callback; + _state = state; + } + + /// + public bool Change(TimeSpan dueTime, TimeSpan period) + { + ArgumentOutOfRangeExceptionHelper.ThrowIfLessThan(dueTime, Timeout.InfiniteTimeSpan); + ArgumentOutOfRangeExceptionHelper.ThrowIfLessThan(period, Timeout.InfiniteTimeSpan); + + IDisposable? previous; + lock (_gate) + { + if (_isDisposed) + { + return false; + } + + previous = _pending; + _period = period > TimeSpan.Zero ? period : TimeSpan.Zero; + _pending = dueTime == Timeout.InfiniteTimeSpan ? null : Queue(dueTime); + } + + previous?.Dispose(); + return true; + } + + /// + public void Dispose() + { + IDisposable? previous; + lock (_gate) + { + _isDisposed = true; + previous = _pending; + _pending = null; + } + + previous?.Dispose(); + } + + /// + public ValueTask DisposeAsync() + { + Dispose(); + return default; + } + + /// Creates a timer and queues its first firing. + /// The clock whose virtual time schedules the firings. + /// The callback invoked at each firing. + /// The state passed to the callback. + /// The delay before the first firing, or to leave the timer stopped. + /// The interval between later firings, or or for a single firing. + /// The timer. + /// is . + /// or is negative and not . + internal static VirtualClockTimer Start( + VirtualClock clock, + TimerCallback callback, + object? state, + TimeSpan dueTime, + TimeSpan period) + { + ArgumentExceptionHelper.ThrowIfNull(callback); + + VirtualClockTimer timer = new(clock, callback, state); + _ = timer.Change(dueTime, period); + return timer; + } + + /// Queues the next periodic firing, then invokes the callback unless the timer is disposed. + /// An empty disposable; the queued firing needs no further cancellation. + internal EmptyDisposable Fire() + { + lock (_gate) + { + if (_isDisposed) + { + return EmptyDisposable.Instance; + } + + _pending = _period > TimeSpan.Zero ? Queue(_period) : null; + } + + _callback(_state); + return EmptyDisposable.Instance; + } + + /// Queues a firing on the virtual clock. + /// The virtual delay before the firing. + /// The handle that cancels the queued firing. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private IDisposable Queue(TimeSpan dueTime) => + _clock.Schedule(this, dueTime, static (_, timer) => timer.Fire()); +} diff --git a/src/ReactiveUI.Primitives/Concurrency/WasmSequencer.cs b/src/ReactiveUI.Primitives/Concurrency/WasmSequencer.cs index ad675efd..53bde083 100644 --- a/src/ReactiveUI.Primitives/Concurrency/WasmSequencer.cs +++ b/src/ReactiveUI.Primitives/Concurrency/WasmSequencer.cs @@ -4,7 +4,6 @@ using System.Runtime.CompilerServices; using ReactiveUI.Primitives.Advanced; -using Timer = System.Threading.Timer; namespace ReactiveUI.Primitives.Concurrency; @@ -17,8 +16,8 @@ public sealed class WasmSequencer : ISequencer, IDisposable /// Serializes timer arming and disposal. private readonly Lock _gate = new(); - /// One-shot timer used to yield a drain to the event loop. - private readonly Timer? _timer; + /// The clock supplying time and the drain timer. + private readonly SequencerClock _clock; /// Posts a drain to the event loop. private readonly Func _postDrain; @@ -26,24 +25,27 @@ public sealed class WasmSequencer : ISequencer, IDisposable /// Schedules the delayed marshal callback. private readonly Action _scheduleDelayed; + /// One-shot timer used to yield a drain to the event loop, created when the first drain is posted. + private ITimer? _timer; + /// Coalescing dispatch engine. private DispatchSequencerState _state; /// Non-zero after disposal releases the timer and queue; timer access is serialized by the gate. private int _isDisposed; + /// Initializes a new instance of the class that reads time and arms its timers through a . + /// The provider supplying the current time, timestamps and timers. + /// is . + public WasmSequencer(TimeProvider timeProvider) + : this(new SequencerClock(timeProvider)) + { + } + /// Initializes a new instance of the class. - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Correctness", - "SST2403:Do not let 'this' escape from a constructor", - Justification = - "The timer is created disarmed, and _state is a struct held inline in this object, so neither reference escapes.")] internal WasmSequencer() + : this(SequencerClock.Default, ThreadPoolSequencer.Instance) { - _timer = CreateTimer(this); - _postDrain = ArmDrainTimer; - _scheduleDelayed = ThreadPoolSequencer.Instance.Schedule; - _state = new(this, Post, RunDrain, ScheduleDelayed); } /// Initializes a new instance of the class. @@ -51,19 +53,38 @@ internal WasmSequencer() /// Schedules the delayed marshal callback. internal WasmSequencer(Func postDrain, Action scheduleDelayed) { + _clock = SequencerClock.Default; _postDrain = postDrain; _scheduleDelayed = scheduleDelayed; _state = new(this, Post, RunDrain, ScheduleDelayed); } + /// Initializes a new instance of the class whose delay queue shares its clock. + /// The clock supplying time and timers. + private WasmSequencer(SequencerClock clock) + : this(clock, new ThreadPoolSequencer(clock)) + { + } + + /// Initializes a new instance of the class. + /// The clock supplying time and the drain timer. + /// The sequencer whose timestamps use and that delivers delayed callbacks. + private WasmSequencer(SequencerClock clock, ISequencer delaySequencer) + { + _clock = clock; + _postDrain = ArmDrainTimer; + _scheduleDelayed = delaySequencer.Schedule; + _state = new(this, Post, RunDrain, ScheduleDelayed, delaySequencer, clock); + } + /// Gets the shared WebAssembly sequencer. public static WasmSequencer Default { get; } = new(); /// - public DateTimeOffset Now => DispatchSequencerState.Now; + public DateTimeOffset Now => _clock.GetUtcNow(); /// - public long Timestamp => DispatchSequencerState.Timestamp; + public long Timestamp => _clock.GetTimestamp(); /// Gets the debugger display text. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] @@ -118,13 +139,6 @@ internal void ScheduleReady(IWorkItem item) ReleaseQueuedIfDisposed(); } - /// Creates a disarmed timer that drains ready work. - /// The sequencer receiving timer callbacks. - /// The disarmed timer. - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - private static Timer CreateTimer(WasmSequencer owner) => - new(static state => ((WasmSequencer)state!).RunDrain(), owner, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); - /// Arms the drain timer to fire on the next event-loop turn. /// The callback to post. /// when the timer accepted the change. @@ -143,12 +157,18 @@ private bool Post(Action drain) private void ScheduleDelayed(IWorkItem item, long dueTimestamp) => _scheduleDelayed(new DelayedWorkItem(this, item), dueTimestamp); - /// Arms the runtime drain timer. + /// Arms the drain timer, creating it disarmed on first use. /// The cached callback carried by the timer. /// Whether the timer accepted the callback. - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool ArmDrainTimer(Action drain) => _timer!.Change(TimeSpan.Zero, Timeout.InfiniteTimeSpan); + private bool ArmDrainTimer(Action drain) + { + _timer ??= _clock.CreateTimer( + static state => ((WasmSequencer)state!).RunDrain(), + this, + Timeout.InfiniteTimeSpan, + Timeout.InfiniteTimeSpan); + return _timer.Change(TimeSpan.Zero, Timeout.InfiniteTimeSpan); + } /// Forwards the cached drain callback to the engine. [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/ReactiveUI.Primitives/PublicAPI/net10.0-android/PublicAPI.txt b/src/ReactiveUI.Primitives/PublicAPI/net10.0-android/PublicAPI.txt index 4d056576..fbc41d3c 100644 --- a/src/ReactiveUI.Primitives/PublicAPI/net10.0-android/PublicAPI.txt +++ b/src/ReactiveUI.Primitives/PublicAPI/net10.0-android/PublicAPI.txt @@ -1686,6 +1686,15 @@ namespace ReactiveUI.Primitives.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -1707,6 +1716,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class CurrentThreadSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public CurrentThreadSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.CurrentThreadSequencer Instance { get; } @@ -1740,6 +1750,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ImmediateSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public ImmediateSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.ImmediateSequencer Instance { get; } @@ -1777,10 +1788,18 @@ namespace ReactiveUI.Primitives.Concurrency public System.IDisposable ScheduleAction(TState state, System.Action action) { } } } + public static class SequencerImmediacyExtensions + { + extension(ReactiveUI.Primitives.Concurrency.ISequencer? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class SynchronizationContextSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public SynchronizationContextSequencer(System.Threading.SynchronizationContext context) { } + public SynchronizationContextSequencer(System.Threading.SynchronizationContext context, System.TimeProvider timeProvider) { } public System.Threading.SynchronizationContext Context { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1792,6 +1811,7 @@ namespace ReactiveUI.Primitives.Concurrency public sealed class TaskPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory) { } + public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory, System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public System.Action? UnhandledExceptionHandler { get; set; } @@ -1803,6 +1823,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ThreadPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public ThreadPoolSequencer(System.TimeProvider timeProvider) { } public static readonly ReactiveUI.Primitives.Concurrency.ThreadPoolSequencer Instance; public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1811,7 +1832,7 @@ namespace ReactiveUI.Primitives.Concurrency public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item, long dueTimestamp) { } } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] - public sealed class VirtualClock : ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider + public sealed class VirtualClock : System.TimeProvider, ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider { public VirtualClock() { } public VirtualClock(System.DateTimeOffset initialClock) { } @@ -1819,10 +1840,14 @@ namespace ReactiveUI.Primitives.Concurrency public bool IsEnabled { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } + public override long TimestampFrequency { get; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceBy(System.TimeSpan time) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceTo(System.DateTimeOffset time) { } + public override System.Threading.ITimer CreateTimer(System.Threading.TimerCallback callback, object? state, System.TimeSpan dueTime, System.TimeSpan period) { } + public override long GetTimestamp() { } + public override System.DateTimeOffset GetUtcNow() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] @@ -1899,6 +1924,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class WasmSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public WasmSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.WasmSequencer Default { get; } @@ -2519,6 +2545,8 @@ namespace ReactiveUI.Primitives.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives/PublicAPI/net10.0-ios/PublicAPI.txt b/src/ReactiveUI.Primitives/PublicAPI/net10.0-ios/PublicAPI.txt index 8dcd9fb8..82235149 100644 --- a/src/ReactiveUI.Primitives/PublicAPI/net10.0-ios/PublicAPI.txt +++ b/src/ReactiveUI.Primitives/PublicAPI/net10.0-ios/PublicAPI.txt @@ -1686,6 +1686,15 @@ namespace ReactiveUI.Primitives.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -1707,6 +1716,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class CurrentThreadSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public CurrentThreadSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.CurrentThreadSequencer Instance { get; } @@ -1726,6 +1736,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ImmediateSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public ImmediateSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.ImmediateSequencer Instance { get; } @@ -1775,10 +1786,18 @@ namespace ReactiveUI.Primitives.Concurrency public System.IDisposable ScheduleAction(TState state, System.Action action) { } } } + public static class SequencerImmediacyExtensions + { + extension(ReactiveUI.Primitives.Concurrency.ISequencer? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class SynchronizationContextSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public SynchronizationContextSequencer(System.Threading.SynchronizationContext context) { } + public SynchronizationContextSequencer(System.Threading.SynchronizationContext context, System.TimeProvider timeProvider) { } public System.Threading.SynchronizationContext Context { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1790,6 +1809,7 @@ namespace ReactiveUI.Primitives.Concurrency public sealed class TaskPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory) { } + public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory, System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public System.Action? UnhandledExceptionHandler { get; set; } @@ -1801,6 +1821,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ThreadPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public ThreadPoolSequencer(System.TimeProvider timeProvider) { } public static readonly ReactiveUI.Primitives.Concurrency.ThreadPoolSequencer Instance; public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1809,7 +1830,7 @@ namespace ReactiveUI.Primitives.Concurrency public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item, long dueTimestamp) { } } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] - public sealed class VirtualClock : ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider + public sealed class VirtualClock : System.TimeProvider, ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider { public VirtualClock() { } public VirtualClock(System.DateTimeOffset initialClock) { } @@ -1817,10 +1838,14 @@ namespace ReactiveUI.Primitives.Concurrency public bool IsEnabled { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } + public override long TimestampFrequency { get; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceBy(System.TimeSpan time) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceTo(System.DateTimeOffset time) { } + public override System.Threading.ITimer CreateTimer(System.Threading.TimerCallback callback, object? state, System.TimeSpan dueTime, System.TimeSpan period) { } + public override long GetTimestamp() { } + public override System.DateTimeOffset GetUtcNow() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] @@ -1897,6 +1922,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class WasmSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public WasmSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.WasmSequencer Default { get; } @@ -2517,6 +2543,8 @@ namespace ReactiveUI.Primitives.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives/PublicAPI/net10.0-maccatalyst/PublicAPI.txt b/src/ReactiveUI.Primitives/PublicAPI/net10.0-maccatalyst/PublicAPI.txt index 807ac2a0..99149615 100644 --- a/src/ReactiveUI.Primitives/PublicAPI/net10.0-maccatalyst/PublicAPI.txt +++ b/src/ReactiveUI.Primitives/PublicAPI/net10.0-maccatalyst/PublicAPI.txt @@ -1686,6 +1686,15 @@ namespace ReactiveUI.Primitives.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -1707,6 +1716,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class CurrentThreadSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public CurrentThreadSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.CurrentThreadSequencer Instance { get; } @@ -1726,6 +1736,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ImmediateSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public ImmediateSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.ImmediateSequencer Instance { get; } @@ -1775,10 +1786,18 @@ namespace ReactiveUI.Primitives.Concurrency public System.IDisposable ScheduleAction(TState state, System.Action action) { } } } + public static class SequencerImmediacyExtensions + { + extension(ReactiveUI.Primitives.Concurrency.ISequencer? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class SynchronizationContextSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public SynchronizationContextSequencer(System.Threading.SynchronizationContext context) { } + public SynchronizationContextSequencer(System.Threading.SynchronizationContext context, System.TimeProvider timeProvider) { } public System.Threading.SynchronizationContext Context { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1790,6 +1809,7 @@ namespace ReactiveUI.Primitives.Concurrency public sealed class TaskPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory) { } + public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory, System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public System.Action? UnhandledExceptionHandler { get; set; } @@ -1801,6 +1821,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ThreadPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public ThreadPoolSequencer(System.TimeProvider timeProvider) { } public static readonly ReactiveUI.Primitives.Concurrency.ThreadPoolSequencer Instance; public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1809,7 +1830,7 @@ namespace ReactiveUI.Primitives.Concurrency public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item, long dueTimestamp) { } } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] - public sealed class VirtualClock : ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider + public sealed class VirtualClock : System.TimeProvider, ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider { public VirtualClock() { } public VirtualClock(System.DateTimeOffset initialClock) { } @@ -1817,10 +1838,14 @@ namespace ReactiveUI.Primitives.Concurrency public bool IsEnabled { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } + public override long TimestampFrequency { get; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceBy(System.TimeSpan time) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceTo(System.DateTimeOffset time) { } + public override System.Threading.ITimer CreateTimer(System.Threading.TimerCallback callback, object? state, System.TimeSpan dueTime, System.TimeSpan period) { } + public override long GetTimestamp() { } + public override System.DateTimeOffset GetUtcNow() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] @@ -1897,6 +1922,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class WasmSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public WasmSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.WasmSequencer Default { get; } @@ -2517,6 +2543,8 @@ namespace ReactiveUI.Primitives.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives/PublicAPI/net10.0-macos/PublicAPI.txt b/src/ReactiveUI.Primitives/PublicAPI/net10.0-macos/PublicAPI.txt index 1bcf9c47..f69580a3 100644 --- a/src/ReactiveUI.Primitives/PublicAPI/net10.0-macos/PublicAPI.txt +++ b/src/ReactiveUI.Primitives/PublicAPI/net10.0-macos/PublicAPI.txt @@ -1686,6 +1686,15 @@ namespace ReactiveUI.Primitives.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -1707,6 +1716,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class CurrentThreadSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public CurrentThreadSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.CurrentThreadSequencer Instance { get; } @@ -1726,6 +1736,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ImmediateSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public ImmediateSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.ImmediateSequencer Instance { get; } @@ -1775,10 +1786,18 @@ namespace ReactiveUI.Primitives.Concurrency public System.IDisposable ScheduleAction(TState state, System.Action action) { } } } + public static class SequencerImmediacyExtensions + { + extension(ReactiveUI.Primitives.Concurrency.ISequencer? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class SynchronizationContextSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public SynchronizationContextSequencer(System.Threading.SynchronizationContext context) { } + public SynchronizationContextSequencer(System.Threading.SynchronizationContext context, System.TimeProvider timeProvider) { } public System.Threading.SynchronizationContext Context { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1790,6 +1809,7 @@ namespace ReactiveUI.Primitives.Concurrency public sealed class TaskPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory) { } + public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory, System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public System.Action? UnhandledExceptionHandler { get; set; } @@ -1801,6 +1821,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ThreadPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public ThreadPoolSequencer(System.TimeProvider timeProvider) { } public static readonly ReactiveUI.Primitives.Concurrency.ThreadPoolSequencer Instance; public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1809,7 +1830,7 @@ namespace ReactiveUI.Primitives.Concurrency public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item, long dueTimestamp) { } } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] - public sealed class VirtualClock : ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider + public sealed class VirtualClock : System.TimeProvider, ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider { public VirtualClock() { } public VirtualClock(System.DateTimeOffset initialClock) { } @@ -1817,10 +1838,14 @@ namespace ReactiveUI.Primitives.Concurrency public bool IsEnabled { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } + public override long TimestampFrequency { get; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceBy(System.TimeSpan time) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceTo(System.DateTimeOffset time) { } + public override System.Threading.ITimer CreateTimer(System.Threading.TimerCallback callback, object? state, System.TimeSpan dueTime, System.TimeSpan period) { } + public override long GetTimestamp() { } + public override System.DateTimeOffset GetUtcNow() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] @@ -1897,6 +1922,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class WasmSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public WasmSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.WasmSequencer Default { get; } @@ -2517,6 +2543,8 @@ namespace ReactiveUI.Primitives.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives/PublicAPI/net10.0-tvos/PublicAPI.txt b/src/ReactiveUI.Primitives/PublicAPI/net10.0-tvos/PublicAPI.txt index cb50ee36..61d2d1da 100644 --- a/src/ReactiveUI.Primitives/PublicAPI/net10.0-tvos/PublicAPI.txt +++ b/src/ReactiveUI.Primitives/PublicAPI/net10.0-tvos/PublicAPI.txt @@ -1686,6 +1686,15 @@ namespace ReactiveUI.Primitives.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -1707,6 +1716,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class CurrentThreadSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public CurrentThreadSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.CurrentThreadSequencer Instance { get; } @@ -1726,6 +1736,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ImmediateSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public ImmediateSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.ImmediateSequencer Instance { get; } @@ -1775,10 +1786,18 @@ namespace ReactiveUI.Primitives.Concurrency public System.IDisposable ScheduleAction(TState state, System.Action action) { } } } + public static class SequencerImmediacyExtensions + { + extension(ReactiveUI.Primitives.Concurrency.ISequencer? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class SynchronizationContextSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public SynchronizationContextSequencer(System.Threading.SynchronizationContext context) { } + public SynchronizationContextSequencer(System.Threading.SynchronizationContext context, System.TimeProvider timeProvider) { } public System.Threading.SynchronizationContext Context { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1790,6 +1809,7 @@ namespace ReactiveUI.Primitives.Concurrency public sealed class TaskPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory) { } + public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory, System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public System.Action? UnhandledExceptionHandler { get; set; } @@ -1801,6 +1821,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ThreadPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public ThreadPoolSequencer(System.TimeProvider timeProvider) { } public static readonly ReactiveUI.Primitives.Concurrency.ThreadPoolSequencer Instance; public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1809,7 +1830,7 @@ namespace ReactiveUI.Primitives.Concurrency public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item, long dueTimestamp) { } } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] - public sealed class VirtualClock : ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider + public sealed class VirtualClock : System.TimeProvider, ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider { public VirtualClock() { } public VirtualClock(System.DateTimeOffset initialClock) { } @@ -1817,10 +1838,14 @@ namespace ReactiveUI.Primitives.Concurrency public bool IsEnabled { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } + public override long TimestampFrequency { get; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceBy(System.TimeSpan time) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceTo(System.DateTimeOffset time) { } + public override System.Threading.ITimer CreateTimer(System.Threading.TimerCallback callback, object? state, System.TimeSpan dueTime, System.TimeSpan period) { } + public override long GetTimestamp() { } + public override System.DateTimeOffset GetUtcNow() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] @@ -1897,6 +1922,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class WasmSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public WasmSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.WasmSequencer Default { get; } @@ -2517,6 +2543,8 @@ namespace ReactiveUI.Primitives.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives/PublicAPI/net10.0/PublicAPI.txt b/src/ReactiveUI.Primitives/PublicAPI/net10.0/PublicAPI.txt index 4912d27c..c99a42df 100644 --- a/src/ReactiveUI.Primitives/PublicAPI/net10.0/PublicAPI.txt +++ b/src/ReactiveUI.Primitives/PublicAPI/net10.0/PublicAPI.txt @@ -1685,6 +1685,15 @@ namespace ReactiveUI.Primitives.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -1706,6 +1715,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class CurrentThreadSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public CurrentThreadSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.CurrentThreadSequencer Instance { get; } @@ -1725,6 +1735,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ImmediateSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public ImmediateSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.ImmediateSequencer Instance { get; } @@ -1762,10 +1773,18 @@ namespace ReactiveUI.Primitives.Concurrency public System.IDisposable ScheduleAction(TState state, System.Action action) { } } } + public static class SequencerImmediacyExtensions + { + extension(ReactiveUI.Primitives.Concurrency.ISequencer? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class SynchronizationContextSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public SynchronizationContextSequencer(System.Threading.SynchronizationContext context) { } + public SynchronizationContextSequencer(System.Threading.SynchronizationContext context, System.TimeProvider timeProvider) { } public System.Threading.SynchronizationContext Context { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1777,6 +1796,7 @@ namespace ReactiveUI.Primitives.Concurrency public sealed class TaskPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory) { } + public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory, System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public System.Action? UnhandledExceptionHandler { get; set; } @@ -1788,6 +1808,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ThreadPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public ThreadPoolSequencer(System.TimeProvider timeProvider) { } public static readonly ReactiveUI.Primitives.Concurrency.ThreadPoolSequencer Instance; public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1796,7 +1817,7 @@ namespace ReactiveUI.Primitives.Concurrency public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item, long dueTimestamp) { } } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] - public sealed class VirtualClock : ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider + public sealed class VirtualClock : System.TimeProvider, ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider { public VirtualClock() { } public VirtualClock(System.DateTimeOffset initialClock) { } @@ -1804,10 +1825,14 @@ namespace ReactiveUI.Primitives.Concurrency public bool IsEnabled { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } + public override long TimestampFrequency { get; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceBy(System.TimeSpan time) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceTo(System.DateTimeOffset time) { } + public override System.Threading.ITimer CreateTimer(System.Threading.TimerCallback callback, object? state, System.TimeSpan dueTime, System.TimeSpan period) { } + public override long GetTimestamp() { } + public override System.DateTimeOffset GetUtcNow() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] @@ -1884,6 +1909,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class WasmSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public WasmSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.WasmSequencer Default { get; } @@ -2504,6 +2530,8 @@ namespace ReactiveUI.Primitives.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives/PublicAPI/net11.0-android/PublicAPI.txt b/src/ReactiveUI.Primitives/PublicAPI/net11.0-android/PublicAPI.txt index 4d056576..fbc41d3c 100644 --- a/src/ReactiveUI.Primitives/PublicAPI/net11.0-android/PublicAPI.txt +++ b/src/ReactiveUI.Primitives/PublicAPI/net11.0-android/PublicAPI.txt @@ -1686,6 +1686,15 @@ namespace ReactiveUI.Primitives.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -1707,6 +1716,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class CurrentThreadSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public CurrentThreadSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.CurrentThreadSequencer Instance { get; } @@ -1740,6 +1750,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ImmediateSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public ImmediateSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.ImmediateSequencer Instance { get; } @@ -1777,10 +1788,18 @@ namespace ReactiveUI.Primitives.Concurrency public System.IDisposable ScheduleAction(TState state, System.Action action) { } } } + public static class SequencerImmediacyExtensions + { + extension(ReactiveUI.Primitives.Concurrency.ISequencer? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class SynchronizationContextSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public SynchronizationContextSequencer(System.Threading.SynchronizationContext context) { } + public SynchronizationContextSequencer(System.Threading.SynchronizationContext context, System.TimeProvider timeProvider) { } public System.Threading.SynchronizationContext Context { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1792,6 +1811,7 @@ namespace ReactiveUI.Primitives.Concurrency public sealed class TaskPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory) { } + public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory, System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public System.Action? UnhandledExceptionHandler { get; set; } @@ -1803,6 +1823,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ThreadPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public ThreadPoolSequencer(System.TimeProvider timeProvider) { } public static readonly ReactiveUI.Primitives.Concurrency.ThreadPoolSequencer Instance; public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1811,7 +1832,7 @@ namespace ReactiveUI.Primitives.Concurrency public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item, long dueTimestamp) { } } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] - public sealed class VirtualClock : ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider + public sealed class VirtualClock : System.TimeProvider, ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider { public VirtualClock() { } public VirtualClock(System.DateTimeOffset initialClock) { } @@ -1819,10 +1840,14 @@ namespace ReactiveUI.Primitives.Concurrency public bool IsEnabled { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } + public override long TimestampFrequency { get; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceBy(System.TimeSpan time) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceTo(System.DateTimeOffset time) { } + public override System.Threading.ITimer CreateTimer(System.Threading.TimerCallback callback, object? state, System.TimeSpan dueTime, System.TimeSpan period) { } + public override long GetTimestamp() { } + public override System.DateTimeOffset GetUtcNow() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] @@ -1899,6 +1924,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class WasmSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public WasmSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.WasmSequencer Default { get; } @@ -2519,6 +2545,8 @@ namespace ReactiveUI.Primitives.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives/PublicAPI/net11.0-ios/PublicAPI.txt b/src/ReactiveUI.Primitives/PublicAPI/net11.0-ios/PublicAPI.txt index 8dcd9fb8..82235149 100644 --- a/src/ReactiveUI.Primitives/PublicAPI/net11.0-ios/PublicAPI.txt +++ b/src/ReactiveUI.Primitives/PublicAPI/net11.0-ios/PublicAPI.txt @@ -1686,6 +1686,15 @@ namespace ReactiveUI.Primitives.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -1707,6 +1716,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class CurrentThreadSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public CurrentThreadSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.CurrentThreadSequencer Instance { get; } @@ -1726,6 +1736,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ImmediateSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public ImmediateSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.ImmediateSequencer Instance { get; } @@ -1775,10 +1786,18 @@ namespace ReactiveUI.Primitives.Concurrency public System.IDisposable ScheduleAction(TState state, System.Action action) { } } } + public static class SequencerImmediacyExtensions + { + extension(ReactiveUI.Primitives.Concurrency.ISequencer? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class SynchronizationContextSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public SynchronizationContextSequencer(System.Threading.SynchronizationContext context) { } + public SynchronizationContextSequencer(System.Threading.SynchronizationContext context, System.TimeProvider timeProvider) { } public System.Threading.SynchronizationContext Context { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1790,6 +1809,7 @@ namespace ReactiveUI.Primitives.Concurrency public sealed class TaskPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory) { } + public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory, System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public System.Action? UnhandledExceptionHandler { get; set; } @@ -1801,6 +1821,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ThreadPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public ThreadPoolSequencer(System.TimeProvider timeProvider) { } public static readonly ReactiveUI.Primitives.Concurrency.ThreadPoolSequencer Instance; public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1809,7 +1830,7 @@ namespace ReactiveUI.Primitives.Concurrency public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item, long dueTimestamp) { } } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] - public sealed class VirtualClock : ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider + public sealed class VirtualClock : System.TimeProvider, ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider { public VirtualClock() { } public VirtualClock(System.DateTimeOffset initialClock) { } @@ -1817,10 +1838,14 @@ namespace ReactiveUI.Primitives.Concurrency public bool IsEnabled { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } + public override long TimestampFrequency { get; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceBy(System.TimeSpan time) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceTo(System.DateTimeOffset time) { } + public override System.Threading.ITimer CreateTimer(System.Threading.TimerCallback callback, object? state, System.TimeSpan dueTime, System.TimeSpan period) { } + public override long GetTimestamp() { } + public override System.DateTimeOffset GetUtcNow() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] @@ -1897,6 +1922,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class WasmSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public WasmSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.WasmSequencer Default { get; } @@ -2517,6 +2543,8 @@ namespace ReactiveUI.Primitives.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives/PublicAPI/net11.0-maccatalyst/PublicAPI.txt b/src/ReactiveUI.Primitives/PublicAPI/net11.0-maccatalyst/PublicAPI.txt index 807ac2a0..99149615 100644 --- a/src/ReactiveUI.Primitives/PublicAPI/net11.0-maccatalyst/PublicAPI.txt +++ b/src/ReactiveUI.Primitives/PublicAPI/net11.0-maccatalyst/PublicAPI.txt @@ -1686,6 +1686,15 @@ namespace ReactiveUI.Primitives.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -1707,6 +1716,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class CurrentThreadSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public CurrentThreadSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.CurrentThreadSequencer Instance { get; } @@ -1726,6 +1736,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ImmediateSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public ImmediateSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.ImmediateSequencer Instance { get; } @@ -1775,10 +1786,18 @@ namespace ReactiveUI.Primitives.Concurrency public System.IDisposable ScheduleAction(TState state, System.Action action) { } } } + public static class SequencerImmediacyExtensions + { + extension(ReactiveUI.Primitives.Concurrency.ISequencer? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class SynchronizationContextSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public SynchronizationContextSequencer(System.Threading.SynchronizationContext context) { } + public SynchronizationContextSequencer(System.Threading.SynchronizationContext context, System.TimeProvider timeProvider) { } public System.Threading.SynchronizationContext Context { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1790,6 +1809,7 @@ namespace ReactiveUI.Primitives.Concurrency public sealed class TaskPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory) { } + public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory, System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public System.Action? UnhandledExceptionHandler { get; set; } @@ -1801,6 +1821,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ThreadPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public ThreadPoolSequencer(System.TimeProvider timeProvider) { } public static readonly ReactiveUI.Primitives.Concurrency.ThreadPoolSequencer Instance; public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1809,7 +1830,7 @@ namespace ReactiveUI.Primitives.Concurrency public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item, long dueTimestamp) { } } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] - public sealed class VirtualClock : ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider + public sealed class VirtualClock : System.TimeProvider, ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider { public VirtualClock() { } public VirtualClock(System.DateTimeOffset initialClock) { } @@ -1817,10 +1838,14 @@ namespace ReactiveUI.Primitives.Concurrency public bool IsEnabled { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } + public override long TimestampFrequency { get; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceBy(System.TimeSpan time) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceTo(System.DateTimeOffset time) { } + public override System.Threading.ITimer CreateTimer(System.Threading.TimerCallback callback, object? state, System.TimeSpan dueTime, System.TimeSpan period) { } + public override long GetTimestamp() { } + public override System.DateTimeOffset GetUtcNow() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] @@ -1897,6 +1922,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class WasmSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public WasmSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.WasmSequencer Default { get; } @@ -2517,6 +2543,8 @@ namespace ReactiveUI.Primitives.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives/PublicAPI/net11.0-macos/PublicAPI.txt b/src/ReactiveUI.Primitives/PublicAPI/net11.0-macos/PublicAPI.txt index 1bcf9c47..f69580a3 100644 --- a/src/ReactiveUI.Primitives/PublicAPI/net11.0-macos/PublicAPI.txt +++ b/src/ReactiveUI.Primitives/PublicAPI/net11.0-macos/PublicAPI.txt @@ -1686,6 +1686,15 @@ namespace ReactiveUI.Primitives.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -1707,6 +1716,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class CurrentThreadSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public CurrentThreadSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.CurrentThreadSequencer Instance { get; } @@ -1726,6 +1736,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ImmediateSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public ImmediateSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.ImmediateSequencer Instance { get; } @@ -1775,10 +1786,18 @@ namespace ReactiveUI.Primitives.Concurrency public System.IDisposable ScheduleAction(TState state, System.Action action) { } } } + public static class SequencerImmediacyExtensions + { + extension(ReactiveUI.Primitives.Concurrency.ISequencer? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class SynchronizationContextSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public SynchronizationContextSequencer(System.Threading.SynchronizationContext context) { } + public SynchronizationContextSequencer(System.Threading.SynchronizationContext context, System.TimeProvider timeProvider) { } public System.Threading.SynchronizationContext Context { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1790,6 +1809,7 @@ namespace ReactiveUI.Primitives.Concurrency public sealed class TaskPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory) { } + public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory, System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public System.Action? UnhandledExceptionHandler { get; set; } @@ -1801,6 +1821,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ThreadPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public ThreadPoolSequencer(System.TimeProvider timeProvider) { } public static readonly ReactiveUI.Primitives.Concurrency.ThreadPoolSequencer Instance; public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1809,7 +1830,7 @@ namespace ReactiveUI.Primitives.Concurrency public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item, long dueTimestamp) { } } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] - public sealed class VirtualClock : ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider + public sealed class VirtualClock : System.TimeProvider, ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider { public VirtualClock() { } public VirtualClock(System.DateTimeOffset initialClock) { } @@ -1817,10 +1838,14 @@ namespace ReactiveUI.Primitives.Concurrency public bool IsEnabled { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } + public override long TimestampFrequency { get; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceBy(System.TimeSpan time) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceTo(System.DateTimeOffset time) { } + public override System.Threading.ITimer CreateTimer(System.Threading.TimerCallback callback, object? state, System.TimeSpan dueTime, System.TimeSpan period) { } + public override long GetTimestamp() { } + public override System.DateTimeOffset GetUtcNow() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] @@ -1897,6 +1922,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class WasmSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public WasmSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.WasmSequencer Default { get; } @@ -2517,6 +2543,8 @@ namespace ReactiveUI.Primitives.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives/PublicAPI/net11.0-tvos/PublicAPI.txt b/src/ReactiveUI.Primitives/PublicAPI/net11.0-tvos/PublicAPI.txt index cb50ee36..61d2d1da 100644 --- a/src/ReactiveUI.Primitives/PublicAPI/net11.0-tvos/PublicAPI.txt +++ b/src/ReactiveUI.Primitives/PublicAPI/net11.0-tvos/PublicAPI.txt @@ -1686,6 +1686,15 @@ namespace ReactiveUI.Primitives.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -1707,6 +1716,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class CurrentThreadSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public CurrentThreadSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.CurrentThreadSequencer Instance { get; } @@ -1726,6 +1736,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ImmediateSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public ImmediateSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.ImmediateSequencer Instance { get; } @@ -1775,10 +1786,18 @@ namespace ReactiveUI.Primitives.Concurrency public System.IDisposable ScheduleAction(TState state, System.Action action) { } } } + public static class SequencerImmediacyExtensions + { + extension(ReactiveUI.Primitives.Concurrency.ISequencer? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class SynchronizationContextSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public SynchronizationContextSequencer(System.Threading.SynchronizationContext context) { } + public SynchronizationContextSequencer(System.Threading.SynchronizationContext context, System.TimeProvider timeProvider) { } public System.Threading.SynchronizationContext Context { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1790,6 +1809,7 @@ namespace ReactiveUI.Primitives.Concurrency public sealed class TaskPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory) { } + public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory, System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public System.Action? UnhandledExceptionHandler { get; set; } @@ -1801,6 +1821,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ThreadPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public ThreadPoolSequencer(System.TimeProvider timeProvider) { } public static readonly ReactiveUI.Primitives.Concurrency.ThreadPoolSequencer Instance; public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1809,7 +1830,7 @@ namespace ReactiveUI.Primitives.Concurrency public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item, long dueTimestamp) { } } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] - public sealed class VirtualClock : ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider + public sealed class VirtualClock : System.TimeProvider, ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider { public VirtualClock() { } public VirtualClock(System.DateTimeOffset initialClock) { } @@ -1817,10 +1838,14 @@ namespace ReactiveUI.Primitives.Concurrency public bool IsEnabled { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } + public override long TimestampFrequency { get; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceBy(System.TimeSpan time) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceTo(System.DateTimeOffset time) { } + public override System.Threading.ITimer CreateTimer(System.Threading.TimerCallback callback, object? state, System.TimeSpan dueTime, System.TimeSpan period) { } + public override long GetTimestamp() { } + public override System.DateTimeOffset GetUtcNow() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] @@ -1897,6 +1922,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class WasmSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public WasmSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.WasmSequencer Default { get; } @@ -2517,6 +2543,8 @@ namespace ReactiveUI.Primitives.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives/PublicAPI/net11.0/PublicAPI.txt b/src/ReactiveUI.Primitives/PublicAPI/net11.0/PublicAPI.txt index 4912d27c..c99a42df 100644 --- a/src/ReactiveUI.Primitives/PublicAPI/net11.0/PublicAPI.txt +++ b/src/ReactiveUI.Primitives/PublicAPI/net11.0/PublicAPI.txt @@ -1685,6 +1685,15 @@ namespace ReactiveUI.Primitives.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -1706,6 +1715,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class CurrentThreadSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public CurrentThreadSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.CurrentThreadSequencer Instance { get; } @@ -1725,6 +1735,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ImmediateSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public ImmediateSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.ImmediateSequencer Instance { get; } @@ -1762,10 +1773,18 @@ namespace ReactiveUI.Primitives.Concurrency public System.IDisposable ScheduleAction(TState state, System.Action action) { } } } + public static class SequencerImmediacyExtensions + { + extension(ReactiveUI.Primitives.Concurrency.ISequencer? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class SynchronizationContextSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public SynchronizationContextSequencer(System.Threading.SynchronizationContext context) { } + public SynchronizationContextSequencer(System.Threading.SynchronizationContext context, System.TimeProvider timeProvider) { } public System.Threading.SynchronizationContext Context { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1777,6 +1796,7 @@ namespace ReactiveUI.Primitives.Concurrency public sealed class TaskPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory) { } + public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory, System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public System.Action? UnhandledExceptionHandler { get; set; } @@ -1788,6 +1808,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ThreadPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public ThreadPoolSequencer(System.TimeProvider timeProvider) { } public static readonly ReactiveUI.Primitives.Concurrency.ThreadPoolSequencer Instance; public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1796,7 +1817,7 @@ namespace ReactiveUI.Primitives.Concurrency public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item, long dueTimestamp) { } } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] - public sealed class VirtualClock : ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider + public sealed class VirtualClock : System.TimeProvider, ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider { public VirtualClock() { } public VirtualClock(System.DateTimeOffset initialClock) { } @@ -1804,10 +1825,14 @@ namespace ReactiveUI.Primitives.Concurrency public bool IsEnabled { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } + public override long TimestampFrequency { get; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceBy(System.TimeSpan time) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceTo(System.DateTimeOffset time) { } + public override System.Threading.ITimer CreateTimer(System.Threading.TimerCallback callback, object? state, System.TimeSpan dueTime, System.TimeSpan period) { } + public override long GetTimestamp() { } + public override System.DateTimeOffset GetUtcNow() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] @@ -1884,6 +1909,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class WasmSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public WasmSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.WasmSequencer Default { get; } @@ -2504,6 +2530,8 @@ namespace ReactiveUI.Primitives.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives/PublicAPI/net462/PublicAPI.txt b/src/ReactiveUI.Primitives/PublicAPI/net462/PublicAPI.txt index d1d5757a..22390c8a 100644 --- a/src/ReactiveUI.Primitives/PublicAPI/net462/PublicAPI.txt +++ b/src/ReactiveUI.Primitives/PublicAPI/net462/PublicAPI.txt @@ -1651,6 +1651,15 @@ namespace ReactiveUI.Primitives.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -1672,6 +1681,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class CurrentThreadSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public CurrentThreadSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.CurrentThreadSequencer Instance { get; } @@ -1691,6 +1701,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ImmediateSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public ImmediateSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.ImmediateSequencer Instance { get; } @@ -1728,10 +1739,18 @@ namespace ReactiveUI.Primitives.Concurrency public System.IDisposable ScheduleAction(TState state, System.Action action) { } } } + public static class SequencerImmediacyExtensions + { + extension(ReactiveUI.Primitives.Concurrency.ISequencer? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class SynchronizationContextSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public SynchronizationContextSequencer(System.Threading.SynchronizationContext context) { } + public SynchronizationContextSequencer(System.Threading.SynchronizationContext context, System.TimeProvider timeProvider) { } public System.Threading.SynchronizationContext Context { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1743,6 +1762,7 @@ namespace ReactiveUI.Primitives.Concurrency public sealed class TaskPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory) { } + public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory, System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public System.Action? UnhandledExceptionHandler { get; set; } @@ -1754,6 +1774,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ThreadPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public ThreadPoolSequencer(System.TimeProvider timeProvider) { } public static readonly ReactiveUI.Primitives.Concurrency.ThreadPoolSequencer Instance; public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1762,7 +1783,7 @@ namespace ReactiveUI.Primitives.Concurrency public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item, long dueTimestamp) { } } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] - public sealed class VirtualClock : ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider + public sealed class VirtualClock : System.TimeProvider, ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider { public VirtualClock() { } public VirtualClock(System.DateTimeOffset initialClock) { } @@ -1770,10 +1791,14 @@ namespace ReactiveUI.Primitives.Concurrency public bool IsEnabled { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } + public override long TimestampFrequency { get; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceBy(System.TimeSpan time) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceTo(System.DateTimeOffset time) { } + public override System.Threading.ITimer CreateTimer(System.Threading.TimerCallback callback, object? state, System.TimeSpan dueTime, System.TimeSpan period) { } + public override long GetTimestamp() { } + public override System.DateTimeOffset GetUtcNow() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] @@ -1850,6 +1875,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class WasmSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public WasmSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.WasmSequencer Default { get; } @@ -2468,6 +2494,8 @@ namespace ReactiveUI.Primitives.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives/PublicAPI/net472/PublicAPI.txt b/src/ReactiveUI.Primitives/PublicAPI/net472/PublicAPI.txt index d1d5757a..22390c8a 100644 --- a/src/ReactiveUI.Primitives/PublicAPI/net472/PublicAPI.txt +++ b/src/ReactiveUI.Primitives/PublicAPI/net472/PublicAPI.txt @@ -1651,6 +1651,15 @@ namespace ReactiveUI.Primitives.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -1672,6 +1681,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class CurrentThreadSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public CurrentThreadSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.CurrentThreadSequencer Instance { get; } @@ -1691,6 +1701,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ImmediateSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public ImmediateSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.ImmediateSequencer Instance { get; } @@ -1728,10 +1739,18 @@ namespace ReactiveUI.Primitives.Concurrency public System.IDisposable ScheduleAction(TState state, System.Action action) { } } } + public static class SequencerImmediacyExtensions + { + extension(ReactiveUI.Primitives.Concurrency.ISequencer? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class SynchronizationContextSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public SynchronizationContextSequencer(System.Threading.SynchronizationContext context) { } + public SynchronizationContextSequencer(System.Threading.SynchronizationContext context, System.TimeProvider timeProvider) { } public System.Threading.SynchronizationContext Context { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1743,6 +1762,7 @@ namespace ReactiveUI.Primitives.Concurrency public sealed class TaskPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory) { } + public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory, System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public System.Action? UnhandledExceptionHandler { get; set; } @@ -1754,6 +1774,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ThreadPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public ThreadPoolSequencer(System.TimeProvider timeProvider) { } public static readonly ReactiveUI.Primitives.Concurrency.ThreadPoolSequencer Instance; public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1762,7 +1783,7 @@ namespace ReactiveUI.Primitives.Concurrency public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item, long dueTimestamp) { } } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] - public sealed class VirtualClock : ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider + public sealed class VirtualClock : System.TimeProvider, ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider { public VirtualClock() { } public VirtualClock(System.DateTimeOffset initialClock) { } @@ -1770,10 +1791,14 @@ namespace ReactiveUI.Primitives.Concurrency public bool IsEnabled { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } + public override long TimestampFrequency { get; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceBy(System.TimeSpan time) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceTo(System.DateTimeOffset time) { } + public override System.Threading.ITimer CreateTimer(System.Threading.TimerCallback callback, object? state, System.TimeSpan dueTime, System.TimeSpan period) { } + public override long GetTimestamp() { } + public override System.DateTimeOffset GetUtcNow() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] @@ -1850,6 +1875,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class WasmSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public WasmSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.WasmSequencer Default { get; } @@ -2468,6 +2494,8 @@ namespace ReactiveUI.Primitives.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives/PublicAPI/net48/PublicAPI.txt b/src/ReactiveUI.Primitives/PublicAPI/net48/PublicAPI.txt index d1d5757a..22390c8a 100644 --- a/src/ReactiveUI.Primitives/PublicAPI/net48/PublicAPI.txt +++ b/src/ReactiveUI.Primitives/PublicAPI/net48/PublicAPI.txt @@ -1651,6 +1651,15 @@ namespace ReactiveUI.Primitives.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -1672,6 +1681,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class CurrentThreadSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public CurrentThreadSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.CurrentThreadSequencer Instance { get; } @@ -1691,6 +1701,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ImmediateSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public ImmediateSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.ImmediateSequencer Instance { get; } @@ -1728,10 +1739,18 @@ namespace ReactiveUI.Primitives.Concurrency public System.IDisposable ScheduleAction(TState state, System.Action action) { } } } + public static class SequencerImmediacyExtensions + { + extension(ReactiveUI.Primitives.Concurrency.ISequencer? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class SynchronizationContextSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public SynchronizationContextSequencer(System.Threading.SynchronizationContext context) { } + public SynchronizationContextSequencer(System.Threading.SynchronizationContext context, System.TimeProvider timeProvider) { } public System.Threading.SynchronizationContext Context { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1743,6 +1762,7 @@ namespace ReactiveUI.Primitives.Concurrency public sealed class TaskPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory) { } + public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory, System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public System.Action? UnhandledExceptionHandler { get; set; } @@ -1754,6 +1774,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ThreadPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public ThreadPoolSequencer(System.TimeProvider timeProvider) { } public static readonly ReactiveUI.Primitives.Concurrency.ThreadPoolSequencer Instance; public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1762,7 +1783,7 @@ namespace ReactiveUI.Primitives.Concurrency public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item, long dueTimestamp) { } } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] - public sealed class VirtualClock : ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider + public sealed class VirtualClock : System.TimeProvider, ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider { public VirtualClock() { } public VirtualClock(System.DateTimeOffset initialClock) { } @@ -1770,10 +1791,14 @@ namespace ReactiveUI.Primitives.Concurrency public bool IsEnabled { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } + public override long TimestampFrequency { get; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceBy(System.TimeSpan time) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceTo(System.DateTimeOffset time) { } + public override System.Threading.ITimer CreateTimer(System.Threading.TimerCallback callback, object? state, System.TimeSpan dueTime, System.TimeSpan period) { } + public override long GetTimestamp() { } + public override System.DateTimeOffset GetUtcNow() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] @@ -1850,6 +1875,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class WasmSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public WasmSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.WasmSequencer Default { get; } @@ -2468,6 +2494,8 @@ namespace ReactiveUI.Primitives.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives/PublicAPI/net481/PublicAPI.txt b/src/ReactiveUI.Primitives/PublicAPI/net481/PublicAPI.txt index d1d5757a..22390c8a 100644 --- a/src/ReactiveUI.Primitives/PublicAPI/net481/PublicAPI.txt +++ b/src/ReactiveUI.Primitives/PublicAPI/net481/PublicAPI.txt @@ -1651,6 +1651,15 @@ namespace ReactiveUI.Primitives.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -1672,6 +1681,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class CurrentThreadSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public CurrentThreadSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.CurrentThreadSequencer Instance { get; } @@ -1691,6 +1701,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ImmediateSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public ImmediateSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.ImmediateSequencer Instance { get; } @@ -1728,10 +1739,18 @@ namespace ReactiveUI.Primitives.Concurrency public System.IDisposable ScheduleAction(TState state, System.Action action) { } } } + public static class SequencerImmediacyExtensions + { + extension(ReactiveUI.Primitives.Concurrency.ISequencer? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class SynchronizationContextSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public SynchronizationContextSequencer(System.Threading.SynchronizationContext context) { } + public SynchronizationContextSequencer(System.Threading.SynchronizationContext context, System.TimeProvider timeProvider) { } public System.Threading.SynchronizationContext Context { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1743,6 +1762,7 @@ namespace ReactiveUI.Primitives.Concurrency public sealed class TaskPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory) { } + public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory, System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public System.Action? UnhandledExceptionHandler { get; set; } @@ -1754,6 +1774,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ThreadPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public ThreadPoolSequencer(System.TimeProvider timeProvider) { } public static readonly ReactiveUI.Primitives.Concurrency.ThreadPoolSequencer Instance; public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1762,7 +1783,7 @@ namespace ReactiveUI.Primitives.Concurrency public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item, long dueTimestamp) { } } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] - public sealed class VirtualClock : ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider + public sealed class VirtualClock : System.TimeProvider, ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider { public VirtualClock() { } public VirtualClock(System.DateTimeOffset initialClock) { } @@ -1770,10 +1791,14 @@ namespace ReactiveUI.Primitives.Concurrency public bool IsEnabled { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } + public override long TimestampFrequency { get; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceBy(System.TimeSpan time) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceTo(System.DateTimeOffset time) { } + public override System.Threading.ITimer CreateTimer(System.Threading.TimerCallback callback, object? state, System.TimeSpan dueTime, System.TimeSpan period) { } + public override long GetTimestamp() { } + public override System.DateTimeOffset GetUtcNow() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] @@ -1850,6 +1875,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class WasmSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public WasmSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.WasmSequencer Default { get; } @@ -2468,6 +2494,8 @@ namespace ReactiveUI.Primitives.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives/PublicAPI/net8.0/PublicAPI.txt b/src/ReactiveUI.Primitives/PublicAPI/net8.0/PublicAPI.txt index 2233e26d..e1f12461 100644 --- a/src/ReactiveUI.Primitives/PublicAPI/net8.0/PublicAPI.txt +++ b/src/ReactiveUI.Primitives/PublicAPI/net8.0/PublicAPI.txt @@ -1651,6 +1651,15 @@ namespace ReactiveUI.Primitives.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -1672,6 +1681,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class CurrentThreadSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public CurrentThreadSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.CurrentThreadSequencer Instance { get; } @@ -1691,6 +1701,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ImmediateSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public ImmediateSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.ImmediateSequencer Instance { get; } @@ -1728,10 +1739,18 @@ namespace ReactiveUI.Primitives.Concurrency public System.IDisposable ScheduleAction(TState state, System.Action action) { } } } + public static class SequencerImmediacyExtensions + { + extension(ReactiveUI.Primitives.Concurrency.ISequencer? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class SynchronizationContextSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public SynchronizationContextSequencer(System.Threading.SynchronizationContext context) { } + public SynchronizationContextSequencer(System.Threading.SynchronizationContext context, System.TimeProvider timeProvider) { } public System.Threading.SynchronizationContext Context { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1743,6 +1762,7 @@ namespace ReactiveUI.Primitives.Concurrency public sealed class TaskPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory) { } + public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory, System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public System.Action? UnhandledExceptionHandler { get; set; } @@ -1754,6 +1774,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ThreadPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public ThreadPoolSequencer(System.TimeProvider timeProvider) { } public static readonly ReactiveUI.Primitives.Concurrency.ThreadPoolSequencer Instance; public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1762,7 +1783,7 @@ namespace ReactiveUI.Primitives.Concurrency public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item, long dueTimestamp) { } } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] - public sealed class VirtualClock : ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider + public sealed class VirtualClock : System.TimeProvider, ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider { public VirtualClock() { } public VirtualClock(System.DateTimeOffset initialClock) { } @@ -1770,10 +1791,14 @@ namespace ReactiveUI.Primitives.Concurrency public bool IsEnabled { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } + public override long TimestampFrequency { get; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceBy(System.TimeSpan time) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceTo(System.DateTimeOffset time) { } + public override System.Threading.ITimer CreateTimer(System.Threading.TimerCallback callback, object? state, System.TimeSpan dueTime, System.TimeSpan period) { } + public override long GetTimestamp() { } + public override System.DateTimeOffset GetUtcNow() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] @@ -1850,6 +1875,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class WasmSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public WasmSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.WasmSequencer Default { get; } @@ -2470,6 +2496,8 @@ namespace ReactiveUI.Primitives.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/ReactiveUI.Primitives/PublicAPI/net9.0/PublicAPI.txt b/src/ReactiveUI.Primitives/PublicAPI/net9.0/PublicAPI.txt index 4912d27c..c99a42df 100644 --- a/src/ReactiveUI.Primitives/PublicAPI/net9.0/PublicAPI.txt +++ b/src/ReactiveUI.Primitives/PublicAPI/net9.0/PublicAPI.txt @@ -1685,6 +1685,15 @@ namespace ReactiveUI.Primitives.Advanced [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void SetSubscription(System.IDisposable subscription) { } } + [System.Diagnostics.DebuggerDisplay("WitnessLatestOnSignal<{typeof(T).Name,nq}>")] + public sealed class WitnessLatestOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread + { + public WitnessLatestOnSignal(System.IObservable source, ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool IsRequiredSubscribeOnCurrentThread() { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IDisposable Subscribe(System.IObserver observer) { } + } [System.Diagnostics.DebuggerDisplay("WitnessOnSignal<{typeof(T).Name,nq}>")] public sealed class WitnessOnSignal : ReactiveUI.Primitives.Advanced.IRequireCurrentThread { @@ -1706,6 +1715,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class CurrentThreadSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public CurrentThreadSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.CurrentThreadSequencer Instance { get; } @@ -1725,6 +1735,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ImmediateSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { + public ImmediateSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.ImmediateSequencer Instance { get; } @@ -1762,10 +1773,18 @@ namespace ReactiveUI.Primitives.Concurrency public System.IDisposable ScheduleAction(TState state, System.Action action) { } } } + public static class SequencerImmediacyExtensions + { + extension(ReactiveUI.Primitives.Concurrency.ISequencer? sequencer) + { + public bool IsImmediate { get; } + } + } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class SynchronizationContextSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public SynchronizationContextSequencer(System.Threading.SynchronizationContext context) { } + public SynchronizationContextSequencer(System.Threading.SynchronizationContext context, System.TimeProvider timeProvider) { } public System.Threading.SynchronizationContext Context { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1777,6 +1796,7 @@ namespace ReactiveUI.Primitives.Concurrency public sealed class TaskPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer { public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory) { } + public TaskPoolSequencer(System.Threading.Tasks.TaskFactory taskFactory, System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public System.Action? UnhandledExceptionHandler { get; set; } @@ -1788,6 +1808,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ThreadPoolSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public ThreadPoolSequencer(System.TimeProvider timeProvider) { } public static readonly ReactiveUI.Primitives.Concurrency.ThreadPoolSequencer Instance; public System.DateTimeOffset Now { get; } public long Timestamp { get; } @@ -1796,7 +1817,7 @@ namespace ReactiveUI.Primitives.Concurrency public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item, long dueTimestamp) { } } [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] - public sealed class VirtualClock : ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider + public sealed class VirtualClock : System.TimeProvider, ReactiveUI.Primitives.Concurrency.ISequencer, ReactiveUI.Primitives.Concurrency.IStopwatchProvider, System.IServiceProvider { public VirtualClock() { } public VirtualClock(System.DateTimeOffset initialClock) { } @@ -1804,10 +1825,14 @@ namespace ReactiveUI.Primitives.Concurrency public bool IsEnabled { get; } public System.DateTimeOffset Now { get; } public long Timestamp { get; } + public override long TimestampFrequency { get; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceBy(System.TimeSpan time) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void AdvanceTo(System.DateTimeOffset time) { } + public override System.Threading.ITimer CreateTimer(System.Threading.TimerCallback callback, object? state, System.TimeSpan dueTime, System.TimeSpan period) { } + public override long GetTimestamp() { } + public override System.DateTimeOffset GetUtcNow() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem item) { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] @@ -1884,6 +1909,7 @@ namespace ReactiveUI.Primitives.Concurrency [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class WasmSequencer : ReactiveUI.Primitives.Concurrency.ISequencer, System.IDisposable { + public WasmSequencer(System.TimeProvider timeProvider) { } public System.DateTimeOffset Now { get; } public long Timestamp { get; } public static ReactiveUI.Primitives.Concurrency.WasmSequencer Default { get; } @@ -2504,6 +2530,8 @@ namespace ReactiveUI.Primitives.Signals { public System.Collections.Generic.IEnumerable ToEnumerable() { } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public System.IObservable WitnessLatestOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public System.IObservable WitnessOn(ReactiveUI.Primitives.Concurrency.ISequencer scheduler) { } } extension(System.IObservable asyncTask) diff --git a/src/tests/ReactiveUI.Primitives.Extensions.Tests/Operators/ThrottleObservableTests.cs b/src/tests/ReactiveUI.Primitives.Extensions.Tests/Operators/ThrottleObservableTests.cs index 4f708c67..34319ff0 100644 --- a/src/tests/ReactiveUI.Primitives.Extensions.Tests/Operators/ThrottleObservableTests.cs +++ b/src/tests/ReactiveUI.Primitives.Extensions.Tests/Operators/ThrottleObservableTests.cs @@ -17,6 +17,9 @@ public class ThrottleObservableTests /// Tick window for the throttle itself. private const int ThrottleTicks = 10; + /// Tick offset that lands inside the throttle window. + private const int HalfWindowTicks = 5; + /// Verifies stale, duplicate, and disposed throttle callbacks cannot emit. /// A task representing the asynchronous test operation. [Test] @@ -100,4 +103,27 @@ public async Task WhenOnCompletedAfterError_ThenDropped() await Assert.That(caught).IsSameReferenceAs(expected); await Assert.That(completed).IsFalse(); } + + /// Verifies a thread-pool sequencer driven by a virtual clock used as a time provider emits the latest value once the throttle window has passed. + /// A representing the asynchronous test operation. + [Test] + public async Task WhenSequencerRunsFromATimeProvider_ThenEmitsTheLatestValueAfterTheWindow() + { + const int Earlier = 1; + const int Latest = 2; + VirtualClock clock = new(DateTimeOffset.UnixEpoch); + ThreadPoolSequencer sequencer = new(clock); + SyncDirectSource source = new(); + List values = []; + using var sub = source.ThrottleOnScheduler(TimeSpan.FromTicks(ThrottleTicks), sequencer).Subscribe(values.Add); + + source.Observer.OnNext(Earlier); + clock.AdvanceBy(TimeSpan.FromTicks(HalfWindowTicks)); + source.Observer.OnNext(Latest); + clock.AdvanceBy(TimeSpan.FromTicks(ThrottleTicks - 1)); + await Assert.That(values).IsEmpty(); + + clock.AdvanceBy(TimeSpan.FromTicks(1)); + await Assert.That(values).IsCollectionEqualTo([Latest]); + } } diff --git a/src/tests/ReactiveUI.Primitives.Reactive.Tests/SequencerImmediacyExtensionsTests.cs b/src/tests/ReactiveUI.Primitives.Reactive.Tests/SequencerImmediacyExtensionsTests.cs new file mode 100644 index 00000000..38c6e1a4 --- /dev/null +++ b/src/tests/ReactiveUI.Primitives.Reactive.Tests/SequencerImmediacyExtensionsTests.cs @@ -0,0 +1,68 @@ +// 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 ReactiveUI.Primitives.Reactive.Concurrency; + +namespace ReactiveUI.Primitives.Reactive.Tests; + +/// Tests immediacy detection against System.Reactive schedulers. +public class SequencerImmediacyExtensionsTests +{ + /// The immediate scheduler instance reports itself as immediate. + /// A representing the asynchronous test operation. + [Test] + public async Task WhenSchedulerIsImmediateInstance_ThenIsImmediateIsTrue() => + await Assert.That(ImmediateScheduler.Instance.IsImmediate).IsTrue(); + + /// The scheduler exposed as the immediate scheduler reports itself as immediate. + /// A representing the asynchronous test operation. + [Test] + public async Task WhenSchedulerIsSchedulerImmediate_ThenIsImmediateIsTrue() => + await Assert.That(System.Reactive.Concurrency.Scheduler.Immediate.IsImmediate).IsTrue(); + + /// The library's immediate sequencer reports itself as immediate. + /// A representing the asynchronous test operation. + [Test] + public async Task WhenSchedulerIsSequencerImmediate_ThenIsImmediateIsTrue() => + await Assert.That(Sequencer.Immediate.IsImmediate).IsTrue(); + + /// The current-thread scheduler is not immediate. + /// A representing the asynchronous test operation. + [Test] + public async Task WhenSchedulerIsCurrentThread_ThenIsImmediateIsFalse() => + await Assert.That(System.Reactive.Concurrency.Scheduler.CurrentThread.IsImmediate).IsFalse(); + + /// The default scheduler is not immediate. + /// A representing the asynchronous test operation. + [Test] + public async Task WhenSchedulerIsDefault_ThenIsImmediateIsFalse() => + await Assert.That(System.Reactive.Concurrency.Scheduler.Default.IsImmediate).IsFalse(); + + /// The thread-pool sequencer is not immediate. + /// A representing the asynchronous test operation. + [Test] + public async Task WhenSchedulerIsThreadPool_ThenIsImmediateIsFalse() => + await Assert.That(ThreadPoolSequencer.Instance.IsImmediate).IsFalse(); + + /// A virtual-time scheduler is not immediate. + /// A representing the asynchronous test operation. + [Test] + public async Task WhenSchedulerIsVirtualTime_ThenIsImmediateIsFalse() + { + HistoricalScheduler scheduler = new(); + + await Assert.That(scheduler.IsImmediate).IsFalse(); + } + + /// A null scheduler is not immediate. + /// A representing the asynchronous test operation. + [Test] + public async Task WhenSchedulerIsNull_ThenIsImmediateIsFalse() + { + IScheduler? scheduler = null; + + await Assert.That(scheduler.IsImmediate).IsFalse(); + } +} diff --git a/src/tests/ReactiveUI.Primitives.Reactive.Tests/WasmSchedulerTests.cs b/src/tests/ReactiveUI.Primitives.Reactive.Tests/WasmSchedulerTests.cs index f3696308..e3a5429b 100644 --- a/src/tests/ReactiveUI.Primitives.Reactive.Tests/WasmSchedulerTests.cs +++ b/src/tests/ReactiveUI.Primitives.Reactive.Tests/WasmSchedulerTests.cs @@ -44,6 +44,27 @@ public sealed class WasmSchedulerTests public async Task DefaultReturnsSingleton() => await Assert.That(WasmScheduler.Default).IsSameReferenceAs(WasmScheduler.Default); + /// Verifies the provider constructor rejects a missing provider. + /// A representing the asynchronous operation. + [Test] + public async Task ConstructorRejectsANullTimeProvider() => + await Assert.That(static () => new WasmScheduler(null!)).ThrowsExactly(); + + /// Verifies the scheduler reports the provider's current time. + /// A representing the asynchronous operation. + [Test] + public async Task NowFollowsTheTimeProvider() + { + ManualTimeProvider timeProvider = new(); + using WasmScheduler scheduler = new(timeProvider); + + await Assert.That(scheduler.Now).IsEqualTo(DateTimeOffset.UnixEpoch); + + timeProvider.UtcNow = DateTimeOffset.UnixEpoch + ValidInterval; + + await Assert.That(scheduler.Now).IsEqualTo(DateTimeOffset.UnixEpoch + ValidInterval); + } + /// Verifies scheduling rejects null actions. /// A representing the asynchronous operation. [Test] @@ -601,6 +622,12 @@ private sealed class ManualTimeProvider : TimeProvider /// Gets the most recently requested timer period. public TimeSpan LastPeriod => _timers[^1].Period; + /// Gets or sets the time reported as the current UTC time. + public DateTimeOffset UtcNow { get; set; } = DateTimeOffset.UnixEpoch; + + /// + public override DateTimeOffset GetUtcNow() => UtcNow; + /// public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period) { diff --git a/src/tests/ReactiveUI.Primitives.Tests/CalmCoordinatorTests.cs b/src/tests/ReactiveUI.Primitives.Tests/CalmCoordinatorTests.cs index 57b5c445..66f6b7d7 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/CalmCoordinatorTests.cs +++ b/src/tests/ReactiveUI.Primitives.Tests/CalmCoordinatorTests.cs @@ -2,6 +2,8 @@ // ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using ReactiveUI.Primitives.Concurrency; + namespace ReactiveUI.Primitives.Tests; /// Tests for the quiet-period coordinator behind Calm and its Throttle alias. @@ -211,4 +213,30 @@ public async Task CalmErrorRaisedDuringEmissionFollowsTheValue() await Assert.That(values.SequenceEqual([One])).IsTrue(); await Assert.That(downstream.Error).IsSameReferenceAs(expected); } + + /// Verifies a sequencer driven by a virtual clock used as a time provider emits the latest value once the quiet period has passed. + /// A task representing the asynchronous operation. + [Test] + public async Task ThrottleOverATimeProviderSequencerEmitsTheLatestValueWhenTheClockPassesTheQuietPeriod() + { + VirtualClock clock = new(DateTimeOffset.UnixEpoch); + var sequencer = InlineThreadPool.Create(clock); + IObserver? source = null; + RecordingWitness witness = new(); + using var subscription = new ScriptedObservable(observer => source = observer) + .Throttle(QuietPeriod, sequencer) + .Subscribe(witness); + + source!.OnNext(One); + clock.AdvanceBy(QuietPeriod / Two); + source.OnNext(Two); + clock.AdvanceBy(QuietPeriod / Two); + await Assert.That(witness.Values).IsEmpty(); + + clock.AdvanceBy((QuietPeriod / Two) - TimeSpan.FromTicks(One)); + await Assert.That(witness.Values).IsEmpty(); + + clock.AdvanceBy(TimeSpan.FromTicks(One)); + await Assert.That(witness.Values.SequenceEqual([Two])).IsTrue(); + } } diff --git a/src/tests/ReactiveUI.Primitives.Tests/Common/InlineThreadPool.cs b/src/tests/ReactiveUI.Primitives.Tests/Common/InlineThreadPool.cs new file mode 100644 index 00000000..d7d72bea --- /dev/null +++ b/src/tests/ReactiveUI.Primitives.Tests/Common/InlineThreadPool.cs @@ -0,0 +1,17 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using ReactiveUI.Primitives.Concurrency; + +namespace ReactiveUI.Primitives.Tests; + +/// Builds thread-pool sequencers whose immediate work runs on the calling thread. +internal static class InlineThreadPool +{ + /// Creates a sequencer that reads time and arms delays through and runs immediate work inline. + /// The provider supplying time and the delay timer. + /// The sequencer. + internal static ThreadPoolSequencer Create(TimeProvider timeProvider) => + new(timeProvider, static (callback, state) => callback(state)); +} diff --git a/src/tests/ReactiveUI.Primitives.Tests/Common/RecordingWorkItem.cs b/src/tests/ReactiveUI.Primitives.Tests/Common/RecordingWorkItem.cs new file mode 100644 index 00000000..cbb1056c --- /dev/null +++ b/src/tests/ReactiveUI.Primitives.Tests/Common/RecordingWorkItem.cs @@ -0,0 +1,33 @@ +// 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.Runtime.CompilerServices; +using ReactiveUI.Primitives.Concurrency; +using ReactiveUI.Primitives.Disposables; + +namespace ReactiveUI.Primitives.Tests; + +/// A cancellable work item that counts its executions. +internal sealed class RecordingWorkItem : IWorkItem, IsDisposed +{ + /// The number of executions. + private int _executeCount; + + /// Non-zero after cancellation. + private int _isDisposed; + + /// + public bool IsDisposed => Volatile.Read(ref _isDisposed) != 0; + + /// Gets the number of times the item executed. + internal int ExecuteCount => Volatile.Read(ref _executeCount); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dispose() => Volatile.Write(ref _isDisposed, 1); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Execute() => Interlocked.Increment(ref _executeCount); +} diff --git a/src/tests/ReactiveUI.Primitives.Tests/Common/ScaledTimeProvider.cs b/src/tests/ReactiveUI.Primitives.Tests/Common/ScaledTimeProvider.cs new file mode 100644 index 00000000..4cde500f --- /dev/null +++ b/src/tests/ReactiveUI.Primitives.Tests/Common/ScaledTimeProvider.cs @@ -0,0 +1,58 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Collections.Concurrent; +using ReactiveUI.Primitives.Concurrency; + +namespace ReactiveUI.Primitives.Tests; + +/// A time provider over a that counts timestamps at a chosen frequency and reports the timers it creates. +internal sealed class ScaledTimeProvider : TimeProvider +{ + /// The virtual clock supplying time and timers. + private readonly VirtualClock _clock; + + /// The number of virtual clock ticks in one timestamp tick. + private readonly long _ticksPerTimestamp; + + /// Completes when the first timer is created. + private readonly TaskCompletionSource _timerCreated = new(TaskCreationOptions.RunContinuationsAsynchronously); + + /// Records the due time of each timer created. + private readonly ConcurrentQueue _timerDueTimes = new(); + + /// Initializes a new instance of the class. + /// The virtual clock supplying time and timers. + /// The timestamp ticks per second, a divisor of . + internal ScaledTimeProvider(VirtualClock clock, long frequency) + { + _clock = clock; + TimestampFrequency = frequency; + _ticksPerTimestamp = TimeSpan.TicksPerSecond / frequency; + } + + /// + public override long TimestampFrequency { get; } + + /// Gets a task that completes when the first timer is created. + internal Task TimerCreated => _timerCreated.Task; + + /// Gets the due times of the timers created so far, in creation order. + internal TimeSpan[] TimerDueTimes => [.. _timerDueTimes]; + + /// + public override DateTimeOffset GetUtcNow() => _clock.Now; + + /// + public override long GetTimestamp() => _clock.Now.UtcTicks / _ticksPerTimestamp; + + /// + public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period) + { + var timer = _clock.CreateTimer(callback, state, dueTime, period); + _timerDueTimes.Enqueue(dueTime); + _ = _timerCreated.TrySetResult(); + return timer; + } +} diff --git a/src/tests/ReactiveUI.Primitives.Tests/Common/SettableTimeProvider.cs b/src/tests/ReactiveUI.Primitives.Tests/Common/SettableTimeProvider.cs new file mode 100644 index 00000000..f677c1b4 --- /dev/null +++ b/src/tests/ReactiveUI.Primitives.Tests/Common/SettableTimeProvider.cs @@ -0,0 +1,19 @@ +// 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.Tests; + +/// A time provider whose timestamp and frequency are set directly by the test. +/// The timestamp ticks per second. +internal sealed class SettableTimeProvider(long frequency) : TimeProvider +{ + /// + public override long TimestampFrequency { get; } = frequency; + + /// Gets or sets the value returned by . + internal long Timestamp { get; set; } + + /// + public override long GetTimestamp() => Timestamp; +} diff --git a/src/tests/ReactiveUI.Primitives.Tests/CurrentThreadSequencerTests.cs b/src/tests/ReactiveUI.Primitives.Tests/CurrentThreadSequencerTests.cs index 831d03a4..1c608f9f 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/CurrentThreadSequencerTests.cs +++ b/src/tests/ReactiveUI.Primitives.Tests/CurrentThreadSequencerTests.cs @@ -6,9 +6,12 @@ namespace ReactiveUI.Primitives.Tests; -/// Tests current-thread scheduling recovery after an interrupted initial wait. +/// Tests current-thread scheduling recovery after an interrupted initial wait, and provider-driven timing. public class CurrentThreadSequencerTests { + /// The delay the blocking tests wait for. + private static readonly TimeSpan OneSecond = TimeSpan.FromSeconds(1); + /// An interrupted initial wait clears the trampoline state so later work on that thread still executes. /// A task representing the asynchronous test. [Test] @@ -55,4 +58,105 @@ public async Task Schedule_InterruptedInitialWait_AllowsSubsequentWork() await Assert.That(subsequentWorkRan).IsTrue(); await Assert.That(resetAfterWork).IsTrue(); } + + /// A sequencer rejects a missing provider. + /// A task representing the asynchronous test. + [Test] + public async Task ConstructorRejectsANullProvider() => + await Assert.That(static () => new CurrentThreadSequencer(null!)).ThrowsExactly(); + + /// The current time and timestamp follow the provider. + /// A task representing the asynchronous test. + [Test] + public async Task NowAndTimestampFollowTheProvider() + { + VirtualClock clock = new(DateTimeOffset.UnixEpoch); + CurrentThreadSequencer sequencer = new(clock); + var start = sequencer.Timestamp; + + clock.AdvanceBy(OneSecond); + + await Assert.That(sequencer.Now).IsEqualTo(DateTimeOffset.UnixEpoch + OneSecond); + await Assert.That(sequencer.Timestamp - start).IsEqualTo(Sequencer.ToTimestampDelta(OneSecond)); + } + + /// Work already due runs on the calling thread without waiting. + /// A task representing the asynchronous test. + [Test] + public async Task WorkAlreadyDueRunsWithoutCreatingATimer() + { + VirtualClock clock = new(); + ScaledTimeProvider provider = new(clock, TimeSpan.TicksPerSecond); + CurrentThreadSequencer sequencer = new(provider); + RecordingWorkItem item = new(); + RecordingWorkItem cancelled = new(); + cancelled.Dispose(); + + sequencer.Schedule(item); + sequencer.Schedule(cancelled, sequencer.Timestamp); + + await Assert.That(item.ExecuteCount).IsEqualTo(1); + await Assert.That(cancelled.ExecuteCount).IsEqualTo(0); + await Assert.That(provider.TimerDueTimes).IsEmpty(); + } + + /// Delayed work blocks the calling thread until the provider's timer fires, then runs. + /// A task representing the asynchronous test. + [Test] + public async Task DelayedWorkBlocksUntilTheProviderIsAdvancedPastTheDueTime() + { + VirtualClock clock = new(); + ScaledTimeProvider provider = new(clock, TimeSpan.TicksPerSecond); + CurrentThreadSequencer sequencer = new(provider); + RecordingWorkItem item = new(); + var due = sequencer.Timestamp + Sequencer.ToTimestampDelta(OneSecond); + + var blocked = Task.Run(() => sequencer.Schedule(item, due)); + await provider.TimerCreated; + + await Assert.That(provider.TimerDueTimes.SequenceEqual([OneSecond])).IsTrue(); + await Assert.That(item.ExecuteCount).IsEqualTo(0); + + clock.AdvanceBy(OneSecond - TimeSpan.FromTicks(1)); + + await Assert.That(blocked.IsCompleted).IsFalse(); + + clock.AdvanceBy(TimeSpan.FromTicks(1)); + await blocked; + + await Assert.That(item.ExecuteCount).IsEqualTo(1); + } + + /// Work scheduled on another instance while a trampoline runs joins that trampoline. + /// A task representing the asynchronous test. + [Test] + public async Task WorkFromAnotherInstanceJoinsTheRunningTrampoline() + { + CurrentThreadSequencer running = new(new VirtualClock()); + CurrentThreadSequencer other = new(new VirtualClock()); + RecordingWorkItem nested = new(); + var ranAfterOuter = false; + + _ = running.Schedule(() => + { + other.Schedule(nested, other.Timestamp); + ranAfterOuter = nested.ExecuteCount == 0; + }); + + await Assert.That(ranAfterOuter).IsTrue(); + await Assert.That(nested.ExecuteCount).IsEqualTo(1); + } + + /// Work scheduled on the same instance while its trampoline runs keeps its own timestamp. + /// A task representing the asynchronous test. + [Test] + public async Task WorkFromTheRunningInstanceJoinsTheTrampolineAtItsOwnTimestamp() + { + CurrentThreadSequencer sequencer = new(new VirtualClock()); + RecordingWorkItem nested = new(); + + _ = sequencer.Schedule(() => sequencer.Schedule(nested, sequencer.Timestamp)); + + await Assert.That(nested.ExecuteCount).IsEqualTo(1); + } } diff --git a/src/tests/ReactiveUI.Primitives.Tests/EverySignalTests.cs b/src/tests/ReactiveUI.Primitives.Tests/EverySignalTests.cs index 10718ba5..de4c9bed 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/EverySignalTests.cs +++ b/src/tests/ReactiveUI.Primitives.Tests/EverySignalTests.cs @@ -42,6 +42,29 @@ public async Task EveryRetainsTheTickArmedByAnInlineFirstTick() await Assert.That(ticks.SequenceEqual([0L, 1L])).IsTrue(); } + /// Verifies a sequencer driven by a virtual clock used as a time provider emits one tick per period as the clock advances. + /// A task representing the asynchronous operation. + [Test] + public async Task EveryOverATimeProviderSequencerTicksOncePerPeriodAsTheClockAdvances() + { + VirtualClock clock = new(DateTimeOffset.UnixEpoch); + var sequencer = InlineThreadPool.Create(clock); + List ticks = []; + using var subscription = Signal.Every(TickPeriod, sequencer).Subscribe(ticks.Add); + + clock.AdvanceBy(TickPeriod - TimeSpan.FromTicks(1)); + + await Assert.That(ticks).IsEmpty(); + + clock.AdvanceBy(TimeSpan.FromTicks(1)); + + await Assert.That(ticks.SequenceEqual([0L])).IsTrue(); + + clock.AdvanceBy(TickPeriod + TickPeriod); + + await Assert.That(ticks.SequenceEqual(ExpectedTicks)).IsTrue(); + } + /// Verifies a bounded Every on the current-thread sequencer terminates instead of livelocking. /// A task representing the asynchronous operation. [Test] diff --git a/src/tests/ReactiveUI.Primitives.Tests/ExpireCoordinatorTests.cs b/src/tests/ReactiveUI.Primitives.Tests/ExpireCoordinatorTests.cs index b060c7eb..ced9d4ad 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/ExpireCoordinatorTests.cs +++ b/src/tests/ReactiveUI.Primitives.Tests/ExpireCoordinatorTests.cs @@ -97,6 +97,31 @@ public async Task TimeoutExpiresWhenSilenceExceedsDueTimeAfterAValue() await Assert.That(errors.SequenceEqual([nameof(TimeoutException)])).IsTrue(); } + /// Verifies a sequencer driven by a virtual clock used as a time provider expires the sequence exactly when the clock reaches the timeout. + /// A task representing the asynchronous operation. + [Test] + public async Task ExpireOverATimeProviderSequencerFiresWhenTheClockReachesTheTimeout() + { + VirtualClock clock = new(DateTimeOffset.UnixEpoch); + var sequencer = InlineThreadPool.Create(clock); + Signal source = new(); + List values = []; + List errors = []; + using var subscription = source.Expire(TimeSpan.FromTicks(DueTicks), sequencer) + .Subscribe(values.Add, ex => errors.Add(ex.GetType().Name)); + + clock.AdvanceBy(TimeSpan.FromTicks(ShortGapTicks)); + source.OnNext(One); + clock.AdvanceBy(TimeSpan.FromTicks(DueTicks - One)); + + await Assert.That(errors).IsEmpty(); + + clock.AdvanceBy(TimeSpan.FromTicks(One)); + + await Assert.That(values.SequenceEqual([One])).IsTrue(); + await Assert.That(errors.SequenceEqual([nameof(TimeoutException)])).IsTrue(); + } + /// Verifies no timer fires once the source has terminated. /// A task representing the asynchronous operation. [Test] diff --git a/src/tests/ReactiveUI.Primitives.Tests/ImmediateSequencerTests.cs b/src/tests/ReactiveUI.Primitives.Tests/ImmediateSequencerTests.cs new file mode 100644 index 00000000..65ed142e --- /dev/null +++ b/src/tests/ReactiveUI.Primitives.Tests/ImmediateSequencerTests.cs @@ -0,0 +1,118 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using ReactiveUI.Primitives.Concurrency; + +namespace ReactiveUI.Primitives.Tests; + +/// Tests driven by a . +public class ImmediateSequencerTests +{ + /// The delay the blocking tests wait for. + private static readonly TimeSpan OneSecond = TimeSpan.FromSeconds(1); + + /// The smallest step of the virtual clock. + private static readonly TimeSpan OneTick = TimeSpan.FromTicks(1); + + /// A sequencer rejects a missing provider. + /// A task representing the asynchronous test. + [Test] + public async Task ConstructorRejectsANullProvider() => + await Assert.That(static () => new ImmediateSequencer(null!)).ThrowsExactly(); + + /// The current time and timestamp follow the provider. + /// A task representing the asynchronous test. + [Test] + public async Task NowAndTimestampFollowTheProvider() + { + VirtualClock clock = new(DateTimeOffset.UnixEpoch); + ImmediateSequencer sequencer = new(clock); + var start = sequencer.Timestamp; + + clock.AdvanceBy(OneSecond); + + await Assert.That(sequencer.Now).IsEqualTo(DateTimeOffset.UnixEpoch + OneSecond); + await Assert.That(sequencer.Timestamp - start).IsEqualTo(Sequencer.ToTimestampDelta(OneSecond)); + } + + /// Work runs before the call returns, and cancelled work is skipped. + /// A task representing the asynchronous test. + [Test] + public async Task ImmediateWorkRunsInlineUnlessCancelled() + { + ImmediateSequencer sequencer = new(new VirtualClock()); + RecordingWorkItem item = new(); + RecordingWorkItem cancelled = new(); + cancelled.Dispose(); + + sequencer.Schedule(item); + sequencer.Schedule(cancelled); + + await Assert.That(item.ExecuteCount).IsEqualTo(1); + await Assert.That(cancelled.ExecuteCount).IsEqualTo(0); + } + + /// Work already due runs without waiting. + /// A task representing the asynchronous test. + [Test] + public async Task WorkAlreadyDueRunsWithoutCreatingATimer() + { + VirtualClock clock = new(); + ScaledTimeProvider provider = new(clock, TimeSpan.TicksPerSecond); + ImmediateSequencer sequencer = new(provider); + RecordingWorkItem item = new(); + + sequencer.Schedule(item, sequencer.Timestamp); + + await Assert.That(item.ExecuteCount).IsEqualTo(1); + await Assert.That(provider.TimerDueTimes).IsEmpty(); + } + + /// Delayed work blocks the caller until the provider's timer fires, then runs. + /// A task representing the asynchronous test. + [Test] + public async Task DelayedWorkBlocksUntilTheProviderIsAdvancedPastTheDueTime() + { + VirtualClock clock = new(); + ScaledTimeProvider provider = new(clock, TimeSpan.TicksPerSecond); + ImmediateSequencer sequencer = new(provider); + RecordingWorkItem item = new(); + var due = sequencer.Timestamp + Sequencer.ToTimestampDelta(OneSecond); + + var blocked = Task.Run(() => sequencer.Schedule(item, due)); + await provider.TimerCreated; + + await Assert.That(provider.TimerDueTimes.SequenceEqual([OneSecond])).IsTrue(); + await Assert.That(item.ExecuteCount).IsEqualTo(0); + + clock.AdvanceBy(OneSecond - OneTick); + + await Assert.That(blocked.IsCompleted).IsFalse(); + + clock.AdvanceBy(OneTick); + await blocked; + + await Assert.That(item.ExecuteCount).IsEqualTo(1); + } + + /// Delayed work cancelled while its caller is blocked does not run. + /// A task representing the asynchronous test. + [Test] + public async Task DelayedWorkCancelledWhileBlockedDoesNotRun() + { + VirtualClock clock = new(); + ScaledTimeProvider provider = new(clock, TimeSpan.TicksPerSecond); + ImmediateSequencer sequencer = new(provider); + RecordingWorkItem item = new(); + var due = sequencer.Timestamp + Sequencer.ToTimestampDelta(OneSecond); + + var blocked = Task.Run(() => sequencer.Schedule(item, due)); + await provider.TimerCreated; + item.Dispose(); + clock.AdvanceBy(OneSecond); + await blocked; + + await Assert.That(item.ExecuteCount).IsEqualTo(0); + } +} diff --git a/src/tests/ReactiveUI.Primitives.Tests/SequencerClockTests.cs b/src/tests/ReactiveUI.Primitives.Tests/SequencerClockTests.cs new file mode 100644 index 00000000..d0b9273f --- /dev/null +++ b/src/tests/ReactiveUI.Primitives.Tests/SequencerClockTests.cs @@ -0,0 +1,155 @@ +// 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.Diagnostics; +using ReactiveUI.Primitives.Concurrency; + +namespace ReactiveUI.Primitives.Tests; + +/// Tests timestamp scaling, waiting and timer creation. +public class SequencerClockTests +{ + /// Timestamp ticks per second of a provider that differs from and divides evenly into it. + private const long MillisecondFrequency = 1000; + + /// Timestamp ticks per second of a provider whose scale factor is not a whole number. + private const long ThirdsFrequency = 3; + + /// The provider timestamp at which a rescaling clock is created. + private const long OriginTimestamp = 5_000_000; + + /// Elapsed provider ticks used by the rescaling tests. + private const long ElapsedMilliseconds = 1500; + + /// The delay used by the waiting and timer tests. + private static readonly TimeSpan OneSecond = TimeSpan.FromSeconds(1); + + /// A clock rejects a missing provider. + /// A task representing the asynchronous test. + [Test] + public async Task ConstructorRejectsANullProvider() => + await Assert.That(static () => new SequencerClock(null!)).ThrowsExactly(); + + /// A provider counting at passes its timestamps through unchanged. + /// A task representing the asynchronous test. + [Test] + public async Task ProviderAtStopwatchFrequencyPassesTimestampsThrough() + { + SettableTimeProvider provider = new(Stopwatch.Frequency) { Timestamp = OriginTimestamp }; + SequencerClock clock = new(provider); + + await Assert.That(clock.GetTimestamp()).IsEqualTo(OriginTimestamp); + + provider.Timestamp += Stopwatch.Frequency; + + await Assert.That(clock.GetTimestamp()).IsEqualTo(OriginTimestamp + Stopwatch.Frequency); + } + + /// A provider with another frequency reports timestamps in stopwatch units counted from the clock's creation. + /// A task representing the asynchronous test. + [Test] + public async Task ProviderWithAnotherFrequencyIsRescaledFromItsCreationTimestamp() + { + SettableTimeProvider provider = new(MillisecondFrequency) { Timestamp = OriginTimestamp }; + SequencerClock clock = new(provider); + + await Assert.That(clock.GetTimestamp()).IsEqualTo(0); + + provider.Timestamp += ElapsedMilliseconds; + + await Assert.That(clock.GetTimestamp()).IsEqualTo(Stopwatch.Frequency * ElapsedMilliseconds / MillisecondFrequency); + } + + /// A scale factor that is not a whole number truncates the fractional stopwatch unit. + /// A task representing the asynchronous test. + [Test] + public async Task RescalingTruncatesAFractionalStopwatchUnit() + { + SettableTimeProvider provider = new(ThirdsFrequency); + SequencerClock clock = new(provider); + + provider.Timestamp = 1; + + await Assert.That(clock.GetTimestamp()).IsEqualTo(Stopwatch.Frequency / ThirdsFrequency); + } + + /// The remaining time is measured on the provider's clock and never goes negative. + /// A task representing the asynchronous test. + [Test] + public async Task TimeUntilMeasuresTheRemainingTimeOnTheProvidersClock() + { + SettableTimeProvider provider = new(MillisecondFrequency) { Timestamp = OriginTimestamp }; + SequencerClock clock = new(provider); + var due = clock.GetTimestamp() + Sequencer.ToTimestampDelta(OneSecond); + + await Assert.That(clock.TimeUntil(due)).IsEqualTo(OneSecond); + + provider.Timestamp += MillisecondFrequency; + + await Assert.That(clock.TimeUntil(due)).IsEqualTo(TimeSpan.Zero); + } + + /// The current time comes from the provider. + /// A task representing the asynchronous test. + [Test] + public async Task GetUtcNowReadsTheProvider() + { + VirtualClock virtualClock = new(DateTimeOffset.UnixEpoch); + SequencerClock clock = new(virtualClock); + + virtualClock.AdvanceBy(OneSecond); + + await Assert.That(clock.GetUtcNow()).IsEqualTo(DateTimeOffset.UnixEpoch + OneSecond); + } + + /// Timers are created through the provider and fire when the provider's time reaches them. + /// A task representing the asynchronous test. + [Test] + public async Task CreateTimerUsesTheProvider() + { + VirtualClock virtualClock = new(); + SequencerClock clock = new(virtualClock); + var fired = 0; + + await using var timer = clock.CreateTimer(_ => fired++, null, OneSecond, Timeout.InfiniteTimeSpan); + virtualClock.AdvanceBy(OneSecond - TimeSpan.FromTicks(1)); + + await Assert.That(fired).IsEqualTo(0); + + virtualClock.AdvanceBy(TimeSpan.FromTicks(1)); + + await Assert.That(fired).IsEqualTo(1); + } + + /// The system clock waits on the calling thread and returns immediately for an empty delay. + /// A task representing the asynchronous test. + [Test] + public async Task SystemClockWaitReturnsForAnEmptyDelay() + { + SequencerClock.Default.Wait(TimeSpan.Zero); + + await Assert.That(SequencerClock.Default.GetTimestamp()).IsGreaterThan(0); + } + + /// A wait on another provider blocks until that provider's timer fires. + /// A task representing the asynchronous test. + [Test] + public async Task WaitBlocksUntilTheProvidersTimerFires() + { + VirtualClock virtualClock = new(); + ScaledTimeProvider provider = new(virtualClock, TimeSpan.TicksPerSecond); + SequencerClock clock = new(provider); + + var waiter = Task.Run(() => clock.Wait(OneSecond)); + await provider.TimerCreated; + + await Assert.That(waiter.IsCompleted).IsFalse(); + await Assert.That(provider.TimerDueTimes.SequenceEqual([OneSecond])).IsTrue(); + + virtualClock.AdvanceBy(OneSecond); + await waiter; + + await Assert.That(waiter.IsCompletedSuccessfully).IsTrue(); + } +} diff --git a/src/tests/ReactiveUI.Primitives.Tests/SequencerImmediacyExtensionsTests.cs b/src/tests/ReactiveUI.Primitives.Tests/SequencerImmediacyExtensionsTests.cs new file mode 100644 index 00000000..5f11161c --- /dev/null +++ b/src/tests/ReactiveUI.Primitives.Tests/SequencerImmediacyExtensionsTests.cs @@ -0,0 +1,71 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using ReactiveUI.Primitives.Concurrency; + +namespace ReactiveUI.Primitives.Tests; + +/// Tests immediacy detection. +public class SequencerImmediacyExtensionsTests +{ + /// The immediate sequencer reports itself as immediate. + /// A task representing the asynchronous test. + [Test] + public async Task IsImmediateIsTrueForTheImmediateSequencer() => + await Assert.That(Sequencer.Immediate.IsImmediate).IsTrue(); + + /// The immediate sequencer reports itself as immediate when held as the sequencer interface. + /// A task representing the asynchronous test. + [Test] + public async Task IsImmediateIsTrueForTheImmediateSequencerBehindTheInterface() + { + ISequencer sequencer = ImmediateSequencer.Instance; + + await Assert.That(sequencer.IsImmediate).IsTrue(); + } + + /// The current-thread sequencer is not immediate. + /// A task representing the asynchronous test. + [Test] + public async Task IsImmediateIsFalseForTheCurrentThreadSequencer() => + await Assert.That(Sequencer.CurrentThread.IsImmediate).IsFalse(); + + /// The thread-pool sequencer is not immediate. + /// A task representing the asynchronous test. + [Test] + public async Task IsImmediateIsFalseForTheThreadPoolSequencer() => + await Assert.That(ThreadPoolSequencer.Instance.IsImmediate).IsFalse(); + + /// The task-pool sequencer is not immediate. + /// A task representing the asynchronous test. + [Test] + public async Task IsImmediateIsFalseForTheTaskPoolSequencer() => + await Assert.That(TaskPoolSequencer.Instance.IsImmediate).IsFalse(); + + /// The default sequencer is not immediate. + /// A task representing the asynchronous test. + [Test] + public async Task IsImmediateIsFalseForTheDefaultSequencer() => + await Assert.That(Sequencer.Default.IsImmediate).IsFalse(); + + /// A virtual clock is not immediate. + /// A task representing the asynchronous test. + [Test] + public async Task IsImmediateIsFalseForAVirtualClock() + { + VirtualClock clock = new(); + + await Assert.That(clock.IsImmediate).IsFalse(); + } + + /// A null sequencer is not immediate. + /// A task representing the asynchronous test. + [Test] + public async Task IsImmediateIsFalseForNull() + { + ISequencer? sequencer = null; + + await Assert.That(sequencer.IsImmediate).IsFalse(); + } +} diff --git a/src/tests/ReactiveUI.Primitives.Tests/SignalOperatorMixinsTests.Delay.cs b/src/tests/ReactiveUI.Primitives.Tests/SignalOperatorMixinsTests.Delay.cs index d50d6662..c4ca2d67 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/SignalOperatorMixinsTests.Delay.cs +++ b/src/tests/ReactiveUI.Primitives.Tests/SignalOperatorMixinsTests.Delay.cs @@ -176,6 +176,35 @@ public async Task ShiftReschedulesWhenNextNotificationIsNotYetDue() await Assert.That(sequencer.ScheduledCount).IsEqualTo(0); } + /// Verifies shift over a sequencer driven by a virtual clock used as a time provider delivers each value one due time after it arrived. + /// A task representing the asynchronous operation. + [Test] + public async Task ShiftOverATimeProviderSequencerDeliversEachValueOneDueTimeLater() + { + VirtualClock clock = new(DateTimeOffset.UnixEpoch); + var sequencer = InlineThreadPool.Create(clock); + var step = TimeSpan.FromTicks(Five); + var dueTime = TimeSpan.FromTicks(Ten); + Signal source = new(); + RecordingWitness observer = new(); + using var subscription = source.Shift(dueTime, sequencer).Subscribe(observer); + + source.OnNext(One); + clock.AdvanceBy(step); + source.OnNext(Two); + clock.AdvanceBy(step - TimeSpan.FromTicks(One)); + + await Assert.That(observer.Values.Count).IsEqualTo(0); + + clock.AdvanceBy(TimeSpan.FromTicks(One)); + + await Assert.That(observer.Values.SequenceEqual([One])).IsTrue(); + + clock.AdvanceBy(step); + + await Assert.That(observer.Values.SequenceEqual([One, Two])).IsTrue(); + } + /// Sequencer that records scheduled work for deterministic execution. private sealed class RecordingSequencer : ISequencer { diff --git a/src/tests/ReactiveUI.Primitives.Tests/SynchronizationContextSequencerTests.cs b/src/tests/ReactiveUI.Primitives.Tests/SynchronizationContextSequencerTests.cs new file mode 100644 index 00000000..e0c33ef5 --- /dev/null +++ b/src/tests/ReactiveUI.Primitives.Tests/SynchronizationContextSequencerTests.cs @@ -0,0 +1,100 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using ReactiveUI.Primitives.Concurrency; + +namespace ReactiveUI.Primitives.Tests; + +/// Tests driven by a . +public class SynchronizationContextSequencerTests +{ + /// The delay used for scheduled work. + private static readonly TimeSpan OneSecond = TimeSpan.FromSeconds(1); + + /// The smallest step of the virtual clock. + private static readonly TimeSpan OneTick = TimeSpan.FromTicks(1); + + /// A sequencer rejects a missing context or provider. + /// A task representing the asynchronous test. + [Test] + public async Task ConstructorRejectsANullContextOrProvider() + { + await Assert.That(static () => new SynchronizationContextSequencer(null!, (TimeProvider)new VirtualClock())) + .ThrowsExactly(); + await Assert.That(static () => new SynchronizationContextSequencer(new(), (TimeProvider)null!)) + .ThrowsExactly(); + } + + /// The current time and timestamp follow the provider. + /// A task representing the asynchronous test. + [Test] + public async Task NowAndTimestampFollowTheProvider() + { + VirtualClock clock = new(DateTimeOffset.UnixEpoch); + TimeProvider provider = clock; + SynchronizationContextSequencer sequencer = new(new(), provider); + var start = sequencer.Timestamp; + + clock.AdvanceBy(OneSecond); + + await Assert.That(sequencer.Now).IsEqualTo(DateTimeOffset.UnixEpoch + OneSecond); + await Assert.That(sequencer.Timestamp - start).IsEqualTo(Sequencer.ToTimestampDelta(OneSecond)); + } + + /// Delayed work is posted to the context only once the provider is advanced to its timestamp. + /// A task representing the asynchronous test. + [Test] + public async Task DelayedWorkIsPostedWhenTheProviderReachesItsTimestamp() + { + VirtualClock clock = new(); + TimeProvider provider = clock; + InlineContext context = new(); + SynchronizationContextSequencer sequencer = new(context, provider); + RecordingWorkItem item = new(); + + sequencer.Schedule(item, sequencer.Timestamp + Sequencer.ToTimestampDelta(OneSecond)); + clock.AdvanceBy(OneSecond - OneTick); + + await Assert.That(context.PostCount).IsEqualTo(0); + await Assert.That(item.ExecuteCount).IsEqualTo(0); + + clock.AdvanceBy(OneTick); + + await Assert.That(context.PostCount).IsEqualTo(1); + await Assert.That(item.ExecuteCount).IsEqualTo(1); + } + + /// Delayed work cancelled before it is due never reaches the context. + /// A task representing the asynchronous test. + [Test] + public async Task CancelledDelayedWorkIsNotPosted() + { + VirtualClock clock = new(); + TimeProvider provider = clock; + InlineContext context = new(); + SynchronizationContextSequencer sequencer = new(context, provider); + RecordingWorkItem item = new(); + + sequencer.Schedule(item, sequencer.Timestamp + Sequencer.ToTimestampDelta(OneSecond)); + item.Dispose(); + clock.AdvanceBy(OneSecond); + + await Assert.That(context.PostCount).IsEqualTo(0); + await Assert.That(item.ExecuteCount).IsEqualTo(0); + } + + /// A synchronization context that runs posted work inline and counts the posts. + private sealed class InlineContext : SynchronizationContext + { + /// Gets the number of posted callbacks. + internal int PostCount { get; private set; } + + /// + public override void Post(SendOrPostCallback d, object? state) + { + PostCount++; + d(state); + } + } +} diff --git a/src/tests/ReactiveUI.Primitives.Tests/TaskPoolSequencerTests.cs b/src/tests/ReactiveUI.Primitives.Tests/TaskPoolSequencerTests.cs index aa252f0e..fd682a2c 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/TaskPoolSequencerTests.cs +++ b/src/tests/ReactiveUI.Primitives.Tests/TaskPoolSequencerTests.cs @@ -157,4 +157,77 @@ public async Task Schedule_AdvanceClockToDue_DispatchesOnce() [Test] public async Task Constructor_NullTaskFactory_ThrowsArgumentNull() => await Assert.That(static () => new TaskPoolSequencer(null!)).ThrowsExactly(); + + /// A null task factory or time provider is rejected by the provider constructor. + /// A task representing the asynchronous test. + [Test] + public async Task ProviderConstructorRejectsANullTaskFactoryOrProvider() + { + TimeProvider provider = new VirtualClock(); + + await Assert.That(() => new TaskPoolSequencer(null!, provider)).ThrowsExactly(); + await Assert.That(static () => new TaskPoolSequencer(new(new ManualTaskScheduler()), (TimeProvider)null!)) + .ThrowsExactly(); + } + + /// The current time and timestamp follow the provider. + /// A task representing the asynchronous test. + [Test] + public async Task NowAndTimestampFollowTheProvider() + { + VirtualClock clock = new(DateTimeOffset.UnixEpoch); + TimeProvider provider = clock; + TaskPoolSequencer sequencer = new(new(new ManualTaskScheduler()), provider); + var start = sequencer.Timestamp; + + clock.AdvanceBy(TimeSpan.FromSeconds(1)); + + await Assert.That(sequencer.Now).IsEqualTo(DateTimeOffset.UnixEpoch + TimeSpan.FromSeconds(1)); + await Assert.That(sequencer.Timestamp - start).IsEqualTo(Sequencer.ToTimestampDelta(TimeSpan.FromSeconds(1))); + } + + /// Delayed work reaches the task factory only when the provider is advanced to its timestamp. + /// A task representing the asynchronous test. + [Test] + public async Task DelayedWorkIsDispatchedWhenTheProviderReachesItsTimestamp() + { + VirtualClock clock = new(); + TimeProvider provider = clock; + ManualTaskScheduler tasks = new(); + TaskPoolSequencer sequencer = new(new(tasks), provider); + RecordingWorkItem item = new(); + var oneSecond = TimeSpan.FromSeconds(1); + + sequencer.Schedule(item, sequencer.Timestamp + Sequencer.ToTimestampDelta(oneSecond)); + clock.AdvanceBy(oneSecond - TimeSpan.FromTicks(1)); + tasks.RunPending(); + + await Assert.That(item.ExecuteCount).IsEqualTo(0); + + clock.AdvanceBy(TimeSpan.FromTicks(1)); + await Assert.That(item.ExecuteCount).IsEqualTo(0); + tasks.RunPending(); + + await Assert.That(item.ExecuteCount).IsEqualTo(1); + } + + /// Delayed work cancelled before its timestamp never runs. + /// A task representing the asynchronous test. + [Test] + public async Task CancelledDelayedWorkIsNotDispatched() + { + VirtualClock clock = new(); + TimeProvider provider = clock; + ManualTaskScheduler tasks = new(); + TaskPoolSequencer sequencer = new(new(tasks), provider); + RecordingWorkItem item = new(); + var oneSecond = TimeSpan.FromSeconds(1); + + sequencer.Schedule(item, sequencer.Timestamp + Sequencer.ToTimestampDelta(oneSecond)); + item.Dispose(); + clock.AdvanceBy(oneSecond); + tasks.RunPending(); + + await Assert.That(item.ExecuteCount).IsEqualTo(0); + } } diff --git a/src/tests/ReactiveUI.Primitives.Tests/ThreadPoolSequencerTests.cs b/src/tests/ReactiveUI.Primitives.Tests/ThreadPoolSequencerTests.cs new file mode 100644 index 00000000..a01390f5 --- /dev/null +++ b/src/tests/ReactiveUI.Primitives.Tests/ThreadPoolSequencerTests.cs @@ -0,0 +1,211 @@ +// 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.Diagnostics; +using ReactiveUI.Primitives.Concurrency; + +namespace ReactiveUI.Primitives.Tests; + +/// Tests driven by a . +public class ThreadPoolSequencerTests +{ + /// Timestamp ticks per second of a provider that counts milliseconds. + private const long MillisecondFrequency = 1000; + + /// Name recorded by the work due after one second. + private const string First = "first"; + + /// Name recorded by the work due after two seconds. + private const string Second = "second"; + + /// A short delay used for scheduled work. + private static readonly TimeSpan OneSecond = TimeSpan.FromSeconds(1); + + /// A delay later than . + private static readonly TimeSpan TwoSeconds = TimeSpan.FromSeconds(2); + + /// The smallest step of the virtual clock. + private static readonly TimeSpan OneTick = TimeSpan.FromTicks(1); + + /// A sequencer rejects a missing provider. + /// A task representing the asynchronous test. + [Test] + public async Task ConstructorRejectsANullProvider() => + await Assert.That(static () => new ThreadPoolSequencer((TimeProvider)null!)).ThrowsExactly(); + + /// The current time and timestamp follow the provider. + /// A task representing the asynchronous test. + [Test] + public async Task NowAndTimestampFollowTheProvider() + { + VirtualClock clock = new(DateTimeOffset.UnixEpoch); + var sequencer = InlineThreadPool.Create(clock); + var start = sequencer.Timestamp; + + clock.AdvanceBy(TwoSeconds); + + await Assert.That(sequencer.Now).IsEqualTo(DateTimeOffset.UnixEpoch + TwoSeconds); + await Assert.That(sequencer.Timestamp - start).IsEqualTo(Sequencer.ToTimestampDelta(TwoSeconds)); + } + + /// Work scheduled for a timestamp runs when the provider is advanced to it and not before. + /// A task representing the asynchronous test. + [Test] + public async Task ScheduledWorkRunsExactlyWhenTheProviderReachesItsTimestamp() + { + VirtualClock clock = new(); + var sequencer = InlineThreadPool.Create(clock); + RecordingWorkItem item = new(); + + sequencer.Schedule(item, sequencer.Timestamp + Sequencer.ToTimestampDelta(OneSecond)); + clock.AdvanceBy(OneSecond - OneTick); + + await Assert.That(item.ExecuteCount).IsEqualTo(0); + + clock.AdvanceBy(OneTick); + + await Assert.That(item.ExecuteCount).IsEqualTo(1); + + clock.AdvanceBy(TwoSeconds); + + await Assert.That(item.ExecuteCount).IsEqualTo(1); + } + + /// Work due at several timestamps runs in due order as the provider advances. + /// A task representing the asynchronous test. + [Test] + public async Task ScheduledWorkRunsInDueOrder() + { + VirtualClock clock = new(); + var sequencer = InlineThreadPool.Create(clock); + List order = []; + + _ = sequencer.Schedule(order, TwoSeconds, static names => names.Add(Second)); + _ = sequencer.Schedule(order, OneSecond, static names => names.Add(First)); + clock.AdvanceBy(OneSecond); + + await Assert.That(order.SequenceEqual([First])).IsTrue(); + + clock.AdvanceBy(OneSecond); + + await Assert.That(order.SequenceEqual([First, Second])).IsTrue(); + } + + /// Work scheduled by due work re-arms the delay timer. + /// A task representing the asynchronous test. + [Test] + public async Task WorkScheduledByDueWorkRunsWhenItsOwnTimestampIsReached() + { + VirtualClock clock = new(); + var sequencer = InlineThreadPool.Create(clock); + RecordingWorkItem later = new(); + + _ = sequencer.Schedule( + (Owner: sequencer, Later: later), + OneSecond, + static state => state.Owner.Schedule(state.Later, state.Owner.Timestamp + Sequencer.ToTimestampDelta(OneSecond))); + clock.AdvanceBy(OneSecond); + + await Assert.That(later.ExecuteCount).IsEqualTo(0); + + clock.AdvanceBy(OneSecond); + + await Assert.That(later.ExecuteCount).IsEqualTo(1); + } + + /// Cancelled work never runs. + /// A task representing the asynchronous test. + [Test] + public async Task CancelledWorkDoesNotRun() + { + VirtualClock clock = new(); + var sequencer = InlineThreadPool.Create(clock); + RecordingWorkItem cancelled = new(); + RecordingWorkItem kept = new(); + + sequencer.Schedule(cancelled, sequencer.Timestamp + Sequencer.ToTimestampDelta(OneSecond)); + sequencer.Schedule(kept, sequencer.Timestamp + Sequencer.ToTimestampDelta(TwoSeconds)); + cancelled.Dispose(); + clock.AdvanceBy(TwoSeconds); + + await Assert.That(cancelled.ExecuteCount).IsEqualTo(0); + await Assert.That(kept.ExecuteCount).IsEqualTo(1); + } + + /// Immediate work runs through the immediate queue without touching the delay timer. + /// A task representing the asynchronous test. + [Test] + public async Task ImmediateWorkDoesNotCreateAnyTimer() + { + VirtualClock clock = new(); + ScaledTimeProvider provider = new(clock, TimeSpan.TicksPerSecond); + var sequencer = InlineThreadPool.Create(provider); + RecordingWorkItem now = new(); + RecordingWorkItem elapsed = new(); + + sequencer.Schedule(now); + sequencer.Schedule(elapsed, sequencer.Timestamp); + + await Assert.That(now.ExecuteCount).IsEqualTo(1); + await Assert.That(elapsed.ExecuteCount).IsEqualTo(1); + await Assert.That(provider.TimerDueTimes).IsEmpty(); + } + + /// All delayed work shares one timer created by the provider on first use. + /// A task representing the asynchronous test. + [Test] + public async Task DelayedWorkSharesOneProviderTimerCreatedOnFirstUse() + { + VirtualClock clock = new(); + ScaledTimeProvider provider = new(clock, TimeSpan.TicksPerSecond); + var sequencer = InlineThreadPool.Create(provider); + + await Assert.That(provider.TimerDueTimes).IsEmpty(); + + sequencer.Schedule(new RecordingWorkItem(), sequencer.Timestamp + Sequencer.ToTimestampDelta(OneSecond)); + sequencer.Schedule(new RecordingWorkItem(), sequencer.Timestamp + Sequencer.ToTimestampDelta(TwoSeconds)); + + await Assert.That(provider.TimerDueTimes.Length).IsEqualTo(1); + } + + /// A provider counting at a different frequency drives delayed work at the right wall time. + /// A task representing the asynchronous test. + [Test] + public async Task ProviderWithAnotherFrequencyDrivesDelayedWork() + { + VirtualClock clock = new(); + ScaledTimeProvider provider = new(clock, MillisecondFrequency); + var sequencer = InlineThreadPool.Create(provider); + RecordingWorkItem item = new(); + + await Assert.That(provider.TimestampFrequency).IsNotEqualTo(Stopwatch.Frequency); + + sequencer.Schedule(item, sequencer.Timestamp + Sequencer.ToTimestampDelta(OneSecond)); + clock.AdvanceBy(OneSecond - TimeSpan.FromMilliseconds(1)); + + await Assert.That(item.ExecuteCount).IsEqualTo(0); + + clock.AdvanceBy(TimeSpan.FromMilliseconds(1)); + + await Assert.That(item.ExecuteCount).IsEqualTo(1); + await Assert.That(sequencer.Timestamp).IsEqualTo(Sequencer.ToTimestampDelta(OneSecond)); + } + + /// Disposal cancels pending delayed work and stops the delay timer. + /// A task representing the asynchronous test. + [Test] + public async Task DisposeCancelsPendingWorkAndStopsTheTimer() + { + VirtualClock clock = new(); + var sequencer = InlineThreadPool.Create(clock); + RecordingWorkItem pending = new(); + + sequencer.Schedule(pending, sequencer.Timestamp + Sequencer.ToTimestampDelta(OneSecond)); + sequencer.Dispose(); + clock.AdvanceBy(TwoSeconds); + + await Assert.That(pending.IsDisposed).IsTrue(); + await Assert.That(pending.ExecuteCount).IsEqualTo(0); + } +} diff --git a/src/tests/ReactiveUI.Primitives.Tests/VirtualClockTests.cs b/src/tests/ReactiveUI.Primitives.Tests/VirtualClockTests.cs new file mode 100644 index 00000000..1b044f1c --- /dev/null +++ b/src/tests/ReactiveUI.Primitives.Tests/VirtualClockTests.cs @@ -0,0 +1,328 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using ReactiveUI.Primitives.Concurrency; + +namespace ReactiveUI.Primitives.Tests; + +/// Tests as a . +public class VirtualClockTests +{ + /// The delay of the first firing in the timer tests. + private static readonly TimeSpan OneSecond = TimeSpan.FromSeconds(1); + + /// The interval between periodic firings in the timer tests. + private static readonly TimeSpan TwoSeconds = TimeSpan.FromSeconds(2); + + /// A delay later than every firing the timer tests expect. + private static readonly TimeSpan TenSeconds = TimeSpan.FromSeconds(10); + + /// The virtual time starts at the unix epoch. + private static readonly DateTimeOffset Epoch = DateTimeOffset.UnixEpoch; + + /// The times a timer that first fires after one second and then every two seconds reaches within ten seconds. + private static readonly TimeSpan[] OddSeconds = + [ + TimeSpan.FromSeconds(1), + TimeSpan.FromSeconds(3), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(7), + TimeSpan.FromSeconds(9), + ]; + + /// The clock is usable wherever a time provider is expected. + /// A task representing the asynchronous test. + [Test] + public async Task ClockIsATimeProvider() + { + TimeProvider provider = new VirtualClock(); + + await Assert.That(provider).IsTypeOf(); + } + + /// The UTC time follows the virtual clock as it advances. + /// A task representing the asynchronous test. + [Test] + public async Task GetUtcNowFollowsTheVirtualClock() + { + VirtualClock clock = new(Epoch); + TimeProvider provider = clock; + + await Assert.That(provider.GetUtcNow()).IsEqualTo(Epoch); + + clock.AdvanceBy(TenSeconds); + + await Assert.That(provider.GetUtcNow()).IsEqualTo(Epoch + TenSeconds); + await Assert.That(provider.GetUtcNow()).IsEqualTo(clock.Now); + } + + /// Timestamps count ticks of the virtual clock at . + /// A task representing the asynchronous test. + [Test] + public async Task GetTimestampCountsTicksOfTheVirtualClock() + { + VirtualClock clock = new(Epoch); + TimeProvider provider = clock; + + await Assert.That(provider.TimestampFrequency).IsEqualTo(TimeSpan.TicksPerSecond); + await Assert.That(provider.GetTimestamp()).IsEqualTo(Epoch.UtcTicks); + + clock.AdvanceBy(OneSecond); + + await Assert.That(provider.GetTimestamp()).IsEqualTo((Epoch + OneSecond).UtcTicks); + } + + /// Elapsed time between two timestamps equals the virtual time that passed. + /// A task representing the asynchronous test. + [Test] + public async Task GetElapsedTimeMeasuresVirtualTime() + { + VirtualClock clock = new(Epoch); + TimeProvider provider = clock; + var start = provider.GetTimestamp(); + + clock.AdvanceBy(TenSeconds); + + await Assert.That(provider.GetElapsedTime(start)).IsEqualTo(TenSeconds); + } + + /// A one-shot timer fires exactly once, when the clock reaches its due time. + /// A task representing the asynchronous test. + [Test] + public async Task OneShotTimerFiresOnceWhenTheClockReachesItsDueTime() + { + VirtualClock clock = new(Epoch); + List fired = []; + await using var timer =clock.CreateTimer(_ => fired.Add(clock.Now), null, OneSecond, Timeout.InfiniteTimeSpan); + + clock.AdvanceBy(OneSecond - TimeSpan.FromTicks(1)); + + await Assert.That(fired).IsEmpty(); + + clock.AdvanceBy(TenSeconds); + + await Assert.That(fired.SequenceEqual([Epoch + OneSecond])).IsTrue(); + } + + /// A timer hands its state to the callback. + /// A task representing the asynchronous test. + [Test] + public async Task TimerPassesItsStateToTheCallback() + { + VirtualClock clock = new(Epoch); + object state = new(); + object? received = null; + await using var timer =clock.CreateTimer(s => received = s, state, OneSecond, Timeout.InfiniteTimeSpan); + + clock.AdvanceBy(OneSecond); + + await Assert.That(received).IsSameReferenceAs(state); + } + + /// A periodic timer fires after its due time and then once per period. + /// A task representing the asynchronous test. + [Test] + public async Task PeriodicTimerFiresOnceEveryPeriod() + { + VirtualClock clock = new(Epoch); + List fired = []; + await using var timer =clock.CreateTimer(_ => fired.Add(clock.Now - Epoch), null, OneSecond, TwoSeconds); + + clock.AdvanceBy(TenSeconds - TimeSpan.FromTicks(1)); + + await Assert.That(fired.SequenceEqual(OddSeconds)).IsTrue(); + } + + /// A timer with a zero period fires once. + /// A task representing the asynchronous test. + [Test] + public async Task TimerWithAZeroPeriodFiresOnce() + { + VirtualClock clock = new(Epoch); + var fired = 0; + await using var timer =clock.CreateTimer(_ => fired++, null, OneSecond, TimeSpan.Zero); + + clock.AdvanceBy(TenSeconds); + + await Assert.That(fired).IsEqualTo(1); + } + + /// A timer with a zero due time fires when the clock next runs work. + /// A task representing the asynchronous test. + [Test] + public async Task TimerWithAZeroDueTimeFiresWhenTheClockStarts() + { + VirtualClock clock = new(Epoch); + var fired = 0; + await using var timer =clock.CreateTimer(_ => fired++, null, TimeSpan.Zero, Timeout.InfiniteTimeSpan); + + await Assert.That(fired).IsEqualTo(0); + + clock.Start(); + + await Assert.That(fired).IsEqualTo(1); + } + + /// A timer created with an infinite due time stays stopped until it is changed. + /// A task representing the asynchronous test. + [Test] + public async Task TimerWithAnInfiniteDueTimeStaysStoppedUntilChanged() + { + VirtualClock clock = new(Epoch); + var fired = 0; + await using var timer =clock.CreateTimer(_ => fired++, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); + + clock.AdvanceBy(TenSeconds); + + await Assert.That(fired).IsEqualTo(0); + + await Assert.That(timer.Change(OneSecond, Timeout.InfiniteTimeSpan)).IsTrue(); + clock.AdvanceBy(TenSeconds); + + await Assert.That(fired).IsEqualTo(1); + } + + /// Changing a timer replaces its pending firing. + /// A task representing the asynchronous test. + [Test] + public async Task ChangeReplacesThePendingFiring() + { + VirtualClock clock = new(Epoch); + List fired = []; + await using var timer =clock.CreateTimer(_ => fired.Add(clock.Now - Epoch), null, TenSeconds, Timeout.InfiniteTimeSpan); + + await Assert.That(timer.Change(TwoSeconds, Timeout.InfiniteTimeSpan)).IsTrue(); + clock.AdvanceBy(TenSeconds + TenSeconds); + + await Assert.That(fired.SequenceEqual([TwoSeconds])).IsTrue(); + } + + /// Changing a timer to an infinite due time cancels its pending firing. + /// A task representing the asynchronous test. + [Test] + public async Task ChangeToAnInfiniteDueTimeCancelsThePendingFiring() + { + VirtualClock clock = new(Epoch); + var fired = 0; + await using var timer =clock.CreateTimer(_ => fired++, null, OneSecond, OneSecond); + + _ = timer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); + clock.AdvanceBy(TenSeconds); + + await Assert.That(fired).IsEqualTo(0); + } + + /// A timer that changes its own period from inside the callback keeps firing at the new period. + /// A task representing the asynchronous test. + [Test] + public async Task ChangeInsideTheCallbackSetsTheNextFiring() + { + VirtualClock clock = new(Epoch); + List fired = []; + ITimer? timer = null; + timer = clock.CreateTimer( + _ => + { + fired.Add(clock.Now - Epoch); + _ = timer!.Change(TwoSeconds, Timeout.InfiniteTimeSpan); + }, + null, + OneSecond, + Timeout.InfiniteTimeSpan); + + clock.AdvanceBy(TenSeconds); + await timer.DisposeAsync(); + + await Assert.That(fired.SequenceEqual(OddSeconds)).IsTrue(); + } + + /// Disposing a timer cancels its pending firing and makes later changes report failure. + /// A task representing the asynchronous test. + [Test] + public async Task DisposeCancelsThePendingFiringAndRejectsChanges() + { + VirtualClock clock = new(Epoch); + var fired = 0; + var timer = clock.CreateTimer(_ => fired++, null, OneSecond, OneSecond); + + timer.Dispose(); + clock.AdvanceBy(TenSeconds); + + await Assert.That(fired).IsEqualTo(0); + await Assert.That(timer.Change(OneSecond, OneSecond)).IsFalse(); + } + + /// Disposing a periodic timer from its callback stops later firings. + /// A task representing the asynchronous test. + [Test] + public async Task DisposeInsideTheCallbackStopsAPeriodicTimer() + { + VirtualClock clock = new(Epoch); + var fired = 0; + ITimer? timer = null; + timer = clock.CreateTimer( + _ => + { + fired++; + timer!.Dispose(); + }, + null, + OneSecond, + OneSecond); + + clock.AdvanceBy(TenSeconds); + + await Assert.That(fired).IsEqualTo(1); + } + + /// A firing that was already dequeued when the timer is disposed does not invoke the callback or queue another firing. + /// A task representing the asynchronous test. + [Test] + public async Task FiringAfterDisposeDoesNothing() + { + VirtualClock clock = new(Epoch); + var fired = 0; + var timer = (VirtualClockTimer)clock.CreateTimer(_ => fired++, null, OneSecond, OneSecond); + + await timer.DisposeAsync(); + _ = timer.Fire(); + clock.AdvanceBy(TenSeconds); + + await Assert.That(fired).IsEqualTo(0); + } + + /// Asynchronous disposal cancels the timer. + /// A task representing the asynchronous test. + [Test] + public async Task DisposeAsyncCancelsThePendingFiring() + { + VirtualClock clock = new(Epoch); + var fired = 0; + var timer = clock.CreateTimer(_ => fired++, null, OneSecond, Timeout.InfiniteTimeSpan); + + await timer.DisposeAsync(); + clock.AdvanceBy(TenSeconds); + + await Assert.That(fired).IsEqualTo(0); + } + + /// Timers reject a missing callback and negative times other than the infinite time span. + /// A task representing the asynchronous test. + [Test] + public async Task TimersRejectInvalidArguments() + { + VirtualClock clock = new(Epoch); + TimeProvider provider = clock; + await using var timer =provider.CreateTimer(static _ => { }, null, OneSecond, Timeout.InfiniteTimeSpan); + + await Assert.That(() => provider.CreateTimer(null!, null, OneSecond, Timeout.InfiniteTimeSpan)) + .ThrowsExactly(); + await Assert.That(() => provider.CreateTimer(static _ => { }, null, -TwoSeconds, Timeout.InfiniteTimeSpan)) + .ThrowsExactly(); + await Assert.That(() => provider.CreateTimer(static _ => { }, null, OneSecond, -TwoSeconds)) + .ThrowsExactly(); + await Assert.That(() => timer.Change(-TwoSeconds, Timeout.InfiniteTimeSpan)) + .ThrowsExactly(); + } +} diff --git a/src/tests/ReactiveUI.Primitives.Tests/WasmSequencerTests.cs b/src/tests/ReactiveUI.Primitives.Tests/WasmSequencerTests.cs index 5442995f..e360f16c 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/WasmSequencerTests.cs +++ b/src/tests/ReactiveUI.Primitives.Tests/WasmSequencerTests.cs @@ -11,9 +11,15 @@ namespace ReactiveUI.Primitives.Tests; /// Tests for . public sealed class WasmSequencerTests { + /// Timestamp ticks per second of a provider that counts milliseconds. + private const long MillisecondFrequency = 1000; + /// Expected values produced by an immediate burst, in FIFO order. private static readonly int[] ExpectedBurst = [1, 2, 3]; + /// The delay used for scheduled work. + private static readonly TimeSpan OneSecond = TimeSpan.FromSeconds(1); + /// Verifies the shared instance is a singleton. /// A representing the asynchronous operation. [Test] @@ -222,6 +228,121 @@ public async Task DisposeDropsDelayedNonDisposableWorkThatComesDueAfterwards() await Assert.That(ran).IsEqualTo(0); } + /// A sequencer rejects a missing provider. + /// A representing the asynchronous operation. + [Test] + public async Task ProviderConstructorRejectsANullProvider() => + await Assert.That(static () => new WasmSequencer(null!)).ThrowsExactly(); + + /// The current time and timestamp follow the provider. + /// A representing the asynchronous operation. + [Test] + public async Task NowAndTimestampFollowTheProvider() + { + VirtualClock clock = new(DateTimeOffset.UnixEpoch); + using WasmSequencer sequencer = new(clock); + var start = sequencer.Timestamp; + + clock.AdvanceBy(OneSecond); + + await Assert.That(sequencer.Now).IsEqualTo(DateTimeOffset.UnixEpoch + OneSecond); + await Assert.That(sequencer.Timestamp - start).IsEqualTo(Sequencer.ToTimestampDelta(OneSecond)); + } + + /// Immediate work runs on the provider's next timer turn. + /// A representing the asynchronous operation. + [Test] + public async Task ImmediateWorkRunsWhenTheProviderFiresItsDrainTimer() + { + VirtualClock clock = new(); + using WasmSequencer sequencer = new(clock); + RecordingWorkItem item = new(); + RecordingWorkItem cancelled = new(); + cancelled.Dispose(); + + sequencer.Schedule(cancelled); + sequencer.Schedule(item); + + await Assert.That(item.ExecuteCount).IsEqualTo(0); + + clock.AdvanceBy(TimeSpan.FromTicks(1)); + + await Assert.That(item.ExecuteCount).IsEqualTo(1); + await Assert.That(cancelled.ExecuteCount).IsEqualTo(0); + } + + /// Delayed work runs when the provider reaches its timestamp and not before. + /// A representing the asynchronous operation. + [Test] + public async Task DelayedWorkRunsExactlyWhenTheProviderReachesItsTimestamp() + { + VirtualClock clock = new(); + using WasmSequencer sequencer = new(clock); + RecordingWorkItem item = new(); + + sequencer.Schedule(item, sequencer.Timestamp + Sequencer.ToTimestampDelta(OneSecond)); + clock.AdvanceBy(OneSecond - TimeSpan.FromTicks(1)); + + await Assert.That(item.ExecuteCount).IsEqualTo(0); + + clock.AdvanceBy(TimeSpan.FromTicks(1)); + + await Assert.That(item.ExecuteCount).IsEqualTo(1); + } + + /// Delayed work cancelled before its timestamp never runs. + /// A representing the asynchronous operation. + [Test] + public async Task DelayedWorkCancelledBeforeItsTimestampDoesNotRun() + { + VirtualClock clock = new(); + using WasmSequencer sequencer = new(clock); + RecordingWorkItem item = new(); + + sequencer.Schedule(item, sequencer.Timestamp + Sequencer.ToTimestampDelta(OneSecond)); + item.Dispose(); + clock.AdvanceBy(OneSecond); + + await Assert.That(item.ExecuteCount).IsEqualTo(0); + } + + /// A provider counting at another frequency drives delayed work at the right wall time. + /// A representing the asynchronous operation. + [Test] + public async Task ProviderWithAnotherFrequencyDrivesDelayedWork() + { + VirtualClock clock = new(); + ScaledTimeProvider provider = new(clock, MillisecondFrequency); + using WasmSequencer sequencer = new(provider); + RecordingWorkItem item = new(); + + sequencer.Schedule(item, sequencer.Timestamp + Sequencer.ToTimestampDelta(OneSecond)); + clock.AdvanceBy(OneSecond - TimeSpan.FromMilliseconds(1)); + + await Assert.That(item.ExecuteCount).IsEqualTo(0); + + clock.AdvanceBy(TimeSpan.FromMilliseconds(1)); + + await Assert.That(item.ExecuteCount).IsEqualTo(1); + } + + /// Disposal cancels queued immediate work before the provider's drain timer fires. + /// A representing the asynchronous operation. + [Test] + public async Task DisposeCancelsQueuedWorkBeforeTheDrainTimerFires() + { + VirtualClock clock = new(); + WasmSequencer sequencer = new(clock); + RecordingWorkItem item = new(); + + sequencer.Schedule(item); + sequencer.Dispose(); + clock.AdvanceBy(OneSecond); + + await Assert.That(item.IsDisposed).IsTrue(); + await Assert.That(item.ExecuteCount).IsEqualTo(0); + } + /// Creates a sequencer whose event loop is driven explicitly. /// The queued drain callbacks. /// The delayed callback queue. diff --git a/src/tests/ReactiveUI.Primitives.Tests/WitnessLatestOnSignalTests.cs b/src/tests/ReactiveUI.Primitives.Tests/WitnessLatestOnSignalTests.cs new file mode 100644 index 00000000..fd1f23f4 --- /dev/null +++ b/src/tests/ReactiveUI.Primitives.Tests/WitnessLatestOnSignalTests.cs @@ -0,0 +1,476 @@ +// 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.Runtime.CompilerServices; +using ReactiveUI.Primitives.Advanced; +using ReactiveUI.Primitives.Concurrency; +using ReactiveUI.Primitives.Signals; + +namespace ReactiveUI.Primitives.Tests; + +/// Verifies the WitnessLatestOn signal, which keeps one pending slot and delivers only the newest value on a sequencer. +public sealed class WitnessLatestOnSignalTests +{ + /// The first value pushed through the signal. + private const int First = 1; + + /// The second value pushed through the signal. + private const int Second = 2; + + /// The third value pushed through the signal. + private const int Third = 3; + + /// A single virtual tick, enough to drain the sequencer queue. + private static readonly TimeSpan SingleTick = TimeSpan.FromTicks(1); + + /// The longest a cross-thread push may take before the sink lock is considered held. + private static readonly TimeSpan ProbeTimeout = TimeSpan.FromSeconds(30); + + /// The signal is built directly from a source and a sequencer and subscribes on the current thread. + /// A task representing the asynchronous operation. + [Test] + public async Task ExtensionBuildsTheSignalThatRequiresTheCurrentThread() + { + VirtualClock clock = new(); + Signal source = new(); + + var viaExtension = source.WitnessLatestOn(clock); + WitnessLatestOnSignal direct = new(source, clock); + + await Assert.That(viaExtension).IsTypeOf>(); + await Assert.That(viaExtension.GetType()).IsEqualTo(direct.GetType()); + await Assert.That(direct.IsRequiredSubscribeOnCurrentThread()).IsTrue(); + } + + /// Values that arrive while a drain is pending overwrite the single slot, so only the newest is delivered. + /// A task representing the asynchronous operation. + [Test] + public async Task PendingValuesAreOverwrittenAndOnlyTheLatestIsDelivered() + { + VirtualClock clock = new(); + Signal source = new(); + LogObserver log = new(); + + using var subscription = source.WitnessLatestOn(clock).Subscribe(log); + source.OnNext(First); + source.OnNext(Second); + source.OnNext(Third); + + await Assert.That(log.Entries.Count).IsEqualTo(0); + + clock.AdvanceBy(SingleTick); + + await Assert.That(log.Entries.SequenceEqual(["N3"])).IsTrue(); + } + + /// A value that arrives after a drain finished schedules a fresh drain. + /// A task representing the asynchronous operation. + [Test] + public async Task ValueAfterAFinishedDrainIsDeliveredByANewDrain() + { + VirtualClock clock = new(); + Signal source = new(); + LogObserver log = new(); + + using var subscription = source.WitnessLatestOn(clock).Subscribe(log); + source.OnNext(First); + clock.AdvanceBy(SingleTick); + source.OnNext(Second); + clock.AdvanceBy(SingleTick); + clock.AdvanceBy(SingleTick); + + await Assert.That(log.Entries.SequenceEqual(["N1", "N2"])).IsTrue(); + } + + /// A value pushed re-entrantly during delivery is held, then delivered once by the same drain. + /// A task representing the asynchronous operation. + [Test] + public async Task ValueArrivingDuringDeliveryIsHeldAndDeliveredOnceByTheSameDrain() + { + VirtualClock clock = new(); + Signal source = new(); + LogObserver log = new(); + log.OnValue = value => + { + if (value == First) + { + source.OnNext(Second); + source.OnNext(Third); + } + }; + + using var subscription = source.WitnessLatestOn(clock).Subscribe(log); + source.OnNext(First); + + clock.AdvanceBy(SingleTick); + + await Assert.That(log.Entries.SequenceEqual(["N1", "N3"])).IsTrue(); + + clock.AdvanceBy(SingleTick); + + await Assert.That(log.Entries.SequenceEqual(["N1", "N3"])).IsTrue(); + } + + /// Completion that arrives during delivery is delivered after the value being delivered. + /// A task representing the asynchronous operation. + [Test] + public async Task CompletionArrivingDuringDeliveryFollowsTheCurrentValue() + { + VirtualClock clock = new(); + Signal source = new(); + LogObserver log = new(); + log.OnValue = _ => source.OnCompleted(); + + using var subscription = source.WitnessLatestOn(clock).Subscribe(log); + source.OnNext(First); + + clock.AdvanceBy(SingleTick); + + await Assert.That(log.Entries.SequenceEqual(["N1", "C"])).IsTrue(); + } + + /// Completion delivers the pending value first and then completes. + /// A task representing the asynchronous operation. + [Test] + public async Task CompletionDeliversThePendingValueThenCompletes() + { + VirtualClock clock = new(); + Signal source = new(); + LogObserver log = new(); + + using var subscription = source.WitnessLatestOn(clock).Subscribe(log); + source.OnNext(First); + source.OnNext(Second); + source.OnCompleted(); + + await Assert.That(log.Entries.Count).IsEqualTo(0); + + clock.AdvanceBy(SingleTick); + + await Assert.That(log.Entries.SequenceEqual(["N2", "C"])).IsTrue(); + } + + /// Completion with no pending value is deferred to the sequencer and delivered alone. + /// A task representing the asynchronous operation. + [Test] + public async Task CompletionWithoutAPendingValueIsDeliveredOnTheSequencer() + { + VirtualClock clock = new(); + Signal source = new(); + LogObserver log = new(); + + using var subscription = source.WitnessLatestOn(clock).Subscribe(log); + source.OnCompleted(); + + await Assert.That(log.Entries.Count).IsEqualTo(0); + + clock.AdvanceBy(SingleTick); + + await Assert.That(log.Entries.SequenceEqual(["C"])).IsTrue(); + } + + /// An error delivers the pending value first and then the error. + /// A task representing the asynchronous operation. + [Test] + public async Task ErrorDeliversThePendingValueThenTheError() + { + VirtualClock clock = new(); + Signal source = new(); + LogObserver log = new(); + InvalidOperationException expected = new("latest"); + + using var subscription = source.WitnessLatestOn(clock).Subscribe(log); + source.OnNext(First); + source.OnNext(Second); + source.OnError(expected); + + clock.AdvanceBy(SingleTick); + + await Assert.That(log.Entries.SequenceEqual(["N2", "E"])).IsTrue(); + await Assert.That(log.Errors[0]).IsSameReferenceAs(expected); + } + + /// An error wins over a completion that arrived before it. + /// A task representing the asynchronous operation. + [Test] + public async Task ErrorWinsOverAnEarlierCompletion() + { + VirtualClock clock = new(); + IObserver? upstream = null; + LogObserver log = new(); + + using var subscription = new ScriptedObservable(observer => upstream = observer) + .WitnessLatestOn(clock) + .Subscribe(log); + upstream!.OnNext(First); + upstream.OnCompleted(); + upstream.OnError(new InvalidOperationException("late")); + + clock.AdvanceBy(SingleTick); + + await Assert.That(log.Entries.SequenceEqual(["N1", "E"])).IsTrue(); + } + + /// A completion that arrives after an error is dropped. + /// A task representing the asynchronous operation. + [Test] + public async Task CompletionAfterAnErrorIsDropped() + { + VirtualClock clock = new(); + IObserver? upstream = null; + LogObserver log = new(); + + using var subscription = new ScriptedObservable(observer => upstream = observer) + .WitnessLatestOn(clock) + .Subscribe(log); + upstream!.OnError(new InvalidOperationException("first")); + upstream.OnCompleted(); + upstream.OnNext(First); + + clock.AdvanceBy(SingleTick); + + await Assert.That(log.Entries.SequenceEqual(["E"])).IsTrue(); + } + + /// Values that arrive after completion are dropped. + /// A task representing the asynchronous operation. + [Test] + public async Task ValuesAfterCompletionAreDropped() + { + VirtualClock clock = new(); + IObserver? upstream = null; + LogObserver log = new(); + + using var subscription = new ScriptedObservable(observer => upstream = observer) + .WitnessLatestOn(clock) + .Subscribe(log); + upstream!.OnCompleted(); + upstream.OnCompleted(); + upstream.OnNext(First); + + clock.AdvanceBy(SingleTick); + + await Assert.That(log.Entries.SequenceEqual(["C"])).IsTrue(); + } + + /// A delivered completion disposes the upstream subscription and nothing more is delivered. + /// A task representing the asynchronous operation. + [Test] + public async Task DeliveredCompletionDisposesUpstreamAndStopsDelivery() + { + VirtualClock clock = new(); + IObserver? upstream = null; + CountingDisposable upstreamSubscription = new(); + LogObserver log = new(); + + using var subscription = new ScriptedObservable(observer => upstream = observer, upstreamSubscription) + .WitnessLatestOn(clock) + .Subscribe(log); + upstream!.OnNext(First); + upstream.OnCompleted(); + + clock.AdvanceBy(SingleTick); + + await Assert.That(upstreamSubscription.DisposeCount).IsEqualTo(1); + + upstream.OnNext(Second); + upstream.OnCompleted(); + clock.AdvanceBy(SingleTick); + + await Assert.That(log.Entries.SequenceEqual(["N1", "C"])).IsTrue(); + await Assert.That(upstreamSubscription.DisposeCount).IsEqualTo(1); + } + + /// A delivered error disposes the upstream subscription and nothing more is delivered. + /// A task representing the asynchronous operation. + [Test] + public async Task DeliveredErrorDisposesUpstreamAndStopsDelivery() + { + VirtualClock clock = new(); + IObserver? upstream = null; + CountingDisposable upstreamSubscription = new(); + LogObserver log = new(); + + using var subscription = new ScriptedObservable(observer => upstream = observer, upstreamSubscription) + .WitnessLatestOn(clock) + .Subscribe(log); + upstream!.OnError(new InvalidOperationException("boom")); + + clock.AdvanceBy(SingleTick); + + await Assert.That(upstreamSubscription.DisposeCount).IsEqualTo(1); + + upstream.OnNext(First); + upstream.OnError(new InvalidOperationException("again")); + clock.AdvanceBy(SingleTick); + + await Assert.That(log.Entries.SequenceEqual(["E"])).IsTrue(); + } + + /// Dispose drops the pending value and disposes the upstream subscription exactly once. + /// A task representing the asynchronous operation. + [Test] + public async Task DisposeDropsThePendingValueAndDisposesUpstreamOnce() + { + VirtualClock clock = new(); + IObserver? upstream = null; + CountingDisposable upstreamSubscription = new(); + LogObserver log = new(); + + var subscription = new ScriptedObservable(observer => upstream = observer, upstreamSubscription) + .WitnessLatestOn(clock) + .Subscribe(log); + upstream!.OnNext(First); + subscription.Dispose(); + subscription.Dispose(); + + upstream.OnNext(Second); + upstream.OnCompleted(); + clock.AdvanceBy(SingleTick); + + await Assert.That(log.Entries.Count).IsEqualTo(0); + await Assert.That(upstreamSubscription.DisposeCount).IsEqualTo(1); + } + + /// Disposing from inside a delivery drops the value that arrived during it. + /// A task representing the asynchronous operation. + [Test] + public async Task DisposeDuringDeliveryDropsTheHeldValue() + { + VirtualClock clock = new(); + Signal source = new(); + LogObserver log = new(); + IDisposable? subscription = null; + log.OnValue = _ => + { + source.OnNext(Second); + subscription?.Dispose(); + }; + + subscription = source.WitnessLatestOn(clock).Subscribe(log); + source.OnNext(First); + + clock.AdvanceBy(SingleTick); + + await Assert.That(log.Entries.SequenceEqual(["N1"])).IsTrue(); + } + + /// Values and completion reach the observer without the sink's lock held, so another thread can push meanwhile. + /// A task representing the asynchronous operation. + [Test] + public async Task ValuesAndCompletionAreDeliveredOutsideTheSinkLock() + { + VirtualClock clock = new(); + IObserver? upstream = null; + List nonBlocking = []; + LogObserver log = new(); + log.OnValue = value => nonBlocking.Add(value == First + ? IsNonBlocking(() => upstream!.OnNext(Second)) + : IsNonBlocking(() => upstream!.OnCompleted())); + log.OnTerminal = () => nonBlocking.Add(IsNonBlocking(() => upstream!.OnNext(Third))); + + using var subscription = new ScriptedObservable(observer => + { + upstream = observer; + observer.OnNext(First); + }) + .WitnessLatestOn(clock) + .Subscribe(log); + + clock.AdvanceBy(SingleTick); + + await Assert.That(log.Entries.SequenceEqual(["N1", "N2", "C"])).IsTrue(); + await Assert.That(nonBlocking.SequenceEqual([true, true, true])).IsTrue(); + } + + /// An error reaches the observer without the sink's lock held, so another thread can push meanwhile. + /// A task representing the asynchronous operation. + [Test] + public async Task ErrorsAreDeliveredOutsideTheSinkLock() + { + VirtualClock clock = new(); + IObserver? upstream = null; + List nonBlocking = []; + LogObserver log = new(); + log.OnValue = _ => nonBlocking.Add(IsNonBlocking(() => upstream!.OnError(new InvalidOperationException("gate")))); + log.OnTerminal = () => nonBlocking.Add(IsNonBlocking(() => upstream!.OnNext(Second))); + + using var subscription = new ScriptedObservable(observer => + { + upstream = observer; + observer.OnNext(First); + }) + .WitnessLatestOn(clock) + .Subscribe(log); + + clock.AdvanceBy(SingleTick); + + await Assert.That(log.Entries.SequenceEqual(["N1", "E"])).IsTrue(); + await Assert.That(nonBlocking.SequenceEqual([true, true])).IsTrue(); + } + + /// Runs an action on another thread and reports whether it finished, which it cannot while this thread holds the sink lock. + /// The action that pushes into the sink. + /// True when the other thread finished within the timeout; otherwise, false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsNonBlocking(Action push) + { + Thread pusher = new(() => push()) { IsBackground = true }; + pusher.Start(); + return pusher.Join(ProbeTimeout); + } + + /// An observer that records values, errors and completion in arrival order. + private sealed class LogObserver : IObserver + { + /// Gets the ordered log; values are N<value>, errors are E and completion is C. + internal List Entries { get; } = []; + + /// Gets the recorded errors. + internal List Errors { get; } = []; + + /// Gets or sets the action invoked after each value is recorded. + internal Action? OnValue { get; set; } + + /// Gets or sets the action invoked after an error or completion is recorded. + internal Action? OnTerminal { get; set; } + + /// Records a completion callback and runs . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void OnCompleted() + { + Entries.Add("C"); + OnTerminal?.Invoke(); + } + + /// Records an error callback and runs . + /// The error to record. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void OnError(Exception error) + { + Entries.Add("E"); + Errors.Add(error); + OnTerminal?.Invoke(); + } + + /// Records a value callback and runs . + /// The value to record. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void OnNext(int value) + { + Entries.Add($"N{value}"); + OnValue?.Invoke(value); + } + } + + /// A disposable that counts how many times it has been disposed. + private sealed class CountingDisposable : IDisposable + { + /// Gets the number of dispose calls. + internal int DisposeCount { get; private set; } + + /// Counts a dispose call. + public void Dispose() => DisposeCount++; + } +}