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.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;
|
||||
}
|
||||
|
||||
// TODO caching
|
||||
IsDirty = false;
|
||||
_result = await _combine();
|
||||
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);
|
||||
}
|
||||
|
||||
public static SignalBase<TResult> Map<T, TResult>(this SignalBase<T> signal, Func<T, Task<TResult>> 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 SignalBase<T> _parentSignal;
|
||||
private bool _isInitialized;
|
||||
private T? _lastParentValue;
|
||||
private TResult? _lastResult;
|
||||
|
||||
@@ -43,12 +44,17 @@ public sealed class MapSignal<T, TResult> : SignalBase<TResult>
|
||||
IsDirty = false;
|
||||
var baseValue = await _parentSignal.GetValueAsync();
|
||||
if (
|
||||
(_lastParentValue == null && baseValue == null) ||
|
||||
(baseValue != null && baseValue.Equals(_lastParentValue)))
|
||||
_isInitialized
|
||||
&&
|
||||
(
|
||||
(baseValue == null && _lastParentValue == null)
|
||||
|| (baseValue != null && baseValue.Equals(_lastParentValue))
|
||||
))
|
||||
{
|
||||
return _lastResult!;
|
||||
}
|
||||
|
||||
_isInitialized = true;
|
||||
_lastParentValue = baseValue;
|
||||
_lastResult = await _map(baseValue);
|
||||
return _lastResult;
|
||||
|
||||
@@ -6,7 +6,7 @@ public abstract class SignalBase : IReadOnlySignal
|
||||
|
||||
public event Action<bool>? IsDirtyChanged;
|
||||
|
||||
public bool IsDirty
|
||||
public virtual bool IsDirty
|
||||
{
|
||||
get => _isDirty;
|
||||
protected set
|
||||
@@ -20,6 +20,7 @@ public abstract class SignalBase : IReadOnlySignal
|
||||
IsDirtyChanged?.Invoke(value);
|
||||
}
|
||||
}
|
||||
|
||||
public event Action<SignalBase> Disposed;
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
@@ -41,16 +42,17 @@ public abstract class SignalBase : IReadOnlySignal
|
||||
public abstract class SignalBase<T> : SignalBase, IReadOnlySignal<T>
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
protected SignalBase(ICollection<SignalBase> parentSignals):base(CreateMultiParentTreeLock(parentSignals))
|
||||
protected SignalBase(ICollection<SignalBase> parentSignals) : base(CreateMultiParentTreeLock(parentSignals))
|
||||
{
|
||||
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;
|
||||
foreach (var parentSignal in parentSignals.Skip(1))
|
||||
|
||||
Reference in New Issue
Block a user