Signal WIP
This commit is contained in:
22
src/Library/Signal.Tests/CombineAllTests.cs
Normal file
22
src/Library/Signal.Tests/CombineAllTests.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Signal.Tests;
|
||||||
|
|
||||||
|
public class CombineAllTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task GetValueAsync_WhenParentSometimesReturnsNull_ShouldInitialize()
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
// Arrange
|
||||||
|
var signal1 = new Signal<string>(null);
|
||||||
|
var signal2 = new Signal<string>(null);
|
||||||
|
var combineAll = new CombineAllSignal<string, string>([signal1, signal2], _ => "TEST");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await combineAll.GetValueAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("TEST", result);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/Library/Signal.Tests/DebounceTests.cs
Normal file
33
src/Library/Signal.Tests/DebounceTests.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Signal.Tests;
|
||||||
|
|
||||||
|
public class DebounceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task Debounce_WhenCalled_ReturnsDebouncedValue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var signal = new Signal<int>(1);
|
||||||
|
var debouncedSignal = signal.Debounce(TimeSpan.FromMilliseconds(100));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result1 = await debouncedSignal.GetValueAsync();
|
||||||
|
await signal.SetValueAsync(2);
|
||||||
|
var result2 = await debouncedSignal.GetValueAsync();
|
||||||
|
await signal.SetValueAsync(3);
|
||||||
|
var result3_1 = await debouncedSignal.GetValueAsync();
|
||||||
|
|
||||||
|
await Task.Delay(300);
|
||||||
|
var result3_2 = await debouncedSignal.GetValueAsync();
|
||||||
|
await signal.SetValueAsync(4);
|
||||||
|
var result4 = await debouncedSignal.GetValueAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(1, result1);
|
||||||
|
Assert.Equal(1, result2);
|
||||||
|
Assert.Equal(1, result3_1);
|
||||||
|
Assert.Equal(3, result3_2);
|
||||||
|
Assert.Equal(3, result4);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -76,4 +76,19 @@ public class MapSignalTests
|
|||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("TEST", result);
|
Assert.Equal("TEST", result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetValueAsync_WhenParentSometimesReturnsNull_ShouldInitialize()
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
// Arrange
|
||||||
|
var signal = new Signal<string>(null);
|
||||||
|
var mapped = signal.Map(_ => "TEST");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await mapped.GetValueAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("TEST", result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
69
src/Library/Signal/CombineAllSignal.cs
Normal file
69
src/Library/Signal/CombineAllSignal.cs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
namespace Signal;
|
||||||
|
|
||||||
|
public class CombineAllSignal<T, TResult> : SignalBase<TResult>
|
||||||
|
{
|
||||||
|
private readonly IReadOnlyList<SignalBase<T>> _parentSignals;
|
||||||
|
private readonly Func<ICollection<T>, ValueTask<TResult>> _combiner;
|
||||||
|
private readonly T[] _lastParentResults;
|
||||||
|
private readonly T[] _currentParentResults;
|
||||||
|
private TResult? _lastResult;
|
||||||
|
private bool _isInitialized;
|
||||||
|
|
||||||
|
public CombineAllSignal(ICollection<SignalBase<T>> parentSignals, Func<ICollection<T>, TResult> combiner) :
|
||||||
|
base(parentSignals)
|
||||||
|
{
|
||||||
|
_parentSignals = parentSignals.ToList();
|
||||||
|
_combiner = c => new ValueTask<TResult>(combiner(c));
|
||||||
|
|
||||||
|
_lastParentResults = new T[_parentSignals.Count];
|
||||||
|
_currentParentResults = new T[_parentSignals.Count];
|
||||||
|
}
|
||||||
|
|
||||||
|
public CombineAllSignal(ICollection<SignalBase<T>> parentSignals, Func<ICollection<T>, ValueTask<TResult>> combiner) :
|
||||||
|
base(parentSignals)
|
||||||
|
{
|
||||||
|
_parentSignals = parentSignals.ToList();
|
||||||
|
_combiner = combiner;
|
||||||
|
|
||||||
|
_lastParentResults = new T[_parentSignals.Count];
|
||||||
|
_currentParentResults = new T[_parentSignals.Count];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async ValueTask<TResult> GetValueInternalAsync()
|
||||||
|
{
|
||||||
|
if (!IsDirty)
|
||||||
|
{
|
||||||
|
return _lastResult!;
|
||||||
|
}
|
||||||
|
|
||||||
|
IsDirty = false;
|
||||||
|
|
||||||
|
if (_isInitialized)
|
||||||
|
{
|
||||||
|
bool anyChanged = false;
|
||||||
|
for (var i = 0; i < _parentSignals.Count; i++)
|
||||||
|
{
|
||||||
|
var parentSignal = _parentSignals[i];
|
||||||
|
var newResult = await parentSignal.GetValueAsync();
|
||||||
|
_currentParentResults[i] = newResult;
|
||||||
|
|
||||||
|
if ((newResult == null && _lastParentResults[i] != null)
|
||||||
|
|| (newResult != null && newResult.Equals(_lastParentResults[i])))
|
||||||
|
{
|
||||||
|
anyChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!anyChanged)
|
||||||
|
{
|
||||||
|
return _lastResult!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_isInitialized = true;
|
||||||
|
Array.Copy(_currentParentResults, _lastParentResults, _currentParentResults.Length);
|
||||||
|
var result = await _combiner(_currentParentResults);
|
||||||
|
_lastResult = result;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,7 @@ public sealed class CombineLatestSignal<T1, T2, TResult> : SignalBase<TResult>
|
|||||||
return _result;
|
return _result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO caching
|
||||||
IsDirty = false;
|
IsDirty = false;
|
||||||
_result = await _combine();
|
_result = await _combine();
|
||||||
return _result;
|
return _result;
|
||||||
|
|||||||
83
src/Library/Signal/DebounceSignal.cs
Normal file
83
src/Library/Signal/DebounceSignal.cs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
namespace Signal;
|
||||||
|
|
||||||
|
public class DebounceSignal<T> : SignalBase<T>
|
||||||
|
{
|
||||||
|
private readonly object _debounceTaskLock = new();
|
||||||
|
private readonly SignalBase<T> _parentSignal;
|
||||||
|
private bool _isDebounceRunning;
|
||||||
|
private DateTime _debounceStartedAt;
|
||||||
|
private readonly Func<TimeSpan> _interval;
|
||||||
|
private T? _lastParentValue;
|
||||||
|
|
||||||
|
public bool ResetTimer { get; init; }
|
||||||
|
public TimeSpan WaitInterval { get; init; } = TimeSpan.FromMilliseconds(10);
|
||||||
|
|
||||||
|
public override bool IsDirty
|
||||||
|
{
|
||||||
|
get => base.IsDirty;
|
||||||
|
protected set
|
||||||
|
{
|
||||||
|
if(!value)
|
||||||
|
{
|
||||||
|
base.IsDirty = value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_debounceTaskLock)
|
||||||
|
{
|
||||||
|
if (_isDebounceRunning)
|
||||||
|
{
|
||||||
|
if (ResetTimer)
|
||||||
|
{
|
||||||
|
_debounceStartedAt = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_isDebounceRunning = true;
|
||||||
|
_debounceStartedAt = DateTime.Now;
|
||||||
|
Task.Run(StartDebouncing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task StartDebouncing()
|
||||||
|
{
|
||||||
|
while (DateTime.Now - _debounceStartedAt < _interval())
|
||||||
|
{
|
||||||
|
await Task.Delay(WaitInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
base.IsDirty = true;
|
||||||
|
lock (_debounceTaskLock)
|
||||||
|
{
|
||||||
|
_isDebounceRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DebounceSignal(SignalBase<T> parentSignal, Func<TimeSpan> interval) : base(parentSignal)
|
||||||
|
{
|
||||||
|
_parentSignal = parentSignal;
|
||||||
|
_interval = interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async ValueTask<T> GetValueInternalAsync()
|
||||||
|
{
|
||||||
|
if (!IsDirty)
|
||||||
|
{
|
||||||
|
return _lastParentValue!;
|
||||||
|
}
|
||||||
|
|
||||||
|
IsDirty = false;
|
||||||
|
var baseValue = await _parentSignal.GetValueAsync();
|
||||||
|
if (
|
||||||
|
(_lastParentValue == null && baseValue == null) ||
|
||||||
|
(baseValue != null && baseValue.Equals(_lastParentValue)))
|
||||||
|
{
|
||||||
|
return _lastParentValue!;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastParentValue = baseValue;
|
||||||
|
return _lastParentValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,8 +6,19 @@ public static class Extensions
|
|||||||
{
|
{
|
||||||
return new MapSignal<T, TResult>(signal, map);
|
return new MapSignal<T, TResult>(signal, map);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SignalBase<TResult> Map<T, TResult>(this SignalBase<T> signal, Func<T, Task<TResult>> map)
|
public static SignalBase<TResult> Map<T, TResult>(this SignalBase<T> signal, Func<T, Task<TResult>> map)
|
||||||
{
|
{
|
||||||
return new MapSignal<T, TResult>(signal, map);
|
return new MapSignal<T, TResult>(signal, map);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static SignalBase<T> Debounce<T>(this SignalBase<T> signal, TimeSpan interval)
|
||||||
|
{
|
||||||
|
return new DebounceSignal<T>(signal, () => interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SignalBase<T> Debounce<T>(this SignalBase<T> signal, Func<TimeSpan> interval)
|
||||||
|
{
|
||||||
|
return new DebounceSignal<T>(signal, interval);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ public sealed class MapSignal<T, TResult> : SignalBase<TResult>
|
|||||||
{
|
{
|
||||||
private readonly Func<T, ValueTask<TResult>> _map;
|
private readonly Func<T, ValueTask<TResult>> _map;
|
||||||
private readonly SignalBase<T> _parentSignal;
|
private readonly SignalBase<T> _parentSignal;
|
||||||
|
private bool _isInitialized;
|
||||||
private T? _lastParentValue;
|
private T? _lastParentValue;
|
||||||
private TResult? _lastResult;
|
private TResult? _lastResult;
|
||||||
|
|
||||||
@@ -43,12 +44,17 @@ public sealed class MapSignal<T, TResult> : SignalBase<TResult>
|
|||||||
IsDirty = false;
|
IsDirty = false;
|
||||||
var baseValue = await _parentSignal.GetValueAsync();
|
var baseValue = await _parentSignal.GetValueAsync();
|
||||||
if (
|
if (
|
||||||
(_lastParentValue == null && baseValue == null) ||
|
_isInitialized
|
||||||
(baseValue != null && baseValue.Equals(_lastParentValue)))
|
&&
|
||||||
|
(
|
||||||
|
(baseValue == null && _lastParentValue == null)
|
||||||
|
|| (baseValue != null && baseValue.Equals(_lastParentValue))
|
||||||
|
))
|
||||||
{
|
{
|
||||||
return _lastResult!;
|
return _lastResult!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_isInitialized = true;
|
||||||
_lastParentValue = baseValue;
|
_lastParentValue = baseValue;
|
||||||
_lastResult = await _map(baseValue);
|
_lastResult = await _map(baseValue);
|
||||||
return _lastResult;
|
return _lastResult;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ public abstract class SignalBase : IReadOnlySignal
|
|||||||
|
|
||||||
public event Action<bool>? IsDirtyChanged;
|
public event Action<bool>? IsDirtyChanged;
|
||||||
|
|
||||||
public bool IsDirty
|
public virtual bool IsDirty
|
||||||
{
|
{
|
||||||
get => _isDirty;
|
get => _isDirty;
|
||||||
protected set
|
protected set
|
||||||
@@ -20,6 +20,7 @@ public abstract class SignalBase : IReadOnlySignal
|
|||||||
IsDirtyChanged?.Invoke(value);
|
IsDirtyChanged?.Invoke(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public event Action<SignalBase> Disposed;
|
public event Action<SignalBase> Disposed;
|
||||||
public bool IsDisposed { get; private set; }
|
public bool IsDisposed { get; private set; }
|
||||||
|
|
||||||
@@ -41,16 +42,17 @@ public abstract class SignalBase : IReadOnlySignal
|
|||||||
public abstract class SignalBase<T> : SignalBase, IReadOnlySignal<T>
|
public abstract class SignalBase<T> : SignalBase, IReadOnlySignal<T>
|
||||||
{
|
{
|
||||||
internal static AsyncLocal<TreeLocker> CurrentTreeLocker { get; } = new();
|
internal static AsyncLocal<TreeLocker> CurrentTreeLocker { get; } = new();
|
||||||
private protected SignalBase():base(new TreeLocker())
|
|
||||||
|
private protected SignalBase() : base(new TreeLocker())
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected SignalBase(SignalBase parentSignal):base(parentSignal.TreeLock)
|
protected SignalBase(SignalBase parentSignal) : base(parentSignal.TreeLock)
|
||||||
{
|
{
|
||||||
SubscribeToParentSignalChanges(parentSignal);
|
SubscribeToParentSignalChanges(parentSignal);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected SignalBase(ICollection<SignalBase> parentSignals):base(CreateMultiParentTreeLock(parentSignals))
|
protected SignalBase(ICollection<SignalBase> parentSignals) : base(CreateMultiParentTreeLock(parentSignals))
|
||||||
{
|
{
|
||||||
ArgumentOutOfRangeException.ThrowIfZero(parentSignals.Count);
|
ArgumentOutOfRangeException.ThrowIfZero(parentSignals.Count);
|
||||||
|
|
||||||
@@ -60,7 +62,20 @@ public abstract class SignalBase<T> : SignalBase, IReadOnlySignal<T>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TreeLocker CreateMultiParentTreeLock(ICollection<SignalBase> parentSignals)
|
protected SignalBase(IEnumerable<SignalBase> parentSignals) : base(CreateMultiParentTreeLock(parentSignals))
|
||||||
|
{
|
||||||
|
if (!parentSignals.Any())
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(parentSignals));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var parentSignal in parentSignals)
|
||||||
|
{
|
||||||
|
SubscribeToParentSignalChanges(parentSignal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TreeLocker CreateMultiParentTreeLock(IEnumerable<SignalBase> parentSignals)
|
||||||
{
|
{
|
||||||
var firstLock = parentSignals.First().TreeLock;
|
var firstLock = parentSignals.First().TreeLock;
|
||||||
foreach (var parentSignal in parentSignals.Skip(1))
|
foreach (var parentSignal in parentSignals.Skip(1))
|
||||||
|
|||||||
Reference in New Issue
Block a user