Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Unifiy implementation of State #1586

Merged
merged 7 commits into from
Jul 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPackable>false</IsPackable>
<ImplicitUsings>false</ImplicitUsings>

<!--
Defines that this project output is a tool of the given package ID,
Expand Down
6 changes: 0 additions & 6 deletions src/Uno.Extensions.Reactive.Testing/FeedTestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,6 @@ public FeedTestContext(TestContext testContext)
_subscription = SourceContext.AsCurrent();

testContext?.CancellationTokenSource.Token.Register(Dispose);

// For tests we prefer to replay all vales
FeedSubscription.IsInitialSyncValuesSkippingAllowed = false;
}

/// <summary>
Expand All @@ -49,9 +46,6 @@ public FeedTestContext([CallerMemberName] string? name = null)
_name = name ?? throw new ArgumentNullException("Context must be named.");
SourceContext = SourceContext.GetOrCreate(this);
_subscription = SourceContext.AsCurrent();

// For tests we prefer to replay all vales
FeedSubscription.IsInitialSyncValuesSkippingAllowed = false;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,22 @@ public async Task When_SubscribeTwice_Then_Replay()
async IAsyncEnumerable<int> Source([EnumeratorCancellation] CancellationToken ct = default)
{
yield return 1;

await Task.Yield(); // Make sure to run async, so listener will receive 1 message.

yield return 2;
yield return 3;
}
var src = Feed<int>.AsyncEnumerable(Source);
var sut = new FeedSubscription<int>(src, Context.SourceContext);

var sub1Message = await sut.GetMessages(Context.SourceContext, CT).FirstAsync(CT);

await Task.Delay(10); // Make sure to run async, so listener will receive next messages.

var sub2Message = await sut.GetMessages(Context.SourceContext, CT).FirstAsync(CT);

sub1Message.Current.Data.SomeOrDefault().Should().Be(1, "We do not allow to ignore first values in test (cf. FeedSubscription.IsInitialSyncValuesSkippingAllowed)");
sub1Message.Current.Data.SomeOrDefault().Should().Be(1, "We added a delay before the second value");
sub2Message.Current.Data.SomeOrDefault().Should().Be(3, "the subscription should have stay active");
}

Expand Down
58 changes: 49 additions & 9 deletions src/Uno.Extensions.Reactive.Tests/Core/Given_MessageBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,39 @@ public void When_TransientAxis_Then_Dismissed()
}

[TestMethod]
public async Task When_TransientAxis_Then_Dismissed_OfT()
public async Task When_TransientAxisNoUpdate_Then_NotDismissed_OfT()
{
var myAxis = new MessageAxis<object>("my test axis", _ => new object()) { IsTransient = true };
var manager = new MessageManager<object, object>();
manager.Update(current => current.With().Set(myAxis, new object()), CT);
var original = manager.Current;

// Note: Even if we don't set any axis in Update, the Current message should be updated only by the fact that the transient axis has been automatically removed
MessageBuilder<object, object> builder = default;
// Note: We need to change at least one value for the Current message to be updated (and the transient axis has been automatically removed)
MessageBuilder<object, object>? builder = default;
manager.Update(current => builder = current.With(), CT);
var updated = manager.Current;

original.Current[myAxis].IsSet.Should().BeTrue();
builder.Get(myAxis).value.IsSet.Should().BeFalse("transient axis should have been cleared at beginning to reflect resulting state");
builder!.Get(myAxis).value.IsSet.Should().BeFalse("transient axis should have been cleared at beginning to reflect resulting state");
updated.Current[myAxis].IsSet.Should().BeTrue("transient axis should have not have been cleared has no valid update was performed");
}

[TestMethod]
public async Task When_TransientAxis_Then_Dismissed_OfT()
{
var myAxis = new MessageAxis<object>("my test axis", _ => new object()) { IsTransient = true };
var myAxis2 = new MessageAxis<object>("my test axis 2", _ => new object());
var manager = new MessageManager<object, object>();
manager.Update(current => current.With().Set(myAxis, new object()), CT);
var original = manager.Current;

// Note: We need to change at least one value for the Current message to be updated (and the transient axis has been automatically removed)
MessageBuilder<object, object>? builder = default;
manager.Update(current => builder = current.With().Set(myAxis2, new object()), CT);
var updated = manager.Current;

original.Current[myAxis].IsSet.Should().BeTrue();
builder!.Get(myAxis).value.IsSet.Should().BeFalse("transient axis should have been cleared at beginning to reflect resulting state");
updated.Current[myAxis].IsSet.Should().BeFalse("transient axis should have been cleared on update");
}

Expand All @@ -53,12 +72,12 @@ public async Task When_TransientAxisByParent_Then_NotDismissed_OfT()
var original = manager.Current;

// Note: Even if we don't set any axis in Update, the Current message should be updated only by the fact that the transient axis has been automatically removed
MessageBuilder<object, object> builder = default;
MessageBuilder<object, object>? builder = default;
manager.Update(current => builder = current.With(), CT);
var updated = manager.Current;

original.Current[myAxis].IsSet.Should().BeTrue();
builder.Get(myAxis).value.IsSet.Should().BeTrue("transient axis should have been kept as it was defined on parent");
builder!.Get(myAxis).value.IsSet.Should().BeTrue("transient axis should have been kept as it was defined on parent");
updated.Current[myAxis].IsSet.Should().BeTrue("transient axis should have been kept as it was defined on parent");
}

Expand All @@ -72,17 +91,17 @@ public async Task When_TransientAxisByParentAndLocally_Then_NotDismissed_OfT()
var original = manager.Current;

// Note: Even if we don't set any axis in Update, the Current message should be updated only by the fact that the transient axis has been automatically removed
MessageBuilder<object, object> builder = default;
MessageBuilder<object, object>? builder = default;
manager.Update(current => builder = current.With(), CT);
var updated = manager.Current;

original.Current[myAxis].IsSet.Should().BeTrue();
builder.Get(myAxis).value.IsSet.Should().BeTrue("transient axis should have been kept as it was also defined on parent");
builder!.Get(myAxis).value.IsSet.Should().BeTrue("transient axis should have been kept as it was also defined on parent");
updated.Current[myAxis].IsSet.Should().BeTrue("transient axis should have been kept as it was also defined on parent");
}

[TestMethod]
public async Task When_TransientAxis_Then_Dismissed_TransactionOfT()
public async Task When_TransientAxisNoUpdate_Then_NotDismissed_TransactionOfT()
{
var myAxis = new MessageAxis<object>("my test axis", _ => new object()) { IsTransient = true };
var manager = new MessageManager<object, object>();
Expand All @@ -96,6 +115,27 @@ public async Task When_TransientAxis_Then_Dismissed_TransactionOfT()
transaction.Update(current => builder = current.With());
var updated = manager.Current;

original.Current[myAxis].IsSet.Should().BeTrue();
builder.Get(myAxis).value.IsSet.Should().BeFalse("transient axis should have been cleared at beginning to reflect resulting state");
updated.Current[myAxis].IsSet.Should().BeTrue("transient axis should have not have been cleared has no valid update was performed");
}

[TestMethod]
public async Task When_TransientAxis_Then_Dismissed_TransactionOfT()
{
var myAxis = new MessageAxis<object>("my test axis", _ => new object()) { IsTransient = true };
var myAxis2 = new MessageAxis<object>("my test axis 2", _ => new object());
var manager = new MessageManager<object, object>();
manager.Update(current => current.With().Set(myAxis, new object()), CT);
var original = manager.Current;

// Note: Even if we don't set any axis in Update, the Current message should be updated only by the fact that the transient axis has been automatically removed
// Note 2: No needs to Commit the transaction, manager.Current should already reflect changes
MessageManager<object, object>.UpdateTransaction.MessageBuilder builder = default;
using var transaction = manager.BeginUpdate(CT);
transaction.Update(current => builder = current.With().Set(myAxis2, new object()));
var updated = manager.Current;

original.Current[myAxis].IsSet.Should().BeTrue();
builder.Get(myAxis).value.IsSet.Should().BeFalse("transient axis should have been cleared at beginning to reflect resulting state");
updated.Current[myAxis].IsSet.Should().BeFalse("transient axis should have been cleared on update");
Expand Down
5 changes: 4 additions & 1 deletion src/Uno.Extensions.Reactive.Tests/Core/Given_StateImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ public async Task When_Create_Then_TaskDoNotLeak()
{
var sut = new StateImpl<string>(Context, Option<string>.None());

var next = sut.GetType().GetField("_next", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(sut)!;
var sub = sut.GetType().GetField("_subscription", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(sut)!;
var src = sub.GetType().GetField("_source", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(sub)!;
var node = src.GetType().GetField("_current", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(src)!;
var next = node.GetType().GetField("_next", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(node)!;
var task = (Task)next.GetType().GetProperty("Task")!.GetValue(next)!;

task.CreationOptions
Expand Down
4 changes: 4 additions & 0 deletions src/Uno.Extensions.Reactive.Tests/Given_ListFeed.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ public async Task When_AsyncEnumerable()
{
async IAsyncEnumerable<IImmutableList<int>> GetSource()
{
await Task.Yield(); // Make sure to run async, so listener will receive all messages.

yield return new[] { 40, 41, 42 }.ToImmutableList();
yield return new[] { 41, 42, 43 }.ToImmutableList();
yield return new[] { 42, 43, 44 }.ToImmutableList();
Expand All @@ -64,6 +66,8 @@ public async Task When_Create()
{
async IAsyncEnumerable<Message<IImmutableList<int>>> GetSource([EnumeratorCancellation] CancellationToken ct)
{
await Task.Yield(); // Make sure to run async, so listener will receive all messages.

var msg = Message<IImmutableList<int>>.Initial;

yield return msg = msg.With().Data(new[] { 40, 41, 42 }.ToImmutableList());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ public class Given_CombineFeed : FeedTests
[TestMethod]
public async Task When_Combine2()
{
var feed1 = new StateImpl<int>(Option<int>.Undefined());
var feed2 = new StateImpl<int>(Option<int>.Undefined());
var feed1 = new StateImpl<int>(Context, Option<int>.Undefined());
var feed2 = new StateImpl<int>(Context, Option<int>.Undefined());

var sut = Feed.Combine(feed1, feed2).Record();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ public async Task When_KeyEquatableNoComparerAndUpdate_Then_TrackItemsUsingKeyEq

async IAsyncEnumerable<IImmutableList<MyKeyedRecord>> GetSource([EnumeratorCancellation] CancellationToken ct = default)
{
await Task.Yield(); // Make sure to run async, so listener will receive all messages.

yield return original.ToImmutableList();
yield return updated.ToImmutableList();
}
Expand Down Expand Up @@ -77,6 +79,8 @@ public async Task When_NotKeyEquatableNoComparerAndUpdate_Then_TrackItemsUsingKe

async IAsyncEnumerable<IImmutableList<MyNotKeyedRecord>> GetSource([EnumeratorCancellation] CancellationToken ct = default)
{
await Task.Yield(); // Make sure to run async, so listener will receive all messages.

yield return original.ToImmutableList();
yield return updated.ToImmutableList();
}
Expand Down Expand Up @@ -105,6 +109,8 @@ public async Task When_ClassNoComparerAndUpdate_Then_TrackItemsUsingKeyEquality(

async IAsyncEnumerable<IImmutableList<MyNotKeyedClass>> GetSource([EnumeratorCancellation] CancellationToken ct = default)
{
await Task.Yield(); // Make sure to run async, so listener will receive all messages.

yield return original.ToImmutableList();
yield return updated.ToImmutableList();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ await selection.Should().BeAsync(r => r
}

[TestMethod]
public async Task When_Single_With_InvalidInitial_And_UpdateSutStateInvalid_Then_ListStateUpdated()
public async Task When_Single_With_InvalidInitial_And_UpdateSutStateInvalid_Then_ListStateNotUpdated()
{
var selection = State.Value(this, () => 15).Record();
var list = ListState.Async(this, async _ => Enumerable.Range(0, 10).ToImmutableList());
Expand Down Expand Up @@ -248,7 +248,7 @@ await selection.Should().BeAsync(r => r
}

[TestMethod]
public async Task When_Single_With_ValidInitial_And_UpdateSutStateInvalid_Then_ListStateUpdated()
public async Task When_Single_With_ValidInitial_And_UpdateSutStateInvalid_Then_ListStateNotUpdated()
{
var selection = State.Value(this, () => 1).Record();
var list = ListState.Async(this, async _ => Enumerable.Range(0, 10).ToImmutableList());
Expand Down Expand Up @@ -458,7 +458,7 @@ await selection.Should().BeAsync(r => r
}

[TestMethod]
public async Task When_Multiple_With_InvalidInitial_And_UpdateSutStateInvalid_Then_ListStateUpdated()
public async Task When_Multiple_With_InvalidInitial_And_UpdateSutStateInvalid_Then_ListStateNotUpdated()
{
var selection = State.Value(this, () => ImmutableList.Create(15) as IImmutableList<int>).Record();
var list = ListState.Async(this, async _ => Enumerable.Range(0, 10).ToImmutableList());
Expand Down Expand Up @@ -503,7 +503,7 @@ await selection.Should().BeAsync(r => r
}

[TestMethod]
public async Task When_Multiple_With_ValidInitial_And_UpdateSutStateInvalid_Then_ListStateUpdated()
public async Task When_Multiple_With_ValidInitial_And_UpdateSutStateInvalid_Then_ListStateNotUpdated()
{
var selection = State.Value(this, () => ImmutableList.Create(1) as IImmutableList<int>).Record();
var list = ListState.Async(this, async _ => Enumerable.Range(0, 10).ToImmutableList());
Expand Down Expand Up @@ -722,7 +722,7 @@ await selection.Should().BeAsync(r => r
}

[TestMethod]
public async Task When_ProjectedSingle_With_InvalidInitial_And_UpdateSutStateInvalid_Then_ListStateUpdated()
public async Task When_ProjectedSingle_With_InvalidInitial_And_UpdateSutStateInvalid_Then_ListStateNotUpdated()
{
var selection = State.Value(this, () => new MyAggregateRoot(15)).Record();
var items = Enumerable.Range(0, 10).Select(i => new MyEntity(i)).ToImmutableList();
Expand Down Expand Up @@ -769,7 +769,7 @@ await selection.Should().BeAsync(r => r
}

[TestMethod]
public async Task When_ProjectedSingle_With_ValidInitial_And_UpdateSutStateInvalid_Then_ListStateUpdated()
public async Task When_ProjectedSingle_With_ValidInitial_And_UpdateSutStateInvalid_Then_ListStateNotUpdated()
{
var selection = State.Value(this, () => new MyAggregateRoot(1)).Record();
var items = Enumerable.Range(0, 10).Select(i => new MyEntity(i)).ToImmutableList();
Expand Down
1 change: 1 addition & 0 deletions src/Uno.Extensions.Reactive.UI/common.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<Project>
<PropertyGroup>
<AssemblyName>Uno.Extensions.Reactive.UI</AssemblyName>
<ImplicitUsings>false</ImplicitUsings>
</PropertyGroup>

<PropertyGroup Condition="'$(_IsUWP)'=='true'">
Expand Down
7 changes: 7 additions & 0 deletions src/Uno.Extensions.Reactive/Core/Axes/MessageAxis.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,11 @@ private static bool Equals(MessageAxis left, MessageAxis right)
[Pure]
public static bool operator !=(MessageAxis left, MessageAxis right)
=> !Equals(left, right);

/// <inheritdoc />
[Pure]
public override string ToString()
=> IsTransient
? Identifier + "~"
: Identifier;
}
9 changes: 9 additions & 0 deletions src/Uno.Extensions.Reactive/Core/Axes/MessageAxisValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,13 @@ public void Deconstruct(out bool isSet, out object? value)
isSet = IsSet;
value = Value;
}

/// <inheritdoc />
public override string ToString()
=> (IsSet, Value) switch
{
(false, _) => "--unset--",
(true, null) => "--null--",
(_, var v) => v.ToString()
};
}
29 changes: 13 additions & 16 deletions src/Uno.Extensions.Reactive/Core/Internal/FeedSubscription.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,42 +12,39 @@

namespace Uno.Extensions.Reactive.Core;

internal class FeedSubscription
{
/// <summary>
/// Determines if we allow FeedSubscription to bypass multiple initial sync values (cf. remarks for more details).
/// </summary>
/// <remarks>
/// This is almost only a test case, but if a source feeds enumerates multiple values at startup (i.e. in the GetSource),
/// the <see cref="ReplayOneAsyncEnumerable{T}"/> which backs the <see cref="FeedSubscription{T}"/> might miss some of those values to replay only the last one.
/// </remarks>
public static bool IsInitialSyncValuesSkippingAllowed { get; set; } = true;
}

internal class FeedSubscription<T> : IAsyncDisposable, ISourceContextOwner
{
private readonly ISignal<Message<T>> _feed;
private readonly SourceContext _rootContext;
private readonly CompositeRequestSource _requests = new();
private readonly SourceContext _context;
private readonly ReplayOneAsyncEnumerable<Message<T>> _source;

public FeedSubscription(ISignal<Message<T>> feed, SourceContext rootContext)
{
_feed = feed;
_rootContext = rootContext;
_context = rootContext.CreateChild(this, _requests);
_source = new ReplayOneAsyncEnumerable<Message<T>>(
feed.GetSource(_context),
isInitialSyncValuesSkippingAllowed: FeedSubscription.IsInitialSyncValuesSkippingAllowed);
isInitialSyncValuesSkippingAllowed: true);
}

string ISourceContextOwner.Name => $"Sub on {_source} for ctx '{_context.Parent!.Owner.Name}'.";
string ISourceContextOwner.Name => $"Sub on '{_feed}' for ctx '{_context.Parent!.Owner.Name}'.";

IDispatcher? ISourceContextOwner.Dispatcher => null;

internal Message<T> Current => _source.TryGetCurrent(out var value) ? value : Message<T>.Initial;

public IDisposable UpdateMode(SubscriptionMode mode)
{
// Not supported yet.
// Here we should compute the stricter mode
return Disposable.Empty;
}

public async IAsyncEnumerable<Message<T>> GetMessages(SourceContext subscriberContext, [EnumeratorCancellation] CancellationToken ct)
{
Debug.Assert(subscriberContext.RootId == _context.RootId);
if (subscriberContext != _rootContext)
{
_requests.Add(subscriberContext.RequestSource, ct);
Expand Down Expand Up @@ -83,6 +80,6 @@ public async ValueTask DisposeAsync()
{
await _context.DisposeAsync();
_requests.Dispose();
_source.Dispose();
await _source.DisposeAsync();
}
}
Loading