Skip to content

Commit

Permalink
Merge pull request #56 from jinaga/graph-notification
Browse files Browse the repository at this point in the history
Correct bugs related to notifying a new graph
  • Loading branch information
michaellperry authored Nov 11, 2023
2 parents c8f8ec2 + 1100813 commit df01ee9
Show file tree
Hide file tree
Showing 28 changed files with 1,391 additions and 335 deletions.
553 changes: 553 additions & 0 deletions Documentation/Invalidation.ipynb

Large diffs are not rendered by default.

113 changes: 113 additions & 0 deletions Jinaga.Store.SQLite.Test/Fakes/FakeNetwork.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using Jinaga.Facts;
using Jinaga.Projections;
using Jinaga.Serialization;
using Jinaga.Services;
using System.Collections.Immutable;
using Xunit.Abstractions;

namespace Jinaga.Store.SQLite.Test.Fakes;
internal class FakeFeed
{
public string Name { get; set; }
public Fact[] Facts { get; set; }
public int Delay { get; set; }
}
internal class FakeNetwork : INetwork
{
private SerializerCache serializerCache = SerializerCache.Empty;
private ITestOutputHelper output;
private readonly List<FakeFeed> feeds = new();
private readonly Dictionary<FactReference, Fact> factByFactReference = new();

public FakeNetwork(ITestOutputHelper output)
{
this.output = output;
}

public void AddFeed(string name, object[] facts, int delay = 0)
{
var collector = new Collector(serializerCache);
foreach (var fact in facts)
{
collector.Serialize(fact);
}
serializerCache = collector.SerializerCache;
var graph = collector.Graph;
var serializedFacts = graph.FactReferences
.Select(graph.GetFact)
.ToArray();
foreach (var fact in serializedFacts)
{
if (!factByFactReference.ContainsKey(fact.Reference))
{
factByFactReference.Add(fact.Reference, fact);
}
}
feeds.Add(new FakeFeed
{
Name = name,
Facts = serializedFacts,
Delay = delay
});
}

public Task<(FactGraph graph, UserProfile profile)> Login(CancellationToken cancellationToken)
{
throw new NotImplementedException();
}

public Task<ImmutableList<string>> Feeds(FactReferenceTuple givenTuple, Specification specification, CancellationToken cancellationToken)
{
return Task.FromResult(feeds.Select(feed => feed.Name).ToImmutableList());
}

public async Task<(ImmutableList<FactReference> references, string bookmark)> FetchFeed(string feed, string bookmark, CancellationToken cancellationToken)
{
if (bookmark == "done")
{
return (ImmutableList<FactReference>.Empty, "done");
}
var fakeFeed = feeds.Single(f => f.Name == feed);
var references = fakeFeed.Facts
.Select(fact => fact.Reference)
.ToImmutableList();
if (fakeFeed.Delay > 0)
{
await Task.Delay(fakeFeed.Delay);
}
return (references, "done");
}

public Task<FactGraph> Load(ImmutableList<FactReference> factReferences, CancellationToken cancellationToken)
{
string references = string.Join(",\n", factReferences.Select(r => $" {r}"));
output.WriteLine($"Load {factReferences.Count} facts:\n{references}");
var graph = FactGraph.Empty;

foreach (var factReference in factReferences)
{
graph = AddFact(graph, factReference);
}

return Task.FromResult(graph);
}

private FactGraph AddFact(FactGraph graph, FactReference factReference)
{
var fact = factByFactReference[factReference];
foreach (var predecessor in fact.Predecessors)
{
foreach (var predecessorReference in predecessor.AllReferences)
{
graph = AddFact(graph, predecessorReference);
}
}
graph = graph.Add(fact);
return graph;
}

public Task Save(ImmutableList<Fact> facts, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}
55 changes: 55 additions & 0 deletions Jinaga.Store.SQLite.Test/Model/Corporate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
namespace Jinaga.Store.SQLite.Test.Model;


[FactType("Corporate.Company")]
record Company(string identifier);

[FactType("Corporate.Office")]
record Office(Company company, City city)
{
public Condition IsClosed => new Condition(facts =>
facts.Any<OfficeClosure>(closure => closure.office == this)
);
}

[FactType("Corporate.Office.Name")]
record OfficeName(Office office, string value, OfficeName[] prior);


[FactType("Corporate.Office.Closure")]
record OfficeClosure(Office office, DateTime closureDate);

[FactType("Corporate.Office.Reopening")]
record OfficeReopening(OfficeClosure officeClosure, DateTime reopeningDate);

[FactType("Corporate.City")]
record City(string name);

[FactType("Corporate.Headcount")]
record Headcount(Office office, int value, Headcount[] prior)
{
public Condition IsCurrent => new Condition(facts => !(
from next in facts.OfType<Headcount>()
where next.prior.Contains(this)
select next
).Any());
}

[FactType("Corporate.Manager")]
record Manager(Office office, int employeeNumber)
{
public Condition IsTerminated => new Condition(facts =>
facts.OfType<ManagerTerminated>(termination => termination.Manager == this)
.Any());
}

[FactType("Corporate.Manager.Name")]
record ManagerName(Manager manager, string value, ManagerName[] prior)
{
public Condition IsCurrent => new Condition(facts => !
facts.OfType<ManagerName>(next => next.prior.Contains(this))
.Any());
}

[FactType("Corporate.Manager.Terminated")]
record ManagerTerminated(Manager Manager, DateTime terminationDate);
201 changes: 201 additions & 0 deletions Jinaga.Store.SQLite.Test/Observers/WatchFromNetworkTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
using FluentAssertions;
using Jinaga.Storage;
using Jinaga.Store.SQLite.Test.Fakes;
using Jinaga.Store.SQLite.Test.Model;
using Xunit.Abstractions;

namespace Jinaga.Store.SQLite.Test.Observers;
public class WatchFromNetworkTest
{
private static string SQLitePath { get; } = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"JinagaSQLiteTest",
"WatchFromNetworkTest.db");

private ITestOutputHelper output;

public WatchFromNetworkTest(ITestOutputHelper output)
{
if (File.Exists(SQLitePath))
File.Delete(SQLitePath);

this.output = output;
}

[Fact]
public async Task Watch_EmptyUpstream()
{
var j = GivenJinagaClient(new FakeNetwork(output));

var viewModel = new CompanyViewModel();
var watch = viewModel.Load(j, "contoso");

try
{
await watch.Loaded;
viewModel.Offices.Should().BeEmpty();
}
finally
{
watch.Stop();
}
}

[Fact]
public async Task Watch_SingleUnnamedOffice()
{
var network = new FakeNetwork(output);
var contoso = new Company("contoso");
var dallas = new City("Dallas");
var dallasOffice = new Office(contoso, dallas);
network.AddFeed("offices", new object[]
{
dallasOffice
});

var j = GivenJinagaClient(network);

var viewModel = new CompanyViewModel();
var watch = viewModel.Load(j, "contoso");

try
{
await watch.Loaded;
viewModel.Offices.Should().ContainSingle().Which
.Name.Should().BeNull();
}
finally
{
watch.Stop();
}
}

[Fact]
public async Task Watch_SingleNamedOffice()
{
var network = new FakeNetwork(output);
var contoso = new Company("contoso");
var dallas = new City("Dallas");
var dallasOffice = new Office(contoso, dallas);
var dallasOfficeName = new OfficeName(dallasOffice, "Dallas", new OfficeName[0]);
network.AddFeed("offices", new object[]
{
dallasOffice
}, 1);
network.AddFeed("officeNames", new object[]
{
dallasOfficeName
}, 1);

var j = GivenJinagaClient(network);

var viewModel = new CompanyViewModel();
var watch = viewModel.Load(j, "contoso");

try
{
await watch.Loaded;
viewModel.Offices.Should().ContainSingle().Which
.Name.Should().Be("Dallas");
}
finally
{
watch.Stop();
}
}

[Fact]
public async Task Watch_SingleOfficeThreeNames()
{
var network = new FakeNetwork(output);
var contoso = new Company("contoso");
var dallas = new City("Dallas");
var dallasOffice = new Office(contoso, dallas);
var dallasOfficeName1 = new OfficeName(dallasOffice, "Dallas One", new OfficeName[0]);
var dallasOfficeName2 = new OfficeName(dallasOffice, "Dallas Two", new OfficeName[] { dallasOfficeName1 });
var dallasOfficeName3 = new OfficeName(dallasOffice, "Dallas Three", new OfficeName[] { dallasOfficeName2 });
network.AddFeed("offices", new object[]
{
dallasOffice
}, 1);
network.AddFeed("officeNames", new object[]
{
dallasOfficeName1,
dallasOfficeName2,
dallasOfficeName3
}, 1);

var j = GivenJinagaClient(network);

var viewModel = new CompanyViewModel();
var watch = viewModel.Load(j, "contoso");

try
{
await watch.Loaded;
viewModel.Offices.Should().ContainSingle().Which
.Name.Should().Be("Dallas Three");
}
finally
{
watch.Stop();
}
}

private static JinagaClient GivenJinagaClient(FakeNetwork network)
{
return new JinagaClient(new SQLiteStore(SQLitePath), network);
}

private class OfficeViewModel
{
public Office Office;
public string Name;
}

private class CompanyViewModel
{
public List<OfficeViewModel> Offices = new();

public IObserver Load(JinagaClient j, string identifier)
{
var officesInCompany = Given<Company>.Match((company, facts) =>
from office in facts.OfType<Office>()
where office.company == company
where !office.IsClosed

select new
{
office,
names = facts.Observable(
from name in facts.OfType<OfficeName>()
where name.office == office &&
!facts.Any<OfficeName>(next => next.prior.Contains(name))
select name.value
)
}
);

var company = new Company(identifier);
var watch = j.Watch(officesInCompany, company, projection =>
{
var officeViewModel = new OfficeViewModel
{
Office = projection.office
};
Offices.Add(officeViewModel);

projection.names.OnAdded(name =>
{
officeViewModel.Name = name;
});

return () =>
{
Offices.Remove(officeViewModel);
};
});
return watch;
}
}
}
Loading

0 comments on commit df01ee9

Please sign in to comment.