diff --git a/src/UniTask.NetCoreTests/AsyncReactivePropertyTest.cs b/src/UniTask.NetCoreTests/AsyncReactivePropertyTest.cs index 3350763..d289cb9 100644 --- a/src/UniTask.NetCoreTests/AsyncReactivePropertyTest.cs +++ b/src/UniTask.NetCoreTests/AsyncReactivePropertyTest.cs @@ -51,6 +51,67 @@ namespace NetCoreTests ar.Should().BeEquivalentTo(new[] { 100, 100, 100, 131, 191 }); } + + [Fact] + public async Task StateIteration() + { + var rp = new State(99); + var setter = rp.GetSetter(); + + var f = await rp.FirstAsync(); + f.Should().Be(99); + + var array = rp.Take(5).ToArrayAsync(); + + setter(100); + setter(100); + setter(100); + setter(131); + + var ar = await array; + + ar.Should().BeEquivalentTo(new[] { 99, 100, 100, 100, 131 }); + } + + [Fact] + public async Task StateWithoutCurrent() + { + var rp = new State(99); + var setter = rp.GetSetter(); + + var array = rp.WithoutCurrent().Take(5).ToArrayAsync(); + setter(100); + setter(100); + setter(100); + setter(131); + setter(191); + + var ar = await array; + + ar.Should().BeEquivalentTo(new[] { 100, 100, 100, 131, 191 }); + } + + + + [Fact] + public void StateFromEnumeration() + { + var rp = new AsyncReactiveProperty(10); + + var state = rp.ToState(CancellationToken.None); + + rp.Value = 10; + state.Value.Should().Be(10); + + rp.Value = 20; + state.Value.Should().Be(20); + + state.Dispose(); + + rp.Value = 30; + state.Value.Should().Be(20); + } + } diff --git a/src/UniTask/Assets/Plugins/UniTask/Runtime/AsyncReactiveProperty.cs b/src/UniTask/Assets/Plugins/UniTask/Runtime/AsyncReactiveProperty.cs index f065760..6d3587d 100644 --- a/src/UniTask/Assets/Plugins/UniTask/Runtime/AsyncReactiveProperty.cs +++ b/src/UniTask/Assets/Plugins/UniTask/Runtime/AsyncReactiveProperty.cs @@ -59,6 +59,24 @@ namespace Cysharp.Threading.Tasks triggerEvent.SetCompleted(); } + public static implicit operator T(AsyncReactiveProperty value) + { + return value.Value; + } + + public override string ToString() + { + if (isValueType) return latestValue.ToString(); + return latestValue?.ToString(); + } + + static bool isValueType; + + static AsyncReactiveProperty() + { + isValueType = typeof(T).IsValueType; + } + class WithoutCurrentEnumerable : IUniTaskAsyncEnumerable { readonly AsyncReactiveProperty parent; @@ -156,4 +174,231 @@ namespace Cysharp.Threading.Tasks } } } -} + + public class State : IReadOnlyAsyncReactiveProperty, IDisposable + { + TriggerEvent triggerEvent; + + T latestValue; + + Action setter; + IUniTaskAsyncEnumerator enumerator; + + public T Value + { + get + { + return latestValue; + } + } + + public State(T value) + { + this.latestValue = value; + this.triggerEvent = default; + } + + public State(T initialValue, IUniTaskAsyncEnumerable source, CancellationToken cancellationToken) + { + latestValue = initialValue; + ConsumeEnumerator(source, cancellationToken).Forget(); + } + + public State(IUniTaskAsyncEnumerable source, CancellationToken cancellationToken) + { + ConsumeEnumerator(source, cancellationToken).Forget(); + } + + async UniTaskVoid ConsumeEnumerator(IUniTaskAsyncEnumerable source, CancellationToken cancellationToken) + { + enumerator = source.GetAsyncEnumerator(cancellationToken); + try + { + while (await enumerator.MoveNextAsync()) + { + SetValue(enumerator.Current); + } + } + finally + { + await enumerator.DisposeAsync(); + enumerator = null; + } + } + + public Action GetSetter() + { + if (enumerator != null) + { + throw new InvalidOperationException("Can not get setter when create from IUniTaskAsyncEnumerable source."); + } + + if (setter != null) + { + throw new InvalidOperationException("GetSetter can only call once."); + } + + setter = SetValue; + return setter; + } + + void SetValue(T value) + { + this.latestValue = value; + triggerEvent.SetResult(value); + } + + public IUniTaskAsyncEnumerable WithoutCurrent() + { + return new WithoutCurrentEnumerable(this); + } + + public IUniTaskAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken) + { + return new Enumerator(this, cancellationToken, true); + } + + public void Dispose() + { + if (enumerator != null) + { + enumerator.DisposeAsync().Forget(); + } + + triggerEvent.SetCompleted(); + } + + public static implicit operator State(T value) + { + return new State(value); + } + + public static implicit operator T(State value) + { + return value.Value; + } + + public override string ToString() + { + if (isValueType) return latestValue.ToString(); + return latestValue?.ToString(); + } + + static bool isValueType; + + static State() + { + isValueType = typeof(T).IsValueType; + } + + class WithoutCurrentEnumerable : IUniTaskAsyncEnumerable + { + readonly State parent; + + public WithoutCurrentEnumerable(State parent) + { + this.parent = parent; + } + + public IUniTaskAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + { + return new Enumerator(parent, cancellationToken, false); + } + } + + sealed class Enumerator : MoveNextSource, IUniTaskAsyncEnumerator, ITriggerHandler + { + static Action cancellationCallback = CancellationCallback; + + readonly State parent; + readonly CancellationToken cancellationToken; + readonly CancellationTokenRegistration cancellationTokenRegistration; + T value; + bool isDisposed; + bool firstCall; + + public Enumerator(State parent, CancellationToken cancellationToken, bool publishCurrentValue) + { + this.parent = parent; + this.cancellationToken = cancellationToken; + this.firstCall = publishCurrentValue; + + parent.triggerEvent.Add(this); + TaskTracker.TrackActiveTask(this, 3); + + if (cancellationToken.CanBeCanceled) + { + cancellationTokenRegistration = cancellationToken.RegisterWithoutCaptureExecutionContext(cancellationCallback, this); + } + } + + public T Current => value; + + public UniTask MoveNextAsync() + { + // raise latest value on first call. + if (firstCall) + { + firstCall = false; + value = parent.Value; + return CompletedTasks.True; + } + + completionSource.Reset(); + return new UniTask(this, completionSource.Version); + } + + public UniTask DisposeAsync() + { + if (!isDisposed) + { + isDisposed = true; + TaskTracker.RemoveTracking(this); + completionSource.TrySetCanceled(cancellationToken); + parent.triggerEvent.Remove(this); + } + return default; + } + + public void OnNext(T value) + { + this.value = value; + completionSource.TrySetResult(true); + } + + public void OnCanceled(CancellationToken cancellationToken) + { + DisposeAsync().Forget(); + } + + public void OnCompleted() + { + completionSource.TrySetResult(false); + } + + public void OnError(Exception ex) + { + completionSource.TrySetException(ex); + } + + static void CancellationCallback(object state) + { + var self = (Enumerator)state; + self.DisposeAsync().Forget(); + } + } + } + + public static class StateExtensions + { + public static State ToState(this IUniTaskAsyncEnumerable source, CancellationToken cancellationToken) + { + return new State(source, cancellationToken); + } + + public static State ToState(this IUniTaskAsyncEnumerable source, T initialValue, CancellationToken cancellationToken) + { + return new State(initialValue, source, cancellationToken); + } + } +} \ No newline at end of file