forked from Manuel/meeting-assistant
177 lines
6.8 KiB
C#
177 lines
6.8 KiB
C#
namespace MeetingAssistant.Notifications;
|
|
|
|
public sealed record DesktopRequestAction(string Id, string Label);
|
|
public sealed record DesktopUserRequest(string Id, string Kind, string Title, string Message,
|
|
DateTimeOffset CreatedAt, DateTimeOffset ExpiresAt, IReadOnlyList<DesktopRequestAction> Actions);
|
|
public sealed record DesktopUserRequestSpec(string Kind, string Title, string Message,
|
|
TimeSpan Timeout, IReadOnlyList<DesktopRequestAction> Actions);
|
|
public sealed record PendingUserRequestRegistration(string Id, Task<string?> Decision);
|
|
public enum DesktopUserRequestRespondResult { Accepted, InvalidAction, NoLongerPending, Failed }
|
|
|
|
public sealed class DesktopUserRequestStore : IDisposable
|
|
{
|
|
private readonly object gate = new();
|
|
private readonly Dictionary<string, PendingRequest> pending = new(StringComparer.Ordinal);
|
|
private readonly TimeProvider timeProvider;
|
|
private readonly IHostApplicationLifetime applicationLifetime;
|
|
private readonly ILogger<DesktopUserRequestStore> logger;
|
|
private readonly CancellationTokenRegistration shutdownRegistration;
|
|
private bool disposed;
|
|
|
|
public DesktopUserRequestStore(TimeProvider timeProvider, IHostApplicationLifetime applicationLifetime,
|
|
ILogger<DesktopUserRequestStore> logger)
|
|
{
|
|
this.timeProvider = timeProvider;
|
|
this.applicationLifetime = applicationLifetime;
|
|
this.logger = logger;
|
|
shutdownRegistration = applicationLifetime.ApplicationStopping.Register(Dispose);
|
|
}
|
|
|
|
public PendingUserRequestRegistration Add(DesktopUserRequestSpec spec, CancellationToken lifetime,
|
|
Func<string, CancellationToken, Task>? onResponse = null)
|
|
{
|
|
lifetime.ThrowIfCancellationRequested();
|
|
var now = timeProvider.GetUtcNow();
|
|
var request = new DesktopUserRequest(Guid.NewGuid().ToString("N"), spec.Kind,
|
|
spec.Title, spec.Message, now, now.Add(spec.Timeout), spec.Actions.ToArray());
|
|
var entry = new PendingRequest(request, lifetime, onResponse);
|
|
lock (gate)
|
|
{
|
|
ObjectDisposedException.ThrowIf(disposed, this);
|
|
pending.Add(request.Id, entry);
|
|
}
|
|
var registration = lifetime.Register(() => Dismiss(request.Id));
|
|
var retained = false;
|
|
lock (gate)
|
|
{
|
|
retained = pending.ContainsKey(request.Id);
|
|
if (retained) entry.CancellationRegistration = registration;
|
|
}
|
|
if (!retained) registration.Dispose();
|
|
var deadlineTimer = timeProvider.CreateTimer(_ => Dismiss(request.Id), null,
|
|
spec.Timeout, Timeout.InfiniteTimeSpan);
|
|
lock (gate)
|
|
{
|
|
retained = pending.ContainsKey(request.Id);
|
|
if (retained) entry.DeadlineTimer = deadlineTimer;
|
|
}
|
|
if (!retained) deadlineTimer.Dispose();
|
|
return new PendingUserRequestRegistration(request.Id, entry.Decision.Task);
|
|
}
|
|
|
|
public IReadOnlyList<DesktopUserRequest> List()
|
|
{
|
|
PendingRequest[] expired;
|
|
DesktopUserRequest[] snapshot;
|
|
lock (gate)
|
|
{
|
|
var now = timeProvider.GetUtcNow();
|
|
expired = pending.Values.Where(entry => entry.Request.ExpiresAt <= now || entry.Lifetime.IsCancellationRequested).ToArray();
|
|
foreach (var entry in expired) pending.Remove(entry.Request.Id);
|
|
snapshot = pending.Values.Select(entry => entry.Request).ToArray();
|
|
}
|
|
foreach (var entry in expired)
|
|
{
|
|
entry.Decision.TrySetResult(null);
|
|
entry.Dispose();
|
|
}
|
|
return snapshot;
|
|
}
|
|
|
|
public async Task<DesktopUserRequestRespondResult> RespondAsync(string id, string action)
|
|
{
|
|
PendingRequest? entry;
|
|
bool expired;
|
|
lock (gate)
|
|
{
|
|
if (!pending.TryGetValue(id, out entry)) return DesktopUserRequestRespondResult.NoLongerPending;
|
|
expired = entry.Request.ExpiresAt <= timeProvider.GetUtcNow();
|
|
if (!expired && !entry.Request.Actions.Any(candidate => candidate.Id == action))
|
|
return DesktopUserRequestRespondResult.InvalidAction;
|
|
pending.Remove(id);
|
|
}
|
|
entry.Dispose();
|
|
if (expired || entry.Lifetime.IsCancellationRequested)
|
|
{
|
|
entry.Decision.TrySetResult(null);
|
|
return DesktopUserRequestRespondResult.NoLongerPending;
|
|
}
|
|
try
|
|
{
|
|
if (entry.OnResponse is not null) await entry.OnResponse(action, entry.Lifetime);
|
|
entry.Decision.TrySetResult(action);
|
|
return DesktopUserRequestRespondResult.Accepted;
|
|
}
|
|
catch (OperationCanceledException) when (entry.Lifetime.IsCancellationRequested)
|
|
{
|
|
entry.Decision.TrySetResult(null);
|
|
return DesktopUserRequestRespondResult.NoLongerPending;
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
entry.Decision.TrySetResult(null);
|
|
logger.LogError(exception, "Desktop request {RequestId} action failed", id);
|
|
return DesktopUserRequestRespondResult.Failed;
|
|
}
|
|
}
|
|
|
|
public void Dismiss(string id)
|
|
{
|
|
PendingRequest? entry;
|
|
lock (gate)
|
|
{
|
|
if (!pending.Remove(id, out entry)) return;
|
|
}
|
|
entry.Decision.TrySetResult(null);
|
|
entry.Dispose();
|
|
}
|
|
public void DismissKind(string kind)
|
|
{
|
|
PendingRequest[] removed;
|
|
lock (gate)
|
|
{
|
|
removed = pending.Values.Where(entry => entry.Request.Kind == kind).ToArray();
|
|
foreach (var entry in removed) pending.Remove(entry.Request.Id);
|
|
}
|
|
foreach (var entry in removed)
|
|
{
|
|
entry.Decision.TrySetResult(null);
|
|
entry.Dispose();
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
PendingRequest[] removed;
|
|
lock (gate)
|
|
{
|
|
if (disposed) return;
|
|
disposed = true;
|
|
removed = pending.Values.ToArray();
|
|
pending.Clear();
|
|
}
|
|
foreach (var entry in removed)
|
|
{
|
|
entry.Decision.TrySetResult(null);
|
|
entry.Dispose();
|
|
}
|
|
shutdownRegistration.Dispose();
|
|
}
|
|
|
|
private sealed class PendingRequest(DesktopUserRequest request, CancellationToken lifetime,
|
|
Func<string, CancellationToken, Task>? onResponse) : IDisposable
|
|
{
|
|
public DesktopUserRequest Request { get; } = request;
|
|
public CancellationToken Lifetime { get; } = lifetime;
|
|
public CancellationTokenRegistration CancellationRegistration { get; set; }
|
|
public ITimer? DeadlineTimer { get; set; }
|
|
public void Dispose()
|
|
{
|
|
CancellationRegistration.Dispose();
|
|
DeadlineTimer?.Dispose();
|
|
}
|
|
public Func<string, CancellationToken, Task>? OnResponse { get; } = onResponse;
|
|
public TaskCompletionSource<string?> Decision { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
}
|
|
}
|