diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 67ee2f1..b3ab7f0 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -16,17 +16,35 @@ jobs: outputs: nbgv: ${{ steps.nbgv.outputs.SemVer2 }} steps: + - name: Get Current Visual Studio Information + shell: bash + run: | + dotnet tool update -g dotnet-vs + echo "## About RELEASE ##" + vs where release + + - name: Update Visual Studio Latest Release + shell: bash + run: | + echo "## Update RELEASE ##" + vs update release Enterprise + vs modify release Enterprise +mobile +desktop +uwp +web + echo "## About RELEASE Updated ##" + vs where release + echo "##vso[task.prependpath]$(vs where release --prop=InstallationPath)\MSBuild\Current\Bin" + - name: Checkout uses: actions/checkout@v3.1.0 with: fetch-depth: 0 lfs: true - - name: Install .NET 6 + - name: Install .NET 6 & .NET7 uses: actions/setup-dotnet@v3 with: - dotnet-version: 6.0.x - dotnet-quality: 'preview' + dotnet-version: | + 6.0.x + 7.0.x - name: NBGV id: nbgv diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 223ae9f..b4aa7e2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,17 +16,35 @@ jobs: outputs: nbgv: ${{ steps.nbgv.outputs.SemVer2 }} steps: + - name: Get Current Visual Studio Information + shell: bash + run: | + dotnet tool update -g dotnet-vs + echo "## About RELEASE ##" + vs where release + + - name: Update Visual Studio Latest Release + shell: bash + run: | + echo "## Update RELEASE ##" + vs update release Enterprise + vs modify release Enterprise +mobile +desktop +uwp +web + echo "## About RELEASE Updated ##" + vs where release + echo "##vso[task.prependpath]$(vs where release --prop=InstallationPath)\MSBuild\Current\Bin" + - name: Checkout uses: actions/checkout@v3.1.0 with: fetch-depth: 0 lfs: true - - - name: Install .NET 6 + + - name: Install .NET 6 & .NET7 uses: actions/setup-dotnet@v3 with: - dotnet-version: 6.0.x - dotnet-quality: 'preview' + dotnet-version: | + 6.0.x + 7.0.x - name: NBGV id: nbgv diff --git a/src/Directory.Build.props b/src/Directory.Build.props index af6c020..1ab9ac3 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -52,7 +52,7 @@ - + diff --git a/src/Minimalist.Reactive.Tests/AsyncSignalTests.cs b/src/Minimalist.Reactive.Tests/AsyncSignalTests.cs new file mode 100644 index 0000000..02a9d8f --- /dev/null +++ b/src/Minimalist.Reactive.Tests/AsyncSignalTests.cs @@ -0,0 +1,314 @@ +// Copyright (c) 2019-2023 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Minimalist.Reactive.Signals; +using Xunit; + +namespace Minimalist.Reactive.Tests; + +/// +/// AsyncSignalTests. +/// +public class AsyncSignalTests +{ + /// + /// Subscribes the argument checking. + /// + [Fact] + public void Subscribe_ArgumentChecking() => + Assert.Throws(() => new AsyncSignal().Subscribe(null!)); + + /// + /// Called when [error argument checking]. + /// + [Fact] + public void OnError_ArgumentChecking() => + Assert.Throws(() => new AsyncSignal().OnError(null!)); + + /// + /// Awaits the blocking. + /// + [Fact] + public void Await_Blocking() + { + var s = new AsyncSignal(); + GetResult_BlockingImpl(s.GetAwaiter()); + } + + /// + /// Awaits the throw. + /// + [Fact] + public void Await_Throw() + { + var s = new AsyncSignal(); + GetResult_Blocking_ThrowImpl(s.GetAwaiter()); + } + + /// + /// Gets the result empty. + /// + [Fact] + public void GetResult_Empty() + { + var s = new AsyncSignal(); + s.OnCompleted(); + Assert.Throws(() => s.GetResult()); + } + + /// + /// Gets the result blocking. + /// + [Fact] + public void GetResult_Blocking() => GetResult_BlockingImpl(new AsyncSignal()); + + /// + /// Gets the result blocking throw. + /// + [Fact] + public void GetResult_Blocking_Throw() => GetResult_Blocking_ThrowImpl(new AsyncSignal()); + + /// + /// Gets the result context. + /// + [Fact] + public void GetResult_Context() + { + var x = new AsyncSignal(); + + var ctx = new MyContext(); + var e = new ManualResetEvent(false); + + Task.Run(() => + { + SynchronizationContext.SetSynchronizationContext(ctx); + + var a = x.GetAwaiter(); + a.OnCompleted(() => e.Set()); + }); + + x.OnNext(42); + x.OnCompleted(); + + e.WaitOne(); + + Assert.True(ctx.Ran); + } + + /// + /// Determines whether this instance has observers. + /// + [Fact] + public void HasObservers() + { + var s = new AsyncSignal(); + Assert.False(s.HasObservers); + + var d1 = s.Subscribe(_ => { }); + Assert.True(s.HasObservers); + + d1.Dispose(); + Assert.False(s.HasObservers); + + var d2 = s.Subscribe(_ => { }); + Assert.True(s.HasObservers); + + var d3 = s.Subscribe(_ => { }); + Assert.True(s.HasObservers); + + d2.Dispose(); + Assert.True(s.HasObservers); + + d3.Dispose(); + Assert.False(s.HasObservers); + } + + /// + /// Determines whether [has observers dispose1]. + /// + [Fact] + public void HasObservers_Dispose1() + { + var s = new AsyncSignal(); + Assert.False(s.HasObservers); + Assert.False(s.IsDisposed); + + var d = s.Subscribe(_ => { }); + Assert.True(s.HasObservers); + Assert.False(s.IsDisposed); + + s.Dispose(); + Assert.False(s.HasObservers); + Assert.True(s.IsDisposed); + + d.Dispose(); + Assert.False(s.HasObservers); + Assert.True(s.IsDisposed); + } + + /// + /// Determines whether [has observers dispose2]. + /// + [Fact] + public void HasObservers_Dispose2() + { + var s = new AsyncSignal(); + Assert.False(s.HasObservers); + Assert.False(s.IsDisposed); + + var d = s.Subscribe(_ => { }); + Assert.True(s.HasObservers); + Assert.False(s.IsDisposed); + + d.Dispose(); + Assert.False(s.HasObservers); + Assert.False(s.IsDisposed); + + s.Dispose(); + Assert.False(s.HasObservers); + Assert.True(s.IsDisposed); + } + + /// + /// Determines whether [has observers dispose3]. + /// + [Fact] + public void HasObservers_Dispose3() + { + var s = new AsyncSignal(); + Assert.False(s.HasObservers); + Assert.False(s.IsDisposed); + + s.Dispose(); + Assert.False(s.HasObservers); + Assert.True(s.IsDisposed); + } + + /// + /// Determines whether [has observers on completed]. + /// + [Fact] + public void HasObservers_OnCompleted() + { + var s = new AsyncSignal(); + Assert.False(s.HasObservers); + + var d = s.Subscribe(_ => { }); + Assert.True(s.HasObservers); + + s.OnNext(42); + Assert.True(s.HasObservers); + + s.OnCompleted(); + Assert.False(s.HasObservers); + } + + /// + /// Determines whether [has observers on error]. + /// + [Fact] + public void HasObservers_OnError() + { + var s = new AsyncSignal(); + Assert.False(s.HasObservers); + + var d = s.Subscribe(_ => { }, _ => { }); + Assert.True(s.HasObservers); + + s.OnNext(42); + Assert.True(s.HasObservers); + + s.OnError(new Exception()); + Assert.False(s.HasObservers); + } + + /// + /// Gets the result blocking implementation. + /// + /// The s. + private static void GetResult_BlockingImpl(IAwaitSignal s) + { + Assert.False(s.IsCompleted); + + var e = new ManualResetEvent(false); + + new Thread(() => + { + e.WaitOne(); + s.OnNext(42); + s.OnCompleted(); + }).Start(); + + var y = default(int); + var t = new Thread(() => y = s.GetResult()); + t.Start(); + + while (t.ThreadState != ThreadState.WaitSleepJoin) + { + } + + e.Set(); + t.Join(); + + Assert.Equal(42, y); + Assert.True(s.IsCompleted); + } + + /// + /// Gets the result blocking throw implementation. + /// + /// The s. + private static void GetResult_Blocking_ThrowImpl(IAwaitSignal s) + { + Assert.False(s.IsCompleted); + + var e = new ManualResetEvent(false); + + var ex = new Exception(); + + new Thread(() => + { + e.WaitOne(); + s.OnError(ex); + }).Start(); + + var y = default(Exception); + var t = new Thread(() => + { + try + { + s.GetResult(); + } + catch (Exception ex_) + { + y = ex_; + } + }); + t.Start(); + + while (t.ThreadState != ThreadState.WaitSleepJoin) + { + } + + e.Set(); + t.Join(); + + Assert.Same(ex, y); + Assert.True(s.IsCompleted); + } + + private class MyContext : SynchronizationContext + { + public bool Ran { get; set; } + + public override void Post(SendOrPostCallback d, object? state) + { + Ran = true; + d(state); + } + } +} diff --git a/src/Minimalist.Reactive.Tests/BehaviourSignalTests.cs b/src/Minimalist.Reactive.Tests/BehaviourSignalTests.cs new file mode 100644 index 0000000..74ad503 --- /dev/null +++ b/src/Minimalist.Reactive.Tests/BehaviourSignalTests.cs @@ -0,0 +1,288 @@ +// Copyright (c) 2019-2023 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; +using Minimalist.Reactive.Signals; +using Xunit; + +namespace Minimalist.Reactive.Tests; + +/// +/// BehaviourSignalTests. +/// +public class BehaviourSignalTests +{ + /// + /// Subscribes the argument checking. + /// + [Fact] + public void Subscribe_ArgumentChecking() => + Assert.Throws(() => new BehaviourSignal(1).Subscribe(null!)); + + /// + /// Called when [error argument checking]. + /// + [Fact] + public void OnError_ArgumentChecking() => + Assert.Throws(() => new BehaviourSignal(1).OnError(null!)); + + /// + /// Determines whether this instance has observers. + /// + [Fact] + public void HasObservers() + { + var s = new BehaviourSignal(42); + Assert.False(s.HasObservers); + + var d1 = s.Subscribe(_ => { }); + Assert.True(s.HasObservers); + + d1.Dispose(); + Assert.False(s.HasObservers); + + var d2 = s.Subscribe(_ => { }); + Assert.True(s.HasObservers); + + var d3 = s.Subscribe(_ => { }); + Assert.True(s.HasObservers); + + d2.Dispose(); + Assert.True(s.HasObservers); + + d3.Dispose(); + Assert.False(s.HasObservers); + } + + /// + /// Determines whether [has observers dispose1]. + /// + [Fact] + public void HasObservers_Dispose1() + { + var s = new BehaviourSignal(42); + Assert.False(s.HasObservers); + Assert.False(s.IsDisposed); + + var d = s.Subscribe(_ => { }); + Assert.True(s.HasObservers); + Assert.False(s.IsDisposed); + + s.Dispose(); + Assert.False(s.HasObservers); + Assert.True(s.IsDisposed); + + d.Dispose(); + Assert.False(s.HasObservers); + Assert.True(s.IsDisposed); + } + + /// + /// Determines whether [has observers dispose2]. + /// + [Fact] + public void HasObservers_Dispose2() + { + var s = new BehaviourSignal(42); + Assert.False(s.HasObservers); + Assert.False(s.IsDisposed); + + var d = s.Subscribe(_ => { }); + Assert.True(s.HasObservers); + Assert.False(s.IsDisposed); + + d.Dispose(); + Assert.False(s.HasObservers); + Assert.False(s.IsDisposed); + + s.Dispose(); + Assert.False(s.HasObservers); + Assert.True(s.IsDisposed); + } + + /// + /// Determines whether [has observers dispose3]. + /// + [Fact] + public void HasObservers_Dispose3() + { + var s = new BehaviourSignal(42); + Assert.False(s.HasObservers); + Assert.False(s.IsDisposed); + + s.Dispose(); + Assert.False(s.HasObservers); + Assert.True(s.IsDisposed); + } + + /// + /// Determines whether [has observers on completed]. + /// + [Fact] + public void HasObservers_OnCompleted() + { + var s = new BehaviourSignal(42); + Assert.False(s.HasObservers); + + var d = s.Subscribe(_ => { }); + Assert.True(s.HasObservers); + + s.OnNext(42); + Assert.True(s.HasObservers); + + s.OnCompleted(); + Assert.False(s.HasObservers); + } + + /// + /// Determines whether [has observers on error]. + /// + [Fact] + public void HasObservers_OnError() + { + var s = new BehaviourSignal(42); + Assert.False(s.HasObservers); + + var d = s.Subscribe(_ => { }, _ => { }); + Assert.True(s.HasObservers); + + s.OnNext(42); + Assert.True(s.HasObservers); + + s.OnError(new Exception()); + Assert.False(s.HasObservers); + } + + /// + /// Values the initial. + /// + [Fact] + public void Value_Initial() + { + var s = new BehaviourSignal(42); + Assert.Equal(42, s.Value); + + Assert.True(s.TryGetValue(out var x)); + Assert.Equal(42, x); + } + + /// + /// Values the first. + /// + [Fact] + public void Value_First() + { + var s = new BehaviourSignal(42); + Assert.Equal(42, s.Value); + + Assert.True(s.TryGetValue(out var x)); + Assert.Equal(42, x); + + s.OnNext(43); + Assert.Equal(43, s.Value); + + Assert.True(s.TryGetValue(out x)); + Assert.Equal(43, x); + } + + /// + /// Values the second. + /// + [Fact] + public void Value_Second() + { + var s = new BehaviourSignal(42); + Assert.Equal(42, s.Value); + + Assert.True(s.TryGetValue(out var x)); + Assert.Equal(42, x); + + s.OnNext(43); + Assert.Equal(43, s.Value); + + Assert.True(s.TryGetValue(out x)); + Assert.Equal(43, x); + + s.OnNext(44); + Assert.Equal(44, s.Value); + + Assert.True(s.TryGetValue(out x)); + Assert.Equal(44, x); + } + + /// + /// Values the frozen after on completed. + /// + [Fact] + public void Value_FrozenAfterOnCompleted() + { + var s = new BehaviourSignal(42); + Assert.Equal(42, s.Value); + + Assert.True(s.TryGetValue(out var x)); + Assert.Equal(42, x); + + s.OnNext(43); + Assert.Equal(43, s.Value); + + Assert.True(s.TryGetValue(out x)); + Assert.Equal(43, x); + + s.OnNext(44); + Assert.Equal(44, s.Value); + + Assert.True(s.TryGetValue(out x)); + Assert.Equal(44, x); + + s.OnCompleted(); + Assert.Equal(44, s.Value); + + Assert.True(s.TryGetValue(out x)); + Assert.Equal(44, x); + + s.OnNext(1234); + Assert.Equal(44, s.Value); + + Assert.True(s.TryGetValue(out x)); + Assert.Equal(44, x); + } + + /// + /// Values the throws after on error. + /// + [Fact] + public void Value_ThrowsAfterOnError() + { + var s = new BehaviourSignal(42); + Assert.Equal(42, s.Value); + + s.OnError(new InvalidOperationException()); + + Assert.Throws(() => + { + var ignored = s.Value; + }); + + Assert.Throws(() => s.TryGetValue(out var x)); + } + + /// + /// Values the throws on dispose. + /// + [Fact] + public void Value_ThrowsOnDispose() + { + var s = new BehaviourSignal(42); + Assert.Equal(42, s.Value); + + s.Dispose(); + + Assert.Throws(() => + { + var ignored = s.Value; + }); + + Assert.False(s.TryGetValue(out var x)); + } +} diff --git a/src/Minimalist.Reactive.Tests/ConcurencyTests.cs b/src/Minimalist.Reactive.Tests/ConcurencyTests.cs new file mode 100644 index 0000000..062c31e --- /dev/null +++ b/src/Minimalist.Reactive.Tests/ConcurencyTests.cs @@ -0,0 +1,119 @@ +// Copyright (c) 2019-2023 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; +using System.Threading; +using Minimalist.Reactive.Concurrency; +using Minimalist.Reactive.Disposables; +using Xunit; + +namespace Minimalist.Reactive.Tests; + +/// +/// ConcurencyTests. +/// +public class ConcurencyTests +{ + /// + /// Tests this instance. + /// + [Fact] + public void TestCreate() + { + var scheduler = TaskPoolScheduler.Instance; + var disposable = scheduler.Schedule(0, (__, _) => Disposable.Empty); + Assert.NotNull(disposable); + disposable.Dispose(); + } + + /// + /// Tasks the pool now. + /// + [Fact] + public void TaskPoolNow() + { + var res = TaskPoolScheduler.Instance.Now - DateTime.Now; + Assert.True(res.Seconds < 1); + } + + /// + /// Tasks the pool schedule action. + /// + [Fact] + public void TaskPoolScheduleAction() + { + var id = Environment.CurrentManagedThreadId; + var nt = TaskPoolScheduler.Instance; + var evt = new ManualResetEvent(false); + nt.Schedule(() => + { + Assert.NotEqual(id, Environment.CurrentManagedThreadId); + evt.Set(); + }); + evt.WaitOne(); + } + + /// + /// Tasks the pool schedule action due now. + /// + [Fact] + public void TaskPoolScheduleActionDueNow() + { + var id = Environment.CurrentManagedThreadId; + var nt = TaskPoolScheduler.Instance; + var evt = new ManualResetEvent(false); + nt.Schedule(TimeSpan.Zero, () => + { + Assert.NotEqual(id, Environment.CurrentManagedThreadId); + evt.Set(); + }); + evt.WaitOne(); + } + + /// + /// Tasks the pool schedule action due. + /// + [Fact] + public void TaskPoolScheduleActionDue() + { + var id = Environment.CurrentManagedThreadId; + var nt = TaskPoolScheduler.Instance; + var evt = new ManualResetEvent(false); + nt.Schedule(TimeSpan.FromMilliseconds(1), () => + { + Assert.NotEqual(id, Environment.CurrentManagedThreadId); + evt.Set(); + }); + evt.WaitOne(); + } + + /// + /// Tasks the pool schedule action cancel. + /// + [Fact] + public void TaskPoolScheduleActionCancel() + { + var id = Environment.CurrentManagedThreadId; + var nt = TaskPoolScheduler.Instance; + var set = false; + var d = nt.Schedule(TimeSpan.FromSeconds(0.2), () => set = true); + d.Dispose(); + Thread.Sleep(400); + Assert.False(set); + } + + /// + /// Tasks the pool delay larger than int maximum value. + /// + [Fact] + public void TaskPoolDelayLargerThanIntMaxValue() + { + var dueTime = TimeSpan.FromMilliseconds((double)int.MaxValue + 1); + + // Just ensuring the call to Schedule does not throw. + var d = TaskPoolScheduler.Instance.Schedule(dueTime, () => { }); + + d.Dispose(); + } +} diff --git a/src/Minimalist.Reactive.Tests/DisposableTests.cs b/src/Minimalist.Reactive.Tests/DisposableTests.cs index 802066d..ed253eb 100644 --- a/src/Minimalist.Reactive.Tests/DisposableTests.cs +++ b/src/Minimalist.Reactive.Tests/DisposableTests.cs @@ -1,114 +1,113 @@ -// Copyright (c) 2019-2022 ReactiveUI Association Incorporated. All rights reserved. +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Disposables; using Xunit; -namespace Minimalist.Reactive.Tests +namespace Minimalist.Reactive.Tests; + +/// +/// DisposableTests. +/// +public class DisposableTests { + /// + /// Called when [dispose once]. + /// + [Fact] + public void OnlyDisposeOnce() + { + var disposed = 0; + var disposable = Disposable.Create(() => disposed++); + + disposable.Dispose(); + + Assert.Equal(1, disposed); + + disposable.Dispose(); + + Assert.Equal(1, disposed); + } /// - /// DisposableTests. + /// Empties the disposable. /// - public class DisposableTests + [Fact] + public void EmptyDisposable() { - /// - /// Called when [dispose once]. - /// - [Fact] - public void OnlyDisposeOnce() - { - var disposed = 0; - var disposable = Disposable.Create(() => disposed++); - - disposable.Dispose(); - - Assert.Equal(1, disposed); - - disposable.Dispose(); - - Assert.Equal(1, disposed); - } - - /// - /// Empties the disposable. - /// - [Fact] - public void EmptyDisposable() - { - var disposable = Disposable.Empty; - disposable.Dispose(); - disposable.Dispose(); - disposable.Dispose(); - } - - /// - /// Singles the disposable dispose. - /// - [Fact] - public void SingleDisposableDispose() - { - var disposable = new SingleDisposable(Disposable.Empty); - disposable.Dispose(); - Assert.True(disposable.IsDisposed); - } - - /// - /// Singles the disposable dispose with action. - /// - [Fact] - public void SingleDisposableDisposeWithAction() - { - var disposed = 0; - var disposable = new SingleDisposable(Disposable.Empty, () => disposed++); - disposable.Dispose(); - Assert.True(disposable.IsDisposed); - Assert.Equal(1, disposed); - - disposable.Dispose(); - Assert.True(disposable.IsDisposed); - Assert.Equal(1, disposed); - - disposable.Dispose(); - Assert.True(disposable.IsDisposed); - Assert.Equal(1, disposed); - } - - /// - /// Multiples the disposable dispose. - /// - [Fact] - public void MultipleDisposableDispose() - { - var disposable = new MultipleDisposable(); - disposable.Dispose(); - Assert.True(disposable.IsDisposed); - } - - /// - /// Multiples the disposable with items dispose. - /// - [Fact] - public void MultipleDisposableWithItemsDispose() - { - var disposable = new MultipleDisposable(); - disposable.Add(Disposable.Empty); - var disposed = 0; - - // create a disposable that will be disposed when the MultipleDisposable is disposed - var singleDisposable = Disposable.Empty.DisposeWith(() => disposed++); - - // add the disposable to the MultipleDisposable - singleDisposable?.DisposeWith(disposable); - - var singleDisposable2 = Disposable.Empty.DisposeWith(); - singleDisposable2?.DisposeWith(disposable); - - disposable.Dispose(); - Assert.True(disposable.IsDisposed); - Assert.True(singleDisposable?.IsDisposed); - Assert.True(singleDisposable2?.IsDisposed); - Assert.Equal(1, disposed); - } + var disposable = Disposable.Empty; + disposable.Dispose(); + disposable.Dispose(); + disposable.Dispose(); + } + + /// + /// Singles the disposable dispose. + /// + [Fact] + public void SingleDisposableDispose() + { + var disposable = new SingleDisposable(Disposable.Empty); + disposable.Dispose(); + Assert.True(disposable.IsDisposed); + } + + /// + /// Singles the disposable dispose with action. + /// + [Fact] + public void SingleDisposableDisposeWithAction() + { + var disposed = 0; + var disposable = new SingleDisposable(Disposable.Empty, () => disposed++); + disposable.Dispose(); + Assert.True(disposable.IsDisposed); + Assert.Equal(1, disposed); + + disposable.Dispose(); + Assert.True(disposable.IsDisposed); + Assert.Equal(1, disposed); + + disposable.Dispose(); + Assert.True(disposable.IsDisposed); + Assert.Equal(1, disposed); + } + + /// + /// Multiples the disposable dispose. + /// + [Fact] + public void MultipleDisposableDispose() + { + var disposable = new MultipleDisposable(); + disposable.Dispose(); + Assert.True(disposable.IsDisposed); + } + + /// + /// Multiples the disposable with items dispose. + /// + [Fact] + public void MultipleDisposableWithItemsDispose() + { + var disposable = new MultipleDisposable(); + disposable.Add(Disposable.Empty); + var disposed = 0; + + // create a disposable that will be disposed when the MultipleDisposable is disposed + var singleDisposable = Disposable.Empty.DisposeWith(() => disposed++); + + // add the disposable to the MultipleDisposable + singleDisposable?.DisposeWith(disposable); + + var singleDisposable2 = Disposable.Empty.DisposeWith(); + singleDisposable2?.DisposeWith(disposable); + + disposable.Dispose(); + Assert.True(disposable.IsDisposed); + Assert.True(singleDisposable?.IsDisposed); + Assert.True(singleDisposable2?.IsDisposed); + Assert.Equal(1, disposed); } } diff --git a/src/Minimalist.Reactive.Tests/DummyDisposable.cs b/src/Minimalist.Reactive.Tests/DummyDisposable.cs new file mode 100644 index 0000000..97ce632 --- /dev/null +++ b/src/Minimalist.Reactive.Tests/DummyDisposable.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2019-2023 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; +#if NET48 +#endif + +namespace Minimalist.Reactive.Tests; + +internal class DummyDisposable : IDisposable +{ + public static readonly DummyDisposable Instance = new(); + + public void Dispose() => throw new NotImplementedException(); +} diff --git a/src/Minimalist.Reactive.Tests/Minimalist.Reactive.Tests.csproj b/src/Minimalist.Reactive.Tests/Minimalist.Reactive.Tests.csproj index 5110094..213c4bf 100644 --- a/src/Minimalist.Reactive.Tests/Minimalist.Reactive.Tests.csproj +++ b/src/Minimalist.Reactive.Tests/Minimalist.Reactive.Tests.csproj @@ -1,16 +1,16 @@ - + - net6.0;net48 + net48;net6.0;net7.0 enable false preview - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/Minimalist.Reactive.Tests/ReplaySignalTests.cs b/src/Minimalist.Reactive.Tests/ReplaySignalTests.cs new file mode 100644 index 0000000..3795714 --- /dev/null +++ b/src/Minimalist.Reactive.Tests/ReplaySignalTests.cs @@ -0,0 +1,239 @@ +// Copyright (c) 2019-2023 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; +using Minimalist.Reactive.Concurrency; +using Minimalist.Reactive.Signals; +using Xunit; + +namespace Minimalist.Reactive.Tests; + +/// +/// ReplaySignalTests. +/// +public class ReplaySignalTests +{ + /// + /// Constructors the argument checking. + /// + [Fact] + public void Constructor_ArgumentChecking() + { + Assert.Throws(() => new ReplaySignal(-1)); + Assert.Throws(() => new ReplaySignal(-1, EmptyScheduler.Instance)); + Assert.Throws(() => new ReplaySignal(-1, TimeSpan.Zero)); + Assert.Throws(() => new ReplaySignal(-1, TimeSpan.Zero, EmptyScheduler.Instance)); + + Assert.Throws(() => new ReplaySignal(TimeSpan.FromTicks(-1))); + Assert.Throws(() => new ReplaySignal(TimeSpan.FromTicks(-1), EmptyScheduler.Instance)); + Assert.Throws(() => new ReplaySignal(0, TimeSpan.FromTicks(-1))); + Assert.Throws(() => new ReplaySignal(0, TimeSpan.FromTicks(-1), EmptyScheduler.Instance)); + + Assert.Throws(() => new ReplaySignal(null!)); + Assert.Throws(() => new ReplaySignal(0, null!)); + Assert.Throws(() => new ReplaySignal(TimeSpan.Zero, null!)); + Assert.Throws(() => new ReplaySignal(0, TimeSpan.Zero, null!)); + + // zero allowed + new ReplaySignal(0); + new ReplaySignal(TimeSpan.Zero); + new ReplaySignal(0, TimeSpan.Zero); + new ReplaySignal(0, EmptyScheduler.Instance); + new ReplaySignal(TimeSpan.Zero, EmptyScheduler.Instance); + new ReplaySignal(0, TimeSpan.Zero, EmptyScheduler.Instance); + } + + /// + /// Determines whether this instance has observers. + /// + [Fact] + public void HasObservers() + { + HasObserversImpl(new ReplaySignal()); + HasObserversImpl(new ReplaySignal(1)); + HasObserversImpl(new ReplaySignal(3)); + HasObserversImpl(new ReplaySignal(TimeSpan.FromSeconds(1))); + } + + /// + /// Determines whether [has observers dispose1]. + /// + [Fact] + public void HasObservers_Dispose1() + { + HasObservers_Dispose1Impl(new ReplaySignal()); + HasObservers_Dispose1Impl(new ReplaySignal(1)); + HasObservers_Dispose1Impl(new ReplaySignal(3)); + HasObservers_Dispose1Impl(new ReplaySignal(TimeSpan.FromSeconds(1))); + } + + /// + /// Determines whether [has observers dispose2]. + /// + [Fact] + public void HasObservers_Dispose2() + { + HasObservers_Dispose2Impl(new ReplaySignal()); + HasObservers_Dispose2Impl(new ReplaySignal(1)); + HasObservers_Dispose2Impl(new ReplaySignal(3)); + HasObservers_Dispose2Impl(new ReplaySignal(TimeSpan.FromSeconds(1))); + } + + /// + /// Determines whether [has observers dispose3]. + /// + [Fact] + public void HasObservers_Dispose3() + { + HasObservers_Dispose3Impl(new ReplaySignal()); + HasObservers_Dispose3Impl(new ReplaySignal(1)); + HasObservers_Dispose3Impl(new ReplaySignal(3)); + HasObservers_Dispose3Impl(new ReplaySignal(TimeSpan.FromSeconds(1))); + } + + /// + /// Determines whether [has observers on completed]. + /// + [Fact] + public void HasObservers_OnCompleted() + { + HasObservers_OnCompletedImpl(new ReplaySignal()); + HasObservers_OnCompletedImpl(new ReplaySignal(1)); + HasObservers_OnCompletedImpl(new ReplaySignal(3)); + HasObservers_OnCompletedImpl(new ReplaySignal(TimeSpan.FromSeconds(1))); + } + + /// + /// Determines whether [has observers on error]. + /// + [Fact] + public void HasObservers_OnError() + { + HasObservers_OnErrorImpl(new ReplaySignal()); + HasObservers_OnErrorImpl(new ReplaySignal(1)); + HasObservers_OnErrorImpl(new ReplaySignal(3)); + HasObservers_OnErrorImpl(new ReplaySignal(TimeSpan.FromSeconds(1))); + } + + /// + /// Called when [error argument checking]. + /// + [Fact] + public void OnError_ArgumentChecking() + { + Assert.Throws(() => new ReplaySignal().OnError(null!)); + Assert.Throws(() => new ReplaySignal(1).OnError(null!)); + Assert.Throws(() => new ReplaySignal(2).OnError(null!)); + Assert.Throws(() => new ReplaySignal(EmptyScheduler.Instance).OnError(null!)); + } + + /// + /// Subscribes the argument checking. + /// + [Fact] + public void Subscribe_ArgumentChecking() + { + Assert.Throws(() => new ReplaySignal().Subscribe(null!)); + Assert.Throws(() => new ReplaySignal(1).Subscribe(null!)); + Assert.Throws(() => new ReplaySignal(2).Subscribe(null!)); + Assert.Throws(() => new ReplaySignal(EmptyScheduler.Instance).Subscribe(null!)); + } + + private static void HasObservers_Dispose1Impl(ReplaySignal s) + { + Assert.False(s.HasObservers); + Assert.False(s.IsDisposed); + + var d = s.Subscribe(_ => { }); + Assert.True(s.HasObservers); + Assert.False(s.IsDisposed); + + s.Dispose(); + Assert.False(s.HasObservers); + Assert.True(s.IsDisposed); + + d.Dispose(); + Assert.False(s.HasObservers); + Assert.True(s.IsDisposed); + } + + private static void HasObservers_Dispose2Impl(ReplaySignal s) + { + Assert.False(s.HasObservers); + Assert.False(s.IsDisposed); + + var d = s.Subscribe(_ => { }); + Assert.True(s.HasObservers); + Assert.False(s.IsDisposed); + + d.Dispose(); + Assert.False(s.HasObservers); + Assert.False(s.IsDisposed); + + s.Dispose(); + Assert.False(s.HasObservers); + Assert.True(s.IsDisposed); + } + + private static void HasObservers_Dispose3Impl(ReplaySignal s) + { + Assert.False(s.HasObservers); + Assert.False(s.IsDisposed); + + s.Dispose(); + Assert.False(s.HasObservers); + Assert.True(s.IsDisposed); + } + + private static void HasObservers_OnCompletedImpl(ReplaySignal s) + { + Assert.False(s.HasObservers); + + var d = s.Subscribe(_ => { }); + Assert.True(s.HasObservers); + + s.OnNext(42); + Assert.True(s.HasObservers); + + s.OnCompleted(); + Assert.False(s.HasObservers); + } + + private static void HasObservers_OnErrorImpl(ReplaySignal s) + { + Assert.False(s.HasObservers); + + var d = s.Subscribe(_ => { }, _ => { }); + Assert.True(s.HasObservers); + + s.OnNext(42); + Assert.True(s.HasObservers); + + s.OnError(new Exception()); + Assert.False(s.HasObservers); + } + + private static void HasObserversImpl(ReplaySignal s) + { + Assert.False(s.HasObservers); + + var d1 = s.Subscribe(_ => { }); + Assert.True(s.HasObservers); + + d1.Dispose(); + Assert.False(s.HasObservers); + + var d2 = s.Subscribe(_ => { }); + Assert.True(s.HasObservers); + + var d3 = s.Subscribe(_ => { }); + Assert.True(s.HasObservers); + + d2.Dispose(); + Assert.True(s.HasObservers); + + d3.Dispose(); + Assert.False(s.HasObservers); + } +} diff --git a/src/Minimalist.Reactive.Tests/SignalCreateTests.cs b/src/Minimalist.Reactive.Tests/SignalCreateTests.cs new file mode 100644 index 0000000..7350037 --- /dev/null +++ b/src/Minimalist.Reactive.Tests/SignalCreateTests.cs @@ -0,0 +1,124 @@ +// Copyright (c) 2019-2023 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Minimalist.Reactive.Disposables; +using Minimalist.Reactive.Signals; +using Xunit; + +namespace Minimalist.Reactive.Tests; + +/// +/// SignalsCreateTests. +/// +public class SignalCreateTests +{ + /// + /// Creates the argument checking. + /// + [Fact] + public void Create_ArgumentChecking() + { + Assert.Throws(() => Signal.Create(default(Func, IDisposable>))); + + Assert.Throws(() => Signal.Create(default).Subscribe(null)); + } + + /// + /// Creates the null coalescing action. + /// + [Fact] + public void Create_NullCoalescingAction() + { + var xs = Signal.Create(o => + { + o.OnNext(42); + return Disposable.Create(default!); + }); + + var lst = new List(); + var d = xs.Subscribe(lst.Add); + d.Dispose(); + + Assert.True(lst.SequenceEqual(new[] { 42 })); + } + + /// + /// Creates the exception. + /// + [Fact] + public void Create_Exception() => + Assert.Throws(() => + Signal.Create(new Func, IDisposable>(_ => throw new InvalidOperationException())).Subscribe()); + + /// + /// Creates the observer throws. + /// + [Fact] + public void Create_ObserverThrows() + { + Assert.Throws(() => + Signal.Create(o => + { + o.OnNext(1); + return Disposable.Empty; + }).Subscribe(x => { throw new InvalidOperationException(); })); + Assert.Throws(() => + Signal.Create(o => + { + o.OnError(new Exception()); + return Disposable.Empty; + }).Subscribe(x => { }, ex => { throw new InvalidOperationException(); })); + Assert.Throws(() => + Signal.Create(o => + { + o.OnCompleted(); + return Disposable.Empty; + }).Subscribe(x => { }, ex => { }, () => { throw new InvalidOperationException(); })); + } + + /// + /// Creates the with disposable argument checking. + /// + [Fact] + public void CreateWithDisposable_ArgumentChecking() + { + Assert.Throws(() => Signal.Create(default(Func, IDisposable>))); + Assert.Throws(() => Signal.Create(_ => DummyDisposable.Instance).Subscribe(null)); + Assert.Throws(() => Signal.Create(o => + { + o.OnError(null); + return DummyDisposable.Instance; + }).Subscribe(null)); + } + + /// + /// Creates the with disposable null coalescing action. + /// + [Fact] + public void CreateWithDisposable_NullCoalescingAction() + { + var xs = Signal.Create(o => + { + o.OnNext(42); + return default!; + }); + + var lst = new List(); + var d = xs.Subscribe(lst.Add); + d.Dispose(); + + Assert.True(lst.SequenceEqual(new[] { 42 })); + } + + /// + /// Creates the with disposable exception. + /// + [Fact] + public void CreateWithDisposable_Exception() => + Assert.Throws(() => + Signal.Create(new Func, IDisposable>(_ => throw new InvalidOperationException())).Subscribe()); +} diff --git a/src/Minimalist.Reactive.Tests/SignalFromTaskTest.cs b/src/Minimalist.Reactive.Tests/SignalFromTaskTest.cs new file mode 100644 index 0000000..041ab59 --- /dev/null +++ b/src/Minimalist.Reactive.Tests/SignalFromTaskTest.cs @@ -0,0 +1,599 @@ +// Copyright (c) 2019-2023 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Minimalist.Reactive.Signals; +using Xunit; + +namespace Minimalist.Reactive.Tests; + +/// +/// SignalFromTaskTest. +/// +public class SignalFromTaskTest +{ + /// + /// Signals from task handles user exceptions. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task SignalFromTaskHandlesUserExceptions() + { + var statusTrail = new List<(int, string)>(); + var position = 0; + Exception? exception = null; + var fixture = Signal.FromTask( + async (cts) => + { + statusTrail.Add((position++, "started command")); + await Task.Delay(10000, cts.Token).HandleCancellation(async () => + { + // User Handles cancellation. + statusTrail.Add((position++, "starting cancelling command")); + + // dummy cleanup + await Task.Delay(5000, CancellationToken.None).ConfigureAwait(false); + statusTrail.Add((position++, "finished cancelling command")); + }).ConfigureAwait(true); + + if (!cts.IsCancellationRequested) + { + statusTrail.Add((position++, "finished command Normally")); + } + + throw new Exception("break execution"); + }).Catch( + ex => + { + exception = ex; + statusTrail.Add((position++, "Exception Should Be here")); + return Signal.Throw(ex); + }).Finally(() => statusTrail.Add((position++, "Should always come here."))); + + var result = false; + var cancel = fixture.Subscribe(_ => result = true); + await Task.Delay(500).ConfigureAwait(true); + + Assert.Contains("started command", statusTrail.Select(x => x.Item2)); + + await Task.Delay(10000).ConfigureAwait(true); + cancel.Dispose(); + + // Wait 6000 ms to allow execution and cleanup to complete + await Task.Delay(6000).ConfigureAwait(false); + + Assert.DoesNotContain("starting cancelling command", statusTrail.Select(x => x.Item2)); + Assert.Contains("Should always come here.", statusTrail.Select(x => x.Item2)); + Assert.DoesNotContain("finished cancelling command", statusTrail.Select(x => x.Item2)); + Assert.Contains("Exception Should Be here", statusTrail.Select(x => x.Item2)); + Assert.Contains("finished command Normally", statusTrail.Select(x => x.Item2)); + Assert.False(result); + //// (0, "started command") + //// (1, "finished command Normally") + //// (2, "Exception Should Be here") + //// (3, "Should always come here.") + } + + /// + /// Signals from task handles cancellation. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task SignalFromTaskHandlesCancellation() + { + var statusTrail = new List<(int, string)>(); + var position = 0; + Exception? exception = null; + var fixture = Signal.FromTask( + async (cts) => + { + statusTrail.Add((position++, "started command")); + await Task.Delay(10000, cts.Token).HandleCancellation(async () => + { + // User Handles cancellation. + statusTrail.Add((position++, "starting cancelling command")); + + // dummy cleanup + await Task.Delay(5000, CancellationToken.None).ConfigureAwait(false); + statusTrail.Add((position++, "finished cancelling command")); + }).ConfigureAwait(true); + + if (!cts.IsCancellationRequested) + { + statusTrail.Add((position++, "finished command Normally")); + } + + return RxVoid.Default; + }).Catch( + ex => + { + exception = ex; + statusTrail.Add((position++, "Exception Should Be here")); + return Signal.Throw(ex); + }).Finally(() => statusTrail.Add((position++, "Should always come here."))); + + var result = false; + var cancel = fixture.Subscribe(_ => result = true); + await Task.Delay(500).ConfigureAwait(true); + + Assert.Contains("started command", statusTrail.Select(x => x.Item2)); + cancel.Dispose(); + + // Wait 6000 ms to allow execution and cleanup to complete + await Task.Delay(6000).ConfigureAwait(false); + + Assert.Contains("starting cancelling command", statusTrail.Select(x => x.Item2)); + Assert.Contains("Should always come here.", statusTrail.Select(x => x.Item2)); + Assert.Contains("finished cancelling command", statusTrail.Select(x => x.Item2)); + Assert.DoesNotContain("finished command Normally", statusTrail.Select(x => x.Item2)); + Assert.False(result); + //// (0, "started command") + //// (1, "starting cancelling command") + //// (2, "Should always come here.") + //// (3, "finished cancelling command") + } + + /// + /// Signals from task handles token cancellation. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task SignalFromTaskHandlesTokenCancellation() + { + var statusTrail = new List<(int, string)>(); + var position = 0; + Exception? exception = null; + var fixture = Signal.FromTask( + async (cts) => + { + statusTrail.Add((position++, "started command")); + await Task.Delay(1000, cts.Token).HandleCancellation(); + _ = Task.Run(async () => + { + // Wait for 1s then cancel + await Task.Delay(1000); + cts.Cancel(); + }); + await Task.Delay(5000, cts.Token).HandleCancellation(async () => + { + // User Handles cancellation. + statusTrail.Add((position++, "starting cancelling command")); + + // dummy cleanup + await Task.Delay(5000, CancellationToken.None).ConfigureAwait(false); + statusTrail.Add((position++, "finished cancelling command")); + }).ConfigureAwait(true); + + if (!cts.IsCancellationRequested) + { + statusTrail.Add((position++, "finished command Normally")); + } + + return RxVoid.Default; + }).Catch( + ex => + { + exception = ex; + statusTrail.Add((position++, "Exception Should Be here")); + return Signal.Throw(ex); + }).Finally(() => statusTrail.Add((position++, "Should always come here."))); + + var result = false; + var cancel = fixture.Subscribe(_ => result = true); + await Task.Delay(500).ConfigureAwait(true); + + Assert.Contains("started command", statusTrail.Select(x => x.Item2)); + + // Wait 8000 ms to allow execution and cleanup to complete + await Task.Delay(8000).ConfigureAwait(false); + + Assert.Contains("starting cancelling command", statusTrail.Select(x => x.Item2)); + Assert.Contains("Should always come here.", statusTrail.Select(x => x.Item2)); + Assert.Contains("finished cancelling command", statusTrail.Select(x => x.Item2)); + Assert.DoesNotContain("finished command Normally", statusTrail.Select(x => x.Item2)); + Assert.False(result); + //// (0, "started command") + //// (1, "starting cancelling command") + //// (2, "Should always come here.") + //// (3, "finished cancelling command") + } + + /// + /// Signals from task handles cancellation in base. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task SignalFromTaskHandlesCancellationInBase() + { + var statusTrail = new List<(int, string)>(); + var position = 0; + Exception? exception = null; + var fixture = Signal.FromTask( + async (cts) => + { + var ex = new Exception(); + statusTrail.Add((position++, "started command")); + await Task.Delay(10000, cts.Token).ConfigureAwait(true); + if (!cts.IsCancellationRequested) + { + statusTrail.Add((position++, "finished command Normally")); + } + + return RxVoid.Default; + }).Catch( + ex => + { + exception = ex; + statusTrail.Add((position++, "Exception Should Be here")); + return Signal.Throw(ex); + }).Finally(() => statusTrail.Add((position++, "Should always come here."))); + + var cancel = fixture.Subscribe(); + await Task.Delay(500).ConfigureAwait(true); + Assert.Contains("started command", statusTrail.Select(x => x.Item2)); + cancel.Dispose(); + + // Wait 5050 ms to allow execution and cleanup to complete + await Task.Delay(6000).ConfigureAwait(false); + + Assert.DoesNotContain("finished command Normally", statusTrail.Select(x => x.Item2)); + Assert.Equal("Should always come here.", statusTrail.Last().Item2); + + //// (0, "started command") + //// (1, "Should always come here.") + } + + /// + /// Signals from task handles completion. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task SignalFromTaskHandlesCompletion() + { + var statusTrail = new List<(int, string)>(); + var position = 0; + Exception? exception = null; + var fixture = Signal.FromTask( + async (cts) => + { + statusTrail.Add((position++, "started command")); + await Task.Delay(10000, cts.Token).HandleCancellation(async () => + { + // NOT EXPECTED TO ENTER HERE + + // User Handles cancellation. + statusTrail.Add((position++, "starting cancelling command")); + + // dummy cleanup + await Task.Delay(5000, CancellationToken.None).ConfigureAwait(false); + statusTrail.Add((position++, "finished cancelling command")); + }).ConfigureAwait(true); + + if (!cts.IsCancellationRequested) + { + statusTrail.Add((position++, "finished command Normally")); + } + + return RxVoid.Default; + }).Catch( + ex => + { + exception = ex; + statusTrail.Add((position++, "Exception Should Be here")); + return Signal.Throw(ex); + }).Finally(() => statusTrail.Add((position++, "Should always come here."))); + + var result = false; + var cancel = fixture.Subscribe(_ => result = true); + await Task.Delay(500).ConfigureAwait(true); + + Assert.Contains("started command", statusTrail.Select(x => x.Item2)); + + // Wait 11000 ms to allow execution complete + await Task.Delay(11000).ConfigureAwait(false); + + Assert.DoesNotContain("starting cancelling command", statusTrail.Select(x => x.Item2)); + Assert.DoesNotContain("finished cancelling command", statusTrail.Select(x => x.Item2)); + Assert.Contains("finished command Normally", statusTrail.Select(x => x.Item2)); + Assert.Equal("Should always come here.", statusTrail.Last().Item2); + Assert.True(result); + //// (0, "started command") + //// (2, "finished command Normally") + //// (1, "Should always come here.") + } + + /// + /// Signals from task t handles user exceptions. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task SignalFromTask_T_HandlesUserExceptions() + { + var statusTrail = new List<(int, string)>(); + var position = 0; + Exception? exception = null; + var fixture = Signal.FromTask( + async (cts) => + { + statusTrail.Add((position++, "started command")); + await Task.Delay(10000, cts.Token).HandleCancellation(async () => + { + // User Handles cancellation. + statusTrail.Add((position++, "starting cancelling command")); + + // dummy cleanup + await Task.Delay(5000, CancellationToken.None).ConfigureAwait(false); + statusTrail.Add((position++, "finished cancelling command")); + }).ConfigureAwait(true); + + if (!cts.IsCancellationRequested) + { + statusTrail.Add((position++, "finished command Normally")); + } + + throw new Exception("break execution"); + }).Catch( + ex => + { + exception = ex; + statusTrail.Add((position++, "Exception Should Be here")); + return Signal.Throw(ex); + }).Finally(() => statusTrail.Add((position++, "Should always come here."))); + + var result = false; + var cancel = fixture.Subscribe(_ => result = true); + await Task.Delay(500).ConfigureAwait(true); + + Assert.Contains("started command", statusTrail.Select(x => x.Item2)); + + await Task.Delay(10000).ConfigureAwait(true); + cancel.Dispose(); + + // Wait 6000 ms to allow execution and cleanup to complete + await Task.Delay(6000).ConfigureAwait(false); + + Assert.DoesNotContain("starting cancelling command", statusTrail.Select(x => x.Item2)); + Assert.Contains("Should always come here.", statusTrail.Select(x => x.Item2)); + Assert.DoesNotContain("finished cancelling command", statusTrail.Select(x => x.Item2)); + Assert.Contains("Exception Should Be here", statusTrail.Select(x => x.Item2)); + Assert.Contains("finished command Normally", statusTrail.Select(x => x.Item2)); + Assert.False(result); + //// (0, "started command") + //// (1, "finished command Normally") + //// (2, "Exception Should Be here") + //// (3, "Should always come here.") + } + + /// + /// Signals from task t handles cancellation. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task SignalFromTask_T_HandlesCancellation() + { + var statusTrail = new List<(int, string)>(); + var position = 0; + Exception? exception = null; + var fixture = Signal.FromTask( + async (cts) => + { + statusTrail.Add((position++, "started command")); + await Task.Delay(10000, cts.Token).HandleCancellation(async () => + { + // User Handles cancellation. + statusTrail.Add((position++, "starting cancelling command")); + + // dummy cleanup + await Task.Delay(5000, CancellationToken.None).ConfigureAwait(false); + statusTrail.Add((position++, "finished cancelling command")); + }).ConfigureAwait(true); + + if (!cts.IsCancellationRequested) + { + statusTrail.Add((position++, "finished command Normally")); + } + + return RxVoid.Default; + }).Catch( + ex => + { + exception = ex; + statusTrail.Add((position++, "Exception Should Be here")); + return Signal.Throw(ex); + }).Finally(() => statusTrail.Add((position++, "Should always come here."))); + + var result = false; + var cancel = fixture.Subscribe(_ => result = true); + await Task.Delay(500).ConfigureAwait(true); + + Assert.Contains("started command", statusTrail.Select(x => x.Item2)); + cancel.Dispose(); + + // Wait 6000 ms to allow execution and cleanup to complete + await Task.Delay(6000).ConfigureAwait(false); + + Assert.Contains("starting cancelling command", statusTrail.Select(x => x.Item2)); + Assert.Contains("Should always come here.", statusTrail.Select(x => x.Item2)); + Assert.Contains("finished cancelling command", statusTrail.Select(x => x.Item2)); + Assert.DoesNotContain("finished command Normally", statusTrail.Select(x => x.Item2)); + Assert.False(result); + //// (0, "started command") + //// (1, "starting cancelling command") + //// (3, "Should always come here.") + //// (2, "finished cancelling command") + } + + /// + /// Signals from task t handles token cancellation. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task SignalFromTask_T_HandlesTokenCancellation() + { + var statusTrail = new List<(int, string)>(); + var position = 0; + Exception? exception = null; + var fixture = Signal.FromTask( + async (cts) => + { + statusTrail.Add((position++, "started command")); + await Task.Delay(1000, cts.Token).HandleCancellation(); + _ = Task.Run(async () => + { + // Wait for 1s then cancel + await Task.Delay(1000); + cts.Cancel(); + }); + await Task.Delay(5000, cts.Token).HandleCancellation(async () => + { + // User Handles cancellation. + statusTrail.Add((position++, "starting cancelling command")); + + // dummy cleanup + await Task.Delay(5000, CancellationToken.None).ConfigureAwait(false); + statusTrail.Add((position++, "finished cancelling command")); + }).ConfigureAwait(true); + + if (!cts.IsCancellationRequested) + { + statusTrail.Add((position++, "finished command Normally")); + } + + return RxVoid.Default; + }).Catch( + ex => + { + exception = ex; + statusTrail.Add((position++, "Exception Should Be here")); + return Signal.Throw(ex); + }).Finally(() => statusTrail.Add((position++, "Should always come here."))); + + var result = false; + var cancel = fixture.Subscribe(_ => result = true); + await Task.Delay(500).ConfigureAwait(true); + + Assert.Contains("started command", statusTrail.Select(x => x.Item2)); + + // Wait 8000 ms to allow execution and cleanup to complete + await Task.Delay(8000).ConfigureAwait(false); + + Assert.Contains("starting cancelling command", statusTrail.Select(x => x.Item2)); + Assert.Contains("Should always come here.", statusTrail.Select(x => x.Item2)); + Assert.Contains("finished cancelling command", statusTrail.Select(x => x.Item2)); + Assert.DoesNotContain("finished command Normally", statusTrail.Select(x => x.Item2)); + Assert.False(result); + //// (0, "started command") + //// (1, "starting cancelling command") + //// (2, "Should always come here.") + //// (3, "finished cancelling command") + } + + /// + /// Signals from task t handles cancellation in base. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task SignalFromTask_T_HandlesCancellationInBase() + { + var statusTrail = new List<(int, string)>(); + var position = 0; + Exception? exception = null; + var fixture = Signal.FromTask( + async (cts) => + { + var ex = new Exception(); + statusTrail.Add((position++, "started command")); + await Task.Delay(10000, cts.Token).ConfigureAwait(true); + if (!cts.IsCancellationRequested) + { + statusTrail.Add((position++, "finished command Normally")); + } + + return RxVoid.Default; + }).Catch( + ex => + { + exception = ex; + statusTrail.Add((position++, "Exception Should Be here")); + return Signal.Throw(ex); + }).Finally(() => statusTrail.Add((position++, "Should always come here."))); + + var cancel = fixture.Subscribe(); + await Task.Delay(500).ConfigureAwait(true); + Assert.Contains("started command", statusTrail.Select(x => x.Item2)); + cancel.Dispose(); + + // Wait 5050 ms to allow execution and cleanup to complete + await Task.Delay(6000).ConfigureAwait(false); + + Assert.DoesNotContain("finished command Normally", statusTrail.Select(x => x.Item2)); + Assert.Equal("Should always come here.", statusTrail.Last().Item2); + + //// (0, "started command") + //// (1, "Should always come here.") + } + + /// + /// Signals from task t handles completion. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task SignalFromTask_T_HandlesCompletion() + { + var statusTrail = new List<(int, string)>(); + var position = 0; + Exception? exception = null; + var fixture = Signal.FromTask( + async (cts) => + { + statusTrail.Add((position++, "started command")); + await Task.Delay(10000, cts.Token).HandleCancellation(async () => + { + // NOT EXPECTED TO ENTER HERE + + // User Handles cancellation. + statusTrail.Add((position++, "starting cancelling command")); + + // dummy cleanup + await Task.Delay(5000, CancellationToken.None).ConfigureAwait(false); + statusTrail.Add((position++, "finished cancelling command")); + }).ConfigureAwait(true); + + if (!cts.IsCancellationRequested) + { + statusTrail.Add((position++, "finished command Normally")); + } + + return RxVoid.Default; + }).Catch( + ex => + { + exception = ex; + statusTrail.Add((position++, "Exception Should Be here")); + return Signal.Throw(ex); + }).Finally(() => statusTrail.Add((position++, "Should always come here."))); + + var result = false; + var cancel = fixture.Subscribe(_ => result = true); + await Task.Delay(500).ConfigureAwait(true); + + Assert.Contains("started command", statusTrail.Select(x => x.Item2)); + + // Wait 11000 ms to allow execution complete + await Task.Delay(11000).ConfigureAwait(false); + + Assert.DoesNotContain("starting cancelling command", statusTrail.Select(x => x.Item2)); + Assert.DoesNotContain("finished cancelling command", statusTrail.Select(x => x.Item2)); + Assert.Contains("finished command Normally", statusTrail.Select(x => x.Item2)); + Assert.Equal("Should always come here.", statusTrail.Last().Item2); + Assert.True(result); + //// (0, "started command") + //// (2, "finished command Normally") + //// (1, "Should always come here.") + } +} diff --git a/src/Minimalist.Reactive.Tests/SignalTests.cs b/src/Minimalist.Reactive.Tests/SignalTests.cs index b0f176b..0b7aa37 100644 --- a/src/Minimalist.Reactive.Tests/SignalTests.cs +++ b/src/Minimalist.Reactive.Tests/SignalTests.cs @@ -1,369 +1,369 @@ -// Copyright (c) 2019-2022 ReactiveUI Association Incorporated. All rights reserved. +// Copyright (c) 2019-2023 ReactiveUI Association Incorporated. All rights reserved. // ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. using System; using System.Collections.Generic; using System.Linq; +using Minimalist.Reactive.Signals; using Xunit; -namespace Minimalist.Reactive.Tests +namespace Minimalist.Reactive.Tests; + +/// +/// SubjectTests. +/// +public class SignalTests { /// - /// SubjectTests. + /// Called when [next]. + /// + [Fact] + public void OnNext() + { + var subject = new Signal(); + var value = 0; + + var subscription = subject.Subscribe(i => value += i); + + subject.OnNext(1); + Assert.Equal(1, value); + + subject.OnNext(1); + Assert.Equal(2, value); + + subscription.Dispose(); + + subject.OnNext(1); + Assert.Equal(2, value); + } + + /// + /// Called when [next disposed]. + /// + [Fact] + public void OnNextDisposed() + { + var subject = new Signal(); + + subject.Dispose(); + + Assert.Throws(() => subject.OnNext(1)); + } + + /// + /// Called when [next disposed subscriber]. + /// + [Fact] + public void OnNextDisposedSubscriber() + { + var subject = new Signal(); + var value = 0; + + subject.Subscribe(i => value += i).Dispose(); + + subject.OnNext(1); + + Assert.Equal(0, value); + } + + /// + /// Called when [completed]. + /// + [Fact] + public void OnCompleted() + { + var subject = new Signal(); + var completed = false; + + var subscription = subject.Subscribe(_ => { }, () => completed = true); + + subject.OnCompleted(); + + Assert.True(completed); + } + + /// + /// Called when [completed no op]. + /// + [Fact] + public void OnCompleted_NoErrors() + { + var subject = new Signal(); + + var subscription = subject.Subscribe(_ => { }); + + subject.OnCompleted(); + } + + /// + /// Called when [completed once]. + /// + [Fact] + public void OnCompletedOnce() + { + var subject = new Signal(); + var completed = 0; + + var subscription = subject.Subscribe(_ => { }, () => completed++); + + subject.OnCompleted(); + + Assert.Equal(1, completed); + + subject.OnCompleted(); + + Assert.Equal(1, completed); + } + + /// + /// Called when [completed disposed]. + /// + [Fact] + public void OnCompletedDisposed() + { + var subject = new Signal(); + + subject.Dispose(); + + Assert.Throws(() => subject.OnCompleted()); + } + + /// + /// Called when [completed disposed subscriber]. + /// + [Fact] + public void OnCompletedDisposedSubscriber() + { + var subject = new Signal(); + var completed = false; + + subject.Subscribe(_ => { }, () => completed = true).Dispose(); + + subject.OnCompleted(); + + Assert.False(completed); + } + + /// + /// Called when [error]. /// - public class SignalTests + [Fact] + public void OnError() { - /// - /// Called when [next]. - /// - [Fact] - public void OnNext() - { - var subject = new Signal(); - var value = 0; + var subject = new Signal(); + var error = false; - var subscription = subject.Subscribe(i => value += i); + var subscription = subject.Subscribe(_ => { }, _ => error = true); - subject.OnNext(1); - Assert.Equal(1, value); + subject.OnError(new Exception()); - subject.OnNext(1); - Assert.Equal(2, value); + Assert.True(error); + } - subscription.Dispose(); + /// + /// Called when [error once]. + /// + [Fact] + public void OnErrorOnce() + { + var subject = new Signal(); + var errors = 0; - subject.OnNext(1); - Assert.Equal(2, value); - } + var subscription = subject.Subscribe(_ => { }, _ => errors++); - /// - /// Called when [next disposed]. - /// - [Fact] - public void OnNextDisposed() - { - var subject = new Signal(); + subject.OnError(new Exception()); - subject.Dispose(); + Assert.Equal(1, errors); - Assert.Throws(() => subject.OnNext(1)); - } - - /// - /// Called when [next disposed subscriber]. - /// - [Fact] - public void OnNextDisposedSubscriber() - { - var subject = new Signal(); - var value = 0; + subject.OnError(new Exception()); - subject.Subscribe(i => value += i).Dispose(); + Assert.Equal(1, errors); + } - subject.OnNext(1); + /// + /// Called when [error disposed]. + /// + [Fact] + public void OnErrorDisposed() + { + var subject = new Signal(); - Assert.Equal(0, value); - } - - /// - /// Called when [completed]. - /// - [Fact] - public void OnCompleted() - { - var subject = new Signal(); - var completed = false; + subject.Dispose(); - var subscription = subject.Subscribe(_ => { }, () => completed = true); + Assert.Throws(() => subject.OnError(new Exception())); + } - subject.OnCompleted(); + /// + /// Called when [error disposed subscriber]. + /// + [Fact] + public void OnErrorDisposedSubscriber() + { + var subject = new Signal(); + var error = false; - Assert.True(completed); - } + subject.Subscribe(_ => { }, _ => error = true).Dispose(); - /// - /// Called when [completed no op]. - /// - [Fact] - public void OnCompleted_NoErrors() - { - var subject = new Signal(); + subject.OnError(new Exception()); + + Assert.False(error); + } + + /// + /// Called when [error rethrows by default]. + /// + [Fact] + public void OnErrorRethrowsByDefault() + { + var subject = new Signal(); + + var subs = subject.Subscribe(_ => { }); + + Assert.Throws(() => subject.OnError(new ArgumentException())); + } + + /// + /// Called when [error null throws]. + /// + [Fact] + public void OnErrorNullThrows() => + Assert.Throws(() => new Signal().OnError(null!)); + + /// + /// Subscribes the null throws. + /// + [Fact] + public void SubscribeNullThrows() => + Assert.Throws(() => new Signal().Subscribe(null!)); - var subscription = subject.Subscribe(_ => { }); + /// + /// Subscribes the disposed throws. + /// + [Fact] + public void SubscribeDisposedThrows() + { + var subject = new Signal(); - subject.OnCompleted(); - } + subject.Dispose(); - /// - /// Called when [completed once]. - /// - [Fact] - public void OnCompletedOnce() - { - var subject = new Signal(); - var completed = 0; + Assert.Throws(() => subject.Subscribe(_ => { })); + } - var subscription = subject.Subscribe(_ => { }, () => completed++); + /// + /// Subscribes the on completed. + /// + [Fact] + public void SubscribeOnCompleted() + { + var subject = new Signal(); + subject.OnCompleted(); + var completed = false; - subject.OnCompleted(); + subject.Subscribe(_ => { }, () => completed = true).Dispose(); - Assert.Equal(1, completed); + Assert.True(completed); + } - subject.OnCompleted(); + /// + /// Subscribes the on error. + /// + [Fact] + public void SubscribeOnError() + { + var subject = new Signal(); + subject.OnError(new Exception()); + var error = false; - Assert.Equal(1, completed); - } + subject.Subscribe(_ => { }, _ => error = true); - /// - /// Called when [completed disposed]. - /// - [Fact] - public void OnCompletedDisposed() - { - var subject = new Signal(); + Assert.True(error); + } - subject.Dispose(); + /// + /// Subjects the where. + /// + [Fact] + public void SubjectWhere() + { + var subject = new Signal(); + subject.Where(i => i % 2 == 0).Subscribe(i => Assert.Equal(2, i)); + subject.OnNext(1); + subject.OnNext(2); + subject.OnNext(3); + subject.Dispose(); + } - Assert.Throws(() => subject.OnCompleted()); - } + /// + /// Subjects the select. + /// + [Fact] + public void SubjectSelect() + { + var subject = new Signal(); + subject.Select(i => i * 2).Subscribe(i => Assert.Equal(4, i)); + subject.OnNext(2); + subject.Dispose(); + } - /// - /// Called when [completed disposed subscriber]. - /// - [Fact] - public void OnCompletedDisposedSubscriber() - { - var subject = new Signal(); - var completed = false; + /// + /// Subjects the buffer. + /// + [Fact] + public void SubjectBuffer() + { + var subject = new Signal(); + var result = new List(); + subject.Buffer(2).Subscribe(i => result = i.ToList()); + subject.OnNext(1); + subject.OnNext(2); + Assert.Equal(new[] { 1, 2 }, result); + subject.OnNext(3); + subject.OnNext(4); + Assert.Equal(new[] { 3, 4 }, result); + subject.OnNext(5); + subject.OnNext(6); + Assert.Equal(new[] { 5, 6 }, result); + subject.Dispose(); + } - subject.Subscribe(_ => { }, () => completed = true).Dispose(); - - subject.OnCompleted(); - - Assert.False(completed); - } + /// + /// Subjects the buffer skip2. + /// + [Fact] + public void SubjectBufferTake2Skip2() + { + var subject = new Signal(); + var result = new List(); + subject.Buffer(2, 2).Subscribe(i => result = i.ToList()); + subject.OnNext(1); + subject.OnNext(2); + Assert.Equal(new[] { 1, 2 }, result); + subject.OnNext(3); + subject.OnNext(4); + Assert.Equal(new[] { 1, 2 }, result); + subject.OnNext(5); + subject.OnNext(6); + Assert.Equal(new[] { 5, 6 }, result); + subject.OnNext(7); + subject.OnNext(8); + Assert.Equal(new[] { 5, 6 }, result); + subject.Dispose(); + } - /// - /// Called when [error]. - /// - [Fact] - public void OnError() - { - var subject = new Signal(); - var error = false; - - var subscription = subject.Subscribe(_ => { }, e => error = true); - - subject.OnError(new Exception()); - - Assert.True(error); - } - - /// - /// Called when [error once]. - /// - [Fact] - public void OnErrorOnce() - { - var subject = new Signal(); - var errors = 0; - - var subscription = subject.Subscribe(_ => { }, e => errors++); - - subject.OnError(new Exception()); - - Assert.Equal(1, errors); - - subject.OnError(new Exception()); - - Assert.Equal(1, errors); - } - - /// - /// Called when [error disposed]. - /// - [Fact] - public void OnErrorDisposed() - { - var subject = new Signal(); - - subject.Dispose(); - - Assert.Throws(() => subject.OnError(new Exception())); - } - - /// - /// Called when [error disposed subscriber]. - /// - [Fact] - public void OnErrorDisposedSubscriber() - { - var subject = new Signal(); - var error = false; - - subject.Subscribe(_ => { }, e => error = true).Dispose(); - - subject.OnError(new Exception()); - - Assert.False(error); - } - - /// - /// Called when [error rethrows by default]. - /// - [Fact] - public void OnErrorRethrowsByDefault() - { - var subject = new Signal(); - - var subs = subject.Subscribe(_ => { }); - - Assert.Throws(() => subject.OnError(new ArgumentException())); - } - - /// - /// Called when [error null throws]. - /// - [Fact] - public void OnErrorNullThrows() => - Assert.Throws(() => new Signal().OnError(null!)); - - /// - /// Subscribes the null throws. - /// - [Fact] - public void SubscribeNullThrows() => - Assert.Throws(() => new Signal().Subscribe(null!)); - - /// - /// Subscribes the disposed throws. - /// - [Fact] - public void SubscribeDisposedThrows() - { - var subject = new Signal(); - - subject.Dispose(); - - Assert.Throws(() => subject.Subscribe(_ => { })); - } - - /// - /// Subscribes the on completed. - /// - [Fact] - public void SubscribeOnCompleted() - { - var subject = new Signal(); - subject.OnCompleted(); - var completed = false; - - subject.Subscribe(_ => { }, () => completed = true).Dispose(); - - Assert.True(completed); - } - - /// - /// Subscribes the on error. - /// - [Fact] - public void SubscribeOnError() - { - var subject = new Signal(); - subject.OnError(new Exception()); - var error = false; - - subject.Subscribe(_ => { }, e => error = true); - - Assert.True(error); - } - - /// - /// Subjects the where. - /// - [Fact] - public void SubjectWhere() - { - var subject = new Signal(); - subject.Where(i => i % 2 == 0).Subscribe(i => Assert.Equal(2, i)); - subject.OnNext(1); - subject.OnNext(2); - subject.OnNext(3); - subject.Dispose(); - } - - /// - /// Subjects the select. - /// - [Fact] - public void SubjectSelect() - { - var subject = new Signal(); - subject.Select(i => i * 2).Subscribe(i => Assert.Equal(4, i)); - subject.OnNext(2); - subject.Dispose(); - } - - /// - /// Subjects the buffer. - /// - [Fact] - public void SubjectBuffer() - { - var subject = new Signal(); - var result = new List(); - subject.Buffer(2).Subscribe(i => result = i.ToList()); - subject.OnNext(1); - subject.OnNext(2); - Assert.Equal(new[] { 1, 2 }, result); - subject.OnNext(3); - subject.OnNext(4); - Assert.Equal(new[] { 3, 4 }, result); - subject.OnNext(5); - subject.OnNext(6); - Assert.Equal(new[] { 5, 6 }, result); - subject.Dispose(); - } - - /// - /// Subjects the buffer skip2. - /// - [Fact] - public void SubjectBufferTake2Skip2() - { - var subject = new Signal(); - var result = new List(); - subject.Buffer(2, 2).Subscribe(i => result = i.ToList()); - subject.OnNext(1); - subject.OnNext(2); - Assert.Equal(new[] { 1, 2 }, result); - subject.OnNext(3); - subject.OnNext(4); - Assert.Equal(new[] { 1, 2 }, result); - subject.OnNext(5); - subject.OnNext(6); - Assert.Equal(new[] { 5, 6 }, result); - subject.OnNext(7); - subject.OnNext(8); - Assert.Equal(new[] { 5, 6 }, result); - subject.Dispose(); - } - - /// - /// Subjects the rx void. - /// - [Fact] - public void SubjectRxVoid() - { - var subject = new Signal(); - var result = new List(); - subject.Subscribe(result.Add); - subject.OnNext(RxVoid.Default); - Assert.Equal(new[] { RxVoid.Default }, result); - subject.OnNext(RxVoid.Default); - Assert.Equal(new[] { RxVoid.Default, RxVoid.Default }, result); - subject.Dispose(); - } + /// + /// Subjects the rx void. + /// + [Fact] + public void SubjectRxVoid() + { + var subject = new Signal(); + var result = new List(); + subject.Subscribe(result.Add); + subject.OnNext(RxVoid.Default); + Assert.Equal(new[] { RxVoid.Default }, result); + subject.OnNext(RxVoid.Default); + Assert.Equal(new[] { RxVoid.Default, RxVoid.Default }, result); + subject.Dispose(); } } diff --git a/src/Minimalist.Reactive.Tests/TestClasses/EmptyScheduler.cs b/src/Minimalist.Reactive.Tests/TestClasses/EmptyScheduler.cs new file mode 100644 index 0000000..083314b --- /dev/null +++ b/src/Minimalist.Reactive.Tests/TestClasses/EmptyScheduler.cs @@ -0,0 +1,23 @@ +// Copyright (c) 2019-2023 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; + +namespace Minimalist.Reactive.Concurrency; + +internal sealed class EmptyScheduler : IScheduler +{ + public static readonly EmptyScheduler Instance = new(); + + public DateTimeOffset Now => DateTimeOffset.MinValue; + + public IDisposable Schedule(TState state, Func action) => + throw new NotImplementedException(); + + public IDisposable Schedule(TState state, TimeSpan dueTime, Func action) => + throw new NotImplementedException(); + + public IDisposable Schedule(TState state, DateTimeOffset dueTime, Func action) => + throw new NotImplementedException(); +} diff --git a/src/Minimalist.Reactive/BufferSignal{T,TResult}.cs b/src/Minimalist.Reactive/BufferSignal{T,TResult}.cs deleted file mode 100644 index 48c10b7..0000000 --- a/src/Minimalist.Reactive/BufferSignal{T,TResult}.cs +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) 2019-2022 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 Minimalist.Reactive -{ - internal class BufferSignal : Signal> - where TResult : IList? - { - private readonly int _skip; - private readonly int _count; - private IList? _buffer; - private int _index; - private IDisposable? _subscription; - - public BufferSignal(IObservable source, int count, int skip) - { - _skip = skip; - _count = count; - _subscription = source.Subscribe( - next => - { - if (IsDisposed) - { - return; - } - - var idx = _index; - var buffer = _buffer; - if (idx == 0) - { - // Reset buffer. - buffer = new List(); - _buffer = buffer; - } - - // Take while not skipping - if (idx >= 0) - { - buffer?.Add(next); - } - - if (++idx == _count) - { - _buffer = null; - - // Set the skip. - idx = 0 - _skip; - OnNext(buffer!); - } - - _index = idx; - }, - (ex) => - { - _buffer = null; - OnError(ex); - }, - () => - { - var buffer = _buffer; - _buffer = null; - - if (buffer != null) - { - OnNext(buffer); - } - - OnCompleted(); - }); - } - - protected override void Dispose(bool disposing) - { - if (IsDisposed) - { - return; - } - - Dispose(disposing); - if (disposing) - { - var buffer = _buffer; - _buffer = null; - - if (buffer != null) - { - OnNext(buffer); - } - - _subscription?.Dispose(); - _subscription = null; - } - } - } -} diff --git a/src/Minimalist.Reactive/Concurrency/CurrentThreadScheduler.cs b/src/Minimalist.Reactive/Concurrency/CurrentThreadScheduler.cs new file mode 100644 index 0000000..2ba3b54 --- /dev/null +++ b/src/Minimalist.Reactive/Concurrency/CurrentThreadScheduler.cs @@ -0,0 +1,206 @@ +// Copyright (c) 2019-2023 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.ComponentModel; +using System.Diagnostics; + +namespace Minimalist.Reactive.Concurrency; + +/// +/// CurrentThreadScheduler. +/// +/// +public sealed class CurrentThreadScheduler : IScheduler +{ + private static readonly Lazy StaticInstance = new(() => new CurrentThreadScheduler()); + + [ThreadStatic] + private static bool _running; + + [ThreadStatic] + private static SchedulerQueue? _threadLocalQueue; + + [ThreadStatic] + private static Stopwatch? clock; + + private CurrentThreadScheduler() + { + } + + /// + /// Gets the singleton instance of the current thread scheduler. + /// + public static CurrentThreadScheduler Instance => StaticInstance.Value; + + /// + /// Gets a value indicating whether gets a value that indicates whether the caller must call a Schedule method. + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] +#pragma warning disable CA1822 // Mark members as static + public bool IsScheduleRequired => !_running; +#pragma warning restore CA1822 // Mark members as static + + /// + /// Gets the scheduler's notion of current time. + /// + public DateTimeOffset Now => DateTimeOffset.UtcNow; + + private static TimeSpan Time + { + get + { + clock ??= Stopwatch.StartNew(); + + return clock.Elapsed; + } + } + + /// + /// Schedules an action to be executed. + /// + /// The type of the state passed to the scheduled action. + /// State passed to the action to be executed. + /// Action to be executed. + /// + /// The disposable object used to cancel the scheduled action (best effort). + /// + public IDisposable Schedule(TState state, Func action) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + return action(this, state); + } + + /// + /// Schedules an action to be executed after dueTime. + /// + /// The type of the state passed to the scheduled action. + /// State passed to the action to be executed. + /// Relative time after which to execute the action. + /// Action to be executed. + /// + /// The disposable object used to cancel the scheduled action (best effort). + /// + /// action. + /// is null. + public IDisposable Schedule(TState state, TimeSpan dueTime, Func action) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + SchedulerQueue? queue; + + // There is no timed task and no task is currently running + if (!_running) + { + _running = true; + + if (dueTime > TimeSpan.Zero) + { + Thread.Sleep(dueTime); + } + + // execute directly without queueing + IDisposable d; + try + { + d = action(this, state); + } + catch + { + SetQueue(null); + _running = false; + throw; + } + + // did recursive tasks arrive? + queue = GetQueue(); + + // yes, run those in the queue as well + if (queue != null) + { + try + { + Trampoline.Run(queue); + } + finally + { + SetQueue(null); + _running = false; + } + } + else + { + _running = false; + } + + return d; + } + + queue = GetQueue(); + + // if there is a task running or there is a queue + if (queue == null) + { + queue = new SchedulerQueue(4); + SetQueue(queue); + } + + var dt = Time + Scheduler.Normalize(dueTime); + + // queue up more work + var si = new ScheduledItem(this, state, action, dt); + queue.Enqueue(si); + return si; + } + + /// + /// Schedules an action to be executed at dueTime. + /// + /// The type of the state passed to the scheduled action. + /// State passed to the action to be executed. + /// Absolute time at which to execute the action. + /// Action to be executed. + /// + /// The disposable object used to cancel the scheduled action (best effort). + /// + public IDisposable Schedule(TState state, DateTimeOffset dueTime, Func action) + { + var due = Scheduler.Normalize(dueTime - Now); + return Schedule(state, TimeSpan.Zero, action); + } + + private static SchedulerQueue? GetQueue() => _threadLocalQueue; + + private static void SetQueue(SchedulerQueue? newQueue) => _threadLocalQueue = newQueue; + + private static class Trampoline + { + public static void Run(SchedulerQueue queue) + { + while (queue.Count > 0) + { + var item = queue.Dequeue(); + if (!item.IsDisposed) + { + var wait = item.DueTime - Time; + if (wait.Ticks > 0) + { + Thread.Sleep(wait); + } + + if (!item.IsDisposed) + { + item.Invoke(); + } + } + } + } + } +} diff --git a/src/Minimalist.Reactive/Concurrency/DispatcherScheduler.cs b/src/Minimalist.Reactive/Concurrency/DispatcherScheduler.cs new file mode 100644 index 0000000..27209d3 --- /dev/null +++ b/src/Minimalist.Reactive/Concurrency/DispatcherScheduler.cs @@ -0,0 +1,119 @@ +// Copyright (c) 2019-2023 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. + +#if WINDOWS + +using System; +using System.Windows.Threading; +using Minimalist.Reactive.Disposables; +using static Minimalist.Reactive.Disposables.Disposable; + +namespace Minimalist.Reactive.Concurrency; + +/// +/// DispatcherScheduler. +/// +/// +public class DispatcherScheduler : IScheduler +{ + /// + /// Initializes a new instance of the class. + /// + /// The dispatcher. + /// dispatcher. + public DispatcherScheduler(Dispatcher dispatcher) => + Dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); + + /// + /// Gets the dispatcher. + /// + /// + /// The dispatcher. + /// + public Dispatcher Dispatcher { get; } + + /// + /// Gets the scheduler's notion of current time. + /// + public DateTimeOffset Now => Scheduler.Now; + + /// + /// Schedules an action to be executed. + /// + /// The type of the state passed to the scheduled action. + /// State passed to the action to be executed. + /// Action to be executed. + /// + /// The disposable object used to cancel the scheduled action (best effort). + /// + /// action. + public IDisposable Schedule(TState state, Func action) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + var cancelable = new BooleanDisposable(); + Dispatcher.BeginInvoke(() => + { + if (cancelable.IsDisposed) + { + return; + } + + action(this, state); + }); + return cancelable; + } + + /// + /// Schedules an action to be executed after dueTime. + /// + /// The type of the state passed to the scheduled action. + /// State passed to the action to be executed. + /// Relative time after which to execute the action. + /// Action to be executed. + /// + /// The disposable object used to cancel the scheduled action (best effort). + /// + /// action. + public IDisposable Schedule(TState state, TimeSpan dueTime, Func action) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + var timeSpan = Scheduler.Normalize(dueTime); + var timer = new DispatcherTimer(); + timer.Tick += (s, e) => + { + timer?.Stop(); + timer = null; + action(this, state); + }; + timer.Interval = timeSpan; + timer.Start(); + return new AnonymousDisposable(() => + { + timer?.Stop(); + timer = null; + }); + } + + /// + /// Schedules an action to be executed at dueTime. + /// + /// The type of the state passed to the scheduled action. + /// State passed to the action to be executed. + /// Absolute time at which to execute the action. + /// Action to be executed. + /// + /// The disposable object used to cancel the scheduled action (best effort). + /// + public IDisposable Schedule(TState state, DateTimeOffset dueTime, Func action) => + Schedule(state, Scheduler.Normalize(dueTime - Now), action); +} +#endif diff --git a/src/Minimalist.Reactive/Concurrency/IScheduledItem.cs b/src/Minimalist.Reactive/Concurrency/IScheduledItem.cs new file mode 100644 index 0000000..40124d3 --- /dev/null +++ b/src/Minimalist.Reactive/Concurrency/IScheduledItem.cs @@ -0,0 +1,22 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Concurrency; + +/// +/// Represents a work item that has been scheduled. +/// +/// Absolute time representation type. +public interface IScheduledItem +{ + /// + /// Gets the absolute time at which the item is due for invocation. + /// + TAbsolute DueTime { get; } + + /// + /// Invokes the work item. + /// + void Invoke(); +} diff --git a/src/Minimalist.Reactive/Concurrency/IScheduler.cs b/src/Minimalist.Reactive/Concurrency/IScheduler.cs new file mode 100644 index 0000000..41b6b77 --- /dev/null +++ b/src/Minimalist.Reactive/Concurrency/IScheduler.cs @@ -0,0 +1,45 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Concurrency; + +/// +/// Represents an object that schedules units of work. +/// +public interface IScheduler +{ + /// + /// Gets the scheduler's notion of current time. + /// + DateTimeOffset Now { get; } + + /// + /// Schedules an action to be executed. + /// + /// The type of the state passed to the scheduled action. + /// State passed to the action to be executed. + /// Action to be executed. + /// The disposable object used to cancel the scheduled action (best effort). + IDisposable Schedule(TState state, Func action); + + /// + /// Schedules an action to be executed after dueTime. + /// + /// The type of the state passed to the scheduled action. + /// State passed to the action to be executed. + /// Relative time after which to execute the action. + /// Action to be executed. + /// The disposable object used to cancel the scheduled action (best effort). + IDisposable Schedule(TState state, TimeSpan dueTime, Func action); + + /// + /// Schedules an action to be executed at dueTime. + /// + /// The type of the state passed to the scheduled action. + /// State passed to the action to be executed. + /// Absolute time at which to execute the action. + /// Action to be executed. + /// The disposable object used to cancel the scheduled action (best effort). + IDisposable Schedule(TState state, DateTimeOffset dueTime, Func action); +} diff --git a/src/Minimalist.Reactive/Concurrency/IStopwatch.cs b/src/Minimalist.Reactive/Concurrency/IStopwatch.cs new file mode 100644 index 0000000..16d81e1 --- /dev/null +++ b/src/Minimalist.Reactive/Concurrency/IStopwatch.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Concurrency; + +/// +/// Abstraction for a stopwatch to compute time relative to a starting point. +/// +public interface IStopwatch +{ + /// + /// Gets the time elapsed since the stopwatch object was obtained. + /// + TimeSpan Elapsed { get; } +} diff --git a/src/Minimalist.Reactive/Concurrency/IStopwatchProvider.cs b/src/Minimalist.Reactive/Concurrency/IStopwatchProvider.cs new file mode 100644 index 0000000..ac64012 --- /dev/null +++ b/src/Minimalist.Reactive/Concurrency/IStopwatchProvider.cs @@ -0,0 +1,17 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Concurrency; + +/// +/// Provider for objects. +/// +public interface IStopwatchProvider +{ + /// + /// Starts a new stopwatch object. + /// + /// New stopwatch object; started at the time of the request. + IStopwatch StartStopwatch(); +} diff --git a/src/Minimalist.Reactive/Concurrency/ImmediateScheduler.cs b/src/Minimalist.Reactive/Concurrency/ImmediateScheduler.cs new file mode 100644 index 0000000..11e1999 --- /dev/null +++ b/src/Minimalist.Reactive/Concurrency/ImmediateScheduler.cs @@ -0,0 +1,83 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Concurrency; + +/// +/// ImmediateScheduler. +/// +/// +public sealed class ImmediateScheduler : IScheduler +{ + private static readonly Lazy StaticInstance = new(static () => new ImmediateScheduler()); + + private ImmediateScheduler() + { + } + + /// + /// Gets the singleton instance of the immediate scheduler. + /// + public static ImmediateScheduler Instance => StaticInstance.Value; + + /// + /// Gets the scheduler's notion of current time. + /// + public DateTimeOffset Now => DateTimeOffset.UtcNow; + + /// + /// Schedules the specified state. + /// + /// The type of the state. + /// The state. + /// The action. + /// An IDisposable. + public IDisposable Schedule(TState state, Func action) + { + if (action is null) + { + throw new ArgumentNullException(nameof(action)); + } + + return action(this, state); + } + + /// + /// Schedules the specified state. + /// + /// The type of the state. + /// The state. + /// The due time. + /// The action. + /// An IDisposable. + public IDisposable Schedule(TState state, TimeSpan dueTime, Func action) + { + if (action is null) + { + throw new ArgumentNullException(nameof(action)); + } + + var dt = Scheduler.Normalize(dueTime); + if (dt.Ticks > 0) + { + Thread.Sleep(dt); + } + + return action(this, state); + } + + /// + /// Schedules the specified state. + /// + /// The type of the state. + /// The state. + /// The due time. + /// The action. + /// An IDisposable. + public IDisposable Schedule(TState state, DateTimeOffset dueTime, Func action) + { + var due = Scheduler.Normalize(dueTime - Now); + return Schedule(state, TimeSpan.Zero, action); + } +} diff --git a/src/Minimalist.Reactive/Concurrency/ScheduledItem.cs b/src/Minimalist.Reactive/Concurrency/ScheduledItem.cs new file mode 100644 index 0000000..40cb76a --- /dev/null +++ b/src/Minimalist.Reactive/Concurrency/ScheduledItem.cs @@ -0,0 +1,169 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Disposables; + +namespace Minimalist.Reactive.Concurrency; + +/// +/// Abstract base class for scheduled work items. +/// +/// Absolute time representation type. +public abstract class ScheduledItem : IScheduledItem, IComparable>, IsDisposed + where TAbsolute : IComparable +{ + private readonly IComparer _comparer; + private SingleDisposable? _disposable; + + /// + /// Initializes a new instance of the class. + /// Creates a new scheduled work item to run at the specified time. + /// + /// Absolute time at which the work item has to be executed. + /// Comparer used to compare work items based on their scheduled time. + /// comparer. + /// is null. + protected ScheduledItem(TAbsolute dueTime, IComparer comparer) + { + DueTime = dueTime; + _comparer = comparer ?? throw new ArgumentNullException(nameof(comparer)); + } + + /// + /// Gets the absolute time at which the item is due for invocation. + /// + public TAbsolute DueTime { get; } + + /// + /// Gets a value indicating whether gets whether the work item has received a cancellation request. + /// + public bool IsDisposed => _disposable?.IsDisposed == true; + + /// + /// Determines whether two specified objects are inequal. + /// + /// The first object to compare. + /// The second object to compare. + /// true if both are inequal; otherwise, false. + /// This operator does not provide results consistent with the IComparable implementation. Instead, it implements reference equality. + public static bool operator !=(ScheduledItem? left, ScheduledItem? right) => !(left == right); + + /// + /// Determines whether one specified object is due before a second specified object. + /// + /// The first object to compare. + /// The second object to compare. + /// true if the value of left is earlier than the value of right; otherwise, false. + /// This operator provides results consistent with the implementation. + public static bool operator <(ScheduledItem left, ScheduledItem right) => Comparer>.Default.Compare(left, right) < 0; + + /// + /// Determines whether one specified object is due before or at the same of a second specified object. + /// + /// The first object to compare. + /// The second object to compare. + /// true if the value of left is earlier than or simultaneous with the value of right; otherwise, false. + /// This operator provides results consistent with the implementation. + public static bool operator <=(ScheduledItem left, ScheduledItem right) => Comparer>.Default.Compare(left, right) <= 0; + + /// + /// Determines whether two specified objects are equal. + /// + /// The first object to compare. + /// The second object to compare. + /// true if both are equal; otherwise, false. + /// This operator does not provide results consistent with the IComparable implementation. Instead, it implements reference equality. + public static bool operator ==(ScheduledItem? left, ScheduledItem? right) => ReferenceEquals(left, right); + + /// + /// Determines whether one specified object is due after a second specified object. + /// + /// The first object to compare. + /// The second object to compare. + /// true if the value of left is later than the value of right; otherwise, false. + /// This operator provides results consistent with the implementation. + public static bool operator >(ScheduledItem left, ScheduledItem right) => Comparer>.Default.Compare(left, right) > 0; + + /// + /// Determines whether one specified object is due after or at the same time of a second specified object. + /// + /// The first object to compare. + /// The second object to compare. + /// true if the value of left is later than or simultaneous with the value of right; otherwise, false. + /// This operator provides results consistent with the implementation. + public static bool operator >=(ScheduledItem left, ScheduledItem right) => Comparer>.Default.Compare(left, right) >= 0; + + /// + /// Cancels the work item by disposing the resource returned by as soon as possible. + /// + public void Cancel() => _disposable?.Dispose(); + + /// + /// Compares the work item with another work item based on absolute time values. + /// + /// Work item to compare the current work item to. + /// Relative ordering between this and the specified work item. + /// The inequality operators are overloaded to provide results consistent with the implementation. Equality operators implement traditional reference equality semantics. + public int CompareTo(ScheduledItem? other) + { + // MSDN: By definition, any object compares greater than null, and two null references compare equal to each other. + if (other is null) + { + return 1; + } + + return _comparer.Compare(DueTime, other.DueTime); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// + /// Determines whether a object is equal to the specified object. + /// + /// The object to compare to the current object. + /// true if the obj parameter is a object and is equal to the current object; otherwise, false. + public override bool Equals(object? obj) => ReferenceEquals(this, obj); + + /// + /// Returns the hash code for the current object. + /// + /// A 32-bit signed integer hash code. + public override int GetHashCode() => base.GetHashCode(); + + /// + /// Invokes the work item. + /// + public void Invoke() + { + if (_disposable?.IsDisposed == false) + { + _disposable = InvokeCore().DisposeWith(); + } + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (_disposable?.IsDisposed == false && disposing) + { + _disposable.Dispose(); + } + } + + /// + /// Implement this method to perform the work item invocation, returning a disposable object for deep cancellation. + /// + /// Disposable object used to cancel the work item and/or derived work items. + protected abstract IDisposable InvokeCore(); +} diff --git a/src/Minimalist.Reactive/Concurrency/ScheduledItem{TAbsolute,TValue}.cs b/src/Minimalist.Reactive/Concurrency/ScheduledItem{TAbsolute,TValue}.cs new file mode 100644 index 0000000..5405fa1 --- /dev/null +++ b/src/Minimalist.Reactive/Concurrency/ScheduledItem{TAbsolute,TValue}.cs @@ -0,0 +1,56 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Concurrency; + +/// +/// Represents a scheduled work item based on the materialization of an IScheduler.Schedule method call. +/// +/// Absolute time representation type. +/// Type of the state passed to the scheduled action. +public sealed class ScheduledItem : ScheduledItem + where TAbsolute : IComparable +{ + private readonly IScheduler _scheduler; + private readonly TValue _state; + private readonly Func _action; + + /// + /// Initializes a new instance of the class. + /// Creates a materialized work item. + /// + /// Recursive scheduler to invoke the scheduled action with. + /// State to pass to the scheduled action. + /// Scheduled action. + /// Time at which to run the scheduled action. + /// Comparer used to compare work items based on their scheduled time. + /// or or is null. + public ScheduledItem(IScheduler scheduler, TValue state, Func action, TAbsolute dueTime, IComparer comparer) + : base(dueTime, comparer) + { + _scheduler = scheduler ?? throw new ArgumentNullException(nameof(scheduler)); + _state = state; + _action = action ?? throw new ArgumentNullException(nameof(action)); + } + + /// + /// Initializes a new instance of the class. + /// Creates a materialized work item. + /// + /// Recursive scheduler to invoke the scheduled action with. + /// State to pass to the scheduled action. + /// Scheduled action. + /// Time at which to run the scheduled action. + /// or is null. + public ScheduledItem(IScheduler scheduler, TValue state, Func action, TAbsolute dueTime) + : this(scheduler, state, action, dueTime, Comparer.Default) + { + } + + /// + /// Invokes the scheduled action with the supplied recursive scheduler and state. + /// + /// Cancellation resource returned by the scheduled action. + protected override IDisposable InvokeCore() => _action(_scheduler, _state); +} diff --git a/src/Minimalist.Reactive/Concurrency/Scheduler.Simple.cs b/src/Minimalist.Reactive/Concurrency/Scheduler.Simple.cs new file mode 100644 index 0000000..eabd37b --- /dev/null +++ b/src/Minimalist.Reactive/Concurrency/Scheduler.Simple.cs @@ -0,0 +1,376 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Disposables; + +namespace Minimalist.Reactive.Concurrency; + +/// +/// Scheduler. +/// +public static partial class Scheduler +{ + /// + /// Schedules an action to be executed. + /// + /// Scheduler to execute the action on. + /// Action to execute. + /// The disposable object used to cancel the scheduled action (best effort). + /// or is null. + public static IDisposable Schedule(this IScheduler scheduler, Action action) + { + if (scheduler == null) + { + throw new ArgumentNullException(nameof(scheduler)); + } + + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + return scheduler.Schedule(action, static (_, a) => Invoke(a)); + } + + /// + /// Schedules an action to be executed after the specified relative due time. + /// + /// Scheduler to execute the action on. + /// Relative time after which to execute the action. + /// Action to execute. + /// + /// The disposable object used to cancel the scheduled action (best effort). + /// + /// + /// scheduler + /// or + /// action. + /// + /// or is null. + public static IDisposable Schedule(this IScheduler scheduler, TimeSpan dueTime, Action action) + { + if (scheduler == null) + { + throw new ArgumentNullException(nameof(scheduler)); + } + + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + return scheduler.Schedule(action, dueTime, static (_, a) => Invoke(a)); + } + + /// + /// Schedules an action to be executed at the specified absolute due time. + /// + /// Scheduler to execute the action on. + /// Absolute time at which to execute the action. + /// Action to execute. + /// + /// The disposable object used to cancel the scheduled action (best effort). + /// + /// + /// scheduler + /// or + /// action. + /// + /// or is null. + public static IDisposable Schedule(this IScheduler scheduler, DateTimeOffset dueTime, Action action) + { + if (scheduler == null) + { + throw new ArgumentNullException(nameof(scheduler)); + } + + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + return scheduler.Schedule(action, dueTime, static (_, a) => Invoke(a)); + } + + /// + /// Schedules the specified action. + /// + /// The scheduler. + /// The action. + /// The disposable object used to cancel the scheduled action (best effort). + public static IDisposable Schedule(this IScheduler scheduler, Action action) + { + // InvokeRec1 + var group = new MultipleDisposable(); + var gate = new object(); + +#pragma warning disable IDE0039 // Use local function + Action? recursiveAction = null; +#pragma warning restore IDE0039 // Use local function + recursiveAction = () => action(() => + { + var isAdded = false; + var isDone = false; + var d = default(IDisposable); + d = scheduler.Schedule(() => + { + lock (gate) + { + if (isAdded) + { + group.Remove(d); + } + else + { + isDone = true; + } + } + + recursiveAction!(); + }); + + lock (gate) + { + if (!isDone) + { + group.Add(d); + isAdded = true; + } + } + }); + + group.Add(scheduler.Schedule(recursiveAction)); + + return group; + } + + /// + /// Schedules an action to be executed. + /// + /// The type of the state. + /// Scheduler to execute the action on. + /// A state object to be passed to . + /// Action to execute. + /// + /// The disposable object used to cancel the scheduled action (best effort). + /// + /// + /// scheduler + /// or + /// action. + /// + /// or is null. + // Note: The naming of that method differs because otherwise, the signature would cause ambiguities. + public static IDisposable ScheduleAction(this IScheduler scheduler, TState state, Action action) + { + if (scheduler == null) + { + throw new ArgumentNullException(nameof(scheduler)); + } + + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + return scheduler.Schedule( + (action, state), + (_, tuple) => + { + tuple.action(tuple.state); + return Disposable.Empty; + }); + } + + /// + /// Schedules an action to be executed. + /// + /// Scheduler to execute the action on. + /// A state object to be passed to . + /// Action to execute. + /// The disposable object used to cancel the scheduled action (best effort). + /// or is null. + // Note: The naming of that method differs because otherwise, the signature would cause ambiguities. + internal static IDisposable ScheduleAction(this IScheduler scheduler, TState state, Func action) + { + if (scheduler == null) + { + throw new ArgumentNullException(nameof(scheduler)); + } + + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + return scheduler.Schedule( + (action, state), + static (_, tuple) => tuple.action(tuple.state)); + } + + /// + /// Schedules an action to be executed after the specified relative due time. + /// + /// The type of the state. + /// Scheduler to execute the action on. + /// A state object to be passed to . + /// Relative time after which to execute the action. + /// Action to execute. + /// + /// The disposable object used to cancel the scheduled action (best effort). + /// + /// + /// scheduler + /// or + /// action. + /// + /// or is null. + internal static IDisposable ScheduleAction(this IScheduler scheduler, TState state, TimeSpan dueTime, Action action) + { + if (scheduler == null) + { + throw new ArgumentNullException(nameof(scheduler)); + } + + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + return scheduler.Schedule((state, action), dueTime, static (_, tuple) => Invoke(tuple)); + } + + /// + /// Schedules an action to be executed after the specified relative due time. + /// + /// The type of the state. + /// Scheduler to execute the action on. + /// A state object to be passed to . + /// Relative time after which to execute the action. + /// Action to execute. + /// + /// The disposable object used to cancel the scheduled action (best effort). + /// + /// + /// scheduler + /// or + /// action. + /// + /// or is null. + internal static IDisposable ScheduleAction(this IScheduler scheduler, TState state, TimeSpan dueTime, Func action) + { + if (scheduler == null) + { + throw new ArgumentNullException(nameof(scheduler)); + } + + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + return scheduler.Schedule((state, action), dueTime, static (_, tuple) => Invoke(tuple)); + } + + /// + /// Schedules an action to be executed after the specified relative due time. + /// + /// The type of the state. + /// Scheduler to execute the action on. + /// A state object to be passed to . + /// Relative time after which to execute the action. + /// Action to execute. + /// + /// The disposable object used to cancel the scheduled action (best effort). + /// + /// + /// scheduler + /// or + /// action. + /// + /// or is null. + internal static IDisposable ScheduleAction(this IScheduler scheduler, TState state, DateTimeOffset dueTime, Action action) + { + if (scheduler == null) + { + throw new ArgumentNullException(nameof(scheduler)); + } + + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + return scheduler.Schedule((state, action), dueTime, static (_, tuple) => Invoke(tuple)); + } + + /// + /// Schedules an action to be executed after the specified relative due time. + /// + /// The type of the state. + /// Scheduler to execute the action on. + /// A state object to be passed to . + /// Relative time after which to execute the action. + /// Action to execute. + /// + /// The disposable object used to cancel the scheduled action (best effort). + /// + /// + /// scheduler + /// or + /// action. + /// + /// or is null. + internal static IDisposable ScheduleAction(this IScheduler scheduler, TState state, DateTimeOffset dueTime, Func action) + { + if (scheduler == null) + { + throw new ArgumentNullException(nameof(scheduler)); + } + + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + return scheduler.Schedule((state, action), dueTime, static (_, tuple) => Invoke(tuple)); + } + + /////// + /////// Schedules an action to be executed. + /////// + /////// Scheduler to execute the action on. + /////// Action to execute. + /////// The disposable object used to cancel the scheduled action (best effort). + /////// or is null. + ////public static IDisposable ScheduleLongRunning(this ISchedulerLongRunning scheduler, Action action) + ////{ + //// if (scheduler == null) + //// { + //// throw new ArgumentNullException(nameof(scheduler)); + //// } + + //// if (action == null) + //// { + //// throw new ArgumentNullException(nameof(action)); + //// } + + //// return scheduler.ScheduleLongRunning(action, static (a, c) => a(c)); + ////} + + private static IDisposable Invoke(Action action) + { + action(); + return Disposable.Empty; + } + + private static IDisposable Invoke((TState state, Action action) tuple) + { + tuple.action(tuple.state); + return Disposable.Empty; + } + + private static IDisposable Invoke((TState state, Func action) tuple) => + tuple.action(tuple.state); +} diff --git a/src/Minimalist.Reactive/Concurrency/Scheduler.cs b/src/Minimalist.Reactive/Concurrency/Scheduler.cs new file mode 100644 index 0000000..c762caf --- /dev/null +++ b/src/Minimalist.Reactive/Concurrency/Scheduler.cs @@ -0,0 +1,30 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Concurrency; + +/// +/// Scheduler. +/// +public static partial class Scheduler +{ + /// + /// Gets a scheduler that schedules work as soon as possible on the current thread. + /// + public static CurrentThreadScheduler CurrentThread => CurrentThreadScheduler.Instance; + + /// + /// Gets a scheduler that schedules work immediately on the current thread. + /// + public static ImmediateScheduler Immediate => ImmediateScheduler.Instance; + + internal static DateTimeOffset Now => DateTime.UtcNow; + + /// + /// Normalizes the specified value to a positive value. + /// + /// The value to normalize. + /// The specified TimeSpan value if it is zero or positive; otherwise, . + public static TimeSpan Normalize(TimeSpan timeSpan) => timeSpan.Ticks < 0 ? TimeSpan.Zero : timeSpan; +} diff --git a/src/Minimalist.Reactive/Concurrency/SchedulerQueue.cs b/src/Minimalist.Reactive/Concurrency/SchedulerQueue.cs new file mode 100644 index 0000000..183fb67 --- /dev/null +++ b/src/Minimalist.Reactive/Concurrency/SchedulerQueue.cs @@ -0,0 +1,73 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Core; + +namespace Minimalist.Reactive.Concurrency; + +/// +/// Efficient scheduler queue that maintains scheduled items sorted by absolute time. +/// +/// Absolute time representation type. +/// This type is not thread safe; users should ensure proper synchronization. +public class SchedulerQueue + where TAbsolute : IComparable +{ + private readonly PriorityQueue> _queue; + + /// + /// Initializes a new instance of the class. + /// Creates a new scheduler queue with a default initial capacity. + /// + public SchedulerQueue() + : this(1024) + { + } + + /// + /// Initializes a new instance of the class. + /// Creates a new scheduler queue with the specified initial capacity. + /// + /// Initial capacity of the scheduler queue. + /// is less than zero. + public SchedulerQueue(int capacity) + { + if (capacity < 0) + { + throw new ArgumentOutOfRangeException(nameof(capacity)); + } + + _queue = new PriorityQueue>(capacity); + } + + /// + /// Gets the number of scheduled items in the scheduler queue. + /// + public int Count => _queue.Count; + + /// + /// Enqueues the specified work item to be scheduled. + /// + /// Work item to be scheduled. + public void Enqueue(ScheduledItem scheduledItem) => _queue.Enqueue(scheduledItem); + + /// + /// Removes the specified work item from the scheduler queue. + /// + /// Work item to be removed from the scheduler queue. + /// true if the item was found; false otherwise. + public bool Remove(ScheduledItem scheduledItem) => _queue.Remove(scheduledItem); + + /// + /// Dequeues the next work item from the scheduler queue. + /// + /// Next work item in the scheduler queue (removed). + public ScheduledItem Dequeue() => _queue.Dequeue(); + + /// + /// Peeks the next work item in the scheduler queue. + /// + /// Next work item in the scheduler queue (not removed). + public ScheduledItem Peek() => _queue.Peek(); +} diff --git a/src/Minimalist.Reactive/Concurrency/TaskPoolScheduler.cs b/src/Minimalist.Reactive/Concurrency/TaskPoolScheduler.cs new file mode 100644 index 0000000..775c576 --- /dev/null +++ b/src/Minimalist.Reactive/Concurrency/TaskPoolScheduler.cs @@ -0,0 +1,109 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Disposables; + +namespace Minimalist.Reactive.Concurrency; + +/// +/// TaskPoolScheduler. +/// +/// +public sealed class TaskPoolScheduler : IScheduler +{ + private readonly TaskFactory _taskFactory; + + /// + /// Initializes a new instance of the class. + /// + /// The task factory. + public TaskPoolScheduler(TaskFactory taskFactory) => _taskFactory = taskFactory; + + /// + /// Gets the instance. + /// + /// + /// The instance. + /// + public static TaskPoolScheduler Instance { get; } = new(Task.Factory); + + /// + /// Gets the scheduler's notion of current time. + /// + public DateTimeOffset Now => Scheduler.Now; + + /// + /// Schedules an action to be executed. + /// + /// The type of the state passed to the scheduled action. + /// State passed to the action to be executed. + /// Action to be executed. + /// + /// The disposable object used to cancel the scheduled action (best effort). + /// + public IDisposable Schedule(TState state, Func action) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + var cancellationDisposable = new CancellationDisposable(); +#pragma warning disable CA2008 // Do not create tasks without passing a TaskScheduler + _taskFactory.StartNew( + (_) => + { + try + { + return action(this, state); + } + catch (Exception ex) + { + var thread = new Thread(() => ex.Rethrow()); + thread.Start(); + thread.Join(); + return Disposable.Empty; + } + }, + cancellationDisposable.Token); +#pragma warning restore CA2008 // Do not create tasks without passing a TaskScheduler + + return cancellationDisposable; + } + + /// + /// Schedules an action to be executed after dueTime. + /// + /// The type of the state passed to the scheduled action. + /// State passed to the action to be executed. + /// Relative time after which to execute the action. + /// Action to be executed. + /// + /// The disposable object used to cancel the scheduled action (best effort). + /// + public IDisposable Schedule(TState state, TimeSpan dueTime, Func action) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + var g = new MultipleDisposable(new IDisposable[0]); + g.Add(ThreadPoolScheduler.Instance.Schedule(state, Scheduler.Normalize(dueTime), action)); + return g; + } + + /// + /// Schedules an action to be executed at dueTime. + /// + /// The type of the state passed to the scheduled action. + /// State passed to the action to be executed. + /// Absolute time at which to execute the action. + /// Action to be executed. + /// + /// The disposable object used to cancel the scheduled action (best effort). + /// + public IDisposable Schedule(TState state, DateTimeOffset dueTime, Func action) => + Schedule(state, Scheduler.Normalize(dueTime - Now), action); +} diff --git a/src/Minimalist.Reactive/Concurrency/ThreadPoolScheduler.cs b/src/Minimalist.Reactive/Concurrency/ThreadPoolScheduler.cs new file mode 100644 index 0000000..2de8e7e --- /dev/null +++ b/src/Minimalist.Reactive/Concurrency/ThreadPoolScheduler.cs @@ -0,0 +1,142 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Disposables; +using static Minimalist.Reactive.Disposables.Disposable; + +namespace Minimalist.Reactive.Concurrency +{ + /// + /// ThreadPoolScheduler. + /// + /// + public sealed class ThreadPoolScheduler : IScheduler + { + internal static readonly ThreadPoolScheduler Instance = new(); + internal static readonly object Gate = new(); + internal static readonly Dictionary Timers = new(); + + private ThreadPoolScheduler() + { + } + + /// + /// Gets the scheduler's notion of current time. + /// + public DateTimeOffset Now => Scheduler.Now; + + /// + /// Schedules an action to be executed. + /// + /// The type of the state passed to the scheduled action. + /// State passed to the action to be executed. + /// Action to be executed. + /// + /// The disposable object used to cancel the scheduled action (best effort). + /// + /// action. + public IDisposable Schedule(TState state, Func action) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + var cancelable = new BooleanDisposable(); + ThreadPool.QueueUserWorkItem( + _ => + { + if (cancelable.IsDisposed) + { + return; + } + + action(this, state); + }, + null); + + return cancelable; + } + + /// + /// Schedules an action to be executed after dueTime. + /// + /// The type of the state passed to the scheduled action. + /// State passed to the action to be executed. + /// Relative time after which to execute the action. + /// Action to be executed. + /// + /// The disposable object used to cancel the scheduled action (best effort). + /// + /// action. + public IDisposable Schedule(TState state, TimeSpan dueTime, Func action) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + var dueTime1 = Scheduler.Normalize(dueTime); + var hasAdded = false; + var hasRemoved = false; + System.Threading.Timer timer = null!; + timer = new( + _ => + { + lock (Gate) + { + if (hasAdded && timer != null) + { + Timers.Remove(timer); + } + + hasRemoved = true; + } + + timer = null!; + action(this, state); + }, + null, + dueTime1, + TimeSpan.FromMilliseconds(-1.0)); + lock (Gate) + { + if (!hasRemoved) + { + Timers.Add(timer, null!); + hasAdded = true; + } + } + + return new AnonymousDisposable(() => + { + var key = timer; + if (key != null) + { + key.Dispose(); + lock (Gate) + { + Timers.Remove(key); + hasRemoved = true; + } + } + + timer = null!; + }); + } + + /// + /// Schedules an action to be executed at dueTime. + /// + /// The type of the state passed to the scheduled action. + /// State passed to the action to be executed. + /// Absolute time at which to execute the action. + /// Action to be executed. + /// + /// The disposable object used to cancel the scheduled action (best effort). + /// + public IDisposable Schedule(TState state, DateTimeOffset dueTime, Func action) => + Schedule(state, Scheduler.Normalize(dueTime - Now), action); + } +} diff --git a/src/Minimalist.Reactive/Concurrency/VirtualTimeSchedulerBase{TAbsolute,TRelative}.cs b/src/Minimalist.Reactive/Concurrency/VirtualTimeSchedulerBase{TAbsolute,TRelative}.cs new file mode 100644 index 0000000..7c23a8f --- /dev/null +++ b/src/Minimalist.Reactive/Concurrency/VirtualTimeSchedulerBase{TAbsolute,TRelative}.cs @@ -0,0 +1,370 @@ +// Copyright (c) 2019-2023 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.Globalization; + +namespace Minimalist.Reactive.Concurrency; + +/// +/// Base class for virtual time schedulers. +/// +/// Absolute time representation type. +/// Relative time representation type. +public abstract class VirtualTimeSchedulerBase : IScheduler, IServiceProvider, IStopwatchProvider + where TAbsolute : IComparable +{ + /// + /// Initializes a new instance of the class. + /// Creates a new virtual time scheduler with the default value of TAbsolute as the initial clock value. + /// + protected VirtualTimeSchedulerBase() + : this(default!, Comparer.Default) + { + //// + //// NB: We allow a default value for TAbsolute here, which typically is a struct. For compat reasons, we can't + //// add a generic constraint (either struct or, better, new()), and maybe a derived class has handled null + //// in all abstract methods. + //// + } + + /// + /// Initializes a new instance of the class. + /// Creates a new virtual time scheduler with the specified initial clock value and absolute time comparer. + /// + /// Initial value for the clock. + /// Comparer to determine causality of events based on absolute time. + /// is null. + protected VirtualTimeSchedulerBase(TAbsolute initialClock, IComparer comparer) + { + Clock = initialClock; + Comparer = comparer ?? throw new ArgumentNullException(nameof(comparer)); + } + + /// + /// Gets or sets the scheduler's absolute time clock value. + /// + public TAbsolute Clock + { + get; + protected set; + } + + /// + /// Gets a value indicating whether gets whether the scheduler is enabled to run work. + /// + public bool IsEnabled { get; private set; } + + /// + /// Gets the scheduler's notion of current time. + /// + public DateTimeOffset Now => ToDateTimeOffset(Clock); + + /// + /// Gets the comparer used to compare absolute time values. + /// + protected IComparer Comparer { get; } + + /// + /// Advances the scheduler's clock by the specified relative time, running all work scheduled for that timespan. + /// + /// Relative time to advance the scheduler's clock by. + /// is negative. + /// The scheduler is already running. VirtualTimeScheduler doesn't support running nested work dispatch loops. To simulate time slippage while running work on the scheduler, use . + public void AdvanceBy(TRelative time) + { + var dt = Add(Clock, time); + + var dueToClock = Comparer.Compare(dt, Clock); + if (dueToClock < 0) + { + throw new ArgumentOutOfRangeException(nameof(time)); + } + + if (dueToClock == 0) + { + return; + } + + if (!IsEnabled) + { + AdvanceTo(dt); + } + else + { + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "{0} cannot be called when the scheduler is already running. Try using Sleep instead.", nameof(AdvanceBy))); + } + } + + /// + /// Advances the scheduler's clock to the specified time, running all work till that point. + /// + /// Absolute time to advance the scheduler's clock to. + /// is in the past. + /// The scheduler is already running. VirtualTimeScheduler doesn't support running nested work dispatch loops. To simulate time slippage while running work on the scheduler, use . + public void AdvanceTo(TAbsolute time) + { + var dueToClock = Comparer.Compare(time, Clock); + if (dueToClock < 0) + { + throw new ArgumentOutOfRangeException(nameof(time)); + } + + if (dueToClock == 0) + { + return; + } + + if (!IsEnabled) + { + IsEnabled = true; + do + { + var next = GetNext(); + if (next != null && Comparer.Compare(next.DueTime, time) <= 0) + { + if (Comparer.Compare(next.DueTime, Clock) > 0) + { + Clock = next.DueTime; + } + + next.Invoke(); + } + else + { + IsEnabled = false; + } + } + while (IsEnabled); + + Clock = time; + } + else + { + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "{0} cannot be called when the scheduler is already running. Try using Sleep instead.", nameof(AdvanceTo))); + } + } + + /// + /// Gets the service object of the specified type. + /// + /// An object that specifies the type of service object to get. + /// + /// A service object of type . + /// -or- + /// if there is no service object of type . + /// + object? IServiceProvider.GetService(Type serviceType) => GetService(serviceType); + + /// + /// Schedules an action to be executed. + /// + /// The type of the state passed to the scheduled action. + /// State passed to the action to be executed. + /// Action to be executed. + /// The disposable object used to cancel the scheduled action (best effort). + /// is null. + public IDisposable Schedule(TState state, Func action) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + return ScheduleAbsolute(state, Clock, action); + } + + /// + /// Schedules an action to be executed after dueTime. + /// + /// The type of the state passed to the scheduled action. + /// State passed to the action to be executed. + /// Relative time after which to execute the action. + /// Action to be executed. + /// The disposable object used to cancel the scheduled action (best effort). + /// is null. + public IDisposable Schedule(TState state, TimeSpan dueTime, Func action) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + return ScheduleRelative(state, ToRelative(dueTime), action); + } + + /// + /// Schedules an action to be executed at dueTime. + /// + /// The type of the state passed to the scheduled action. + /// State passed to the action to be executed. + /// Absolute time at which to execute the action. + /// Action to be executed. + /// The disposable object used to cancel the scheduled action (best effort). + /// is null. + public IDisposable Schedule(TState state, DateTimeOffset dueTime, Func action) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + return ScheduleRelative(state, ToRelative(dueTime - Now), action); + } + + /// + /// Schedules an action to be executed at dueTime. + /// + /// The type of the state passed to the scheduled action. + /// State passed to the action to be executed. + /// Absolute time at which to execute the action. + /// Action to be executed. + /// The disposable object used to cancel the scheduled action (best effort). + public abstract IDisposable ScheduleAbsolute(TState state, TAbsolute dueTime, Func action); + + /// + /// Schedules an action to be executed at dueTime. + /// + /// The type of the state passed to the scheduled action. + /// State passed to the action to be executed. + /// Relative time after which to execute the action. + /// Action to be executed. + /// The disposable object used to cancel the scheduled action (best effort). + public IDisposable ScheduleRelative(TState state, TRelative dueTime, Func action) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + var runAt = Add(Clock, dueTime); + + return ScheduleAbsolute(state, runAt, action); + } + + /// + /// Advances the scheduler's clock by the specified relative time. + /// + /// Relative time to advance the scheduler's clock by. + /// is negative. + public void Sleep(TRelative time) + { + var dt = Add(Clock, time); + + var dueToClock = Comparer.Compare(dt, Clock); + if (dueToClock < 0) + { + throw new ArgumentOutOfRangeException(nameof(time)); + } + + Clock = dt; + } + + /// + /// Starts the virtual time scheduler. + /// + public void Start() + { + if (!IsEnabled) + { + IsEnabled = true; + do + { + var next = GetNext(); + if (next != null) + { + if (Comparer.Compare(next.DueTime, Clock) > 0) + { + Clock = next.DueTime; + } + + next.Invoke(); + } + else + { + IsEnabled = false; + } + } + while (IsEnabled); + } + } + + /// + /// Starts a new stopwatch object. + /// + /// New stopwatch object; started at the time of the request. + public IStopwatch StartStopwatch() + { + var start = ClockToDateTimeOffset(); + return new VirtualTimeStopwatch(this, start); + } + + /// + /// Stops the virtual time scheduler. + /// + public void Stop() + { + IsEnabled = false; + } + + /// + /// Adds a relative time value to an absolute time value. + /// + /// Absolute time value. + /// Relative time value to add. + /// The resulting absolute time sum value. + protected abstract TAbsolute Add(TAbsolute absolute, TRelative relative); + + /// + /// Gets the next scheduled item to be executed. + /// + /// The next scheduled item. + protected abstract IScheduledItem? GetNext(); + + /// + /// Discovers scheduler services by interface type. The base class implementation supports + /// only the IStopwatchProvider service. To influence service discovery - such as adding + /// support for other scheduler services - derived types can override this method. + /// + /// Scheduler service interface type to discover. + /// Object implementing the requested service, if available; null otherwise. + protected virtual object? GetService(Type serviceType) + { + if (serviceType == typeof(IStopwatchProvider)) + { + return this; + } + + return null; + } + + /// + /// Converts the absolute time value to a DateTimeOffset value. + /// + /// Absolute time value to convert. + /// The corresponding DateTimeOffset value. + protected abstract DateTimeOffset ToDateTimeOffset(TAbsolute absolute); + + /// + /// Converts the TimeSpan value to a relative time value. + /// + /// TimeSpan value to convert. + /// The corresponding relative time value. + protected abstract TRelative ToRelative(TimeSpan timeSpan); + + private DateTimeOffset ClockToDateTimeOffset() => ToDateTimeOffset(Clock); + + private sealed class VirtualTimeStopwatch : IStopwatch + { + private readonly VirtualTimeSchedulerBase _parent; + private readonly DateTimeOffset _start; + + public VirtualTimeStopwatch(VirtualTimeSchedulerBase parent, DateTimeOffset start) + { + _parent = parent; + _start = start; + } + + public TimeSpan Elapsed => _parent.ClockToDateTimeOffset() - _start; + } +} diff --git a/src/Minimalist.Reactive/Concurrency/VirtualTimeSchedulerExtensions.cs b/src/Minimalist.Reactive/Concurrency/VirtualTimeSchedulerExtensions.cs new file mode 100644 index 0000000..0b56a21 --- /dev/null +++ b/src/Minimalist.Reactive/Concurrency/VirtualTimeSchedulerExtensions.cs @@ -0,0 +1,75 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Disposables; + +namespace Minimalist.Reactive.Concurrency; + +/// +/// Provides a set of extension methods for virtual time scheduling. +/// +public static class VirtualTimeSchedulerExtensions +{ + /// + /// Schedules an action to be executed at . + /// + /// Absolute time representation type. + /// Relative time representation type. + /// Scheduler to execute the action on. + /// Relative time after which to execute the action. + /// Action to be executed. + /// The disposable object used to cancel the scheduled action (best effort). + /// or is null. + public static IDisposable ScheduleRelative(this VirtualTimeSchedulerBase scheduler, TRelative dueTime, Action action) + where TAbsolute : IComparable + { + if (scheduler == null) + { + throw new ArgumentNullException(nameof(scheduler)); + } + + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + // As stated in Scheduler.Simple.cs, + // an anonymous delegate will allow delegate caching. + // Watch https://github.com/dotnet/roslyn/issues/5835 for compiler + // support for caching delegates from method groups. + return scheduler.ScheduleRelative(action, dueTime, static (_, a) => Invoke(a)); + } + + /// + /// Schedules an action to be executed at . + /// + /// Absolute time representation type. + /// Relative time representation type. + /// Scheduler to execute the action on. + /// Absolute time at which to execute the action. + /// Action to be executed. + /// The disposable object used to cancel the scheduled action (best effort). + /// or is null. + public static IDisposable ScheduleAbsolute(this VirtualTimeSchedulerBase scheduler, TAbsolute dueTime, Action action) + where TAbsolute : IComparable + { + if (scheduler == null) + { + throw new ArgumentNullException(nameof(scheduler)); + } + + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + return scheduler.ScheduleAbsolute(action, dueTime, static (_, a) => Invoke(a)); + } + + private static IDisposable Invoke(Action action) + { + action(); + return Disposable.Empty; + } +} diff --git a/src/Minimalist.Reactive/Concurrency/VirtualTimeScheduler{TAbsolute,TRelative}.cs b/src/Minimalist.Reactive/Concurrency/VirtualTimeScheduler{TAbsolute,TRelative}.cs new file mode 100644 index 0000000..e9bc0f8 --- /dev/null +++ b/src/Minimalist.Reactive/Concurrency/VirtualTimeScheduler{TAbsolute,TRelative}.cs @@ -0,0 +1,102 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Concurrency; + +/// +/// Base class for virtual time schedulers using a priority queue for scheduled items. +/// +/// Absolute time representation type. +/// Relative time representation type. +public abstract class VirtualTimeScheduler : VirtualTimeSchedulerBase + where TAbsolute : IComparable +{ + private readonly SchedulerQueue _queue = new(); + + /// + /// Initializes a new instance of the class. + /// Creates a new virtual time scheduler with the default value of TAbsolute as the initial clock value. + /// + protected VirtualTimeScheduler() + { + } + + /// + /// Initializes a new instance of the class. + /// Creates a new virtual time scheduler. + /// + /// Initial value for the clock. + /// Comparer to determine causality of events based on absolute time. + /// is null. + protected VirtualTimeScheduler(TAbsolute initialClock, IComparer comparer) + : base(initialClock, comparer) + { + } + + /// + /// Schedules an action to be executed at dueTime. + /// + /// The type of the state passed to the scheduled action. + /// State passed to the action to be executed. + /// Absolute time at which to execute the action. + /// Action to be executed. + /// + /// The disposable object used to cancel the scheduled action (best effort). + /// + /// action. + /// is null. + public override IDisposable ScheduleAbsolute(TState state, TAbsolute dueTime, Func action) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + ScheduledItem? si = null; + + var run = new Func((scheduler, state1) => + { + lock (_queue) + { + _queue.Remove(si!); // NB: Assigned before function is invoked. + } + + return action(scheduler, state1); + }); + + si = new ScheduledItem(this, state, run, dueTime, Comparer); + + lock (_queue) + { + _queue.Enqueue(si); + } + + return si; + } + + /// + /// Gets the next scheduled item to be executed. + /// + /// The next scheduled item. + protected override IScheduledItem? GetNext() + { + lock (_queue) + { + while (_queue.Count > 0) + { + var next = _queue.Peek(); + if (next.IsDisposed) + { + _queue.Dequeue(); + } + else + { + return next; + } + } + } + + return null; + } +} \ No newline at end of file diff --git a/src/Minimalist.Reactive/Core/DisposedWitness{T}.cs b/src/Minimalist.Reactive/Core/DisposedWitness{T}.cs new file mode 100644 index 0000000..6d43211 --- /dev/null +++ b/src/Minimalist.Reactive/Core/DisposedWitness{T}.cs @@ -0,0 +1,20 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Core; + +internal sealed class DisposedWitness : IObserver +{ + public static readonly DisposedWitness Instance = new(); + + private DisposedWitness() + { + } + + public void OnCompleted() => throw new ObjectDisposedException(string.Empty); + + public void OnError(Exception error) => throw new ObjectDisposedException(string.Empty, error); + + public void OnNext(T value) => throw new ObjectDisposedException(string.Empty); +} diff --git a/src/Minimalist.Reactive/Core/EmptyWitness{T}.cs b/src/Minimalist.Reactive/Core/EmptyWitness{T}.cs new file mode 100644 index 0000000..72d383b --- /dev/null +++ b/src/Minimalist.Reactive/Core/EmptyWitness{T}.cs @@ -0,0 +1,55 @@ +// Copyright (c) 2019-2023 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.ExceptionServices; + +namespace Minimalist.Reactive.Core; +internal class EmptyWitness : IObserver +{ + public static readonly EmptyWitness Instance = new(_ => { }); + private static readonly Action rethrow = e => ExceptionDispatchInfo.Capture(e).Throw(); + private static readonly Action nop = () => { }; + private static readonly Action nope = _ => { }; + + private readonly Action _onNext; + private readonly Action _onError; + private readonly Action _onCompleted; + + public EmptyWitness(Action onNext) + : this(onNext, rethrow, nop) + { + } + + public EmptyWitness(Action onNext, Action onError) + : this(onNext, onError, nop) + { + } + + public EmptyWitness(Action onNext, Action onCompleted) + : this(onNext, rethrow, onCompleted) + { + } + + public EmptyWitness(Action onNext, Action onError, Action onCompleted) + { + _onNext = onNext; + _onError = onError; + _onCompleted = onCompleted; + } + + /// + /// Calls the action implementing . + /// + public void OnCompleted() => (_onCompleted ?? nop)(); + + /// + /// Calls the action implementing . + /// + public void OnError(Exception error) => (_onError ?? nope)(error); + + /// + /// Calls the action implementing . + /// + public void OnNext(T value) => _onNext(value); +} diff --git a/src/Minimalist.Reactive/Core/IObserver{TValue,TResult}.cs b/src/Minimalist.Reactive/Core/IObserver{TValue,TResult}.cs new file mode 100644 index 0000000..d6ecfc4 --- /dev/null +++ b/src/Minimalist.Reactive/Core/IObserver{TValue,TResult}.cs @@ -0,0 +1,39 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Core; + +/// +/// Provides a mechanism for receiving push-based notifications and returning a response. +/// +/// +/// The type of the elements received by the observer. +/// This type parameter is contravariant. That is, you can use either the type you specified or any type that is less derived. For more information about covariance and contravariance, see Covariance and Contravariance in Generics. +/// +/// +/// The type of the result returned from the observer's notification handlers. +/// This type parameter is covariant. That is, you can use either the type you specified or any type that is more derived. For more information about covariance and contravariance, see Covariance and Contravariance in Generics. +/// +public interface IObserver +{ + /// + /// Notifies the observer of a new element in the sequence. + /// + /// The new element in the sequence. + /// Result returned upon observation of a new element. + TResult OnNext(TValue value); + + /// + /// Notifies the observer that an exception has occurred. + /// + /// The exception that occurred. + /// Result returned upon observation of an error. + TResult OnError(Exception exception); + + /// + /// Notifies the observer of the end of the sequence. + /// + /// Result returned upon observation of the sequence completion. + TResult OnCompleted(); +} diff --git a/src/Minimalist.Reactive/Core/IRequireCurrentThread.cs b/src/Minimalist.Reactive/Core/IRequireCurrentThread.cs new file mode 100644 index 0000000..6837c10 --- /dev/null +++ b/src/Minimalist.Reactive/Core/IRequireCurrentThread.cs @@ -0,0 +1,20 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Core; + +/// +/// IRequireCurrentThread. +/// +/// The Type. +public interface IRequireCurrentThread : IObservable +{ + /// + /// Determines whether [is required subscribe on current thread]. + /// + /// + /// true if [is required subscribe on current thread]; otherwise, false. + /// + bool IsRequiredSubscribeOnCurrentThread(); +} diff --git a/src/Minimalist.Reactive/Core/ImmutableList{T}.cs b/src/Minimalist.Reactive/Core/ImmutableList{T}.cs new file mode 100644 index 0000000..d9ee47b --- /dev/null +++ b/src/Minimalist.Reactive/Core/ImmutableList{T}.cs @@ -0,0 +1,59 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Core; + +internal class ImmutableList +{ + public static readonly ImmutableList Empty = new(); + + public ImmutableList(T[] data) => Items = data; + + private ImmutableList() => Items = new T[0]; + + public T[] Items { get; } + + public ImmutableList Add(T value) + { + var newData = new T[Items.Length + 1]; + Array.Copy(Items, newData, Items.Length); + newData[Items.Length] = value; + return new ImmutableList(newData); + } + + public ImmutableList Remove(T value) + { + var i = IndexOf(value); + if (i < 0) + { + return this; + } + + var length = Items.Length; + if (length == 1) + { + return Empty; + } + + var newData = new T[length - 1]; + + Array.Copy(Items, 0, newData, 0, i); + Array.Copy(Items, i + 1, newData, i, length - i - 1); + + return new ImmutableList(newData); + } + + public int IndexOf(T value) + { + for (var i = 0; i < Items.Length; ++i) + { + if (Equals(Items[i], value)) + { + return i; + } + } + + return -1; + } +} diff --git a/src/Minimalist.Reactive/Core/ListWitness{T}.cs b/src/Minimalist.Reactive/Core/ListWitness{T}.cs new file mode 100644 index 0000000..5fa2859 --- /dev/null +++ b/src/Minimalist.Reactive/Core/ListWitness{T}.cs @@ -0,0 +1,58 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Core; +internal class ListWitness : IObserver +{ + private readonly ImmutableList> _observers; + + public ListWitness(ImmutableList> observers) => _observers = observers; + + public bool HasObservers => _observers.Items.Length > 0; + + public void OnCompleted() + { + var targetObservers = _observers.Items; + for (var i = 0; i < targetObservers.Length; i++) + { + targetObservers[i].OnCompleted(); + } + } + + public void OnError(Exception error) + { + var targetObservers = _observers.Items; + for (var i = 0; i < targetObservers.Length; i++) + { + targetObservers[i].OnError(error); + } + } + + public void OnNext(T value) + { + var targetObservers = _observers.Items; + for (var i = 0; i < targetObservers.Length; i++) + { + targetObservers[i].OnNext(value); + } + } + + internal IObserver Add(IObserver observer) => new ListWitness(_observers.Add(observer)); + + internal IObserver Remove(IObserver observer) + { + var i = Array.IndexOf(_observers.Items, observer); + if (i < 0) + { + return this; + } + + if (_observers.Items.Length == 1) + { + return _observers.Items[0]; + } + + return new ListWitness(_observers.Remove(observer)); + } +} diff --git a/src/Minimalist.Reactive/Core/PriorityQueue.cs b/src/Minimalist.Reactive/Core/PriorityQueue.cs new file mode 100644 index 0000000..a083716 --- /dev/null +++ b/src/Minimalist.Reactive/Core/PriorityQueue.cs @@ -0,0 +1,160 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Core; + +internal sealed class PriorityQueue + where T : IComparable +{ + private long _count = long.MinValue; + private IndexedItem[] _items; + + public PriorityQueue() + : this(16) + { + } + + public PriorityQueue(int capacity) + { + _items = new IndexedItem[capacity]; + Count = 0; + } + + public int Count { get; private set; } + + public T Dequeue() + { + var result = Peek(); + RemoveAt(0); + return result; + } + + public void Enqueue(T item) + { + if (Count >= _items.Length) + { + var temp = _items; + _items = new IndexedItem[_items.Length * 2]; + Array.Copy(temp, _items, temp.Length); + } + + var index = Count++; + _items[index] = new IndexedItem { Value = item, Id = ++_count }; + Percolate(index); + } + + public T Peek() + { + if (Count == 0) + { + throw new InvalidOperationException("Heap is empty."); + } + + return _items[0].Value; + } + + public bool Remove(T item) + { + for (var i = 0; i < Count; ++i) + { + if (EqualityComparer.Default.Equals(_items[i].Value, item)) + { + RemoveAt(i); + return true; + } + } + + return false; + } + + private void Heapify(int index) + { + if (index >= Count || index < 0) + { + return; + } + + while (true) + { + var left = (2 * index) + 1; + var right = (2 * index) + 2; + var first = index; + + if (left < Count && IsHigherPriority(left, first)) + { + first = left; + } + + if (right < Count && IsHigherPriority(right, first)) + { + first = right; + } + + if (first == index) + { + break; + } + + // swap index and first + (_items[first], _items[index]) = (_items[index], _items[first]); + index = first; + } + } + + private bool IsHigherPriority(int left, int right) => _items[left].CompareTo(_items[right]) < 0; + + private int Percolate(int index) + { + if (index >= Count || index < 0) + { + return index; + } + + var parent = (index - 1) / 2; + while (parent >= 0 && parent != index && IsHigherPriority(index, parent)) + { + // swap index and parent + (_items[parent], _items[index]) = (_items[index], _items[parent]); + index = parent; + parent = (index - 1) / 2; + } + + return index; + } + + private void RemoveAt(int index) + { + _items[index] = _items[--Count]; + _items[Count] = default; + + if (Percolate(index) == index) + { + Heapify(index); + } + + if (Count < _items.Length / 4) + { + var temp = _items; + _items = new IndexedItem[_items.Length / 2]; + Array.Copy(temp, 0, _items, 0, Count); + } + } + + private struct IndexedItem : IComparable + { + public long Id; + public T Value; + + public int CompareTo(IndexedItem other) + { + var c = Value.CompareTo(other.Value); + if (c == 0) + { + c = Id.CompareTo(other.Id); + } + + return c; + } + } +} diff --git a/src/Minimalist.Reactive/Core/Spark.cs b/src/Minimalist.Reactive/Core/Spark.cs new file mode 100644 index 0000000..625f06a --- /dev/null +++ b/src/Minimalist.Reactive/Core/Spark.cs @@ -0,0 +1,43 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Core; + +/// +/// Provides a set of static methods for constructing spark. +/// +public static class Spark +{ + /// + /// Creates an object that represents an OnNext spark to an observer. + /// + /// The type of the elements received by the observer. Upon dematerialization of the spark into an observable sequence, this type is used as the element type for the sequence. + /// The value contained in the spark. + /// The OnNext spark containing the value. + public static Spark CreateOnNext(T value) => new Spark.OnNextSpark(value); + + /// + /// Creates an object that represents an OnError spark to an observer. + /// + /// The type of the elements received by the observer. Upon dematerialization of the spark into an observable sequence, this type is used as the element type for the sequence. + /// The exception contained in the spark. + /// The OnError spark containing the exception. + /// is null. + public static Spark CreateOnError(Exception error) + { + if (error == null) + { + throw new ArgumentNullException(nameof(error)); + } + + return new Spark.OnErrorSpark(error); + } + + /// + /// Creates an object that represents an OnCompleted spark to an observer. + /// + /// The type of the elements received by the observer. Upon dematerialization of the spark into an observable sequence, this type is used as the element type for the sequence. + /// The OnCompleted spark. + public static Spark CreateOnCompleted() => new Spark.OnCompletedSpark(); +} diff --git a/src/Minimalist.Reactive/Core/SparkKind.cs b/src/Minimalist.Reactive/Core/SparkKind.cs new file mode 100644 index 0000000..d974a8d --- /dev/null +++ b/src/Minimalist.Reactive/Core/SparkKind.cs @@ -0,0 +1,27 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Core +{ + /// + /// Indicates the type of a spark. + /// + public enum SparkKind + { + /// + /// Represents an OnNext spark. + /// + OnNext, + + /// + /// Represents an OnError spark. + /// + OnError, + + /// + /// Represents an OnCompleted spark. + /// + OnCompleted + } +} diff --git a/src/Minimalist.Reactive/Core/Spark{T}.cs b/src/Minimalist.Reactive/Core/Spark{T}.cs new file mode 100644 index 0000000..a318423 --- /dev/null +++ b/src/Minimalist.Reactive/Core/Spark{T}.cs @@ -0,0 +1,614 @@ +// Copyright (c) 2019-2023 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.Globalization; +using Minimalist.Reactive.Concurrency; +using Minimalist.Reactive.Signals; + +namespace Minimalist.Reactive.Core +{ + /// + /// Represents a spark to an observer. + /// + /// The type of the elements received by the observer. + [Serializable] +#pragma warning disable CS0659 // Type overrides Object.Equals(object o) but does not override Object.GetHashCode() +#pragma warning disable CS0661 // Type defines operator == or operator != but does not override Object.GetHashCode() + public abstract class Spark : IEquatable> +#pragma warning restore CS0661 // Type defines operator == or operator != but does not override Object.GetHashCode() +#pragma warning restore CS0659 // Type overrides Object.Equals(object o) but does not override Object.GetHashCode() + { + /// + /// Initializes a new instance of the class. + /// Default constructor used by derived types. + /// + protected internal Spark() + { + } + + /// + /// Gets the value of an OnNext spark or throws an exception. + /// + public abstract T Value { get; } + + /// + /// Gets a value indicating whether returns a value that indicates whether the spark has a value. + /// + public abstract bool HasValue { get; } + + /// + /// Gets the exception of an OnError spark or returns null. + /// + public abstract Exception Exception { get; } + + /// + /// Gets the kind of Spark that is represented. + /// + public abstract SparkKind Kind { get; } + + /// + /// Determines whether the two specified Spark<T> objects have a different observer message payload. + /// + /// The first Spark<T> to compare, or null. + /// The second Spark<T> to compare, or null. + /// true if the first Spark<T> value has a different observer message payload as the second Spark<T> value; otherwise, false. + /// + /// Equality of Spark<T> objects is based on the equality of the observer message payload they represent, including the Spark Kind and the Value or Exception (if any). + /// This means two Spark<T> objects can be equal even though they don't represent the same observer method call, but have the same Kind and have equal parameters passed to the observer method. + /// In case one wants to determine whether two Spark<T> objects represent a different observer method call, use Object.ReferenceEquals identity equality instead. + /// + public static bool operator !=(Spark left, Spark right) => !(left == right); + + /// + /// Determines whether the two specified Spark<T> objects have the same observer message payload. + /// + /// The first Spark<T> to compare, or null. + /// The second Spark<T> to compare, or null. + /// true if the first Spark<T> value has the same observer message payload as the second Spark<T> value; otherwise, false. + /// + /// Equality of Spark<T> objects is based on the equality of the observer message payload they represent, including the Spark Kind and the Value or Exception (if any). + /// This means two Spark<T> objects can be equal even though they don't represent the same observer method call, but have the same Kind and have equal parameters passed to the observer method. + /// In case one wants to determine whether two Spark<T> objects represent a different observer method call, use Object.ReferenceEquals identity equality instead. + /// + public static bool operator ==(Spark left, Spark right) => left == right; + + /// + /// Determines whether the current Spark<T> object has the same observer message payload as a specified Spark<T> value. + /// + /// An object to compare to the current Spark<T> object. + /// true if both Spark<T> objects have the same observer message payload; otherwise, false. + /// + /// Equality of Spark<T> objects is based on the equality of the observer message payload they represent, including the Spark Kind and the Value or Exception (if any). + /// This means two Spark<T> objects can be equal even though they don't represent the same observer method call, but have the same Kind and have equal parameters passed to the observer method. + /// In case one wants to determine whether two Spark<T> objects represent the same observer method call, use Object.ReferenceEquals identity equality instead. + /// + public abstract bool Equals(Spark? other); + + /// + /// Determines whether the specified System.Object is equal to the current Spark<T>. + /// + /// The System.Object to compare with the current Spark<T>. + /// true if the specified System.Object is equal to the current Spark<T>; otherwise, false. + /// + /// Equality of Spark<T> objects is based on the equality of the observer message payload they represent, including the Spark Kind and the Value or Exception (if any). + /// This means two Spark<T> objects can be equal even though they don't represent the same observer method call, but have the same Kind and have equal parameters passed to the observer method. + /// In case one wants to determine whether two Spark<T> objects represent the same observer method call, use Object.ReferenceEquals identity equality instead. + /// + public override bool Equals(object? obj) => Equals(obj as Spark); + + /// + /// Invokes the observer's method corresponding to the Spark. + /// + /// Observer to invoke the Spark on. + public abstract void Accept(IObserver observer); + + /// + /// Invokes the observer's method corresponding to the Spark and returns the produced result. + /// + /// The type of the result returned from the observer's Spark handlers. + /// Observer to invoke the Spark on. + /// Result produced by the observation. + public abstract TResult Accept(IObserver observer); + + /// + /// Invokes the delegate corresponding to the Spark. + /// + /// Delegate to invoke for an OnNext Spark. + /// Delegate to invoke for an OnError Spark. + /// Delegate to invoke for an OnCompleted Spark. + public abstract void Accept(Action onNext, Action onError, Action onCompleted); + + /// + /// Invokes the delegate corresponding to the Spark and returns the produced result. + /// + /// The type of the result returned from the Spark handler delegates. + /// Delegate to invoke for an OnNext Spark. + /// Delegate to invoke for an OnError Spark. + /// Delegate to invoke for an OnCompleted Spark. + /// Result produced by the observation. + public abstract TResult Accept(Func onNext, Func onError, Func onCompleted); + + /// + /// Returns an observable sequence with a single Spark, using the immediate scheduler. + /// + /// The observable sequence that surfaces the behavior of the Spark upon subscription. + public IObservable ToObservable() => ToObservable(Scheduler.Immediate); + + /// + /// Returns an observable sequence with a single Spark. + /// + /// Scheduler to send out the Spark calls on. + /// The observable sequence that surfaces the behavior of the Spark upon subscription. + public IObservable ToObservable(IScheduler scheduler) + { + if (scheduler == null) + { + throw new ArgumentNullException(nameof(scheduler)); + } + + return Signal.Create(observer => scheduler.Schedule(() => + { + Accept(observer); + if (Kind == SparkKind.OnNext) + { + observer.OnCompleted(); + } + })); + } + + /// + /// Represents an OnNext spark to an observer. + /// + [DebuggerDisplay("OnNext({Value})")] + [Serializable] + internal sealed class OnNextSpark : Spark + { + /// + /// Initializes a new instance of the class. + /// Constructs a Spark of a new value. + /// + public OnNextSpark(T value) => Value = value; + + /// + /// Gets the value of an OnNext Spark. + /// + public override T Value { get; } + + /// + /// Gets null. + /// + public override Exception Exception => null!; + + /// + /// Gets a value indicating whether returns true. + /// + public override bool HasValue => true; + + /// + /// Gets SparkKind.OnNext. + /// + public override SparkKind Kind => SparkKind.OnNext; + + /// + /// Returns the hash code for this instance. + /// + public override int GetHashCode() => EqualityComparer.Default.GetHashCode(Value!); + + /// + /// Indicates whether this instance and a specified object are equal. + /// + /// The other. + /// A bool. + public override bool Equals(Spark? other) + { + if (ReferenceEquals(this, other)) + { + return true; + } + + if (other is null) + { + return false; + } + + if (other.Kind != SparkKind.OnNext) + { + return false; + } + + return EqualityComparer.Default.Equals(Value, other.Value); + } + + /// + /// Returns a string representation of this instance. + /// + public override string ToString() => string.Format(CultureInfo.CurrentCulture, "OnNext({0})", Value); + + /// + /// Invokes the observer's method corresponding to the Spark. + /// + /// Observer to invoke the Spark on. + public override void Accept(IObserver observer) + { + if (observer == null) + { + throw new ArgumentNullException(nameof(observer)); + } + + observer.OnNext(Value); + } + + /// + /// Invokes the observer's method corresponding to the Spark and returns the produced result. + /// + /// Observer to invoke the Spark on. + /// Result produced by the observation. + public override TResult Accept(IObserver observer) + { + if (observer == null) + { + throw new ArgumentNullException(nameof(observer)); + } + + return observer.OnNext(Value); + } + + /// + /// Invokes the delegate corresponding to the Spark. + /// + /// Delegate to invoke for an OnNext Spark. + /// Delegate to invoke for an OnError Spark. + /// Delegate to invoke for an OnCompleted Spark. + public override void Accept(Action onNext, Action onError, Action onCompleted) + { + if (onNext == null) + { + throw new ArgumentNullException(nameof(onNext)); + } + + if (onError == null) + { + throw new ArgumentNullException(nameof(onError)); + } + + if (onCompleted == null) + { + throw new ArgumentNullException(nameof(onCompleted)); + } + + onNext(Value); + } + + /// + /// Invokes the delegate corresponding to the Spark and returns the produced result. + /// + /// Delegate to invoke for an OnNext Spark. + /// Delegate to invoke for an OnError Spark. + /// Delegate to invoke for an OnCompleted Spark. + /// Result produced by the observation. + public override TResult Accept(Func onNext, Func onError, Func onCompleted) + { + if (onNext == null) + { + throw new ArgumentNullException(nameof(onNext)); + } + + if (onError == null) + { + throw new ArgumentNullException(nameof(onError)); + } + + if (onCompleted == null) + { + throw new ArgumentNullException(nameof(onCompleted)); + } + + return onNext(Value); + } + } + + /// + /// Represents an OnError Spark to an observer. + /// + [DebuggerDisplay("OnError({Exception})")] + [Serializable] + internal sealed class OnErrorSpark : Spark + { + /// + /// Initializes a new instance of the class. + /// Constructs a Spark of an exception. + /// + public OnErrorSpark(Exception exception) => Exception = exception; + + /// + /// Gets throws the exception. + /// + public override T Value + { + get + { + Exception.Throw(); + throw Exception; + } + } + + /// + /// Gets the exception. + /// + public override Exception Exception { get; } + + /// + /// Gets a value indicating whether returns false. + /// + public override bool HasValue => false; + + /// + /// Gets SparkKind.OnError. + /// + public override SparkKind Kind => SparkKind.OnError; + + /// + /// Returns the hash code for this instance. + /// + public override int GetHashCode() => Exception.GetHashCode(); + + /// + /// Indicates whether this instance and other are equal. + /// + public override bool Equals(Spark? other) + { + if (ReferenceEquals(this, other)) + { + return true; + } + + if (other is null) + { + return false; + } + + if (other.Kind != SparkKind.OnError) + { + return false; + } + + return Equals(Exception, other.Exception); + } + + /// + /// Returns a string representation of this instance. + /// + public override string ToString() => string.Format(CultureInfo.CurrentCulture, "OnError({0})", Exception.GetType().FullName); + + /// + /// Invokes the observer's method corresponding to the Spark. + /// + /// Observer to invoke the Spark on. + public override void Accept(IObserver observer) + { + if (observer == null) + { + throw new ArgumentNullException(nameof(observer)); + } + + observer.OnError(Exception); + } + + /// + /// Invokes the observer's method corresponding to the Spark and returns the produced result. + /// + /// Observer to invoke the Spark on. + /// Result produced by the observation. + public override TResult Accept(IObserver observer) + { + if (observer == null) + { + throw new ArgumentNullException(nameof(observer)); + } + + return observer.OnError(Exception); + } + + /// + /// Invokes the delegate corresponding to the Spark. + /// + /// Delegate to invoke for an OnNext Spark. + /// Delegate to invoke for an OnError Spark. + /// Delegate to invoke for an OnCompleted Spark. + public override void Accept(Action onNext, Action onError, Action onCompleted) + { + if (onNext == null) + { + throw new ArgumentNullException(nameof(onNext)); + } + + if (onError == null) + { + throw new ArgumentNullException(nameof(onError)); + } + + if (onCompleted == null) + { + throw new ArgumentNullException(nameof(onCompleted)); + } + + onError(Exception); + } + + /// + /// Invokes the delegate corresponding to the Spark and returns the produced result. + /// + /// Delegate to invoke for an OnNext Spark. + /// Delegate to invoke for an OnError Spark. + /// Delegate to invoke for an OnCompleted Spark. + /// Result produced by the observation. + public override TResult Accept(Func onNext, Func onError, Func onCompleted) + { + if (onNext == null) + { + throw new ArgumentNullException(nameof(onNext)); + } + + if (onError == null) + { + throw new ArgumentNullException(nameof(onError)); + } + + if (onCompleted == null) + { + throw new ArgumentNullException(nameof(onCompleted)); + } + + return onError(Exception); + } + } + + /// + /// Represents an OnCompleted spark to an observer. + /// + [DebuggerDisplay("OnCompleted()")] + [Serializable] + internal sealed class OnCompletedSpark : Spark + { + /// + /// Initializes a new instance of the class. + /// Constructs a Spark of the end of a sequence. + /// + public OnCompletedSpark() + { + } + + /// + /// Gets throws an InvalidOperationException. + /// + public override T Value => throw new InvalidOperationException("No Value"); + + /// + /// Gets null. + /// + public override Exception Exception => null!; + + /// + /// Gets a value indicating whether returns false. + /// + public override bool HasValue => false; + + /// + /// Gets SparkKind.OnCompleted. + /// + public override SparkKind Kind => SparkKind.OnCompleted; + + /// + /// Returns the hash code for this instance. + /// + public override int GetHashCode() => typeof(T).GetHashCode() ^ 8510; + + /// + /// Indicates whether this instance and other are equal. + /// + public override bool Equals(Spark? other) + { + if (ReferenceEquals(this, other)) + { + return true; + } + + if (other is null) + { + return false; + } + + return other.Kind == SparkKind.OnCompleted; + } + + /// + /// Returns a string representation of this instance. + /// + public override string ToString() => "OnCompleted()"; + + /// + /// Invokes the observer's method corresponding to the Spark. + /// + /// Observer to invoke the Spark on. + public override void Accept(IObserver observer) + { + if (observer == null) + { + throw new ArgumentNullException(nameof(observer)); + } + + observer.OnCompleted(); + } + + /// + /// Invokes the observer's method corresponding to the Spark and returns the produced result. + /// + /// Observer to invoke the Spark on. + /// Result produced by the observation. + public override TResult Accept(IObserver observer) + { + if (observer == null) + { + throw new ArgumentNullException(nameof(observer)); + } + + return observer.OnCompleted(); + } + + /// + /// Invokes the delegate corresponding to the Spark. + /// + /// Delegate to invoke for an OnNext Spark. + /// Delegate to invoke for an OnError Spark. + /// Delegate to invoke for an OnCompleted Spark. + public override void Accept(Action onNext, Action onError, Action onCompleted) + { + if (onNext == null) + { + throw new ArgumentNullException(nameof(onNext)); + } + + if (onError == null) + { + throw new ArgumentNullException(nameof(onError)); + } + + if (onCompleted == null) + { + throw new ArgumentNullException(nameof(onCompleted)); + } + + onCompleted(); + } + + /// + /// Invokes the delegate corresponding to the Spark and returns the produced result. + /// + /// Delegate to invoke for an OnNext Spark. + /// Delegate to invoke for an OnError Spark. + /// Delegate to invoke for an OnCompleted Spark. + /// Result produced by the observation. + public override TResult Accept(Func onNext, Func onError, Func onCompleted) + { + if (onNext == null) + { + throw new ArgumentNullException(nameof(onNext)); + } + + if (onError == null) + { + throw new ArgumentNullException(nameof(onError)); + } + + if (onCompleted == null) + { + throw new ArgumentNullException(nameof(onCompleted)); + } + + return onCompleted(); + } + } + } +} diff --git a/src/Minimalist.Reactive/Core/ThrowWitness{T}.cs b/src/Minimalist.Reactive/Core/ThrowWitness{T}.cs new file mode 100644 index 0000000..3f71ed3 --- /dev/null +++ b/src/Minimalist.Reactive/Core/ThrowWitness{T}.cs @@ -0,0 +1,24 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Core; + +internal sealed class ThrowWitness : IObserver +{ + public static readonly ThrowWitness Instance = new(); + + private ThrowWitness() + { + } + + public void OnCompleted() + { + } + + public void OnError(Exception error) => error.Rethrow(); + + public void OnNext(T value) + { + } +} diff --git a/src/Minimalist.Reactive/Core/TimeInterval{T}.cs b/src/Minimalist.Reactive/Core/TimeInterval{T}.cs new file mode 100644 index 0000000..3347acf --- /dev/null +++ b/src/Minimalist.Reactive/Core/TimeInterval{T}.cs @@ -0,0 +1,96 @@ +// Copyright (c) 2019-2023 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.Globalization; + +namespace Minimalist.Reactive.Core; +/// +/// Represents a value associated with time interval information. +/// The time interval can represent the time it took to produce the value, the interval relative to a previous value, the value's delivery time relative to a base, etc. +/// +/// The type of the value being annotated with time interval information. +[Serializable] +public readonly struct TimeInterval : IEquatable> +{ + /// + /// Initializes a new instance of the struct. + /// + /// The value to be annotated with a time interval. + /// Time interval associated with the value. + public TimeInterval(T value, TimeSpan interval) + { + Interval = interval; + Value = value; + } + + /// + /// Gets the value. + /// + public T Value { get; } + + /// + /// Gets the interval. + /// + public TimeSpan Interval { get; } + + /// + /// Determines whether the two specified TimeInterval values have the same Value and Interval. + /// + /// The first TimeInterval value to compare. + /// The second TimeInterval value to compare. + /// true if the first TimeInterval value has the same Value and Interval as the second TimeInterval value; otherwise, false. + public static bool operator ==(TimeInterval first, TimeInterval second) => + first.Equals(second); + + /// + /// Determines whether the two specified TimeInterval values don't have the same Value and Interval. + /// + /// The first TimeInterval value to compare. + /// The second TimeInterval value to compare. + /// true if the first TimeInterval value has a different Value or Interval as the second TimeInterval value; otherwise, false. + public static bool operator !=(TimeInterval first, TimeInterval second) => + !first.Equals(second); + + /// + /// Determines whether the current TimeInterval value has the same Value and Interval as a specified TimeInterval value. + /// + /// An object to compare to the current TimeInterval value. + /// true if both TimeInterval values have the same Value and Interval; otherwise, false. + public bool Equals(TimeInterval other) => + other.Interval.Equals(Interval) && EqualityComparer.Default.Equals(Value, other.Value); + + /// + /// Determines whether the specified System.Object is equal to the current TimeInterval. + /// + /// The System.Object to compare with the current TimeInterval. + /// true if the specified System.Object is equal to the current TimeInterval; otherwise, false. + public override bool Equals(object? obj) + { + if (obj is not TimeInterval) + { + return false; + } + + var other = (TimeInterval)obj; + return Equals(other); + } + + /// + /// Returns the hash code for the current TimeInterval value. + /// + /// A hash code for the current TimeInterval value. + public override int GetHashCode() + { + var valueHashCode = Value == null ? 1963 : Value.GetHashCode(); + + return Interval.GetHashCode() ^ valueHashCode; + } + + /// + /// Returns a string representation of the current TimeInterval value. + /// + /// String representation of the current TimeInterval value. + public override string ToString() => + string.Format(CultureInfo.CurrentCulture, "{0}@{1}", Value, Interval); +} diff --git a/src/Minimalist.Reactive/Disposable.cs b/src/Minimalist.Reactive/Disposable.cs deleted file mode 100644 index 651cd26..0000000 --- a/src/Minimalist.Reactive/Disposable.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) 2019-2022 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. -// See the LICENSE file in the project root for full license information. - -using System; -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; - -namespace Minimalist.Reactive -{ - /// - /// Disposable. - /// - public static class Disposable - { - /// - /// Gets the disposable that does nothing when disposed. - /// - public static IDisposable Empty { get; } = new EmptyDisposable(); - - /// - /// Creates a disposable object that invokes the specified action when disposed. - /// - /// Action to run during the first call to . The action is guaranteed to be run at most once. - /// The disposable object that runs the given action upon disposal. - /// is null. - public static IDisposable Create(Action dispose) => - new AnonymousDisposable(dispose ?? throw new ArgumentNullException(nameof(dispose))); - - internal sealed class EmptyDisposable : IDisposable - { - public void Dispose() - { - } - } - - /// - /// Represents an Action-based disposable. - /// - internal sealed class AnonymousDisposable : IDisposable - { - private volatile Action? _dispose; - - /// - /// Initializes a new instance of the class. - /// - /// The dispose. - public AnonymousDisposable(Action dispose) => - _dispose = dispose; - - /// - /// Calls the disposal action if and only if the current instance hasn't been disposed yet. - /// - public void Dispose() => - Interlocked.Exchange(ref _dispose, null)?.Invoke(); - } - } -} diff --git a/src/Minimalist.Reactive/Disposables/BooleanDisposable.cs b/src/Minimalist.Reactive/Disposables/BooleanDisposable.cs new file mode 100644 index 0000000..550a125 --- /dev/null +++ b/src/Minimalist.Reactive/Disposables/BooleanDisposable.cs @@ -0,0 +1,25 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Disposables; + +/// +/// BooleanDisposable. +/// +/// +public sealed class BooleanDisposable : IsDisposed +{ + /// + /// Gets a value indicating whether this instance is disposed. + /// + /// + /// true if this instance is disposed; otherwise, false. + /// + public bool IsDisposed { get; private set; } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() => IsDisposed = true; +} diff --git a/src/Minimalist.Reactive/Disposables/CancellationDisposable.cs b/src/Minimalist.Reactive/Disposables/CancellationDisposable.cs new file mode 100644 index 0000000..26f6736 --- /dev/null +++ b/src/Minimalist.Reactive/Disposables/CancellationDisposable.cs @@ -0,0 +1,68 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Disposables; + +/// +/// CancellationDisposable. +/// +/// +public sealed class CancellationDisposable : IsDisposed +{ + private readonly CancellationTokenSource _cts; + + /// + /// Initializes a new instance of the class. + /// + /// The CTS. + /// cts. + public CancellationDisposable(CancellationTokenSource cts) => _cts = cts ?? throw new ArgumentNullException(nameof(cts)); + + /// + /// Initializes a new instance of the class. + /// + public CancellationDisposable() + : this(new CancellationTokenSource()) + { + } + + /// + /// Gets the token. + /// + /// + /// The token. + /// + public CancellationToken Token => _cts.Token; + + /// + /// Gets a value indicating whether this instance is disposed. + /// + /// + /// true if this instance is disposed; otherwise, false. + /// + public bool IsDisposed { get; private set; } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!IsDisposed) + { + if (disposing) + { + _cts.Cancel(); + } + + IsDisposed = true; + } + } +} diff --git a/src/Minimalist.Reactive/Disposables/Disposable.cs b/src/Minimalist.Reactive/Disposables/Disposable.cs new file mode 100644 index 0000000..a4bbc8e --- /dev/null +++ b/src/Minimalist.Reactive/Disposables/Disposable.cs @@ -0,0 +1,53 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Disposables; + +/// +/// Disposable. +/// +public static class Disposable +{ + /// + /// Gets the disposable that does nothing when disposed. + /// + public static IDisposable Empty { get; } = new EmptyDisposable(); + + /// + /// Creates a disposable object that invokes the specified action when disposed. + /// + /// Action to run during the first call to . The action is guaranteed to be run at most once. + /// The disposable object that runs the given action upon disposal. + /// is null. + public static IDisposable Create(Action dispose) => + new AnonymousDisposable(dispose); + + internal sealed class EmptyDisposable : IDisposable + { + public void Dispose() + { + } + } + + /// + /// Represents an Action-based disposable. + /// + internal sealed class AnonymousDisposable : IDisposable + { + private volatile Action? _dispose; + + /// + /// Initializes a new instance of the class. + /// + /// The dispose. + public AnonymousDisposable(Action dispose) => + _dispose = dispose; + + /// + /// Calls the disposal action if and only if the current instance hasn't been disposed yet. + /// + public void Dispose() => + Interlocked.Exchange(ref _dispose, null)?.Invoke(); + } +} diff --git a/src/Minimalist.Reactive/Disposables/IsDisposed.cs b/src/Minimalist.Reactive/Disposables/IsDisposed.cs new file mode 100644 index 0000000..f73dd0d --- /dev/null +++ b/src/Minimalist.Reactive/Disposables/IsDisposed.cs @@ -0,0 +1,20 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Disposables; + +/// +/// Is Disposed. +/// +/// +public interface IsDisposed : IDisposable +{ + /// + /// Gets a value indicating whether this instance is disposed. + /// + /// + /// true if this instance is disposed; otherwise, false. + /// + bool IsDisposed { get; } +} diff --git a/src/Minimalist.Reactive/Disposables/MultipleDisposable.cs b/src/Minimalist.Reactive/Disposables/MultipleDisposable.cs new file mode 100644 index 0000000..a219369 --- /dev/null +++ b/src/Minimalist.Reactive/Disposables/MultipleDisposable.cs @@ -0,0 +1,128 @@ +// Copyright (c) 2019-2023 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; + +namespace Minimalist.Reactive.Disposables; + +/// +/// A CompositeDisposable is a disposable that contains a list of disposables. +/// +public class MultipleDisposable : IsDisposed +{ + private readonly ConcurrentBag _disposables; + private readonly object _gate = new(); + + /// + /// Initializes a new instance of the class from a group of disposables. + /// + /// Disposables that will be disposed together. + /// is . + public MultipleDisposable(params IDisposable[] disposables) => + _disposables = new ConcurrentBag(disposables); + + /// + /// Gets a value indicating whether gets a value that indicates whether the object is disposed. + /// + public bool IsDisposed { get; private set; } + + /// + /// Creates a new group of disposable resources that are disposed together. + /// + /// Disposable resources to add to the group. + /// Group of disposable resources that are disposed together. + public static IDisposable Create(params IDisposable[] disposables) => new MultipleDisposableBase(disposables); + + /// + /// Adds a disposable to the or disposes the disposable if the is disposed. + /// + /// Disposable to add. + public void Add(IDisposable disposable) + { + if (IsDisposed) + { + disposable?.Dispose(); + } + else + { + _disposables.Add(disposable); + } + } + + /// + /// Removes and disposes the first occurrence of a disposable from the CompositeDisposable. + /// + /// Disposable to remove. + /// true if found; false otherwise. + /// is null. + public bool Remove(IDisposable? item) + { + var shouldDispose = false; + + lock (_gate) + { + if (!IsDisposed) + { + shouldDispose = _disposables.TryTake(out item!); + } + } + + if (shouldDispose) + { + item?.Dispose(); + } + + return shouldDispose; + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (IsDisposed) + { + return; + } + + if (disposing) + { + IsDisposed = true; + while (_disposables.TryTake(out var disposable)) + { + disposable?.Dispose(); + } + } + } + + private sealed class MultipleDisposableBase : IDisposable + { + private IDisposable[]? _disposables; + + public MultipleDisposableBase(IDisposable[] disposables) => + Volatile.Write(ref _disposables, disposables ?? throw new ArgumentNullException(nameof(disposables))); + + public void Dispose() + { + var disopsables = Interlocked.Exchange(ref _disposables, null); + if (disopsables != null) + { + foreach (var disposable in disopsables) + { + disposable?.Dispose(); + } + } + } + } +} diff --git a/src/Minimalist.Reactive/Disposables/SingleDisposable.cs b/src/Minimalist.Reactive/Disposables/SingleDisposable.cs new file mode 100644 index 0000000..14c369c --- /dev/null +++ b/src/Minimalist.Reactive/Disposables/SingleDisposable.cs @@ -0,0 +1,81 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Disposables; + +/// +/// SingleDisposable. +/// +public class SingleDisposable : IsDisposed +{ + private readonly Action? _action; + private IDisposable? _disposable; + + /// + /// Initializes a new instance of the class. + /// + /// The action. + public SingleDisposable(Action? action = null) => + _action = action; + + /// + /// Initializes a new instance of the class. + /// + /// The disposable. + /// The action to call before disposal. + public SingleDisposable(IDisposable disposable, Action? action = null) => + _disposable = Disposable.Create(() => + { + action?.Invoke(); + disposable.Dispose(); + IsDisposed = true; + }); + + /// + /// Gets a value indicating whether this instance is disposed. + /// + /// + /// true if this instance is disposed; otherwise, false. + /// + public bool IsDisposed { get; private set; } + + /// + /// Creates the specified disposable. + /// + /// The disposable. + public void Create(IDisposable disposable) => + _disposable = Disposable.Create(() => + { + _action?.Invoke(); + disposable.Dispose(); + IsDisposed = true; + }); + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (!IsDisposed && disposing) + { + if (_disposable == null) + { + IsDisposed = true; + } + + _disposable?.Dispose(); + } + } +} diff --git a/src/Minimalist.Reactive/Disposables/SingleReplaceableDisposable.cs b/src/Minimalist.Reactive/Disposables/SingleReplaceableDisposable.cs new file mode 100644 index 0000000..2241384 --- /dev/null +++ b/src/Minimalist.Reactive/Disposables/SingleReplaceableDisposable.cs @@ -0,0 +1,110 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Disposables; + +/// +/// SingleReplaceableDisposable. +/// +public class SingleReplaceableDisposable : IsDisposed +{ + private readonly object _gate = new(); + private readonly Action? _action; + private IDisposable? _disposable; + private bool _disposed; + + /// + /// Initializes a new instance of the class. + /// + /// The action. + public SingleReplaceableDisposable(Action? action = null) => + _action = action; + + /// + /// Initializes a new instance of the class. + /// + /// The disposable. + /// The action to call before disposal. + public SingleReplaceableDisposable(IDisposable disposable, Action? action = null) + { + Create(disposable); + _action = action; + } + + /// + /// Gets a value indicating whether this instance is disposed. + /// + /// + /// true if this instance is disposed; otherwise, false. + /// + public bool IsDisposed + { + get + { + lock (_gate) + { + return _disposed; + } + } + } + + /// + /// Creates the specified disposable. + /// + /// The disposable. + public void Create(IDisposable disposable) + { + var shouldDispose = false; + var old = default(IDisposable); + lock (_gate) + { + shouldDispose = _disposed; + if (!shouldDispose) + { + old = _disposable; + _disposable = disposable; + } + } + + old?.Dispose(); + + if (shouldDispose && disposable != null) + { + disposable.Dispose(); + _action?.Invoke(); + } + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + var old = default(IDisposable); + + lock (_gate) + { + if (!_disposed) + { + _disposed = true; + old = _disposable; + _disposable = null; + } + } + + old?.Dispose(); + _action?.Invoke(); + } +} diff --git a/src/Minimalist.Reactive/EmptyObserver{T}.cs b/src/Minimalist.Reactive/EmptyObserver{T}.cs deleted file mode 100644 index 8aa0681..0000000 --- a/src/Minimalist.Reactive/EmptyObserver{T}.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) 2019-2022 ReactiveUI Association Incorporated. All rights reserved. -// ReactiveUI Association Incorporated licenses this file to you under the MIT license. -// See the LICENSE file in the project root for full license information. - -using System; -using System.Runtime.ExceptionServices; - -namespace Minimalist.Reactive -{ - internal class EmptyObserver : IObserver - { - private static readonly Action rethrow = e => ExceptionDispatchInfo.Capture(e).Throw(); - private static readonly Action nop = () => { }; - - private readonly Action _onNext; - private readonly Action _onError; - private readonly Action _onCompleted; - - public EmptyObserver(Action onNext) - : this(onNext, rethrow, nop) - { - } - - public EmptyObserver(Action onNext, Action onError) - : this(onNext, onError, nop) - { - } - - public EmptyObserver(Action onNext, Action onCompleted) - : this(onNext, rethrow, onCompleted) - { - } - - public EmptyObserver(Action onNext, Action onError, Action onCompleted) - { - _onNext = onNext; - _onError = onError; - _onCompleted = onCompleted; - } - - /// - /// Calls the action implementing . - /// - public void OnCompleted() => _onCompleted(); - - /// - /// Calls the action implementing . - /// - public void OnError(Exception error) => _onError(error); - - /// - /// Calls the action implementing . - /// - public void OnNext(T value) => _onNext(value); - } -} diff --git a/src/Minimalist.Reactive/ExceptionMixins.cs b/src/Minimalist.Reactive/ExceptionMixins.cs new file mode 100644 index 0000000..5928e1d --- /dev/null +++ b/src/Minimalist.Reactive/ExceptionMixins.cs @@ -0,0 +1,15 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive; +internal static class ExceptionMixins +{ + public static void Throw(this Exception exception) + { +#if NET472 || NETSTANDARD2_0 + System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(exception).Throw(); +#endif + throw exception; + } +} diff --git a/src/Minimalist.Reactive/Handle.cs b/src/Minimalist.Reactive/Handle.cs new file mode 100644 index 0000000..003b332 --- /dev/null +++ b/src/Minimalist.Reactive/Handle.cs @@ -0,0 +1,15 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Signals; + +namespace Minimalist.Reactive; +internal static class Handle +{ + public static readonly Action Nop = () => { }; + public static readonly Action Throw = ex => ex.Throw(); + + public static IObservable CatchIgnore(Exception ex) => + Signal.Empty(); +} diff --git a/src/Minimalist.Reactive/Handle{T1,T2,T3}.cs b/src/Minimalist.Reactive/Handle{T1,T2,T3}.cs new file mode 100644 index 0000000..dafda23 --- /dev/null +++ b/src/Minimalist.Reactive/Handle{T1,T2,T3}.cs @@ -0,0 +1,13 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive; + +internal static class Handle +{ +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + public static readonly Action Ignore = (_, __, ___) => { }; + public static readonly Action Throw = (ex, _, __, ___) => ex.Throw(); +#pragma warning restore SA1313 // Parameter names should begin with lower-case letter +} diff --git a/src/Minimalist.Reactive/Handle{T1,T2}.cs b/src/Minimalist.Reactive/Handle{T1,T2}.cs new file mode 100644 index 0000000..1f29d3b --- /dev/null +++ b/src/Minimalist.Reactive/Handle{T1,T2}.cs @@ -0,0 +1,11 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive; + +internal static class Handle +{ + public static readonly Action Ignore = (_, __) => { }; + public static readonly Action Throw = (ex, _, __) => ex.Throw(); +} diff --git a/src/Minimalist.Reactive/Handle{T}.cs b/src/Minimalist.Reactive/Handle{T}.cs new file mode 100644 index 0000000..5798a08 --- /dev/null +++ b/src/Minimalist.Reactive/Handle{T}.cs @@ -0,0 +1,12 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive; + +internal static class Handle +{ + public static readonly Action Ignore = (T _) => { }; + public static readonly Func Identity = (T t) => t; + public static readonly Action Throw = (ex, _) => ex.Throw(); +} diff --git a/src/Minimalist.Reactive/ISignal{TSource,TResult}.cs b/src/Minimalist.Reactive/ISignal{TSource,TResult}.cs deleted file mode 100644 index 1a788e9..0000000 --- a/src/Minimalist.Reactive/ISignal{TSource,TResult}.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) 2019-2022 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 Minimalist.Reactive -{ - /// - /// ISubject. - /// - /// The type of the source. - /// The type of the result. - /// - /// - public interface ISignal : IObserver, IObservable, IsDisposed - { - /// - /// Gets a value indicating whether this instance has observers. - /// - /// - /// true if this instance has observers; otherwise, false. - /// - bool HasObservers { get; } - } -} diff --git a/src/Minimalist.Reactive/ISignal{T}.cs b/src/Minimalist.Reactive/ISignal{T}.cs deleted file mode 100644 index 9c3a1b1..0000000 --- a/src/Minimalist.Reactive/ISignal{T}.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) 2019-2022 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 Minimalist.Reactive -{ - /// - /// ISubject. - /// - /// The Type. - public interface ISignal : ISignal - { - } -} diff --git a/src/Minimalist.Reactive/IsDisposed.cs b/src/Minimalist.Reactive/IsDisposed.cs deleted file mode 100644 index 9926ecf..0000000 --- a/src/Minimalist.Reactive/IsDisposed.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) 2019-2022 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 Minimalist.Reactive -{ - /// - /// Is Disposed. - /// - /// - public interface IsDisposed : IDisposable - { - /// - /// Gets a value indicating whether this instance is disposed. - /// - /// - /// true if this instance is disposed; otherwise, false. - /// - bool IsDisposed { get; } - } -} diff --git a/src/Minimalist.Reactive/LinqMixins.cs b/src/Minimalist.Reactive/LinqMixins.cs index 3a6a482..2506803 100644 --- a/src/Minimalist.Reactive/LinqMixins.cs +++ b/src/Minimalist.Reactive/LinqMixins.cs @@ -1,120 +1,124 @@ -// Copyright (c) 2019-2022 ReactiveUI Association Incorporated. All rights reserved. +// Copyright (c) 2019-2023 ReactiveUI Association Incorporated. All rights reserved. // ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. -using System; +using Minimalist.Reactive.Disposables; +using Minimalist.Reactive.Signals; -namespace Minimalist.Reactive +namespace Minimalist.Reactive; + +/// +/// SelectMixins. +/// +public static partial class LinqMixins { /// - /// SelectMixins. + /// Selects the specified selector. /// - public static partial class LinqMixins - { - /// - /// Selects the specified selector. - /// - /// The type of the source. - /// The type of the result. - /// The source. - /// The selector. - /// A IObservable. - /// - /// source - /// or - /// selector. - /// - public static IObservable Select(this IObservable source, Func selector) - => new SelectSignal(source ?? throw new ArgumentNullException(nameof(source)), selector ?? throw new ArgumentNullException(nameof(selector))); + /// The type of the source. + /// The type of the result. + /// The source. + /// The selector. + /// A ISignals. + /// + /// source + /// or + /// selector. + /// + public static IObservable Select(this IObservable source, Func selector) + => new SelectSignal(source ?? throw new ArgumentNullException(nameof(source)), selector ?? throw new ArgumentNullException(nameof(selector))); - /// - /// Buffers the specified count. - /// - /// The type of the source. - /// The source. - /// The count of each buffer. - /// An observable sequence of buffers. - /// source. - /// count. - public static IObservable> Buffer(this IObservable source, int count) + /// + /// Buffers the specified count. + /// + /// The type of the source. + /// The source. + /// The count of each buffer. + /// An Signals sequence of buffers. + /// source. + /// count. + public static IObservable> Buffer(this IObservable source, int count) + { + if (source == null) { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (count <= 0) - { - throw new ArgumentOutOfRangeException(nameof(count)); - } - - return new BufferSignal>(source, count, 0); + throw new ArgumentNullException(nameof(source)); } - /// - /// Buffers the specified count then skips the specified count, then repeats. - /// - /// The type of the source. - /// The source. - /// Length of each buffer before being skipped. - /// Number of elements to skip between creation of consecutive buffers. - /// An observable sequence of buffers taking the count then skipping the skipped value, the sequecnce is then repeated. - /// source. - /// - /// count - /// or - /// skip. - /// - public static IObservable> Buffer(this IObservable source, int count, int skip) + if (count <= 0) { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } + throw new ArgumentOutOfRangeException(nameof(count)); + } - if (count <= 0) - { - throw new ArgumentOutOfRangeException(nameof(count)); - } + return new BufferSignal>(source, count, 0); + } - if (skip <= 0) - { - throw new ArgumentOutOfRangeException(nameof(skip)); - } + /// + /// Buffers the specified count then skips the specified count, then repeats. + /// + /// The type of the source. + /// The source. + /// Length of each buffer before being skipped. + /// Number of elements to skip between creation of consecutive buffers. + /// An Signals sequence of buffers taking the count then skipping the skipped value, the sequecnce is then repeated. + /// source. + /// + /// count + /// or + /// skip. + /// + public static IObservable> Buffer(this IObservable source, int count, int skip) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } - return new BufferSignal>(source, count, skip); + if (count <= 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); } - /// - /// Disposes the IDisposable with the disposables instance. - /// - /// The disposable. - /// The disposables. - public static void DisposeWith(this IDisposable disposable, MultipleDisposable disposables) => - disposables?.Add(disposable); + if (skip <= 0) + { + throw new ArgumentOutOfRangeException(nameof(skip)); + } - /// - /// Disposes the with. - /// - /// The disposable. - /// The action. - /// A SingleDisposable. - public static SingleDisposable DisposeWith(this IDisposable disposable, Action? action = null) => - new(disposable, action); + return new BufferSignal>(source, count, skip); + } - /// - /// Wheres the specified predicate. - /// - /// The Type. - /// The source. - /// The predicate. - /// An IObservable. - /// - /// source - /// or - /// predicate. - /// - public static IObservable Where(this IObservable source, Func predicate) - => new WhereSignal(source ?? throw new ArgumentNullException(nameof(source)), predicate ?? throw new ArgumentNullException(nameof(predicate))); + /// + /// Disposes the IDisposable with the disposables instance. + /// + /// The disposable. + /// The disposables. + /// An IDisposable. + public static IDisposable DisposeWith(this IDisposable disposable, MultipleDisposable disposables) + { + disposables?.Add(disposable); + return disposable; } + + /// + /// Disposes the with. + /// + /// The disposable. + /// The action. + /// A SingleDisposable. + public static SingleDisposable DisposeWith(this IDisposable disposable, Action? action = null) => + new(disposable, action); + + /// + /// Wheres the specified predicate. + /// + /// The Type. + /// The source. + /// The predicate. + /// An ISignals. + /// + /// source + /// or + /// predicate. + /// + public static IObservable Where(this IObservable source, Func predicate) + => new WhereSignal(source ?? throw new ArgumentNullException(nameof(source)), predicate ?? throw new ArgumentNullException(nameof(predicate))); } diff --git a/src/Minimalist.Reactive/Minimalist.Reactive.csproj b/src/Minimalist.Reactive/Minimalist.Reactive.csproj index b633be3..16d7325 100644 --- a/src/Minimalist.Reactive/Minimalist.Reactive.csproj +++ b/src/Minimalist.Reactive/Minimalist.Reactive.csproj @@ -1,10 +1,16 @@ - + - net6.0;netstandard2.0 + net472;netstandard2.0;net6.0;net6.0-windows;net7.0;net7.0-windows enable enable preview + + true + true + WINDOWS + + diff --git a/src/Minimalist.Reactive/MultipleDisposable.cs b/src/Minimalist.Reactive/MultipleDisposable.cs deleted file mode 100644 index 351ba48..0000000 --- a/src/Minimalist.Reactive/MultipleDisposable.cs +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) 2019-2022 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; - -namespace Minimalist.Reactive -{ - /// - /// A CompositeDisposable is a disposable that contains a list of disposables. - /// - public class MultipleDisposable : IsDisposed - { - private readonly ConcurrentBag _disposables; - private bool _disposed; - - /// - /// Initializes a new instance of the class from a group of disposables. - /// - /// Disposables that will be disposed together. - /// is . - public MultipleDisposable(params IDisposable[] disposables) => - _disposables = new ConcurrentBag(disposables); - - /// - /// Gets a value indicating whether gets a value that indicates whether the object is disposed. - /// - public bool IsDisposed => _disposed; - - /// - /// Creates a new group of disposable resources that are disposed together. - /// - /// Disposable resources to add to the group. - /// Group of disposable resources that are disposed together. - public static IDisposable Create(params IDisposable[] disposables) => new MultipleDisposableBase(disposables); - - /// - /// Adds a disposable to the or disposes the disposable if the is disposed. - /// - /// Disposable to add. - public void Add(IDisposable disposable) - { - if (_disposed) - { - disposable?.Dispose(); - } - else - { - _disposables.Add(disposable); - } - } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - /// - /// Releases unmanaged and - optionally - managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - if (disposing) - { - _disposed = true; - while (_disposables.TryTake(out var disposable)) - { - disposable?.Dispose(); - } - } - } - - private sealed class MultipleDisposableBase : IDisposable - { - private IDisposable[]? _disposables; - - public MultipleDisposableBase(IDisposable[] disposables) => - Volatile.Write(ref _disposables, disposables ?? throw new ArgumentNullException(nameof(disposables))); - - public void Dispose() - { - var disopsables = Interlocked.Exchange(ref _disposables, null); - if (disopsables != null) - { - foreach (var disposable in disopsables) - { - disposable?.Dispose(); - } - } - } - } - } -} diff --git a/src/Minimalist.Reactive/RxVoid.cs b/src/Minimalist.Reactive/RxVoid.cs index 07cd8f9..17ecdec 100644 --- a/src/Minimalist.Reactive/RxVoid.cs +++ b/src/Minimalist.Reactive/RxVoid.cs @@ -1,64 +1,63 @@ -// Copyright (c) 2019-2022 ReactiveUI Association Incorporated. All rights reserved. +// Copyright (c) 2019-2023 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 Minimalist.Reactive +namespace Minimalist.Reactive; + +/// +/// A Reactive Void. +/// +[Serializable] +public readonly struct RxVoid : IEquatable { /// - /// A Reactive Void. + /// Gets the single value. /// - [Serializable] - public readonly struct RxVoid : IEquatable - { - /// - /// Gets the single value. - /// - public static RxVoid Default => default; + public static RxVoid Default => default; - /// - /// Determines whether the two specified values are not equal. Because has a single value, this always returns false. - /// - /// The first value to compare. - /// The second value to compare. - /// Because has a single value, this always returns false. + /// + /// Determines whether the two specified values are not equal. Because has a single value, this always returns false. + /// + /// The first value to compare. + /// The second value to compare. + /// Because has a single value, this always returns false. #pragma warning disable RCS1163 // Unused parameter. - public static bool operator !=(RxVoid first, RxVoid second) => false; + public static bool operator !=(RxVoid first, RxVoid second) => false; #pragma warning restore RCS1163 // Unused parameter. - /// - /// Determines whether the two specified values are equal. Because has a single value, this always returns true. - /// - /// The first value to compare. - /// The second value to compare. - /// Because has a single value, this always returns true. + /// + /// Determines whether the two specified values are equal. Because has a single value, this always returns true. + /// + /// The first value to compare. + /// The second value to compare. + /// Because has a single value, this always returns true. #pragma warning disable RCS1163 // Unused parameter. - public static bool operator ==(RxVoid first, RxVoid second) => true; + public static bool operator ==(RxVoid first, RxVoid second) => true; #pragma warning restore RCS1163 // Unused parameter. - /// - /// Determines whether the specified value is equal to the current . Because has a single value, this always returns true. - /// - /// An object to compare to the current value. - /// Because has a single value, this always returns true. - public bool Equals(RxVoid other) => true; + /// + /// Determines whether the specified value is equal to the current . Because has a single value, this always returns true. + /// + /// An object to compare to the current value. + /// Because has a single value, this always returns true. + public bool Equals(RxVoid other) => true; - /// - /// Determines whether the specified System.Object is equal to the current . - /// - /// The System.Object to compare with the current . - /// true if the specified System.Object is a value; otherwise, false. - public override bool Equals(object? obj) => obj is RxVoid; + /// + /// Determines whether the specified System.Object is equal to the current . + /// + /// The System.Object to compare with the current . + /// true if the specified System.Object is a value; otherwise, false. + public override bool Equals(object? obj) => obj is RxVoid; - /// - /// Returns the hash code for the current value. - /// - /// A hash code for the current value. - public override int GetHashCode() => 0; + /// + /// Returns the hash code for the current value. + /// + /// A hash code for the current value. + public override int GetHashCode() => 0; - /// - /// Returns a string representation of the current value. - /// - /// String representation of the current value. - public override string ToString() => "()"; - } + /// + /// Returns a string representation of the current value. + /// + /// String representation of the current value. + public override string ToString() => "()"; } diff --git a/src/Minimalist.Reactive/SelectSignal{TSource,TResult}.cs b/src/Minimalist.Reactive/SelectSignal{TSource,TResult}.cs deleted file mode 100644 index 00bb585..0000000 --- a/src/Minimalist.Reactive/SelectSignal{TSource,TResult}.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) 2019-2022 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 Minimalist.Reactive -{ - internal class SelectSignal : Signal - { - private Func? _selector; - private IDisposable? _subscription; - - public SelectSignal(IObservable source, Func selector) - { - _selector = selector; - _subscription = source.Subscribe( - next => - { - if (IsDisposed) - { - return; - } - - OnNext(_selector(next)); - }, - OnError, - OnCompleted); - } - - protected override void Dispose(bool disposing) - { - if (IsDisposed) - { - return; - } - - Dispose(disposing); - if (disposing) - { - _subscription?.Dispose(); - _subscription = null; - _selector = null; - } - } - } -} diff --git a/src/Minimalist.Reactive/Signal/AsyncSignal{T}.cs b/src/Minimalist.Reactive/Signal/AsyncSignal{T}.cs new file mode 100644 index 0000000..0295f9d --- /dev/null +++ b/src/Minimalist.Reactive/Signal/AsyncSignal{T}.cs @@ -0,0 +1,370 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Core; +using Minimalist.Reactive.Disposables; + +namespace Minimalist.Reactive.Signals; + +/// +/// AsyncSignal. +/// +/// The Type. +/// +public class AsyncSignal : IAwaitSignal +{ + private readonly object _observerLock = new(); + private T? _lastValue; + private bool _hasValue; + private Exception? _lastError; + private IObserver _outObserver = EmptyWitness.Instance; + + /// + /// Gets a value indicating whether this instance is disposed. + /// + /// + /// true if this instance is disposed; otherwise, false. + /// + public bool IsDisposed { get; private set; } + + /// + /// Gets the value. + /// + /// + /// The value. + /// + /// AsyncSubject is not completed yet. + public T Value + { + get + { + ThrowIfDisposed(); + if (!IsCompleted) + { + throw new InvalidOperationException("AsyncSubject is not completed yet"); + } + + _lastError.Rethrow(); + + return _lastValue!; + } + } + + /// + /// Gets a value indicating whether this instance has observers. + /// + /// + /// true if this instance has observers; otherwise, false. + /// + public bool HasObservers => _outObserver is not EmptyWitness && !IsCompleted && !IsDisposed; + + /// + /// Gets a value indicating whether this instance is completed. + /// + /// + /// true if this instance is completed; otherwise, false. + /// + public bool IsCompleted { get; private set; } + + /// + /// Called when [completed]. + /// + public void OnCompleted() + { + IObserver old; + T? v; + bool hv; + lock (_observerLock) + { + ThrowIfDisposed(); + if (IsCompleted) + { + return; + } + + old = _outObserver; + _outObserver = EmptyWitness.Instance; + IsCompleted = true; + v = _lastValue; + hv = _hasValue; + } + + if (hv) + { + old.OnNext(v!); + old.OnCompleted(); + } + else + { + old.OnCompleted(); + } + } + + /// + /// Called when [error]. + /// + /// The error. + /// error. + public void OnError(Exception error) + { + if (error == null) + { + throw new ArgumentNullException(nameof(error)); + } + + IObserver old; + lock (_observerLock) + { + ThrowIfDisposed(); + if (IsCompleted) + { + return; + } + + old = _outObserver; + _outObserver = EmptyWitness.Instance; + IsCompleted = true; + _lastError = error; + } + + old.OnError(error); + } + + /// + /// Called when [next]. + /// + /// The value. + public void OnNext(T value) + { + lock (_observerLock) + { + ThrowIfDisposed(); + if (IsCompleted) + { + return; + } + + _hasValue = true; + _lastValue = value; + } + } + + /// + /// Subscribes the specified observer. + /// + /// The observer. + /// A Disposable. + /// observer. + public IDisposable Subscribe(IObserver observer) + { + if (observer == null) + { + throw new ArgumentNullException(nameof(observer)); + } + + var ex = default(Exception); + var v = default(T); + var hv = false; + + lock (_observerLock) + { + ThrowIfDisposed(); + if (!IsCompleted) + { + if (_outObserver is ListWitness listObserver) + { + _outObserver = listObserver.Add(observer); + } + else + { + var current = _outObserver; + if (current is EmptyWitness) + { + _outObserver = new ListWitness(new ImmutableList>(new[] { observer })); + } + else + { + _outObserver = new ListWitness(new ImmutableList>(new[] { current, observer })); + } + } + + return new ObserverHandler(this, observer); + } + + ex = _lastError; + v = _lastValue; + hv = _hasValue; + } + + if (ex != null) + { + observer.OnError(ex); + } + else if (hv) + { + observer.OnNext(v!); + observer.OnCompleted(); + } + else + { + observer.OnCompleted(); + } + + return Disposable.Empty; + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// + /// Gets an awaitable object for the current AsyncSubject. + /// + /// Object that can be awaited. + public IAwaitSignal GetAwaiter() => this; + + /// + /// Specifies a callback action that will be invoked when the subject completes. + /// + /// Callback action that will be invoked when the subject completes. + /// is null. + public void OnCompleted(Action continuation) + { + if (continuation == null) + { + throw new ArgumentNullException(nameof(continuation)); + } + + OnCompleted(continuation, true); + } + + /// + /// Gets the last element of the subject, potentially blocking until the subject completes successfully or exceptionally. + /// + /// The last element of the subject. Throws an InvalidOperationException if no element was received. + /// The source sequence is empty. + public T GetResult() + { + if (!IsCompleted) + { + var e = new ManualResetEvent(false); + OnCompleted(() => e.Set(), false); + e.WaitOne(); + } + + _lastError.Rethrow(); + + if (!_hasValue) + { + throw new InvalidOperationException("NO_ELEMENTS"); + } + + return _lastValue!; + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (!IsDisposed) + { + if (disposing) + { + lock (_observerLock) + { + _outObserver = DisposedWitness.Instance; + _lastError = null; + _lastValue = default; + } + } + + IsDisposed = true; + } + } + + private void ThrowIfDisposed() + { + if (IsDisposed) + { + throw new ObjectDisposedException(string.Empty); + } + } + + private void OnCompleted(Action continuation, bool originalContext) => + Subscribe(new AwaitObserver(continuation, originalContext)); + + private class AwaitObserver : IObserver + { + private readonly SynchronizationContext? _context; + private readonly Action _callback; + + public AwaitObserver(Action callback, bool originalContext) + { + if (originalContext) + { + _context = SynchronizationContext.Current; + } + + _callback = callback; + } + + public void OnCompleted() => InvokeOnOriginalContext(); + + public void OnError(Exception error) => InvokeOnOriginalContext(); + + public void OnNext(T value) + { + } + + private void InvokeOnOriginalContext() + { + if (_context != null) + { + _context.Post(c => ((Action)c!)(), _callback); + } + else + { + _callback(); + } + } + } + + private class ObserverHandler : IDisposable + { + private readonly object _gate = new(); + private AsyncSignal? _subject; + private IObserver? _observer; + + public ObserverHandler(AsyncSignal subject, IObserver observer) + { + _subject = subject; + _observer = observer; + } + + public void Dispose() + { + lock (_gate) + { + if (_subject != null) + { + lock (_subject._observerLock) + { + _subject._outObserver = _subject._outObserver is ListWitness listObserver ? listObserver.Remove(_observer!) : EmptyWitness.Instance; + + _observer = null; + _subject = null; + } + } + } + } + } +} diff --git a/src/Minimalist.Reactive/Signal/BehaviourSignal{T}.cs b/src/Minimalist.Reactive/Signal/BehaviourSignal{T}.cs new file mode 100644 index 0000000..c6110ee --- /dev/null +++ b/src/Minimalist.Reactive/Signal/BehaviourSignal{T}.cs @@ -0,0 +1,305 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Core; +using Minimalist.Reactive.Disposables; + +namespace Minimalist.Reactive.Signals; + +/// +/// BehaviourSignal. +/// +/// The Type. +public class BehaviourSignal : ISignal +{ + private readonly object _observerLock = new(); + private bool _isStopped; + private T? _lastValue; + private Exception? _lastError; + private IObserver _outObserver = EmptyWitness.Instance; + + /// + /// Initializes a new instance of the class. + /// + /// The default value. + public BehaviourSignal(T defaultValue) => _lastValue = defaultValue; + + /// + /// Gets the current value or throws an exception. + /// + /// The initial value passed to the constructor until is called; after which, the last value passed to . + /// + /// is frozen after is called. + /// After is called, always throws the specified exception. + /// An exception is always thrown after is called. + /// + /// Reading is a thread-safe operation, though there's a potential race condition when or are being invoked concurrently. + /// In some cases, it may be necessary for a caller to use external synchronization to avoid race conditions. + /// + /// + public T Value + { + get + { + ThrowIfDisposed(); + _lastError.Rethrow(); + + return _lastValue!; + } + } + + /// + /// Gets a value indicating whether this instance has observers. + /// + /// + /// true if this instance has observers; otherwise, false. + /// + public bool HasObservers => _outObserver is not EmptyWitness && !_isStopped && !IsDisposed; + + /// + /// Gets a value indicating whether this instance is disposed. + /// + /// + /// true if this instance is disposed; otherwise, false. + /// + public bool IsDisposed { get; private set; } + + /// + /// Tries to get the current value or throws an exception. + /// + /// The initial value passed to the constructor until is called; after which, the last value passed to . + /// true if a value is available; false if the subject was disposed. + /// + /// The value returned from is frozen after is called. + /// After is called, always throws the specified exception. + /// + /// Calling is a thread-safe operation, though there's a potential race condition when or are being invoked concurrently. + /// In some cases, it may be necessary for a caller to use external synchronization to avoid race conditions. + /// + /// + public bool TryGetValue(out T? value) + { + lock (_observerLock) + { + if (IsDisposed) + { + value = default; + return false; + } + + _lastError.Rethrow(); + + value = _lastValue!; + return true; + } + } + + /// + /// Notifies all subscribed observers about the end of the sequence. + /// + public void OnCompleted() + { + IObserver old; + lock (_observerLock) + { + ThrowIfDisposed(); + if (_isStopped) + { + return; + } + + old = _outObserver; + _outObserver = EmptyWitness.Instance; + _isStopped = true; + } + + old.OnCompleted(); + } + + /// + /// Notifies all subscribed observers about the exception. + /// + /// The exception to send to all observers. + /// is null. + public void OnError(Exception error) + { + if (error == null) + { + throw new ArgumentNullException(nameof(error)); + } + + IObserver old; + lock (_observerLock) + { + ThrowIfDisposed(); + if (_isStopped) + { + return; + } + + old = _outObserver; + _outObserver = EmptyWitness.Instance; + _isStopped = true; + _lastError = error; + } + + old.OnError(error); + } + + /// + /// Notifies all subscribed observers about the arrival of the specified element in the sequence. + /// + /// The value to send to all observers. + public void OnNext(T value) + { + IObserver current; + lock (_observerLock) + { + if (_isStopped) + { + return; + } + + _lastValue = value; + current = _outObserver; + } + + current.OnNext(value); + } + + /// + /// Subscribes an observer to the subject. + /// + /// Observer to subscribe to the subject. + /// Disposable object that can be used to unsubscribe the observer from the subject. + /// is null. + public IDisposable Subscribe(IObserver observer) + { + if (observer == null) + { + throw new ArgumentNullException(nameof(observer)); + } + + var ex = default(Exception); + var v = default(T); + var subscription = default(ObserverHandler); + + lock (_observerLock) + { + ThrowIfDisposed(); + if (!_isStopped) + { + if (_outObserver is ListWitness listObserver) + { + _outObserver = listObserver.Add(observer); + } + else + { + var current = _outObserver; + if (current is EmptyWitness) + { + _outObserver = new ListWitness(new ImmutableList>(new[] { observer })); + } + else + { + _outObserver = new ListWitness(new ImmutableList>(new[] { current, observer })); + } + } + + v = _lastValue; + subscription = new ObserverHandler(this, observer); + } + else + { + ex = _lastError; + } + } + + if (subscription != null) + { + observer.OnNext(v!); + return subscription; + } + else if (ex != null) + { + observer.OnError(ex); + } + else + { + observer.OnCompleted(); + } + + return Disposable.Empty; + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (!IsDisposed) + { + if (disposing) + { + lock (_observerLock) + { + _outObserver = DisposedWitness.Instance; + _lastError = null; + _lastValue = default; + } + } + + IsDisposed = true; + } + } + + private void ThrowIfDisposed() + { + if (IsDisposed) + { + throw new ObjectDisposedException(string.Empty); + } + } + + private class ObserverHandler : IDisposable + { + private readonly object _lock = new(); + private BehaviourSignal? _subject; + private IObserver? _observer; + + public ObserverHandler(BehaviourSignal subject, IObserver observer) + { + _subject = subject; + _observer = observer; + } + + public void Dispose() + { + lock (_lock) + { + if (_subject != null) + { + lock (_subject._observerLock) + { + _subject._outObserver = _subject._outObserver is ListWitness listObserver ? listObserver.Remove(_observer!) : EmptyWitness.Instance; + + _observer = null; + _subject = null; + } + } + } + } + } +} diff --git a/src/Minimalist.Reactive/Signal/BufferSignal{T,TResult}.cs b/src/Minimalist.Reactive/Signal/BufferSignal{T,TResult}.cs new file mode 100644 index 0000000..82205b5 --- /dev/null +++ b/src/Minimalist.Reactive/Signal/BufferSignal{T,TResult}.cs @@ -0,0 +1,95 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Signals; + +internal class BufferSignal : Signal> + where TResult : IList? +{ + private readonly int _skip; + private readonly int _count; + private IList? _buffer; + private int _index; + private IDisposable? _subscription; + + public BufferSignal(IObservable source, int count, int skip) + { + _skip = skip; + _count = count; + _subscription = source.Subscribe( + next => + { + if (IsDisposed) + { + return; + } + + var idx = _index; + var buffer = _buffer; + if (idx == 0) + { + // Reset buffer. + buffer = new List(); + _buffer = buffer; + } + + // Take while not skipping + if (idx >= 0) + { + buffer?.Add(next); + } + + if (++idx == _count) + { + _buffer = null; + + // Set the skip. + idx = 0 - _skip; + OnNext(buffer!); + } + + _index = idx; + }, + (ex) => + { + _buffer = null; + OnError(ex); + }, + () => + { + var buffer = _buffer; + _buffer = null; + + if (buffer != null) + { + OnNext(buffer); + } + + OnCompleted(); + }); + } + + protected override void Dispose(bool disposing) + { + if (IsDisposed) + { + return; + } + + Dispose(disposing); + if (disposing) + { + var buffer = _buffer; + _buffer = null; + + if (buffer != null) + { + OnNext(buffer); + } + + _subscription?.Dispose(); + _subscription = null; + } + } +} diff --git a/src/Minimalist.Reactive/Signal/IAwaitSignal{T}.cs b/src/Minimalist.Reactive/Signal/IAwaitSignal{T}.cs new file mode 100644 index 0000000..c988704 --- /dev/null +++ b/src/Minimalist.Reactive/Signal/IAwaitSignal{T}.cs @@ -0,0 +1,34 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Signals; + +/// +/// IAwaitSignal. +/// +/// The Type of Signal. +/// +/// +public interface IAwaitSignal : ISignal, System.Runtime.CompilerServices.INotifyCompletion +{ + /// + /// Gets a value indicating whether this instance is completed. + /// + /// + /// true if this instance is completed; otherwise, false. + /// + bool IsCompleted { get; } + + /// + /// Gets the awaiter. + /// + /// An IAwaitSignal. + IAwaitSignal GetAwaiter(); + + /// + /// Gets the result. + /// + /// A value of T. + T GetResult(); +} diff --git a/src/Minimalist.Reactive/Signal/ISignal{TSource,TResult}.cs b/src/Minimalist.Reactive/Signal/ISignal{TSource,TResult}.cs new file mode 100644 index 0000000..01ba3f8 --- /dev/null +++ b/src/Minimalist.Reactive/Signal/ISignal{TSource,TResult}.cs @@ -0,0 +1,23 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Disposables; + +namespace Minimalist.Reactive.Signals; + +/// +/// ISubject. +/// +/// The type of the source. +/// The type of the result. +public interface ISignal : IObserver, IObservable, IsDisposed +{ + /// + /// Gets a value indicating whether this instance has observers. + /// + /// + /// true if this instance has observers; otherwise, false. + /// + bool HasObservers { get; } +} diff --git a/src/Minimalist.Reactive/Signal/ISignal{T}.cs b/src/Minimalist.Reactive/Signal/ISignal{T}.cs new file mode 100644 index 0000000..64afa27 --- /dev/null +++ b/src/Minimalist.Reactive/Signal/ISignal{T}.cs @@ -0,0 +1,13 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Signals; + +/// +/// ISubject. +/// +/// The Type. +public interface ISignal : ISignal +{ +} diff --git a/src/Minimalist.Reactive/Signal/ITaskSignal{T}.cs b/src/Minimalist.Reactive/Signal/ITaskSignal{T}.cs new file mode 100644 index 0000000..6a76ba4 --- /dev/null +++ b/src/Minimalist.Reactive/Signal/ITaskSignal{T}.cs @@ -0,0 +1,45 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Disposables; + +namespace Minimalist.Reactive.Signals; + +/// +/// ITaskSignal. +/// +/// The object that provides notification information. +/// +public interface ITaskSignal : IObservable, IsDisposed +{ + /// + /// Gets the cancellation token source. + /// + /// + /// The cancellation token source. + /// + CancellationTokenSource? CancellationTokenSource { get; } + + /// + /// Gets a value indicating whether this instance is cancellation requested. + /// + /// + /// true if this instance is cancellation requested; otherwise, false. + /// + bool IsCancellationRequested { get; } + + /// + /// Gets the source. + /// + /// + /// The source. + /// + IObservable? Source { get; } + + /// + /// Gets the operation canceled. + /// + /// The observer. + void GetOperationCanceled(IObserver observer); +} diff --git a/src/Minimalist.Reactive/Signal/ReplaySignal{T}.cs b/src/Minimalist.Reactive/Signal/ReplaySignal{T}.cs new file mode 100644 index 0000000..10fbe25 --- /dev/null +++ b/src/Minimalist.Reactive/Signal/ReplaySignal{T}.cs @@ -0,0 +1,363 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Concurrency; +using Minimalist.Reactive.Core; +using Minimalist.Reactive.Disposables; + +namespace Minimalist.Reactive.Signals; + +/// +/// ReplaySignal. +/// +/// The Type. +public class ReplaySignal : ISignal +{ + private readonly int _bufferSize; + private readonly TimeSpan _window; + private readonly DateTimeOffset _startTime; + private readonly IScheduler _scheduler; + private readonly object _observerLock = new(); + private bool _isStopped; + private Exception? _lastError; + private IObserver _outObserver = EmptyWitness.Instance; + private Queue>? _queue = new(); + + /// + /// Initializes a new instance of the class. + /// + /// Size of the buffer. + /// The window. + /// The scheduler. + /// + /// bufferSize + /// or + /// window. + /// + /// scheduler. + public ReplaySignal(int bufferSize, TimeSpan window, IScheduler scheduler) + { + if (bufferSize < 0) + { + throw new ArgumentOutOfRangeException(nameof(bufferSize)); + } + + if (window < TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(window)); + } + + _scheduler = scheduler ?? throw new ArgumentNullException(nameof(scheduler)); + _bufferSize = bufferSize; + _window = window; + _startTime = scheduler.Now; + } + + /// + /// Initializes a new instance of the class. + /// + /// Size of the buffer. + /// The window. + public ReplaySignal(int bufferSize, TimeSpan window) + : this(bufferSize, window, Scheduler.CurrentThread) + { + } + + /// + /// Initializes a new instance of the class. + /// + public ReplaySignal() + : this(int.MaxValue, TimeSpan.MaxValue, Scheduler.CurrentThread) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The scheduler. + public ReplaySignal(IScheduler scheduler) + : this(int.MaxValue, TimeSpan.MaxValue, scheduler) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Size of the buffer. + /// The scheduler. + public ReplaySignal(int bufferSize, IScheduler scheduler) + : this(bufferSize, TimeSpan.MaxValue, scheduler) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Size of the buffer. + public ReplaySignal(int bufferSize) + : this(bufferSize, TimeSpan.MaxValue, Scheduler.CurrentThread) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The window. + /// The scheduler. + public ReplaySignal(TimeSpan window, IScheduler scheduler) + : this(int.MaxValue, window, scheduler) => _window = window; + + /// + /// Initializes a new instance of the class. + /// + /// The window. + public ReplaySignal(TimeSpan window) + : this(int.MaxValue, window, Scheduler.CurrentThread) + { + } + + /// + /// Gets a value indicating whether this instance has observers. + /// + /// + /// true if this instance has observers; otherwise, false. + /// + public bool HasObservers => (_outObserver as ListWitness)?.HasObservers ?? false; + + /// + /// Gets a value indicating whether this instance is disposed. + /// + /// + /// true if this instance is disposed; otherwise, false. + /// + public bool IsDisposed { get; private set; } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// + /// Called when [completed]. + /// + public void OnCompleted() + { + IObserver old; + lock (_observerLock) + { + ThrowIfDisposed(); + if (_isStopped) + { + return; + } + + old = _outObserver; + _outObserver = EmptyWitness.Instance; + _isStopped = true; + Trim(); + } + + old.OnCompleted(); + } + + /// + /// Called when [error]. + /// + /// The exception. + /// exception. + public void OnError(Exception exception) + { + if (exception == null) + { + throw new ArgumentNullException(nameof(exception)); + } + + IObserver old; + lock (_observerLock) + { + ThrowIfDisposed(); + if (_isStopped) + { + return; + } + + old = _outObserver; + _outObserver = EmptyWitness.Instance; + _isStopped = true; + _lastError = exception; + Trim(); + } + + old.OnError(exception); + } + + /// + /// Called when [next]. + /// + /// The value. + public void OnNext(T value) + { + IObserver current; + lock (_observerLock) + { + ThrowIfDisposed(); + if (_isStopped) + { + return; + } + + _queue?.Enqueue(new TimeInterval(value, _scheduler.Now - _startTime)); + Trim(); + + current = _outObserver; + } + + current.OnNext(value); + } + + /// + /// Subscribes the specified observer. + /// + /// The observer. + /// A Disposable. + /// observer. + public IDisposable Subscribe(IObserver observer) + { + if (observer == null) + { + throw new ArgumentNullException(nameof(observer)); + } + + var ex = default(Exception); + var subscription = default(ObserverHandler); + + lock (_observerLock) + { + ThrowIfDisposed(); + if (!_isStopped) + { + if (_outObserver is ListWitness listObserver) + { + _outObserver = listObserver.Add(observer); + } + else + { + var current = _outObserver; + if (current is EmptyWitness) + { + _outObserver = new ListWitness(new ImmutableList>(new[] { observer })); + } + else + { + _outObserver = new ListWitness(new ImmutableList>(new[] { current, observer })); + } + } + + subscription = new ObserverHandler(this, observer); + } + + ex = _lastError; + Trim(); + foreach (var item in _queue!) + { + observer.OnNext(item.Value); + } + } + + if (subscription != null) + { + return subscription; + } + else if (ex != null) + { + observer.OnError(ex); + } + else + { + observer.OnCompleted(); + } + + return Disposable.Empty; + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (!IsDisposed) + { + if (disposing) + { + lock (_observerLock) + { + _outObserver = DisposedWitness.Instance; + _lastError = null; + _queue = null; + } + } + + IsDisposed = true; + } + } + + private void ThrowIfDisposed() + { + if (IsDisposed) + { + throw new ObjectDisposedException(string.Empty); + } + } + + private void Trim() + { + var elapsedTime = Scheduler.Normalize(_scheduler.Now - _startTime); + + while (_queue!.Count > _bufferSize) + { + _queue.Dequeue(); + } + + while (_queue.Count > 0 && elapsedTime.Subtract(_queue.Peek().Interval).CompareTo(_window) > 0) + { + _queue.Dequeue(); + } + } + + private class ObserverHandler : IDisposable + { + private readonly object _lock = new(); + private ReplaySignal? _subject; + private IObserver? _observer; + + public ObserverHandler(ReplaySignal subject, IObserver observer) + { + _subject = subject; + _observer = observer; + } + + public void Dispose() + { + lock (_lock) + { + if (_subject != null) + { + lock (_subject._observerLock) + { + _subject._outObserver = _subject._outObserver is ListWitness listObserver ? listObserver.Remove(_observer!) : EmptyWitness.Instance; + + _observer = null; + _subject = null; + } + } + } + } + } +} diff --git a/src/Minimalist.Reactive/Signal/SelectSignal{TSource,TResult}.cs b/src/Minimalist.Reactive/Signal/SelectSignal{TSource,TResult}.cs new file mode 100644 index 0000000..ed88f30 --- /dev/null +++ b/src/Minimalist.Reactive/Signal/SelectSignal{TSource,TResult}.cs @@ -0,0 +1,44 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Signals; + +internal class SelectSignal : Signal +{ + private Func? _selector; + private IDisposable? _subscription; + + public SelectSignal(IObservable source, Func selector) + { + _selector = selector; + _subscription = source.Subscribe( + next => + { + if (IsDisposed) + { + return; + } + + OnNext(_selector(next)); + }, + OnError, + OnCompleted); + } + + protected override void Dispose(bool disposing) + { + if (IsDisposed) + { + return; + } + + Dispose(disposing); + if (disposing) + { + _subscription?.Dispose(); + _subscription = null; + _selector = null; + } + } +} diff --git a/src/Minimalist.Reactive/Signal/Signal{T}.cs b/src/Minimalist.Reactive/Signal/Signal{T}.cs new file mode 100644 index 0000000..3513f16 --- /dev/null +++ b/src/Minimalist.Reactive/Signal/Signal{T}.cs @@ -0,0 +1,276 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Disposables; + +namespace Minimalist.Reactive.Signals; + +/// +/// Subject. +/// +/// The Type. +public class Signal : ISignal +{ + private static readonly ObserverHandler[] disposedCompare = new ObserverHandler[0]; + private static readonly ObserverHandler[] terminatedCompare = new ObserverHandler[0]; + private ObserverHandler[] _observers = Array.Empty(); + private Exception? _exception; + + /// + /// Gets a value indicating whether indicates whether the subject has observers subscribed to it. + /// + public virtual bool HasObservers => Volatile.Read(ref _observers).Length != 0; + + /// + /// Gets a value indicating whether indicates whether the subject has been disposed. + /// + public virtual bool IsDisposed => Volatile.Read(ref _observers) == disposedCompare; + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// + /// Called when [completed]. + /// + public void OnCompleted() + { +#pragma warning disable SA1009 // Closing parenthesis should be spaced correctly + for (; ; ) + { + var observers = Volatile.Read(ref _observers); + if (observers == disposedCompare) + { + _exception = null; + ThrowDisposed(); + break; + } + + if (observers == terminatedCompare) + { + break; + } + + if (Interlocked.CompareExchange(ref _observers, terminatedCompare, observers) == observers) + { + foreach (var observer in observers) + { + observer.Observer?.OnCompleted(); + } + + break; + } + } +#pragma warning restore SA1009 // Closing parenthesis should be spaced correctly + } + + /// + /// Called when [error]. + /// + /// The error. + public void OnError(Exception error) + { + if (error == null) + { + throw new ArgumentNullException(nameof(error)); + } + +#pragma warning disable SA1009 // Closing parenthesis should be spaced correctly + for (; ; ) + { + var observers = Volatile.Read(ref _observers); + if (observers == disposedCompare) + { + _exception = null; + ThrowDisposed(); + break; + } + + if (observers == terminatedCompare) + { + break; + } + + _exception = error; + if (Interlocked.CompareExchange(ref _observers, terminatedCompare, observers) == observers) + { + foreach (var observer in observers) + { + observer.Observer?.OnError(error); + } + + break; + } + } +#pragma warning restore SA1009 // Closing parenthesis should be spaced correctly + } + + /// + /// Called when [next]. + /// + /// The value. + public void OnNext(T value) + { + var observers = Volatile.Read(ref _observers); + if (observers == disposedCompare) + { + _exception = null; + ThrowDisposed(); + return; + } + + foreach (var observer in observers) + { + observer.Observer?.OnNext(value); + } + } + + /// + /// Subscribes the specified observer. + /// + /// The observer. + /// + /// A IDisposable. + /// + public IDisposable Subscribe(IObserver observer) + { + if (observer == null) + { + throw new ArgumentNullException(nameof(observer)); + } + + var disposable = default(ObserverHandler); +#pragma warning disable SA1009 // Closing parenthesis should be spaced correctly + for (; ; ) + { + var observers = Volatile.Read(ref _observers); + if (observers == disposedCompare) + { + _exception = null; + ThrowDisposed(); + break; + } + + if (observers == terminatedCompare) + { + var ex = _exception; + if (ex != null) + { + observer.OnError(ex); + } + else + { + observer.OnCompleted(); + } + + break; + } + + disposable ??= new ObserverHandler(this, observer); + + var n = observers.Length; + var b = new ObserverHandler[n + 1]; + + Array.Copy(observers, 0, b, 0, n); + + b[n] = disposable; + if (Interlocked.CompareExchange(ref _observers, b, observers) == observers) + { + return disposable; + } + } +#pragma warning restore SA1009 // Closing parenthesis should be spaced correctly + + return Disposable.Empty; + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (!IsDisposed) + { + if (disposing) + { + Interlocked.Exchange(ref _observers, disposedCompare); + _exception = null; + } + } + } + + private static void ThrowDisposed() => throw new ObjectDisposedException(string.Empty); + + private void RemoveObserver(ObserverHandler observer) + { +#pragma warning disable SA1009 // Closing parenthesis should be spaced correctly + for (; ; ) + { + var a = Volatile.Read(ref _observers); + var n = a.Length; + if (n == 0) + { + break; + } + + var j = Array.IndexOf(a, observer); + + if (j < 0) + { + break; + } + + ObserverHandler[] b; + + if (n == 1) + { + b = Array.Empty(); + } + else + { + b = new ObserverHandler[n - 1]; + Array.Copy(a, 0, b, 0, j); + Array.Copy(a, j + 1, b, j, n - j - 1); + } + + if (Interlocked.CompareExchange(ref _observers, b, a) == a) + { + break; + } + } +#pragma warning restore SA1009 // Closing parenthesis should be spaced correctly + } + + private class ObserverHandler : IDisposable + { + private IObserver? _observer; + private Signal _subject; + + public ObserverHandler(Signal subject, IObserver observer) + { + _subject = subject; + _observer = observer; + } + + public IObserver? Observer => _observer; + + public void Dispose() + { + var observer = Interlocked.Exchange(ref _observer, null); + if (observer == null) + { + return; + } + + _subject.RemoveObserver(this); + _subject = null!; + } + } +} diff --git a/src/Minimalist.Reactive/Signal/TaskSignal.cs b/src/Minimalist.Reactive/Signal/TaskSignal.cs new file mode 100644 index 0000000..50e9840 --- /dev/null +++ b/src/Minimalist.Reactive/Signal/TaskSignal.cs @@ -0,0 +1,37 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Concurrency; + +namespace Minimalist.Reactive.Signals; + +/// +/// TaskSignal. +/// +public static class TaskSignal +{ + /// + /// Creates the specified source. + /// + /// The type of the result. + /// The observable factory. + /// The scheduler. + /// The cancellation token source. + /// + /// An AsyncObservable. + /// + /// observableFactory. + public static ITaskSignal Create(Func, IObservable> observableFactory, IScheduler? scheduler = null, CancellationTokenSource? cancellationTokenSource = null) => + Instance(observableFactory, scheduler, cancellationTokenSource); + + private static ITaskSignal Instance(Func, IObservable> observableFactory, IScheduler? scheduler, CancellationTokenSource? cancellationTokenSource) + { + if (observableFactory is null) + { + throw new ArgumentNullException(nameof(observableFactory)); + } + + return new TaskSignal(observableFactory, scheduler, cancellationTokenSource); + } +} diff --git a/src/Minimalist.Reactive/Signal/TaskSignal{T}.cs b/src/Minimalist.Reactive/Signal/TaskSignal{T}.cs new file mode 100644 index 0000000..f2cba08 --- /dev/null +++ b/src/Minimalist.Reactive/Signal/TaskSignal{T}.cs @@ -0,0 +1,110 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Concurrency; +using Minimalist.Reactive.Disposables; + +namespace Minimalist.Reactive.Signals; + +/// +/// TaskSignal. +/// +/// The object that provides notification information. +internal class TaskSignal : ITaskSignal +{ + private readonly IScheduler _scheduler; + private readonly MultipleDisposable? _cleanUp = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The observable factory. + /// The scheduler. + /// The cancellation token source. + public TaskSignal(Func, IObservable> observableFactory, IScheduler? scheduler = null, CancellationTokenSource? cancellationTokenSource = null) + { + if (observableFactory is null) + { + throw new ArgumentNullException(nameof(observableFactory)); + } + + CancellationTokenSource = cancellationTokenSource ?? new(); + _scheduler = scheduler ?? CurrentThreadScheduler.Instance; + Source = observableFactory(this); + } + + /// + /// Gets or sets the source. + /// + /// + /// The source. + /// + public IObservable? Source { get; set; } + + /// + /// Gets the cancellation token source. + /// + /// + /// The cancellation token source. + /// + public CancellationTokenSource? CancellationTokenSource { get; } + + /// + /// Gets a value indicating whether this instance is cancellation requested. + /// + /// + /// true if this instance is cancellation requested; otherwise, false. + /// + public bool IsCancellationRequested => CancellationTokenSource?.IsCancellationRequested == true; + + /// + /// Gets a value indicating whether gets a value that indicates whether the object is disposed. + /// + public bool IsDisposed => _cleanUp?.IsDisposed ?? true; + + /// + /// Gets the operation canceled. + /// + /// The observer. + public void GetOperationCanceled(IObserver observer) => + CancellationTokenSource?.Token.Register(() => observer.OnNext(new OperationCanceledException())).DisposeWith(_cleanUp!); + + /// + /// Subscribes the specified observer. + /// + /// The observer. + /// A Disposable. + public IDisposable Subscribe(IObserver observer) => + Source!.WitnessOn(_scheduler).Subscribe(observer).DisposeWith(_cleanUp!); + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (_cleanUp?.IsDisposed == false && disposing) + { + try + { + CancellationTokenSource?.Cancel(); + } + catch (ObjectDisposedException) + { + } + + _cleanUp?.Dispose(); + CancellationTokenSource?.Dispose(); + } + } +} diff --git a/src/Minimalist.Reactive/Signal/WhereSignal{T}.cs b/src/Minimalist.Reactive/Signal/WhereSignal{T}.cs new file mode 100644 index 0000000..57f4731 --- /dev/null +++ b/src/Minimalist.Reactive/Signal/WhereSignal{T}.cs @@ -0,0 +1,47 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Signals; + +internal class WhereSignal : Signal +{ + private Func? _predicate; + private IDisposable? _subscription; + + public WhereSignal(IObservable source, Func predicate) + { + _predicate = predicate; + _subscription = source.Subscribe( + next => + { + if (IsDisposed) + { + return; + } + + if (_predicate(next)) + { + OnNext(next); + } + }, + OnError, + OnCompleted); + } + + protected override void Dispose(bool disposing) + { + if (IsDisposed) + { + return; + } + + Dispose(disposing); + if (disposing) + { + _subscription?.Dispose(); + _subscription = null; + _predicate = null; + } + } +} diff --git a/src/Minimalist.Reactive/Signals/Core/CatchSignal{T,TException}.cs b/src/Minimalist.Reactive/Signals/Core/CatchSignal{T,TException}.cs new file mode 100644 index 0000000..20c44d2 --- /dev/null +++ b/src/Minimalist.Reactive/Signals/Core/CatchSignal{T,TException}.cs @@ -0,0 +1,101 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Disposables; + +namespace Minimalist.Reactive.Signals.Core; + +internal class CatchSignal : SignalsBase + where TException : Exception +{ + private readonly IObservable _source; + private readonly Func> _errorHandler; + + public CatchSignal(IObservable source, Func> errorHandler) + : base(true) + { + _source = source; + _errorHandler = errorHandler; + } + + protected override IDisposable SubscribeCore(IObserver observer, IDisposable cancel) => + new Catch(this, observer, cancel).Run(); + + private class Catch : WitnessBase + { + private readonly CatchSignal _parent; + private SingleDisposable? _sourceSubscription; + private SingleDisposable? _exceptionSubscription; + + public Catch(CatchSignal parent, IObserver observer, IDisposable cancel) + : base(observer, cancel) => _parent = parent; + + public IDisposable Run() + { + _sourceSubscription = new SingleDisposable(_parent._source.Subscribe(this)); + _exceptionSubscription = new SingleDisposable(); + + return new MultipleDisposable(_sourceSubscription, _exceptionSubscription); + } + + public override void OnNext(T value) => Observer.OnNext(value); + + public override void OnError(Exception error) + { + if (error is TException e) + { + IObservable next; + try + { + if (_parent._errorHandler == Handle.CatchIgnore) + { + next = Signal.Empty(); + } + else + { + next = _parent._errorHandler(e); + } + } + catch (Exception ex) + { + try + { + Observer.OnError(ex); + } + finally + { + Dispose(); + } + + return; + } + + _exceptionSubscription?.Create(next.Subscribe(Observer)); + } + else + { + try + { + Observer.OnError(error); + } + finally + { + Dispose(); + } + } + } + + public override void OnCompleted() + { + try + { + Observer.OnCompleted(); + } + finally + { + Dispose(); + } + } + } +} diff --git a/src/Minimalist.Reactive/Signals/Core/CatchSignal{T}.cs b/src/Minimalist.Reactive/Signals/Core/CatchSignal{T}.cs new file mode 100644 index 0000000..a010073 --- /dev/null +++ b/src/Minimalist.Reactive/Signals/Core/CatchSignal{T}.cs @@ -0,0 +1,154 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Concurrency; +using Minimalist.Reactive.Disposables; + +namespace Minimalist.Reactive.Signals.Core; + +internal class CatchSignal : SignalsBase +{ + private readonly IEnumerable> _sources; + + public CatchSignal(IEnumerable> sources) + : base(true) => _sources = sources; + + protected override IDisposable SubscribeCore(IObserver observer, IDisposable cancel) => + new Catch(this, observer, cancel).Run(); + + private class Catch : WitnessBase + { + private readonly CatchSignal _parent; + private readonly object _gate = new(); + private bool _isDisposed; + private IEnumerator>? _e; + private SingleReplaceableDisposable? _subscription; + private Exception? _lastException; + private Action? _nextSelf; + + public Catch(CatchSignal parent, IObserver observer, IDisposable cancel) + : base(observer, cancel) => _parent = parent; + + public IDisposable Run() + { + _isDisposed = false; + _e = _parent._sources.GetEnumerator(); + _subscription = new SingleReplaceableDisposable(); + + var schedule = Scheduler.Immediate.Schedule(RecursiveRun); + + return new MultipleDisposable(schedule, _subscription, Disposable.Create(() => + { + lock (_gate) + { + _isDisposed = true; + _e.Dispose(); + } + })); + } + + public override void OnNext(T value) => Observer.OnNext(value); + + public override void OnError(Exception error) + { + _lastException = error; + _nextSelf!(); + } + + public override void OnCompleted() + { + try + { + Observer.OnCompleted(); + } + finally + { + Dispose(); + } + } + + private void RecursiveRun(Action self) + { + lock (_gate) + { + _nextSelf = self; + if (_isDisposed) + { + return; + } + + var current = default(IObservable); + var hasNext = false; + var ex = default(Exception); + + try + { + hasNext = _e!.MoveNext(); + if (hasNext) + { + current = _e.Current; + if (current == null) + { + throw new InvalidOperationException("sequence is null."); + } + } + else + { + _e.Dispose(); + } + } + catch (Exception exception) + { + ex = exception; + _e?.Dispose(); + } + + if (ex != null) + { + try + { + Observer.OnError(ex); + } + finally + { + Dispose(); + } + + return; + } + + if (!hasNext) + { + if (_lastException != null) + { + try + { + Observer.OnError(_lastException); + } + finally + { + Dispose(); + } + } + else + { + try + { + Observer.OnCompleted(); + } + finally + { + Dispose(); + } + } + + return; + } + + var source = current; + _subscription?.Create(new SingleDisposable(source!.Subscribe(this))); + } + } + } +} diff --git a/src/Minimalist.Reactive/Signals/Core/CreateSafeSignal{T}.cs b/src/Minimalist.Reactive/Signals/Core/CreateSafeSignal{T}.cs new file mode 100644 index 0000000..2229841 --- /dev/null +++ b/src/Minimalist.Reactive/Signals/Core/CreateSafeSignal{T}.cs @@ -0,0 +1,68 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Disposables; + +namespace Minimalist.Reactive.Signals.Core; +internal class CreateSafeSignal : SignalsBase +{ + private readonly Func, IDisposable> _subscribe; + + public CreateSafeSignal(Func, IDisposable> subscribe) + : base(true) => _subscribe = subscribe; // fail safe + + public CreateSafeSignal(Func, IDisposable> subscribe, bool isRequiredSubscribeOnCurrentThread) + : base(isRequiredSubscribeOnCurrentThread) => _subscribe = subscribe; + + protected override IDisposable SubscribeCore(IObserver observer, IDisposable cancel) + { + observer = new CreateSafe(observer, cancel); + return _subscribe(observer) ?? Disposable.Empty; + } + + private class CreateSafe : WitnessBase + { + public CreateSafe(IObserver observer, IDisposable cancel) + : base(observer, cancel) + { + } + + public override void OnNext(T value) + { + try + { + Observer.OnNext(value); + } + catch + { + Dispose(); // safe + throw; + } + } + + public override void OnError(Exception error) + { + try + { + Observer.OnError(error); + } + finally + { + Dispose(); + } + } + + public override void OnCompleted() + { + try + { + Observer.OnCompleted(); + } + finally + { + Dispose(); + } + } + } +} diff --git a/src/Minimalist.Reactive/Signals/Core/CreateSignal{T,TState}.cs b/src/Minimalist.Reactive/Signals/Core/CreateSignal{T,TState}.cs new file mode 100644 index 0000000..82872fd --- /dev/null +++ b/src/Minimalist.Reactive/Signals/Core/CreateSignal{T,TState}.cs @@ -0,0 +1,67 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Disposables; + +namespace Minimalist.Reactive.Signals.Core; + +internal class CreateSignal : SignalsBase +{ + private readonly TState _state; + private readonly Func, IDisposable> _subscribe; + + public CreateSignal(TState state, Func, IDisposable> subscribe) + : base(true) // fail safe + { + _state = state; + _subscribe = subscribe; + } + + public CreateSignal(TState state, Func, IDisposable> subscribe, bool isRequiredSubscribeOnCurrentThread) + : base(isRequiredSubscribeOnCurrentThread) + { + _state = state; + _subscribe = subscribe; + } + + protected override IDisposable SubscribeCore(IObserver observer, IDisposable cancel) + { + observer = new Create(observer, cancel); + return _subscribe(_state, observer) ?? Disposable.Empty; + } + + private class Create : WitnessBase + { + public Create(IObserver observer, IDisposable cancel) + : base(observer, cancel) + { + } + + public override void OnNext(T value) => Observer.OnNext(value); + + public override void OnError(Exception error) + { + try + { + Observer.OnError(error); + } + finally + { + Dispose(); + } + } + + public override void OnCompleted() + { + try + { + Observer.OnCompleted(); + } + finally + { + Dispose(); + } + } + } +} diff --git a/src/Minimalist.Reactive/Signals/Core/CreateSignal{T}.cs b/src/Minimalist.Reactive/Signals/Core/CreateSignal{T}.cs new file mode 100644 index 0000000..c8fa345 --- /dev/null +++ b/src/Minimalist.Reactive/Signals/Core/CreateSignal{T}.cs @@ -0,0 +1,58 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Disposables; + +namespace Minimalist.Reactive.Signals.Core; + +internal class CreateSignal : SignalsBase +{ + private readonly Func, IDisposable> _subscribe; + + public CreateSignal(Func, IDisposable> subscribe) + : base(true) => _subscribe = subscribe; // fail safe + + public CreateSignal(Func, IDisposable> subscribe, bool isRequiredSubscribeOnCurrentThread) + : base(isRequiredSubscribeOnCurrentThread) => _subscribe = subscribe; + + protected override IDisposable SubscribeCore(IObserver observer, IDisposable cancel) + { + observer = new Create(observer, cancel); + return _subscribe(observer) ?? Disposable.Empty; + } + + private class Create : WitnessBase + { + public Create(IObserver observer, IDisposable cancel) + : base(observer, cancel) + { + } + + public override void OnNext(T value) => Observer.OnNext(value); + + public override void OnError(Exception error) + { + try + { + Observer.OnError(error); + } + finally + { + Dispose(); + } + } + + public override void OnCompleted() + { + try + { + Observer.OnCompleted(); + } + finally + { + Dispose(); + } + } + } +} diff --git a/src/Minimalist.Reactive/Signals/Core/DeferSignal{T}.cs b/src/Minimalist.Reactive/Signals/Core/DeferSignal{T}.cs new file mode 100644 index 0000000..9152c18 --- /dev/null +++ b/src/Minimalist.Reactive/Signals/Core/DeferSignal{T}.cs @@ -0,0 +1,75 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Signals.Core; + +internal class DeferSignal : SignalsBase +{ + private readonly Func> _observableFactory; + + public DeferSignal(Func> observableFactory) + : base(false) => _observableFactory = observableFactory; + + protected override IDisposable SubscribeCore(IObserver observer, IDisposable cancel) + { + observer = new Defer(observer, cancel); + + IObservable source; + try + { + source = _observableFactory(); + } + catch (Exception ex) + { + source = Signal.Throw(ex); + } + + return source.Subscribe(observer); + } + + private class Defer : WitnessBase + { + public Defer(IObserver observer, IDisposable cancel) + : base(observer, cancel) + { + } + + public override void OnNext(T value) + { + try + { + Observer.OnNext(value); + } + catch + { + Dispose(); + throw; + } + } + + public override void OnError(Exception error) + { + try + { + Observer.OnError(error); + } + finally + { + Dispose(); + } + } + + public override void OnCompleted() + { + try + { + Observer.OnCompleted(); + } + finally + { + Dispose(); + } + } + } +} diff --git a/src/Minimalist.Reactive/Signals/Core/EmptySignal{T}.cs b/src/Minimalist.Reactive/Signals/Core/EmptySignal{T}.cs new file mode 100644 index 0000000..a845577 --- /dev/null +++ b/src/Minimalist.Reactive/Signals/Core/EmptySignal{T}.cs @@ -0,0 +1,74 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Concurrency; +using Minimalist.Reactive.Disposables; + +namespace Minimalist.Reactive.Signals.Core; + +internal class EmptySignal : SignalsBase +{ + private readonly IScheduler _scheduler; + + public EmptySignal(IScheduler scheduler) + : base(false) => _scheduler = scheduler; + + protected override IDisposable SubscribeCore(IObserver observer, IDisposable cancel) + { + observer = new Empty(observer, cancel); + + if (_scheduler == Scheduler.Immediate) + { + observer.OnCompleted(); + return Disposable.Empty; + } + + return _scheduler.Schedule(observer.OnCompleted); + } + + private class Empty : WitnessBase + { + public Empty(IObserver observer, IDisposable cancel) + : base(observer, cancel) + { + } + + public override void OnNext(T value) + { + try + { + Observer.OnNext(value); + } + catch + { + Dispose(); + throw; + } + } + + public override void OnError(Exception error) + { + try + { + Observer.OnError(error); + } + finally + { + Dispose(); + } + } + + public override void OnCompleted() + { + try + { + Observer.OnCompleted(); + } + finally + { + Dispose(); + } + } + } +} diff --git a/src/Minimalist.Reactive/Signals/Core/FinallySignal{T}.cs b/src/Minimalist.Reactive/Signals/Core/FinallySignal{T}.cs new file mode 100644 index 0000000..da13f5b --- /dev/null +++ b/src/Minimalist.Reactive/Signals/Core/FinallySignal{T}.cs @@ -0,0 +1,73 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Disposables; + +namespace Minimalist.Reactive.Signals.Core; + +internal class FinallySignal : SignalsBase +{ + private readonly IObservable _source; + private readonly Action _finallyAction; + + public FinallySignal(IObservable source, Action finallyAction) + : base(true) + { + _source = source; + _finallyAction = finallyAction; + } + + protected override IDisposable SubscribeCore(IObserver observer, IDisposable cancel) => + new Finally(this, observer, cancel).Run(); + + private class Finally : WitnessBase + { + private readonly FinallySignal _parent; + + public Finally(FinallySignal parent, IObserver observer, IDisposable cancel) + : base(observer, cancel) => _parent = parent; + + public IDisposable Run() + { + IDisposable subscription; + try + { + subscription = _parent._source.Subscribe(this); + } + catch + { + _parent._finallyAction(); + throw; + } + + return new MultipleDisposable(subscription, Disposable.Create(() => _parent._finallyAction())); + } + + public override void OnNext(T value) => Observer.OnNext(value); + + public override void OnError(Exception error) + { + try + { + Observer.OnError(error); + } + finally + { + Dispose(); + } + } + + public override void OnCompleted() + { + try + { + Observer.OnCompleted(); + } + finally + { + Dispose(); + } + } + } +} diff --git a/src/Minimalist.Reactive/Signals/Core/ImmediateReturnSignal{T}.cs b/src/Minimalist.Reactive/Signals/Core/ImmediateReturnSignal{T}.cs new file mode 100644 index 0000000..65e432c --- /dev/null +++ b/src/Minimalist.Reactive/Signals/Core/ImmediateReturnSignal{T}.cs @@ -0,0 +1,24 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Core; +using Minimalist.Reactive.Disposables; + +namespace Minimalist.Reactive.Signals.Core; + +internal class ImmediateReturnSignal : IObservable, IRequireCurrentThread +{ + private readonly T _value; + + public ImmediateReturnSignal(T value) => _value = value; + + public bool IsRequiredSubscribeOnCurrentThread() => false; + + public IDisposable Subscribe(IObserver observer) + { + observer.OnNext(_value); + observer.OnCompleted(); + return Disposable.Empty; + } +} diff --git a/src/Minimalist.Reactive/Signals/Core/ImmutableEmptySignal{T}.cs b/src/Minimalist.Reactive/Signals/Core/ImmutableEmptySignal{T}.cs new file mode 100644 index 0000000..5d53f85 --- /dev/null +++ b/src/Minimalist.Reactive/Signals/Core/ImmutableEmptySignal{T}.cs @@ -0,0 +1,27 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Core; +using Minimalist.Reactive.Disposables; + +namespace Minimalist.Reactive.Signals.Core; + +internal sealed class ImmutableEmptySignal : IRequireCurrentThread +{ +#pragma warning disable SA1401 // Fields should be private + internal static ImmutableEmptySignal Instance = new(); +#pragma warning restore SA1401 // Fields should be private + + private ImmutableEmptySignal() + { + } + + public bool IsRequiredSubscribeOnCurrentThread() => false; + + public IDisposable Subscribe(IObserver observer) + { + observer.OnCompleted(); + return Disposable.Empty; + } +} diff --git a/src/Minimalist.Reactive/Signals/Core/ImmutableNeverSignal{T}.cs b/src/Minimalist.Reactive/Signals/Core/ImmutableNeverSignal{T}.cs new file mode 100644 index 0000000..73712aa --- /dev/null +++ b/src/Minimalist.Reactive/Signals/Core/ImmutableNeverSignal{T}.cs @@ -0,0 +1,20 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Core; +using Minimalist.Reactive.Disposables; + +namespace Minimalist.Reactive.Signals.Core; + +internal class ImmutableNeverSignal : IRequireCurrentThread +{ +#pragma warning disable SA1401 // Fields should be private + internal static ImmutableNeverSignal Instance = new(); +#pragma warning restore SA1401 // Fields should be private + + public bool IsRequiredSubscribeOnCurrentThread() => false; + + public IDisposable Subscribe(IObserver observer) => + Disposable.Empty; +} diff --git a/src/Minimalist.Reactive/Signals/Core/ImmutableReturnFalseSignal.cs b/src/Minimalist.Reactive/Signals/Core/ImmutableReturnFalseSignal.cs new file mode 100644 index 0000000..0b18f33 --- /dev/null +++ b/src/Minimalist.Reactive/Signals/Core/ImmutableReturnFalseSignal.cs @@ -0,0 +1,28 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Core; +using Minimalist.Reactive.Disposables; + +namespace Minimalist.Reactive.Signals.Core; + +internal class ImmutableReturnFalseSignal : IObservable, IRequireCurrentThread +{ +#pragma warning disable SA1401 // Fields should be private + internal static ImmutableReturnFalseSignal Instance = new(); +#pragma warning restore SA1401 // Fields should be private + + private ImmutableReturnFalseSignal() + { + } + + public bool IsRequiredSubscribeOnCurrentThread() => false; + + public IDisposable Subscribe(IObserver observer) + { + observer.OnNext(false); + observer.OnCompleted(); + return Disposable.Empty; + } +} diff --git a/src/Minimalist.Reactive/Signals/Core/ImmutableReturnInt32Signal.cs b/src/Minimalist.Reactive/Signals/Core/ImmutableReturnInt32Signal.cs new file mode 100644 index 0000000..626bf09 --- /dev/null +++ b/src/Minimalist.Reactive/Signals/Core/ImmutableReturnInt32Signal.cs @@ -0,0 +1,49 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Core; +using Minimalist.Reactive.Disposables; + +namespace Minimalist.Reactive.Signals.Core; + +internal class ImmutableReturnInt32Signal : IObservable, IRequireCurrentThread +{ + private static readonly ImmutableReturnInt32Signal[] Caches = new ImmutableReturnInt32Signal[] + { + new ImmutableReturnInt32Signal(-1), + new ImmutableReturnInt32Signal(0), + new ImmutableReturnInt32Signal(1), + new ImmutableReturnInt32Signal(2), + new ImmutableReturnInt32Signal(3), + new ImmutableReturnInt32Signal(4), + new ImmutableReturnInt32Signal(5), + new ImmutableReturnInt32Signal(6), + new ImmutableReturnInt32Signal(7), + new ImmutableReturnInt32Signal(8), + new ImmutableReturnInt32Signal(9), + }; + + private readonly int _x; + + internal ImmutableReturnInt32Signal(int x) => _x = x; + + public static IObservable GetInt32Signals(int x) + { + if (x >= -1 && x <= 9) + { + return Caches[x + 1]; + } + + return new ImmediateReturnSignal(x); + } + + public bool IsRequiredSubscribeOnCurrentThread() => false; + + public IDisposable Subscribe(IObserver observer) + { + observer.OnNext(_x); + observer.OnCompleted(); + return Disposable.Empty; + } +} diff --git a/src/Minimalist.Reactive/Signals/Core/ImmutableReturnRxVoidSignal.cs b/src/Minimalist.Reactive/Signals/Core/ImmutableReturnRxVoidSignal.cs new file mode 100644 index 0000000..d922923 --- /dev/null +++ b/src/Minimalist.Reactive/Signals/Core/ImmutableReturnRxVoidSignal.cs @@ -0,0 +1,28 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Core; +using Minimalist.Reactive.Disposables; + +namespace Minimalist.Reactive.Signals.Core; + +internal sealed class ImmutableReturnRxVoidSignal : IObservable, IRequireCurrentThread +{ +#pragma warning disable SA1401 // Fields should be private + internal static ImmutableReturnRxVoidSignal Instance = new(); +#pragma warning restore SA1401 // Fields should be private + + private ImmutableReturnRxVoidSignal() + { + } + + public bool IsRequiredSubscribeOnCurrentThread() => false; + + public IDisposable Subscribe(IObserver observer) + { + observer.OnNext(RxVoid.Default); + observer.OnCompleted(); + return Disposable.Empty; + } +} diff --git a/src/Minimalist.Reactive/Signals/Core/ImmutableReturnTrueSignal.cs b/src/Minimalist.Reactive/Signals/Core/ImmutableReturnTrueSignal.cs new file mode 100644 index 0000000..07d5c1a --- /dev/null +++ b/src/Minimalist.Reactive/Signals/Core/ImmutableReturnTrueSignal.cs @@ -0,0 +1,28 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Core; +using Minimalist.Reactive.Disposables; + +namespace Minimalist.Reactive.Signals.Core; + +internal sealed class ImmutableReturnTrueSignal : IObservable, IRequireCurrentThread +{ +#pragma warning disable SA1401 // Fields should be private + internal static ImmutableReturnTrueSignal Instance = new ImmutableReturnTrueSignal(); +#pragma warning restore SA1401 // Fields should be private + + private ImmutableReturnTrueSignal() + { + } + + public bool IsRequiredSubscribeOnCurrentThread() => false; + + public IDisposable Subscribe(IObserver observer) + { + observer.OnNext(true); + observer.OnCompleted(); + return Disposable.Empty; + } +} diff --git a/src/Minimalist.Reactive/Signals/Core/ReturnSignal{T}.cs b/src/Minimalist.Reactive/Signals/Core/ReturnSignal{T}.cs new file mode 100644 index 0000000..c5aff5a --- /dev/null +++ b/src/Minimalist.Reactive/Signals/Core/ReturnSignal{T}.cs @@ -0,0 +1,84 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Concurrency; +using Minimalist.Reactive.Disposables; + +namespace Minimalist.Reactive.Signals.Core; + +internal class ReturnSignal : SignalsBase +{ + private readonly T _value; + private readonly IScheduler _scheduler; + + public ReturnSignal(T value, IScheduler scheduler) + : base(scheduler == Scheduler.CurrentThread) + { + _value = value; + _scheduler = scheduler; + } + + protected override IDisposable SubscribeCore(IObserver observer, IDisposable cancel) + { + observer = new Return(observer, cancel); + + if (_scheduler == Scheduler.Immediate) + { + observer.OnNext(_value); + observer.OnCompleted(); + return Disposable.Empty; + } + + return _scheduler.Schedule(() => + { + observer.OnNext(_value); + observer.OnCompleted(); + }); + } + + private class Return : WitnessBase + { + public Return(IObserver observer, IDisposable cancel) + : base(observer, cancel) + { + } + + public override void OnNext(T value) + { + try + { + Observer.OnNext(value); + } + catch + { + Dispose(); + throw; + } + } + + public override void OnError(Exception error) + { + try + { + Observer.OnError(error); + } + finally + { + Dispose(); + } + } + + public override void OnCompleted() + { + try + { + Observer.OnCompleted(); + } + finally + { + Dispose(); + } + } + } +} diff --git a/src/Minimalist.Reactive/Signals/Core/SignalsBase{T}.cs b/src/Minimalist.Reactive/Signals/Core/SignalsBase{T}.cs new file mode 100644 index 0000000..867f4e2 --- /dev/null +++ b/src/Minimalist.Reactive/Signals/Core/SignalsBase{T}.cs @@ -0,0 +1,42 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Concurrency; +using Minimalist.Reactive.Core; +using Minimalist.Reactive.Disposables; + +namespace Minimalist.Reactive.Signals.Core; + +internal abstract class SignalsBase : IRequireCurrentThread +{ + private readonly bool _isRequiredSubscribeOnCurrentThread; + + internal SignalsBase(bool isRequiredSubscribeOnCurrentThread) => + _isRequiredSubscribeOnCurrentThread = isRequiredSubscribeOnCurrentThread; + + public bool IsRequiredSubscribeOnCurrentThread() => _isRequiredSubscribeOnCurrentThread; + + public IDisposable Subscribe(IObserver observer) + { + if (observer == null) + { + throw new ArgumentNullException(nameof(observer)); + } + + var subscription = new SingleDisposable(); + + if (_isRequiredSubscribeOnCurrentThread && Scheduler.CurrentThread.IsScheduleRequired) + { + Scheduler.CurrentThread.Schedule(() => subscription.Create(SubscribeCore(observer, subscription))); + } + else + { + subscription.Create(SubscribeCore(observer, subscription)); + } + + return subscription; + } + + protected abstract IDisposable SubscribeCore(IObserver observer, IDisposable cancel); +} diff --git a/src/Minimalist.Reactive/Signals/Core/ThrowSignal{T}.cs b/src/Minimalist.Reactive/Signals/Core/ThrowSignal{T}.cs new file mode 100644 index 0000000..134f6e1 --- /dev/null +++ b/src/Minimalist.Reactive/Signals/Core/ThrowSignal{T}.cs @@ -0,0 +1,83 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Concurrency; +using Minimalist.Reactive.Disposables; + +namespace Minimalist.Reactive.Signals.Core; + +internal class ThrowSignal : SignalsBase +{ + private readonly Exception _error; + private readonly IScheduler _scheduler; + + public ThrowSignal(Exception error, IScheduler scheduler) + : base(scheduler == Scheduler.CurrentThread) + { + _error = error; + _scheduler = scheduler; + } + + protected override IDisposable SubscribeCore(IObserver observer, IDisposable cancel) + { + observer = new Throw(observer, cancel); + + if (_scheduler == Scheduler.Immediate) + { + observer.OnError(_error); + return Disposable.Empty; + } + + return _scheduler.Schedule(() => + { + observer.OnError(_error); + observer.OnCompleted(); + }); + } + + private class Throw : WitnessBase + { + public Throw(IObserver observer, IDisposable cancel) + : base(observer, cancel) + { + } + + public override void OnNext(T value) + { + try + { + Observer.OnNext(value); + } + catch + { + Dispose(); + throw; + } + } + + public override void OnError(Exception error) + { + try + { + Observer.OnError(error); + } + finally + { + Dispose(); + } + } + + public override void OnCompleted() + { + try + { + Observer.OnCompleted(); + } + finally + { + Dispose(); + } + } + } +} diff --git a/src/Minimalist.Reactive/Signals/Core/WitnessBase{TSource,TResult}.cs b/src/Minimalist.Reactive/Signals/Core/WitnessBase{TSource,TResult}.cs new file mode 100644 index 0000000..56fbae7 --- /dev/null +++ b/src/Minimalist.Reactive/Signals/Core/WitnessBase{TSource,TResult}.cs @@ -0,0 +1,35 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Core; + +namespace Minimalist.Reactive.Signals.Core +{ + internal abstract class WitnessBase : IDisposable, IObserver + { +#pragma warning disable SA1401 // Fields should be private + protected internal volatile IObserver Observer; +#pragma warning restore SA1401 // Fields should be private + private IDisposable? _cancel; + + internal WitnessBase(IObserver observer, IDisposable cancel) + { + _cancel = cancel ?? throw new ArgumentNullException(nameof(cancel)); + Observer = observer; + } + + public abstract void OnNext(TSource value); + + public abstract void OnError(Exception error); + + public abstract void OnCompleted(); + + public void Dispose() + { + Observer = EmptyWitness.Instance; + var target = Interlocked.Exchange(ref _cancel, null); + target?.Dispose(); + } + } +} diff --git a/src/Minimalist.Reactive/Signals/Core/WitnessOnSignal{T}.cs b/src/Minimalist.Reactive/Signals/Core/WitnessOnSignal{T}.cs new file mode 100644 index 0000000..88cd44a --- /dev/null +++ b/src/Minimalist.Reactive/Signals/Core/WitnessOnSignal{T}.cs @@ -0,0 +1,232 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Concurrency; +using Minimalist.Reactive.Core; +using Minimalist.Reactive.Disposables; + +namespace Minimalist.Reactive.Signals.Core; + +internal class WitnessOnSignal : SignalsBase +{ + private readonly IObservable _source; + private readonly IScheduler _scheduler; + + public WitnessOnSignal(IObservable source, IScheduler scheduler) + : base(true) + { + _source = source; + _scheduler = scheduler; + } + + protected override IDisposable SubscribeCore(IObserver observer, IDisposable cancel) + { + if (_scheduler is not ThreadPoolScheduler queueing) + { + return new WitnessOn(this, observer, cancel).Run(); + } + + return new WitnessOn_(this, queueing, observer, cancel).Run(); + } + + private class WitnessOn : WitnessBase + { + private readonly WitnessOnSignal _parent; + private readonly LinkedList _actions = new(); + private bool _isDisposed; + + public WitnessOn(WitnessOnSignal parent, IObserver observer, IDisposable cancel) + : base(observer, cancel) => _parent = parent; + + public IDisposable Run() + { + _isDisposed = false; + + var sourceDisposable = _parent._source.Subscribe(this); + + return new MultipleDisposable(sourceDisposable, Disposable.Create(() => + { + lock (_actions) + { + _isDisposed = true; + + while (_actions.Count > 0) + { + // Dispose will both cancel the action (if not already running) + // and remove it from 'actions' + _actions.First?.Value.Dispose(); + } + } + })); + } + + public override void OnNext(T value) => QueueAction(new Spark.OnNextSpark(value)); + + public override void OnError(Exception error) => QueueAction(new Spark.OnErrorSpark(error)); + + public override void OnCompleted() => QueueAction(new Spark.OnCompletedSpark()); + + private void QueueAction(Spark data) + { + var action = new SchedulableAction(data); + lock (_actions) + { + if (_isDisposed) + { + return; + } + + action.Node = _actions.AddLast(action); + ProcessNext(); + } + } + + private void ProcessNext() + { + lock (_actions) + { + if (_actions.Count == 0 || _isDisposed) + { + return; + } + + var action = _actions.First?.Value; + + if (action?.IsScheduled == true) + { + return; + } + + action!.Schedule = _parent._scheduler.Schedule(() => + { + try + { + switch (action.Data?.Kind) + { + case SparkKind.OnNext: + Observer.OnNext(action.Data.Value); + break; + case SparkKind.OnError: + Observer.OnError(action.Data.Exception); + break; + case SparkKind.OnCompleted: + Observer.OnCompleted(); + break; + } + } + finally + { + lock (_actions) + { + action.Dispose(); + } + + if (action.Data?.Kind == SparkKind.OnNext) + { + ProcessNext(); + } + else + { + Dispose(); + } + } + }); + } + } + + private class SchedulableAction : IDisposable + { + public SchedulableAction(Spark data) + { + Data = data; + } + + public Spark Data { get; } + + public LinkedListNode? Node { get; set; } + + public IDisposable? Schedule { get; set; } + + public bool IsScheduled => Schedule != null; + + public void Dispose() + { + Schedule?.Dispose(); + + Schedule = null; + + if (Node?.List != null) + { + Node.List.Remove(Node); + } + } + } + } + + private class WitnessOn_ : WitnessBase + { + private readonly WitnessOnSignal _parent; + private readonly ThreadPoolScheduler _scheduler; + private readonly BooleanDisposable _isDisposed; + private readonly Action _onNext; + + public WitnessOn_(WitnessOnSignal parent, ThreadPoolScheduler scheduler, IObserver observer, IDisposable cancel) + : base(observer, cancel) + { + _parent = parent; + _scheduler = scheduler; + _isDisposed = new BooleanDisposable(); + _onNext = new Action(OnNext_); + } + + public IDisposable Run() + { + var sourceDisposable = _parent._source.Subscribe(this); + return new MultipleDisposable(sourceDisposable, _isDisposed); + } + + public override void OnNext(T value) => + _scheduler.Schedule(value, (s, v) => + { + _onNext(v); + return _isDisposed; + }); + + public override void OnError(Exception error) => + _scheduler.Schedule(error, (s, v) => + { + OnError_(v); + return _isDisposed; + }); + + public override void OnCompleted() => + _scheduler.Schedule(() => OnCompleted_(RxVoid.Default)); + + private void OnNext_(T value) => Observer.OnNext(value); + + private void OnError_(Exception error) + { + try + { + Observer.OnError(error); + } + finally + { + Dispose(); + } + } + + private void OnCompleted_(RxVoid v) + { + try + { + Observer.OnCompleted(); + } + finally + { + Dispose(); + } + } + } +} diff --git a/src/Minimalist.Reactive/Signals/Signal{Catch}.cs b/src/Minimalist.Reactive/Signals/Signal{Catch}.cs new file mode 100644 index 0000000..0c6b569 --- /dev/null +++ b/src/Minimalist.Reactive/Signals/Signal{Catch}.cs @@ -0,0 +1,82 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Signals.Core; + +namespace Minimalist.Reactive.Signals; + +/// +/// Signals. +/// +public static partial class Signal +{ + /// + /// Continues an observable sequence that is terminated by an exception of the specified type with the observable sequence produced by the handler. + /// + /// The type of the elements in the source sequence and sequences returned by the exception handler function. + /// The type of the exception to catch and handle. Needs to derive from . + /// Source sequence. + /// Exception handler function, producing another observable sequence. + /// An observable sequence containing the source sequence's elements, followed by the elements produced by the handler's resulting observable sequence in case an exception occurred. + /// or is null. + public static IObservable Catch(this IObservable source, Func> handler) + where TException : Exception + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (handler == null) + { + throw new ArgumentNullException(nameof(handler)); + } + + return new CatchSignal(source, handler); + } + + /// + /// Continues an observable sequence that is terminated by an exception with the next observable sequence. + /// + /// The type of the elements in the source and handler sequences. + /// Observable sequences to catch exceptions for. + /// An observable sequence containing elements from consecutive source sequences until a source sequence terminates successfully. + /// is null. + public static IObservable Catch(params IObservable[] sources) + { + if (sources == null) + { + throw new ArgumentNullException(nameof(sources)); + } + + return new CatchSignal(sources); + } + + /// + /// Continues an observable sequence that is terminated by an exception with the next observable sequence. + /// + /// The type of the elements in the source and handler sequences. + /// Observable sequences to catch exceptions for. + /// An observable sequence containing elements from consecutive source sequences until a source sequence terminates successfully. + /// is null. + public static IObservable Catch(this IEnumerable> sources) + { + if (sources == null) + { + throw new ArgumentNullException(nameof(sources)); + } + + return new CatchSignal(sources); + } + + /// + /// Finallies the specified finally action. + /// + /// The type of the elements in the source and handler sequences. + /// The source. + /// The finally action. + /// An observable sequence containing elements from consecutive source sequences until a source sequence terminates successfully. + public static IObservable Finally(this IObservable source, Action finallyAction) => + new FinallySignal(source, finallyAction); +} diff --git a/src/Minimalist.Reactive/Signals/Signal{Create}.cs b/src/Minimalist.Reactive/Signals/Signal{Create}.cs new file mode 100644 index 0000000..6bd724a --- /dev/null +++ b/src/Minimalist.Reactive/Signals/Signal{Create}.cs @@ -0,0 +1,154 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Concurrency; +using Minimalist.Reactive.Signals.Core; + +namespace Minimalist.Reactive.Signals; + +/// +/// Create Signals functionality. +/// +public static partial class Signal +{ + /// + /// Create anonymous Signals. Observer has exception durability. + /// This is recommended for make operator and event, generating a HotSignals. + /// + /// The type. + /// The subscribe. + /// An Signals. + /// subscribe. + /// is null. + public static IObservable Create(Func, IDisposable> subscribe) + { + if (subscribe == null) + { + throw new ArgumentNullException(nameof(subscribe)); + } + + return new CreateSignal(subscribe); + } + + /// + /// Create anonymous Signals. Observer has exception durability. + /// This is recommended for make operator and event, generating a HotSignals. + /// + /// The type. + /// The subscribe. + /// if set to true [is required subscribe on current thread]. + /// An Signals. + /// subscribe. + /// is null. + public static IObservable Create(Func, IDisposable> subscribe, bool isRequiredSubscribeOnCurrentThread) + { + if (subscribe == null) + { + throw new ArgumentNullException(nameof(subscribe)); + } + + return new CreateSignal(subscribe, isRequiredSubscribeOnCurrentThread); + } + + /// + /// Create anonymous Signals. Observer has exception durability. + /// This is recommended for make operator and event, generating a HotSignals. + /// + /// The type. + /// The type of the state. + /// The state. + /// The subscribe. + /// An Signals. + /// subscribe. + /// is null. + public static IObservable CreateWithState(TState state, Func, IDisposable> subscribe) + { + if (subscribe == null) + { + throw new ArgumentNullException(nameof(subscribe)); + } + + return new CreateSignal(state, subscribe); + } + + /// + /// Create anonymous Signals. Observer has exception durability. + /// This is recommended for make operator and event, generating a HotSignals. + /// + /// The type. + /// The type of the state. + /// The state. + /// The subscribe. + /// if set to true [is required subscribe on current thread]. + /// An Signals. + /// subscribe. + /// is null. + public static IObservable CreateWithState(TState state, Func, IDisposable> subscribe, bool isRequiredSubscribeOnCurrentThread) + { + if (subscribe == null) + { + throw new ArgumentNullException(nameof(subscribe)); + } + + return new CreateSignal(state, subscribe, isRequiredSubscribeOnCurrentThread); + } + + /// + /// Create anonymous Signals. Safe means auto detach when error raised in onNext pipeline. + /// This is recommended for making a ColdSignals. + /// + /// The type. + /// The subscribe. + /// An Signals. + /// subscribe. + /// is null. + public static IObservable CreateSafe(Func, IDisposable> subscribe) + { + if (subscribe == null) + { + throw new ArgumentNullException(nameof(subscribe)); + } + + return new CreateSafeSignal(subscribe); + } + + /// + /// Create anonymous Signals. Safe means auto detach when error raised in onNext pipeline. + /// This is recommended for making a ColdSignals. + /// + /// The type. + /// The subscribe. + /// if set to true [is required subscribe on current thread]. + /// An Observable. + /// subscribe. + /// is null. + public static IObservable CreateSafe(Func, IDisposable> subscribe, bool isRequiredSubscribeOnCurrentThread) + { + if (subscribe == null) + { + throw new ArgumentNullException(nameof(subscribe)); + } + + return new CreateSafeSignal(subscribe, isRequiredSubscribeOnCurrentThread); + } + + /// + /// Defers the specified observable factory. + /// + /// The type. + /// The observable factory. + /// An Observable. + public static IObservable Defer(Func> observableFactory) => + new DeferSignal(observableFactory); + + /// + /// Witnesses the on. + /// + /// The type. + /// The source. + /// The scheduler. + /// An Observable. + public static IObservable WitnessOn(this IObservable source, IScheduler scheduler) => + new WitnessOnSignal(source, scheduler); +} diff --git a/src/Minimalist.Reactive/Signals/Signal{Empty}.cs b/src/Minimalist.Reactive/Signals/Signal{Empty}.cs new file mode 100644 index 0000000..6726ba6 --- /dev/null +++ b/src/Minimalist.Reactive/Signals/Signal{Empty}.cs @@ -0,0 +1,61 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Concurrency; +using Minimalist.Reactive.Signals.Core; + +namespace Minimalist.Reactive.Signals; + +/// +/// Signals. +/// +public static partial class Signal +{ + /// + /// Empty Signals. Returns only OnCompleted on specified scheduler. + /// + /// The Type. + /// The scheduler. + /// An Signals. + public static IObservable Empty(IScheduler scheduler) + { + if (scheduler == Scheduler.Immediate) + { + return ImmutableEmptySignal.Instance; + } + + return new EmptySignal(scheduler); + } + + /// + /// Empty Signals. Returns only OnCompleted on specified scheduler. witness is for type inference. + /// + /// The Type. + /// The scheduler. + /// The witness. + /// An Signals. +#pragma warning disable RCS1163 // Unused parameter. + public static IObservable Empty(IScheduler scheduler, T witness) => + Empty(scheduler); +#pragma warning restore RCS1163 // Unused parameter. + + /// + /// Empty Signals. Returns only OnCompleted. + /// + /// The Type. + /// An Signals. + public static IObservable Empty() => + Empty(Scheduler.Immediate); + + /// + /// Empty Signals. Returns only OnCompleted. witness is for type inference. + /// + /// The Type. + /// The witness. + /// An Signals. +#pragma warning disable RCS1163 // Unused parameter. + public static IObservable Empty(T witness) => + Empty(Scheduler.Immediate); +#pragma warning restore RCS1163 // Unused parameter. +} diff --git a/src/Minimalist.Reactive/Signals/Signal{FromTask}.cs b/src/Minimalist.Reactive/Signals/Signal{FromTask}.cs new file mode 100644 index 0000000..c81071d --- /dev/null +++ b/src/Minimalist.Reactive/Signals/Signal{FromTask}.cs @@ -0,0 +1,249 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Concurrency; +using Minimalist.Reactive.Disposables; + +namespace Minimalist.Reactive.Signals; + +/// +/// Signals. +/// +public static partial class Signal +{ + /// + /// Handles Asnyc Tasks with cancellation. + /// + /// The function to execute. + /// The scheduler. + /// The cancellation token source. + /// + /// An ITaskSignal of T. + /// + public static ITaskSignal FromTask(Func> execution, IScheduler? scheduler = null, CancellationTokenSource? cancellationTokenSource = null) => + TaskSignal.Create( + ao => Defer(() => Create( + obs => + { + // CancelationToken + var src = ao.CancellationTokenSource!; + var ct = src.Token; + ct.ThrowIfCancellationRequested(); + var hasError = false; + var hasCompleted = false; + var cancellableTask = Task.Factory.StartNew(() => execution(src), ct, TaskCreationOptions.None, TaskScheduler.Current).WhenCancelled(ct); + + Task.Run(async () => + { + try + { +#pragma warning disable IDE0042 // Deconstruct variable declaration + var cancellableTaskHandler = await cancellableTask; +#pragma warning restore IDE0042 // Deconstruct variable declaration + var result = await cancellableTaskHandler.Result; + if (!cancellableTaskHandler.IsCanceled && !src.IsCancellationRequested) + { + obs.OnNext(result); + hasCompleted = !src.IsCancellationRequested; + obs.OnCompleted(); + } + else + { + obs.OnError(new OperationCanceledException()); + } + } + catch (Exception ex) + { + hasError = true; + + // Catch the exception and pass it to the observer if not user handled. + obs.OnError(ex); + await Task.Delay(1); + } + }); + return Disposable.Create(() => + { + if (hasError) + { + Task.Delay(2).Wait(); + } + + if (hasError || !hasCompleted) + { + try + { + src.Cancel(); + } + catch (ObjectDisposedException) + { + throw new OperationCanceledException(); + } + } + + src.Dispose(); + }); + })), + scheduler, + cancellationTokenSource); + + /// + /// Froms the asynchronous. + /// + /// The type of the return value. + /// The action asynchronous. + /// The scheduler. + /// The cancellation token source. + /// + /// An TaskSignal of T. + /// + public static ITaskSignal FromTask(Func> actionAsync, IScheduler? scheduler = null, CancellationTokenSource? cancellationTokenSource = null) => + TaskSignal.Create( + ao => Defer(() => Create( + obs => + { + // CancelationToken + var src = ao.CancellationTokenSource!; + var ct = src.Token; + ct.ThrowIfCancellationRequested(); + var hasError = false; + var hasCompleted = false; + var cancellableTask = Task.Factory.StartNew(() => actionAsync(src), ct, TaskCreationOptions.None, TaskScheduler.Current).WhenCancelled(ct); + + Task.Run(async () => + { + try + { +#pragma warning disable IDE0042 // Deconstruct variable declaration + var cancellableTaskHandler = await cancellableTask; +#pragma warning restore IDE0042 // Deconstruct variable declaration + var result = await cancellableTaskHandler.Result; + if (result != null && !src.IsCancellationRequested) + { + obs.OnNext(result); + hasCompleted = !src.IsCancellationRequested; + obs.OnCompleted(); + } + else + { + obs.OnError(new OperationCanceledException()); + } + } + catch (Exception ex) + { + hasError = true; + + // Catch the exception and pass it to the observer if not user handled. + obs.OnError(ex); + await Task.Delay(1); + } + }); + return Disposable.Create(() => + { + if (hasError) + { + Task.Delay(2).Wait(); + } + + if (hasError || !hasCompleted) + { + try + { + src.Cancel(); + } + catch (ObjectDisposedException) + { + throw new OperationCanceledException(); + } + } + + src.Dispose(); + }); + })), + scheduler, + cancellationTokenSource); + + /// + /// Handles the cancellation. + /// + /// The asynchronous task. + /// The action. + /// A Task. + public static async Task HandleCancellation(this Task asyncTask, Action? action = null) + { + try + { + await asyncTask; + } + catch (OperationCanceledException) + { + action?.Invoke(); + } + } + + /// + /// Handles the cancellation. + /// + /// The type of the result. + /// The asynchronous task. + /// The action. + /// A Task of TResult. + public static async Task HandleCancellation(this Task asyncTask, Action? action = null) + { + try + { + return await asyncTask; + } + catch (OperationCanceledException) + { + action?.Invoke(); + } + + return default; + } + + /// + /// Handles the cancellation. + /// + /// The type. + /// The asynchronous task. + /// The token. + /// The action. + /// + /// A Task. + /// + public static async Task HandleCancellation(this IObservable asyncTask, CancellationToken token, Action? action = null) + { + try + { + token.ThrowIfCancellationRequested(); + return await Task.Run(async () => await asyncTask, token); + } + catch (OperationCanceledException) + { + action?.Invoke(); + } + + return default; + } + + private static async Task<(TResult Result, bool IsCanceled)> WhenCancelled(this Task asyncTask, CancellationToken cancellationToken) + { + var tcs = new TaskCompletionSource(); + cancellationToken.Register(() => tcs.TrySetCanceled(), useSynchronizationContext: false); + var cancellationTask = tcs.Task; + + // Create a task that completes when either the async operation completes, + // or cancellation is requested. + var readyTask = await Task.WhenAny(asyncTask, cancellationTask); + + // In case of cancellation, register a continuation to observe any unhandled. + // exceptions from the asynchronous operation (once it completes). + if (readyTask == cancellationTask) + { + await asyncTask.ContinueWith(_ => asyncTask.Exception, cancellationToken, TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Current); + } + + return (await readyTask, tcs.Task.IsCanceled || readyTask.IsCanceled); + } +} diff --git a/src/Minimalist.Reactive/Signals/Signal{GetAwaiter}.cs b/src/Minimalist.Reactive/Signals/Signal{GetAwaiter}.cs new file mode 100644 index 0000000..b11dc22 --- /dev/null +++ b/src/Minimalist.Reactive/Signals/Signal{GetAwaiter}.cs @@ -0,0 +1,88 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Signals; + +/// +/// Signal. +/// +public static partial class Signal +{ + /// + /// Gets an awaiter that returns the last value of the observable sequence or throws an exception if the sequence is empty. + /// This operation subscribes to the observable sequence, making it hot. + /// + /// The type of the source. + /// Source sequence to await. + /// An AsyncSignal. + /// source. + public static IAwaitSignal GetAwaiter(this IObservable source) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + return RunAsync(source, CancellationToken.None); + } + + /// + /// Gets an awaiter that returns the last value of the observable sequence or throws an exception if the sequence is empty. + /// This operation subscribes to the observable sequence, making it hot. + /// + /// The type of the source. + /// Source sequence to await. + /// Cancellation token. + /// + /// An AsyncSignal. + /// + /// source. + public static IAwaitSignal GetAwaiter(this IObservable source, CancellationToken cancellationToken) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + return RunAsync(source, cancellationToken); + } + +#pragma warning disable RCS1047 // Non-asynchronous method name should not end with 'Async'. + private static IAwaitSignal RunAsync(IObservable source, CancellationToken cancellationToken) +#pragma warning restore RCS1047 // Non-asynchronous method name should not end with 'Async'. + { + var s = new AsyncSignal(); + + if (cancellationToken.IsCancellationRequested) + { + return Cancel(s, cancellationToken); + } + + var d = source.Subscribe(s); + + if (cancellationToken.CanBeCanceled) + { + RegisterCancelation(s, d, cancellationToken); + } + + return s; + } + + private static IAwaitSignal Cancel(IAwaitSignal subject, CancellationToken cancellationToken) + { + subject.OnError(new OperationCanceledException(cancellationToken)); + return subject; + } + + private static void RegisterCancelation(IAwaitSignal subject, IDisposable subscription, CancellationToken token) + { + var ctr = token.Register(() => + { + subscription.Dispose(); + Cancel(subject, token); + }); + + subject.Subscribe(Handle.Ignore, _ => ctr.Dispose(), ctr.Dispose); + } +} diff --git a/src/Minimalist.Reactive/Signals/Signal{Never}.cs b/src/Minimalist.Reactive/Signals/Signal{Never}.cs new file mode 100644 index 0000000..c187002 --- /dev/null +++ b/src/Minimalist.Reactive/Signals/Signal{Never}.cs @@ -0,0 +1,30 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Signals.Core; + +namespace Minimalist.Reactive.Signals; + +/// +/// Signals. +/// +public static partial class Signal +{ + /// + /// Non-Terminating Signals. It's no returns, never finish. + /// + /// The type. + /// An Signals. + public static IObservable Never() => ImmutableNeverSignal.Instance; + + /// + /// Non-Terminating Signals. It's no returns, never finish. witness is for type inference. + /// + /// The type. + /// The witness. + /// An Signals. +#pragma warning disable RCS1163 // Unused parameter. + public static IObservable Never(T witness) => ImmutableNeverSignal.Instance; +#pragma warning restore RCS1163 // Unused parameter. +} diff --git a/src/Minimalist.Reactive/Signals/Signal{Return}.cs b/src/Minimalist.Reactive/Signals/Signal{Return}.cs new file mode 100644 index 0000000..ccc0ab7 --- /dev/null +++ b/src/Minimalist.Reactive/Signals/Signal{Return}.cs @@ -0,0 +1,74 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Concurrency; +using Minimalist.Reactive.Signals.Core; + +namespace Minimalist.Reactive.Signals; + +/// +/// Signals. +/// +public static partial class Signal +{ + /// + /// Return single sequence on specified scheduler. + /// + /// The type. + /// The value. + /// The scheduler. + /// An Signals. + public static IObservable Return(T value, IScheduler scheduler) + { + if (scheduler == Scheduler.Immediate) + { + return new ImmediateReturnSignal(value); + } + + return new ReturnSignal(value, scheduler); + } + + /// + /// Return single sequence Immediately. + /// + /// The type. + /// The value. + /// An Signals. + public static IObservable Return(T value) => + Return(value, Scheduler.Immediate); + + /// + /// Return single sequence Immediately, optimized for RxVoid(no allocate memory). + /// + /// The value. + /// An Signals. +#pragma warning disable RCS1163 // Unused parameter. + public static IObservable Return(RxVoid value) => + ImmutableReturnRxVoidSignal.Instance; +#pragma warning restore RCS1163 // Unused parameter. + + /// + /// Return single sequence Immediately, optimized for Boolean(no allocate memory). + /// + /// if set to true [value]. + /// An Signals. + public static IObservable Return(bool value) => value + ? ImmutableReturnTrueSignal.Instance + : ImmutableReturnFalseSignal.Instance; + + /// + /// Return single sequence Immediately, optimized for Int32. + /// + /// The value. + /// An Signals. + public static IObservable Return(int value) => + ImmutableReturnInt32Signal.GetInt32Signals(value); + + /// + /// Same as Signals.Return(RxVoid.Default); but no allocate memory. + /// + /// An Signals. + public static IObservable ReturnRxVoid() => + ImmutableReturnRxVoidSignal.Instance; +} diff --git a/src/Minimalist.Reactive/Signals/Signal{Throw}.cs b/src/Minimalist.Reactive/Signals/Signal{Throw}.cs new file mode 100644 index 0000000..6367664 --- /dev/null +++ b/src/Minimalist.Reactive/Signals/Signal{Throw}.cs @@ -0,0 +1,56 @@ +// Copyright (c) 2019-2023 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 Minimalist.Reactive.Concurrency; +using Minimalist.Reactive.Signals.Core; + +namespace Minimalist.Reactive.Signals; + +/// +/// Signals. +/// +public static partial class Signal +{ + /// + /// Empty Signals. Returns only onError on specified scheduler. + /// + /// The type. + /// The error. + /// The scheduler. + /// An Signals. + public static IObservable Throw(Exception error, IScheduler scheduler) => + new ThrowSignal(error, scheduler); + + /// + /// Empty Signals. Returns only onError. + /// + /// The type. + /// The error. + /// An Signals. + public static IObservable Throw(Exception error) => + Throw(error, Scheduler.Immediate); + + /// + /// Empty Signals. Returns only onError. witness if for Type inference. + /// + /// The type. + /// The error. + /// The witness. + /// An Signals. +#pragma warning disable RCS1163 // Unused parameter. + public static IObservable Throw(Exception error, T witness) => + Throw(error, Scheduler.Immediate); + + /// + /// Empty Signals. Returns only onError on specified scheduler. witness if for Type inference. + /// + /// The type. + /// The error. + /// The scheduler. + /// The witness. + /// An Signals. + public static IObservable Throw(Exception error, IScheduler scheduler, T witness) => + Throw(error, scheduler); +#pragma warning restore RCS1163 // Unused parameter. +} diff --git a/src/Minimalist.Reactive/Signal{T}.cs b/src/Minimalist.Reactive/Signal{T}.cs deleted file mode 100644 index 9f6a4d4..0000000 --- a/src/Minimalist.Reactive/Signal{T}.cs +++ /dev/null @@ -1,275 +0,0 @@ -// Copyright (c) 2019-2022 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 Minimalist.Reactive -{ - /// - /// Subject. - /// - /// The Type. - public class Signal : ISignal - { - private static readonly ObserverHandler[] disposedCompare = new ObserverHandler[0]; - private static readonly ObserverHandler[] terminatedCompare = new ObserverHandler[0]; - private ObserverHandler[] _observers = Array.Empty(); - private Exception? _exception; - - /// - /// Gets a value indicating whether indicates whether the subject has observers subscribed to it. - /// - public virtual bool HasObservers => Volatile.Read(ref _observers).Length != 0; - - /// - /// Gets a value indicating whether indicates whether the subject has been disposed. - /// - public virtual bool IsDisposed => Volatile.Read(ref _observers) == disposedCompare; - - /// - /// Releases unmanaged and - optionally - managed resources. - /// - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - /// - /// Called when [completed]. - /// - public void OnCompleted() - { -#pragma warning disable SA1009 // Closing parenthesis should be spaced correctly - for (; ; ) - { - var observers = Volatile.Read(ref _observers); - if (observers == disposedCompare) - { - _exception = null; - ThrowDisposed(); - break; - } - - if (observers == terminatedCompare) - { - break; - } - - if (Interlocked.CompareExchange(ref _observers, terminatedCompare, observers) == observers) - { - foreach (var observer in observers) - { - observer.Observer?.OnCompleted(); - } - - break; - } - } -#pragma warning restore SA1009 // Closing parenthesis should be spaced correctly - } - - /// - /// Called when [error]. - /// - /// The error. - public void OnError(Exception error) - { - if (error == null) - { - throw new ArgumentNullException(nameof(error)); - } - -#pragma warning disable SA1009 // Closing parenthesis should be spaced correctly - for (; ; ) - { - var observers = Volatile.Read(ref _observers); - if (observers == disposedCompare) - { - _exception = null; - ThrowDisposed(); - break; - } - - if (observers == terminatedCompare) - { - break; - } - - _exception = error; - if (Interlocked.CompareExchange(ref _observers, terminatedCompare, observers) == observers) - { - foreach (var observer in observers) - { - observer.Observer?.OnError(error); - } - - break; - } - } -#pragma warning restore SA1009 // Closing parenthesis should be spaced correctly - } - - /// - /// Called when [next]. - /// - /// The value. - public void OnNext(T value) - { - var observers = Volatile.Read(ref _observers); - if (observers == disposedCompare) - { - _exception = null; - ThrowDisposed(); - return; - } - - foreach (var observer in observers) - { - observer.Observer?.OnNext(value); - } - } - - /// - /// Subscribes the specified observer. - /// - /// The observer. - /// - /// A IDisposable. - /// - public IDisposable Subscribe(IObserver observer) - { - if (observer == null) - { - throw new ArgumentNullException(nameof(observer)); - } - - var disposable = default(ObserverHandler); -#pragma warning disable SA1009 // Closing parenthesis should be spaced correctly - for (; ; ) - { - var observers = Volatile.Read(ref _observers); - if (observers == disposedCompare) - { - _exception = null; - ThrowDisposed(); - break; - } - - if (observers == terminatedCompare) - { - var ex = _exception; - if (ex != null) - { - observer.OnError(ex); - } - else - { - observer.OnCompleted(); - } - - break; - } - - disposable ??= new ObserverHandler(this, observer); - - var n = observers.Length; - var b = new ObserverHandler[n + 1]; - - Array.Copy(observers, 0, b, 0, n); - - b[n] = disposable; - if (Interlocked.CompareExchange(ref _observers, b, observers) == observers) - { - return disposable; - } - } -#pragma warning restore SA1009 // Closing parenthesis should be spaced correctly - - return Disposable.Empty; - } - - /// - /// Releases unmanaged and - optionally - managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected virtual void Dispose(bool disposing) - { - if (!IsDisposed) - { - if (disposing) - { - Interlocked.Exchange(ref _observers, disposedCompare); - _exception = null; - } - } - } - - private static void ThrowDisposed() => throw new ObjectDisposedException(string.Empty); - - private void RemoveObserver(ObserverHandler observer) - { -#pragma warning disable SA1009 // Closing parenthesis should be spaced correctly - for (; ; ) - { - var a = Volatile.Read(ref _observers); - var n = a.Length; - if (n == 0) - { - break; - } - - var j = Array.IndexOf(a, observer); - - if (j < 0) - { - break; - } - - ObserverHandler[] b; - - if (n == 1) - { - b = Array.Empty(); - } - else - { - b = new ObserverHandler[n - 1]; - Array.Copy(a, 0, b, 0, j); - Array.Copy(a, j + 1, b, j, n - j - 1); - } - - if (Interlocked.CompareExchange(ref _observers, b, a) == a) - { - break; - } - } -#pragma warning restore SA1009 // Closing parenthesis should be spaced correctly - } - - private class ObserverHandler : IDisposable - { - private IObserver? _observer; - private Signal _subject; - - public ObserverHandler(Signal subject, IObserver observer) - { - _subject = subject; - _observer = observer; - } - - public IObserver? Observer => _observer; - - public void Dispose() - { - var observer = Interlocked.Exchange(ref _observer, null); - if (observer == null) - { - return; - } - - _subject.RemoveObserver(this); - _subject = null!; - } - } - } -} diff --git a/src/Minimalist.Reactive/SingleDisposable.cs b/src/Minimalist.Reactive/SingleDisposable.cs deleted file mode 100644 index 722af8b..0000000 --- a/src/Minimalist.Reactive/SingleDisposable.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) 2019-2022 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 Minimalist.Reactive -{ - /// - /// SingleDisposable. - /// - public class SingleDisposable : IsDisposed - { - private readonly IDisposable _disposable; - - /// - /// Initializes a new instance of the class. - /// - /// The disposable. - /// The action to call before disposal. - public SingleDisposable(IDisposable disposable, Action? action = null) => - _disposable = Disposable.Create(() => - { - action?.Invoke(); - disposable.Dispose(); - IsDisposed = true; - }); - - /// - /// Gets a value indicating whether this instance is disposed. - /// - /// - /// true if this instance is disposed; otherwise, false. - /// - public bool IsDisposed { get; private set; } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - /// - /// Releases unmanaged and - optionally - managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected virtual void Dispose(bool disposing) - { - if (!IsDisposed && disposing) - { - _disposable.Dispose(); - } - } - } -} diff --git a/src/Minimalist.Reactive/SubscribeMixins.cs b/src/Minimalist.Reactive/SubscribeMixins.cs index 24497eb..145c189 100644 --- a/src/Minimalist.Reactive/SubscribeMixins.cs +++ b/src/Minimalist.Reactive/SubscribeMixins.cs @@ -1,65 +1,95 @@ -// Copyright (c) 2019-2022 ReactiveUI Association Incorporated. All rights reserved. +// Copyright (c) 2019-2023 ReactiveUI Association Incorporated. All rights reserved. // ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. -using System; using System.Runtime.ExceptionServices; +using Minimalist.Reactive.Core; -namespace Minimalist.Reactive +namespace Minimalist.Reactive; +/// +/// SubscribeMixins. +/// +public static class SubscribeMixins { + private static readonly Action rethrow = e => ExceptionDispatchInfo.Capture(e).Throw(); + private static readonly Action nop = () => { }; + /// - /// SubscribeMixins. + /// Subscribes to the Signals sequence without specifying any handlers. + /// This method can be used to evaluate the Signals sequence for its side-effects only. /// - public static class SubscribeMixins + /// The type of the elements in the source sequence. + /// Signals sequence to subscribe to. + /// object used to unsubscribe from the Signals sequence. + /// is null. + public static IDisposable Subscribe(this IObservable source) { - private static readonly Action rethrow = e => ExceptionDispatchInfo.Capture(e).Throw(); - private static readonly Action nop = () => { }; + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + return Subscribe(source, OnNextNoOp(), nop); + } + + /// + /// Subscribes to the Signals providing just the delegate. + /// + /// The Type. + /// The source. + /// The on next. + /// A IDisposable. + public static IDisposable Subscribe(this IObservable source, Action onNext) + => Subscribe(source, onNext, rethrow, nop); - /// - /// Subscribes to the observable providing just the delegate. - /// - /// The Type. - /// The source. - /// The on next. - /// A IDisposable. - public static IDisposable Subscribe(this IObservable source, Action onNext) - => Subscribe(source, onNext, rethrow, nop); + /// + /// Subscribes to the Signals providing both the and + /// delegates. + /// + /// The Type. + /// The source. + /// The on next. + /// The on error. + /// A IDisposable. + public static IDisposable Subscribe(this IObservable source, Action onNext, Action onError) + => Subscribe(source, onNext, onError, nop); - /// - /// Subscribes to the observable providing both the and - /// delegates. - /// - /// The Type. - /// The source. - /// The on next. - /// The on error. - /// A IDisposable. - public static IDisposable Subscribe(this IObservable source, Action onNext, Action onError) - => Subscribe(source, onNext, onError, nop); + /// + /// Subscribes to the Signals providing both the and + /// delegates. + /// + /// The Type. + /// The source. + /// The on next. + /// The on completed. + /// A IDisposable. + public static IDisposable Subscribe(this IObservable source, Action onNext, Action onCompleted) + => Subscribe(source, onNext, rethrow, onCompleted); - /// - /// Subscribes to the observable providing both the and - /// delegates. - /// - /// The Type. - /// The source. - /// The on next. - /// The on completed. - /// A IDisposable. - public static IDisposable Subscribe(this IObservable source, Action onNext, Action onCompleted) - => Subscribe(source, onNext, rethrow, onCompleted); + /// + /// Subscribes to the Signals providing all three , + /// and delegates. + /// + /// The Type. + /// The source. + /// The on next. + /// The on error. + /// The on completed. + /// A IDisposable. + public static IDisposable Subscribe(this IObservable source, Action onNext, Action onError, Action onCompleted) + => source?.Subscribe(new EmptyWitness(onNext, onError, onCompleted))!; - /// - /// Subscribes to the observable providing all three , - /// and delegates. - /// - /// The Type. - /// The source. - /// The on next. - /// The on error. - /// The on completed. - /// A IDisposable. - public static IDisposable Subscribe(this IObservable source, Action onNext, Action onError, Action onCompleted) - => source?.Subscribe(new EmptyObserver(onNext, onError, onCompleted))!; + /// + /// Rethrows Exception. + /// + /// The exception. + public static void Rethrow(this Exception? exception) + { + if (exception != null) + { + throw exception; + } } + + private static Action OnNextNoOp() => _ => { }; } diff --git a/src/Minimalist.Reactive/WhereSignal{T}.cs b/src/Minimalist.Reactive/WhereSignal{T}.cs deleted file mode 100644 index b0a03af..0000000 --- a/src/Minimalist.Reactive/WhereSignal{T}.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) 2019-2022 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 Minimalist.Reactive -{ - internal class WhereSignal : Signal - { - private Func? _predicate; - private IDisposable? _subscription; - - public WhereSignal(IObservable source, Func predicate) - { - _predicate = predicate; - _subscription = source.Subscribe( - next => - { - if (IsDisposed) - { - return; - } - - if (_predicate(next)) - { - OnNext(next); - } - }, - OnError, - OnCompleted); - } - - protected override void Dispose(bool disposing) - { - if (IsDisposed) - { - return; - } - - Dispose(disposing); - if (disposing) - { - _subscription?.Dispose(); - _subscription = null; - _predicate = null; - } - } - } -} diff --git a/src/stylecop.json b/src/stylecop.json index 038eadf..695f2aa 100644 --- a/src/stylecop.json +++ b/src/stylecop.json @@ -13,7 +13,7 @@ "documentPrivateFields": false, "documentationCulture": "en-US", "companyName": "ReactiveUI Association Incorporated", - "copyrightText": "Copyright (c) 2019-2022 {companyName}. All rights reserved.\n{companyName} licenses this file to you under the {licenseName} license.\nSee the {licenseFile} file in the project root for full license information.", + "copyrightText": "Copyright (c) 2019-2023 {companyName}. All rights reserved.\n{companyName} licenses this file to you under the {licenseName} license.\nSee the {licenseFile} file in the project root for full license information.", "variables": { "licenseName": "MIT", "licenseFile": "LICENSE"