Skip to content

Commit ca9212b

Browse files
authored
Merge pull request #15 from WindowsAppCommunity/implementation/eventstreamhandlers/virtual/images
Initial implementation of virtual event stream handler for Images collections
2 parents f7af157 + 89d0e65 commit ca9212b

7 files changed

Lines changed: 233 additions & 19 deletions

src/Models/Image.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public record Image : IStorable
1414
public required string Id { get; init; }
1515

1616
/// <summary>
17-
/// A display name for this image.
17+
/// A display name for this image, with the file extension.
1818
/// </summary>
1919
public required string Name { get; set; }
2020

src/Models/Publisher.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace WindowsAppCommunity.Sdk.Models;
66
/// <summary>
77
/// Represents a content publisher.
88
/// </summary>
9-
public record Publisher : IEntity, ILinkCollection, IUserRoleCollection, IConnections, IAccentColor
9+
public record Publisher : IEntity, ILinkCollection, IProjectRoleCollection, IUserRoleCollection, IConnections, IAccentColor
1010
{
1111
/// <summary>
1212
/// The name of the publisher.
@@ -43,6 +43,11 @@ public record Publisher : IEntity, ILinkCollection, IUserRoleCollection, IConnec
4343
/// </summary>
4444
public Dictionary<DagCid, Role> Users { get; set; } = [];
4545

46+
/// <summary>
47+
/// Projects who are registered to participate in this publisher, along with their roles.
48+
/// </summary>
49+
public Dictionary<DagCid, Role> Projects { get; set; } = [];
50+
4651
/// <summary>
4752
/// A list of other publishers who are managed under this publisher.
4853
/// </summary>

src/Models/ValueUpdateEvent.cs

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -76,16 +76,4 @@ namespace WindowsAppCommunity.Sdk.Models;
7676
/// <para/>
7777
/// In the scenario set up here, the hyperedge picked (positional --- non-positional) was enough information to understand how to flatten it down to a single record type that other types build on top on without changing, while being stored in a time-ordered event stream.
7878
/// </remarks>
79-
public record ValueUpdateEvent(string TargetId, DagCid? Value, DagCid? Key, bool Unset);
80-
81-
82-
83-
84-
85-
86-
87-
88-
89-
90-
91-
79+
public record ValueUpdateEvent(string TargetId, string EventId, DagCid? Key, DagCid? Value, bool Unset);

src/Nomad/IdOverriddenIpfsFile.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using Ipfs;
2+
using Ipfs.CoreApi;
3+
using OwlCore.Kubo;
4+
using OwlCore.Storage;
5+
6+
namespace WindowsAppCommunity.Sdk.Nomad;
7+
8+
/// <summary>
9+
/// A wrapper that overrides the <see cref="IStorable.Id"/> of an <see cref="IpfsFile"/>.
10+
/// </summary>
11+
public class IdOverriddenIpfsFile : IpfsFile
12+
{
13+
/// <summary>
14+
/// Creates a new instance of <see cref="IdOverriddenIpfsFile"/>.
15+
/// </summary>
16+
public IdOverriddenIpfsFile(Cid cid, string name, string id, ICoreApi client)
17+
: base(cid, name, client)
18+
{
19+
Id = id;
20+
}
21+
22+
/// <inheritdoc />
23+
public override string Id { get; }
24+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using CommunityToolkit.Diagnostics;
4+
using Ipfs;
5+
using Ipfs.CoreApi;
6+
using OwlCore.ComponentModel;
7+
using OwlCore.Kubo;
8+
using OwlCore.Nomad;
9+
using OwlCore.Nomad.Kubo;
10+
using OwlCore.Storage;
11+
using WindowsAppCommunity.Sdk.Models;
12+
13+
namespace WindowsAppCommunity.Sdk.Nomad;
14+
15+
/// <inheritdoc cref="IModifiableImagesCollection" />
16+
public class ModifiableImagesCollection : NomadKuboEventStreamHandler<ValueUpdateEvent>, IModifiableImagesCollection, IDelegable<ReadOnlyImagesCollection>
17+
{
18+
/// <inheritdoc />
19+
public required ReadOnlyImagesCollection Inner { get; init; }
20+
21+
/// <summary>
22+
/// A unique identifier for this instance, persistent across machines and reruns.
23+
/// </summary>
24+
public required string Id { get; init; }
25+
26+
/// <inheritdoc />
27+
public async Task AddImageAsync(IFile imageFile, CancellationToken cancellationToken)
28+
{
29+
var imageCid = await imageFile.GetCidAsync(Inner.Client, new AddFileOptions { Pin = KuboOptions.ShouldPin, }, cancellationToken);
30+
31+
var newImage = new Image
32+
{
33+
Id = imageFile.Id,
34+
Name = imageFile.Name,
35+
Cid = (DagCid)imageCid,
36+
};
37+
38+
var keyCid = await Client.Dag.PutAsync(newImage.Id, pin: KuboOptions.ShouldPin, cancel: cancellationToken);
39+
var valueCid = await Client.Dag.PutAsync(newImage, pin: KuboOptions.ShouldPin, cancel: cancellationToken);
40+
41+
var updateEvent = new ValueUpdateEvent(Id, nameof(AddImageAsync), (DagCid)keyCid, (DagCid)valueCid, false);
42+
43+
await ApplyEntryUpdateAsync(updateEvent, newImage, cancellationToken);
44+
var appendedEntry = await AppendNewEntryAsync(updateEvent, cancellationToken);
45+
46+
EventStreamPosition = appendedEntry;
47+
48+
// Append entry to event stream
49+
// TODO
50+
}
51+
52+
/// <inheritdoc />
53+
public Task RemoveImageAsync(IFile imageFile, CancellationToken cancellationToken)
54+
{
55+
throw new NotImplementedException();
56+
}
57+
58+
/// <inheritdoc />
59+
public IAsyncEnumerable<IFile> GetImageFilesAsync(CancellationToken cancellationToken) => Inner.GetImageFilesAsync(cancellationToken);
60+
61+
/// <inheritdoc />
62+
public event EventHandler<IFile[]>? ImagesAdded;
63+
64+
/// <inheritdoc />
65+
public event EventHandler<IFile[]>? ImagesRemoved;
66+
67+
/// <summary>
68+
/// Applies an event stream update event and raises the relevant events.
69+
/// </summary>
70+
/// <remarks>
71+
/// This method will call <see cref="ReadOnlyImagesCollection.GetAsync(string, CancellationToken)"/> and create a new instance to pass to the event handlers.
72+
/// <para/>
73+
/// If already have a resolved instance of <see cref="Image"/>, you should call <see cref="ApplyEntryUpdateAsync(ValueUpdateEvent, Image, CancellationToken)"/> instead.
74+
/// </remarks>
75+
/// <param name="updateEvent">The update event to apply.</param>
76+
/// <param name="cancellationToken">A token that can be used to cancel the ongoing operation.</param>
77+
public override async Task ApplyEntryUpdateAsync(ValueUpdateEvent updateEvent, CancellationToken cancellationToken)
78+
{
79+
cancellationToken.ThrowIfCancellationRequested();
80+
81+
if (updateEvent.TargetId != Id)
82+
return;
83+
84+
Guard.IsNotNull(updateEvent.Value);
85+
var (image, _) = await Client.ResolveDagCidAsync<Image>(updateEvent.Value.Value, nocache: !KuboOptions.UseCache, cancellationToken);
86+
87+
Guard.IsNotNull(image);
88+
await ApplyEntryUpdateAsync(updateEvent, image, cancellationToken);
89+
}
90+
91+
/// <summary>
92+
/// Applies an event stream update event and raises the relevant events.
93+
/// </summary>
94+
/// <param name="updateEvent">The update event to apply.</param>
95+
/// <param name="image">The resolved image data for this event.</param>
96+
/// <param name="cancellationToken">A token that can be used to cancel the ongoing operation.</param>
97+
public async Task ApplyEntryUpdateAsync(ValueUpdateEvent updateEvent, Image image, CancellationToken cancellationToken)
98+
{
99+
cancellationToken.ThrowIfCancellationRequested();
100+
101+
switch (updateEvent.EventId)
102+
{
103+
case nameof(AddImageAsync):
104+
{
105+
var imageFile = await Inner.GetAsync(image.Id, cancellationToken);
106+
Inner.Inner.Images = [..Inner.Inner.Images, image];
107+
ImagesAdded?.Invoke(this, [imageFile]);
108+
break;
109+
}
110+
case nameof(RemoveImageAsync):
111+
{
112+
var imageFile = await Inner.GetAsync(image.Id, cancellationToken);
113+
Inner.Inner.Images = [.. Inner.Inner.Images.Except([image])];
114+
ImagesRemoved?.Invoke(this, [imageFile]);
115+
break;
116+
}
117+
}
118+
}
119+
120+
/// <inheritdoc cref="INomadKuboEventStreamHandler{TEventEntryContent}.AppendNewEntryAsync" />
121+
public override async Task<EventStreamEntry<Cid>> AppendNewEntryAsync(ValueUpdateEvent updateEvent, CancellationToken cancellationToken = default)
122+
{
123+
// Use extension method for code deduplication (can't use inheritance).
124+
var localUpdateEventCid = await Client.Dag.PutAsync(updateEvent, pin: KuboOptions.ShouldPin, cancel: cancellationToken);
125+
var newEntry = await this.AppendEventStreamEntryAsync(localUpdateEventCid, updateEvent.EventId, updateEvent.TargetId, cancellationToken);
126+
return newEntry;
127+
}
128+
129+
/// <inheritdoc />
130+
public override Task ResetEventStreamPositionAsync(CancellationToken cancellationToken)
131+
{
132+
EventStreamPosition = null;
133+
Inner.Inner.Images = [];
134+
135+
return Task.CompletedTask;
136+
}
137+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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 OwlCore.Storage;
8+
using WindowsAppCommunity.Sdk.Models;
9+
10+
namespace WindowsAppCommunity.Sdk.Nomad;
11+
12+
/// <inheritdoc cref="IReadOnlyImagesCollection" />
13+
public class ReadOnlyImagesCollection : IReadOnlyImagesCollection, IDelegable<IImages>, IReadOnlyNomadKuboRegistry<IFile>
14+
{
15+
/// <summary>
16+
/// The client to use for communicating with ipfs.
17+
/// </summary>
18+
public required ICoreApi Client { get; init; }
19+
20+
/// <inheritdoc/>
21+
public required IImages Inner { get; init; }
22+
23+
/// <inheritdoc/>
24+
public event EventHandler<IFile[]>? ImagesAdded;
25+
26+
/// <inheritdoc/>
27+
public event EventHandler<IFile[]>? ImagesRemoved;
28+
29+
/// <inheritdoc/>
30+
public event EventHandler<IFile[]>? ItemsAdded
31+
{
32+
add => ImagesAdded += value;
33+
remove => ImagesAdded -= value;
34+
}
35+
36+
/// <inheritdoc/>
37+
public event EventHandler<IFile[]>? ItemsRemoved
38+
{
39+
add => ImagesRemoved += value;
40+
remove => ImagesRemoved -= value;
41+
}
42+
43+
/// <inheritdoc/>
44+
public IAsyncEnumerable<IFile> GetImageFilesAsync(CancellationToken cancellationToken) => GetAsync(cancellationToken);
45+
46+
/// <inheritdoc/>
47+
public Task<IFile> GetAsync(string id, CancellationToken cancellationToken)
48+
{
49+
cancellationToken.ThrowIfCancellationRequested();
50+
var image = Inner.Images.First(image => image.Id == id);
51+
return Task.FromResult<IFile>(new IdOverriddenIpfsFile(image.Cid, image.Name, image.Id, Client));
52+
}
53+
54+
/// <inheritdoc/>
55+
public async IAsyncEnumerable<IFile> GetAsync([EnumeratorCancellation] CancellationToken cancellationToken)
56+
{
57+
await Task.Yield();
58+
foreach (var image in Inner.Images)
59+
{
60+
cancellationToken.ThrowIfCancellationRequested();
61+
yield return new IdOverriddenIpfsFile(image.Cid, image.Name, image.Id, Client);
62+
}
63+
}
64+
}

src/WindowsAppCommunity.Sdk.csproj

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,4 @@ Initial release of WindowsAppCommunity.Sdk.
7373
<PackagePath>\</PackagePath>
7474
</None>
7575
</ItemGroup>
76-
77-
<ItemGroup>
78-
<Folder Include="Nomad\" />
79-
</ItemGroup>
8076
</Project>

0 commit comments

Comments
 (0)