Skip to content

Commit 6435b5e

Browse files
authored
Merge pull request #37 from WindowsAppCommunity/arlo/esh/virtual/links
Implement Links virtual event stream handler
2 parents 9d5af5d + 2fa292b commit 6435b5e

14 files changed

+299
-23
lines changed

src/Link.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
1+
using OwlCore.Storage;
2+
13
namespace WindowsAppCommunity.Sdk;
24

35
/// <summary>
46
/// Represents the data for a link.
57
/// </summary>
6-
public class Link
8+
public class Link : IStorable
79
{
810
/// <summary>
9-
/// The uri this link points to.
11+
/// A unique identifier for this Link that is persistent across runs and property updates.
12+
/// </summary>
13+
public required string Id { get; init; }
14+
15+
/// <summary>
16+
/// The external url this link points to.
1017
/// </summary>
11-
public required string Uri { get; set; }
18+
public required string Url { get; set; }
1219

1320
/// <summary>
1421
/// A display name for this link.

src/Models/Project.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace WindowsAppCommunity.Sdk.Models;
77
/// <summary>
88
/// Represents a project.
99
/// </summary>
10-
public record Project : IEntity, IUserRoleCollection, IAccentColor, IProjectCollection, ISources<Cid>
10+
public record Project : IEntity, IUserRoleCollection, IAccentColor, IProjectCollection, ILinkCollection, ISources<Cid>
1111
{
1212
/// <summary>
1313
/// The canonical publisher for this project.
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
using System.Linq;
2+
using CommunityToolkit.Diagnostics;
3+
using Ipfs;
4+
using OwlCore.ComponentModel;
5+
using OwlCore.Kubo;
6+
using OwlCore.Nomad;
7+
using OwlCore.Nomad.Kubo;
8+
using OwlCore.Nomad.Kubo.Events;
9+
10+
namespace WindowsAppCommunity.Sdk.Nomad;
11+
12+
/// <inheritdoc cref="IModifiableLinksCollection" />
13+
public class ModifiableLinksCollection : NomadKuboEventStreamHandler<ValueUpdateEvent>, IModifiableLinksCollection, IDelegable<ReadOnlyLinksCollection>
14+
{
15+
/// <inheritdoc />
16+
public required ReadOnlyLinksCollection Inner { get; init; }
17+
18+
/// <summary>
19+
/// A unique identifier for this instance, persistent across machines and reruns.
20+
/// </summary>
21+
public required string Id { get; init; }
22+
23+
/// <inheritdoc />
24+
public Link[] Links => Inner.Links;
25+
26+
/// <inheritdoc />
27+
public async Task AddLinkAsync(Link link, CancellationToken cancellationToken)
28+
{
29+
var newLink = new Models.Link
30+
{
31+
Id = link.Id,
32+
Name = link.Name,
33+
Description = link.Description,
34+
Url = link.Url,
35+
};
36+
37+
var keyCid = await Client.Dag.PutAsync(link.Id, pin: KuboOptions.ShouldPin, cancel: cancellationToken);
38+
var valueCid = await Client.Dag.PutAsync(newLink, pin: KuboOptions.ShouldPin, cancel: cancellationToken);
39+
40+
var updateEvent = new ValueUpdateEvent((DagCid)keyCid, (DagCid)valueCid, false);
41+
42+
var appendedEntry = await AppendNewEntryAsync(targetId: Id, eventId: nameof(AddLinkAsync), updateEvent, DateTime.UtcNow, cancellationToken);
43+
await ApplyEntryUpdateAsync(appendedEntry, updateEvent, newLink, link, cancellationToken);
44+
45+
EventStreamPosition = appendedEntry;
46+
}
47+
48+
/// <inheritdoc />
49+
public async Task RemoveLinkAsync(Link link, CancellationToken cancellationToken)
50+
{
51+
var Link = Inner.Inner.Links.FirstOrDefault(img => img.Id == link.Id);
52+
if (Link == null)
53+
{
54+
throw new ArgumentException("Link not found in the collection.", nameof(link));
55+
}
56+
57+
var keyCid = await Client.Dag.PutAsync(Link.Id, pin: KuboOptions.ShouldPin, cancel: cancellationToken);
58+
var valueCid = await Client.Dag.PutAsync(Link, pin: KuboOptions.ShouldPin, cancel: cancellationToken);
59+
60+
var updateEvent = new ValueUpdateEvent((DagCid)keyCid, (DagCid)valueCid, true);
61+
62+
var appendedEntry = await AppendNewEntryAsync(Id, nameof(RemoveLinkAsync), updateEvent, DateTime.UtcNow, cancellationToken);
63+
await ApplyEntryUpdateAsync(appendedEntry, updateEvent, Link, link, cancellationToken);
64+
65+
EventStreamPosition = appendedEntry;
66+
}
67+
68+
/// <inheritdoc />
69+
public event EventHandler<Link[]>? LinksAdded;
70+
71+
/// <inheritdoc />
72+
public event EventHandler<Link[]>? LinksRemoved;
73+
74+
/// <summary>
75+
/// Applies an event stream update event and raises the relevant events.
76+
/// </summary>
77+
/// <remarks>
78+
/// This method will call <see cref="ReadOnlyLinksCollection.GetAsync(string, CancellationToken)"/> and create a new instance to pass to the event handlers.
79+
/// <para/>
80+
/// If already have a resolved instance of <see cref="Link"/>, you should call <see cref="ApplyEntryUpdateAsync(EventStreamEntry{DagCid}, ValueUpdateEvent, Models.Link, Link, CancellationToken)"/> instead.
81+
/// </remarks>
82+
/// <param name="eventStreamEntry">The event stream entry to apply.</param>
83+
/// <param name="updateEvent">The update event to apply.</param>
84+
/// <param name="cancellationToken">A token that can be used to cancel the ongoing operation.</param>
85+
public override async Task ApplyEntryUpdateAsync(EventStreamEntry<DagCid> eventStreamEntry, ValueUpdateEvent updateEvent, CancellationToken cancellationToken)
86+
{
87+
cancellationToken.ThrowIfCancellationRequested();
88+
89+
if (eventStreamEntry.TargetId != Id)
90+
return;
91+
92+
Guard.IsNotNull(updateEvent.Value);
93+
var (link, _) = await Client.ResolveDagCidAsync<Models.Link>(updateEvent.Value.Value, nocache: !KuboOptions.UseCache, cancellationToken);
94+
95+
Guard.IsNotNull(link);
96+
await ApplyEntryUpdateAsync(eventStreamEntry, updateEvent, link, null, cancellationToken);
97+
}
98+
99+
/// <summary>
100+
/// Applies an event stream update event and raises the relevant events.
101+
/// </summary>
102+
/// <param name="eventStreamEntry">The event stream entry to apply.</param>
103+
/// <param name="updateEvent">The update event to apply.</param>
104+
/// <param name="linkModel">The deserialized Link model data for this event.</param>
105+
/// <param name="linkAppModel">The runtime application model for this event.</param>
106+
/// <param name="cancellationToken">A token that can be used to cancel the ongoing operation.</param>
107+
public async Task ApplyEntryUpdateAsync(EventStreamEntry<DagCid> eventStreamEntry, ValueUpdateEvent updateEvent, Models.Link linkModel, Link? linkAppModel = null, CancellationToken cancellationToken = default)
108+
{
109+
cancellationToken.ThrowIfCancellationRequested();
110+
111+
switch (eventStreamEntry.EventId)
112+
{
113+
case nameof(AddLinkAsync):
114+
{
115+
Inner.Inner.Links = [.. Inner.Inner.Links, linkModel];
116+
linkAppModel ??= await Inner.GetAsync(linkModel.Id, cancellationToken);
117+
LinksAdded?.Invoke(this, [linkAppModel]);
118+
break;
119+
}
120+
case nameof(RemoveLinkAsync):
121+
{
122+
Inner.Inner.Links = [.. Inner.Inner.Links.Except([linkModel])];
123+
linkAppModel ??= await Inner.GetAsync(linkModel.Id, cancellationToken);
124+
LinksRemoved?.Invoke(this, [linkAppModel]);
125+
break;
126+
}
127+
}
128+
}
129+
130+
/// <inheritdoc />
131+
public override Task ResetEventStreamPositionAsync(CancellationToken cancellationToken)
132+
{
133+
EventStreamPosition = null;
134+
Inner.Inner.Links = [];
135+
136+
return Task.CompletedTask;
137+
}
138+
}

src/Nomad/ModifiableProject.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,19 @@ public static ModifiableProject FromHandlerConfig(NomadKuboEventStreamHandlerCon
5151
KuboOptions = kuboOptions,
5252
Client = client,
5353
};
54-
IModifiableLinksCollection modifiableLinksCollection = null!;
54+
ModifiableLinksCollection modifiableLinksCollection = new()
55+
{
56+
Id = handlerConfig.RoamingKey.Id,
57+
Inner = readOnlyEntity.InnerLinks,
58+
RoamingKey = handlerConfig.RoamingKey,
59+
EventStreamHandlerId = handlerConfig.RoamingKey.Id,
60+
LocalEventStream = handlerConfig.LocalValue,
61+
LocalEventStreamKey = handlerConfig.LocalKey,
62+
Sources = handlerConfig.RoamingValue.Sources,
63+
KuboOptions = kuboOptions,
64+
Client = client,
65+
};
66+
5567
ModifiableImagesCollection modifiableImagesCollection = new()
5668
{
5769
Id = handlerConfig.RoamingKey.Id,

src/Nomad/ModifiablePublisher.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,18 @@ public static ModifiablePublisher FromHandlerConfig(NomadKuboEventStreamHandlerC
6262
Client = client,
6363
};
6464

65-
IModifiableLinksCollection modifiableLinksCollection = null!;
65+
ModifiableLinksCollection modifiableLinksCollection = new()
66+
{
67+
Id = handlerConfig.RoamingKey.Id,
68+
Inner = readonlyPublisher.InnerEntity.InnerLinks,
69+
RoamingKey = handlerConfig.RoamingKey,
70+
EventStreamHandlerId = handlerConfig.RoamingKey.Id,
71+
LocalEventStream = handlerConfig.LocalValue,
72+
LocalEventStreamKey = handlerConfig.LocalKey,
73+
Sources = handlerConfig.RoamingValue.Sources,
74+
KuboOptions = kuboOptions,
75+
Client = client,
76+
};
6677

6778
ModifiableEntity modifiableEntity = new()
6879
{

src/Nomad/ModifiableUser.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,20 @@ public static ModifiableUser FromHandlerConfig(NomadKuboEventStreamHandlerConfig
4949
KuboOptions = kuboOptions,
5050
Client = client,
5151
};
52-
IModifiableLinksCollection modifiableLinksCollection = null!;
52+
53+
ModifiableLinksCollection modifiableLinksCollection = new()
54+
{
55+
Id = handlerConfig.RoamingKey.Id,
56+
Inner = readOnlyEntity.InnerLinks,
57+
RoamingKey = handlerConfig.RoamingKey,
58+
EventStreamHandlerId = handlerConfig.RoamingKey.Id,
59+
LocalEventStream = handlerConfig.LocalValue,
60+
LocalEventStreamKey = handlerConfig.LocalKey,
61+
Sources = handlerConfig.RoamingValue.Sources,
62+
KuboOptions = kuboOptions,
63+
Client = client,
64+
};
65+
5366
ModifiableImagesCollection modifiableImagesCollection = new()
5467
{
5568
Id = handlerConfig.RoamingKey.Id,

src/Nomad/ReadOnlyEntity.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public class ReadOnlyEntity : IDelegable<IEntity>, IReadOnlyEntity
3030
/// <summary>
3131
/// Handles the links collection for this entity.
3232
/// </summary>
33-
public required IReadOnlyLinksCollection InnerLinks { get; init; }
33+
public required ReadOnlyLinksCollection InnerLinks { get; init; }
3434

3535
/// <inheritdoc />
3636
public required IEntity Inner { get; init; }
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using System.Runtime.CompilerServices;
4+
using Ipfs.CoreApi;
5+
using OwlCore.ComponentModel;
6+
using OwlCore.Nomad.Kubo;
7+
using WindowsAppCommunity.Sdk.Models;
8+
9+
namespace WindowsAppCommunity.Sdk.Nomad;
10+
11+
/// <inheritdoc cref="IReadOnlyLinksCollection" />
12+
public class ReadOnlyLinksCollection : IReadOnlyLinksCollection, IDelegable<ILinkCollection>, IReadOnlyNomadKuboRegistry<Link>
13+
{
14+
/// <summary>
15+
/// The client to use for communicating with ipfs.
16+
/// </summary>
17+
public required ICoreApi Client { get; init; }
18+
19+
/// <inheritdoc/>
20+
public required ILinkCollection Inner { get; init; }
21+
22+
/// <summary>
23+
/// The links in this collection.
24+
/// </summary>
25+
public Link[] Links => Inner.Links.Select(link => new Link
26+
{
27+
Id = link.Id,
28+
Url = link.Url,
29+
Name = link.Name,
30+
Description = link.Description,
31+
}).ToArray();
32+
33+
/// <inheritdoc/>
34+
public event EventHandler<Link[]>? LinksAdded;
35+
36+
/// <inheritdoc/>
37+
public event EventHandler<Link[]>? LinksRemoved;
38+
39+
/// <inheritdoc/>
40+
public event EventHandler<Link[]>? ItemsAdded
41+
{
42+
add => LinksAdded += value;
43+
remove => LinksAdded -= value;
44+
}
45+
46+
/// <inheritdoc/>
47+
public event EventHandler<Link[]>? ItemsRemoved
48+
{
49+
add => LinksRemoved += value;
50+
remove => LinksRemoved -= value;
51+
}
52+
53+
/// <inheritdoc/>
54+
public Task<Link> GetAsync(string id, CancellationToken cancellationToken)
55+
{
56+
cancellationToken.ThrowIfCancellationRequested();
57+
var link = Inner.Links.First(link => link.Id == id);
58+
return Task.FromResult(new Link
59+
{
60+
Id = link.Id,
61+
Name = link.Name,
62+
Url = link.Url,
63+
Description = link.Description
64+
});
65+
}
66+
67+
/// <inheritdoc/>
68+
public async IAsyncEnumerable<Link> GetAsync([EnumeratorCancellation] CancellationToken cancellationToken)
69+
{
70+
await Task.Yield();
71+
foreach (var link in Inner.Links)
72+
{
73+
cancellationToken.ThrowIfCancellationRequested();
74+
yield return new Link
75+
{
76+
Id = link.Id,
77+
Name = link.Name,
78+
Url = link.Url,
79+
Description = link.Description
80+
};
81+
}
82+
}
83+
}

src/Nomad/ReadOnlyProject.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@ public static ReadOnlyProject FromHandlerConfig(NomadKuboEventStreamHandlerConfi
4040
Client = client,
4141
};
4242

43-
IReadOnlyLinksCollection readOnlyLinksCollection = null!;
43+
ReadOnlyLinksCollection readOnlyLinksCollection = new ReadOnlyLinksCollection
44+
{
45+
Inner = handlerConfig.RoamingValue,
46+
Client = client,
47+
};
4448

4549
ReadOnlyEntity readOnlyEntity = new ReadOnlyEntity
4650
{

src/Nomad/ReadOnlyPublisher.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@ public static ReadOnlyPublisher FromHandlerConfig(NomadKuboEventStreamHandlerCon
4040
Client = client,
4141
};
4242

43-
IReadOnlyLinksCollection readOnlyLinksCollection = null!;
43+
ReadOnlyLinksCollection readOnlyLinksCollection = new ReadOnlyLinksCollection
44+
{
45+
Inner = handlerConfig.RoamingValue,
46+
Client = client,
47+
};
4448

4549
ReadOnlyEntity readOnlyEntity = new ReadOnlyEntity
4650
{

0 commit comments

Comments
 (0)