forked from Manuel/meeting-assistant
ci: consolidate platform pipelines and test prerequisites on macOS support
This commit is contained in:
1 parent
1b19b08f2e
commit
728e66dd72
76 files changed
+7477
-239
No files matched your search
@@ -0,0 +1,176 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user