File: Utils\CancellationSeries.cs
Web Access
Project: src\src\Aspire.Dashboard\Aspire.Dashboard.csproj (Aspire.Dashboard)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
namespace Aspire.Dashboard.Utils;
 
/// <summary>
/// Produces a series of <see cref="CancellationToken"/>s that are used to coordinate
/// cancellation of non-overlapping operations in a concurrent environment.
/// </summary>
/// <remarks>
/// The class produces a stream of cancellation tokens, where each represents the lifetime of an operation that
/// started when <see cref="NextAsync"/> was called. Each call of that method cancels the last token it issued, then returns a new one.
/// In that way, you can set up operations with cancellation and no overlap.
/// The effectiveness of this approach depends upon good support for cancellation in operations under that token.
/// It's important that any await can be promptly cancelled.
/// </remarks>
internal sealed class CancellationSeries
{
    private CancellationTokenSource? _cts;
 
    /// <summary>
    /// Produces the next token, cancelling the one before.
    /// </summary>
    /// <remarks>
    /// The returned token represents the lifetime of an operation, in a concurrent environment where operations will not overlap.
    /// This method cancels token it returned the last time it was called. In that way, you can set up operations with cancellation and no overlap.
    /// The effectiveness of this approach depends upon good support for cancellation in operations under that token.
    /// It's important that any await can be promptly cancelled.
    /// </remarks>
    /// <returns>A cancellation token that manages the lifetime of a non-overlapping operation in a concurrent environment.</returns>
    public async Task<CancellationToken> NextAsync()
    {
        var nextCts = new CancellationTokenSource();
 
        // Obtain the token before exchange, as otherwise the CTS may be cancelled before
        // we request the Token, which will result in an ObjectDisposedException.
        // This way we would return a cancelled token, which is reasonable.
        var nextToken = nextCts.Token;
 
        await Next(nextCts).ConfigureAwait(false);
 
        return nextToken;
    }
 
    /// <summary>
    /// Cancels the current <see cref="CancellationToken"/> but doesn't create a new one.
    /// </summary>
    /// <remarks>
    /// Multiple calls to this method will be no-ops until <see cref="NextAsync"/> is called,
    /// at which point there's something to cancel.
    /// </remarks>
    /// <returns></returns>
    public Task ClearAsync()
    {
        return Next(null);
    }
 
    private async Task Next(CancellationTokenSource? next)
    {
        using var priorCts = Interlocked.Exchange(ref _cts, next);
 
        if (priorCts is not null)
        {
            await priorCts.CancelAsync().ConfigureAwait(false);
        }
    }
}