SubscriptionClient carried over.
All checks were successful
Pack and Push NuGet Package / publish (push) Successful in 43s

This commit is contained in:
2025-07-05 04:38:31 +02:00
commit 9b5a0d7ea5
13 changed files with 854 additions and 0 deletions

View File

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Title>$(AssemblyName)</Title>
<AssemblyVersion>$([System.DateTime]::UtcNow.ToString("yyyy.MM.dd.HHmm"))</AssemblyVersion>
<PackageVersion>$([System.DateTime]::UtcNow.ToString("yyyy.MM.dd.HHmm"))</PackageVersion>
<BaseOutputPath>..\out\</BaseOutputPath>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<Title>$(AssemblyName)</Title>
<PackageProjectUrl>https://gitea.masita.net/mc/messengerapi.SubscriptionClient</PackageProjectUrl>
<RepositoryUrl>https://gitea.masita.net/mc/messengerapi.SubscriptionClient</RepositoryUrl>
<PackageTags>logging;log;logger</PackageTags>
<PackageLicenseExpression>mit-0</PackageLicenseExpression>
<Description>Allows subscription-based consumption of MessengerApi. Simply subscribe to pattern in PayloadType property of the message and SubscriptionClient will let you know when a message arrives.</Description>
<Copyright>mc @ 2024</Copyright>
<Authors>mc</Authors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MessengerApi.Client" />
<PackageReference Include="portaloggy" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,52 @@
using MessengerApi.Model.Messages;
namespace MessengerApi.Model
{
/// <summary>
/// See <see cref="SubscriptionClient"/> if you need to subscribe. It will give you one of these upon subscribing.
/// Also see <see cref="OnMessage"/> for what happens where you actually receive message for this sub.
/// </summary>
public class Subscription : IDisposable
{
public string MessageTypeMask { get; private set; }
public event EventHandler<InboxMessage> OnMessage;
internal SubscriptionClient client;
private bool isDisposed;
internal Subscription(string messageTypeMask, SubscriptionClient client)
{
MessageTypeMask = messageTypeMask;
this.client = client;
}
internal void Message(InboxMessage message)
{
OnMessage?.Invoke(this, message);
}
public void Dispose()
{
if (isDisposed)
{
return;
}
AssertNotDisposedOrThrow();
client.Unsubscribe(this);
Dispose();
isDisposed = true;
}
private void AssertNotDisposedOrThrow()
{
if (isDisposed)
{
throw new ObjectDisposedException(MessageTypeMask);
}
}
}
}

View File

@ -0,0 +1,61 @@
using MessengerApi.Model;
using portaloggy;
namespace MessengerApi
{
/// <summary>
/// This exists so you can mock it.
/// </summary>
public interface ISubscriptionClient : IDisposable
{
/// <summary>
/// Subscribes to given message type mask. Thread-safe.
/// </summary>
/// <remarks>Expected format of mask: "MY-MESSAGE-TYPE". No wildcards or placeholders, we only compare MessageType.StartsWith using this value.</remarks>
Subscription Subscribe(string payloadTypeMask);
/// <summary>
/// Unsubscribes from given subscriptions. Thread-safe.
/// </summary>
void Unsubscribe(Subscription subscription);
}
/// <summary>
/// This is where you begin. Instantiate one of these and start subscribing with <see cref="Subscribe(string)"/>.
/// </summary>
public class SubscriptionClient : Client, ISubscriptionClient
{
private readonly SubscriptionPollingEngine _engine;
private bool _isDisposed;
public SubscriptionClient(Credentials credentials, HttpClient client = null, ILogger logger = null) : base(credentials, client, logger)
{
this._engine = new SubscriptionPollingEngine(this._logger, this);
}
public void Dispose()
{
if (this._isDisposed)
{
return;
}
this._engine.Dispose();
this._isDisposed = true;
}
public Subscription Subscribe(string messageTypeMask)
{
var sub = new Subscription(messageTypeMask, this);
this._engine.AddSubscription(sub);
return sub;
}
public void Unsubscribe(Subscription subscription)
{
this._engine.RemoveSubscription(subscription);
}
}
}

View File

@ -0,0 +1,129 @@
using portaloggy;
using MessengerApi.Model.Messages;
using MessengerApi.Model;
namespace MessengerApi
{
internal class SubscriptionPollingEngine : IDisposable
{
private readonly ILogger logger;
private readonly IClient client;
private readonly List<Subscription> subscriptions = new List<Subscription>();
private readonly object subscriptionsLocker = new object();
private Task executionTask;
private CancellationTokenSource executionCts;
private bool isDisposed;
internal SubscriptionPollingEngine(
ILogger logger,
IClient client)
{
this.logger = logger;
this.client = client;
}
public Task AddSubscription(Subscription subscription)
{
this.AssertNotDisposedOrThrow();
lock (subscriptionsLocker)
{
subscriptions.Add(subscription);
}
this.logger.Log($"Subscription added for message {subscription.MessageTypeMask}.");
if(this.executionTask == null && this.executionCts == null)
{
this.BeginPolling();
}
return Task.CompletedTask;
}
public void Dispose()
{
if (this.isDisposed)
{
return;
}
this.executionCts.Cancel();
this.executionCts.Dispose();
this.isDisposed = true;
}
internal void BeginPolling()
{
this.executionCts = new CancellationTokenSource();
this.executionTask = this.PollEndlessly(this.executionCts.Token);
this.logger.Log("Polling endlessly now.");
}
internal void RemoveSubscription(Subscription subscription)
{
lock (subscriptionsLocker)
{
this.subscriptions.Remove(subscription);
}
}
private async Task PollEndlessly(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
var messages = Enumerable.Empty<InboxMessage>();
try
{
messages = this.client.GetMessages();
this.logger.Info($"Received {messages.Count()} messages.");
}
catch (Exception ex)
{
this.logger.Error("Can't obtain messages.", ex);
}
foreach (var message in messages)
{
try
{
var sub = (Subscription)null;
lock (this.subscriptionsLocker)
{
sub = this.subscriptions.FirstOrDefault(x => message.PayloadType.StartsWith(x.MessageTypeMask));
}
if (sub == null)
{
this.logger.Log($"This message has no subscription and will be ignored: {message.PayloadType}.");
continue;
}
sub.Message(message);
}
catch (Exception ex)
{
this.logger.Error("Can't process received message.", ex);
}
}
await Task.Delay(1000);
}
this.logger.Info("Polling ended.");
}
private void AssertNotDisposedOrThrow()
{
if (this.isDisposed)
{
throw new ObjectDisposedException(null);
}
}
}
}