#if NETCOREAPP3_1_OR_GREATER
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
//using JetBrains.Annotations;
using Telegram.Bot.Requests;
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;
// ReSharper disable once CheckNamespace
namespace Telegram.Bot.Polling {
///
/// Supports asynchronous iteration over s.
/// Updates are received on a different thread and enqueued.
///
//[PublicAPI]
public class QueuedUpdateReceiver : IAsyncEnumerable {
readonly ITelegramBotClient _botClient;
readonly ReceiverOptions? _receiverOptions;
readonly Func? _pollingErrorHandler;
int _inProcess;
Enumerator? _enumerator;
///
/// Constructs a new for the specified
///
/// The used for making GetUpdates calls
///
///
/// The function used to handle s thrown by GetUpdates requests
///
public QueuedUpdateReceiver(
ITelegramBotClient botClient,
ReceiverOptions? receiverOptions = default,
Func? pollingErrorHandler = default) {
_botClient = botClient ?? throw new ArgumentNullException(nameof(botClient));
_receiverOptions = receiverOptions;
_pollingErrorHandler = pollingErrorHandler;
}
///
/// Indicates how many s are ready to be returned the enumerator
///
public int PendingUpdates => _enumerator?.PendingUpdates ?? 0;
///
/// Gets the . This method may only be called once.
///
///
/// The with which you can stop receiving
///
public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) {
if(Interlocked.CompareExchange(ref _inProcess, 1, 0) == 1) {
throw new InvalidOperationException(nameof(GetAsyncEnumerator) + " may only be called once");
}
_enumerator = new(receiver: this, cancellationToken: cancellationToken);
return _enumerator;
}
class Enumerator : IAsyncEnumerator {
readonly QueuedUpdateReceiver _receiver;
readonly CancellationTokenSource _cts;
readonly CancellationToken _token;
readonly UpdateType[]? _allowedUpdates;
readonly int? _limit;
Exception? _uncaughtException;
readonly Channel _channel;
Update? _current;
int _pendingUpdates;
int _messageOffset;
public int PendingUpdates => _pendingUpdates;
public Enumerator(QueuedUpdateReceiver receiver, CancellationToken cancellationToken) {
_receiver = receiver;
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, default);
_token = _cts.Token;
_messageOffset = receiver._receiverOptions?.Offset ?? 0;
_limit = receiver._receiverOptions?.Limit ?? default;
_allowedUpdates = receiver._receiverOptions?.AllowedUpdates;
_channel = Channel.CreateUnbounded(
new() {
SingleReader = true,
SingleWriter = true
}
);
#pragma warning disable CA2016
Task.Run(ReceiveUpdatesAsync);
#pragma warning restore CA2016
}
public ValueTask MoveNextAsync() {
if(_uncaughtException is not null) {
throw _uncaughtException;
}
_token.ThrowIfCancellationRequested();
if(_channel.Reader.TryRead(out _current)) {
Interlocked.Decrement(ref _pendingUpdates);
return new(true);
}
return new(ReadAsync());
}
async Task ReadAsync() {
_current = await _channel.Reader.ReadAsync(_token).ConfigureAwait(false);
Interlocked.Decrement(ref _pendingUpdates);
return true;
}
async Task ReceiveUpdatesAsync() {
if(_receiver._receiverOptions?.ThrowPendingUpdates is true) {
try {
_messageOffset = await _receiver._botClient.ThrowOutPendingUpdatesAsync(
cancellationToken: _token
).ConfigureAwait(false);
} catch(OperationCanceledException) {
// ignored
}
}
while(!_cts.IsCancellationRequested) {
try {
Update[] updateArray = await _receiver._botClient
.MakeRequestAsync(
request: new GetUpdatesRequest {
Offset = _messageOffset,
Timeout = (int)_receiver._botClient.Timeout.TotalSeconds,
AllowedUpdates = _allowedUpdates,
Limit = _limit,
},
cancellationToken: _token
)
.ConfigureAwait(false);
if(updateArray.Length > 0) {
_messageOffset = updateArray[^1].Id + 1;
Interlocked.Add(ref _pendingUpdates, updateArray.Length);
ChannelWriter writer = _channel.Writer;
foreach(Update update in updateArray) {
// ReSharper disable once RedundantAssignment
var success = writer.TryWrite(update);
Debug.Assert(success, "TryWrite should succeed as we are using an unbounded channel");
}
}
} catch(OperationCanceledException) {
// Ignore
}
#pragma warning disable CA1031
catch(Exception ex)
#pragma warning restore CA1031
{
Debug.Assert(_uncaughtException is null);
// If there is no errorHandler or the errorHandler throws, stop receiving
if(_receiver._pollingErrorHandler is null) {
_uncaughtException = ex;
_cts.Cancel();
} else {
try {
await _receiver._pollingErrorHandler(ex, _token).ConfigureAwait(false);
}
#pragma warning disable CA1031
catch(Exception errorHandlerException)
#pragma warning restore CA1031
{
_uncaughtException = new AggregateException(
message: "Exception was not caught by the errorHandler.",
ex,
errorHandlerException
);
_cts.Cancel();
}
}
if(_uncaughtException is not null) {
#pragma warning disable CA2201
_uncaughtException = new(
message: "Exception was not caught by the errorHandler.",
innerException: _uncaughtException
);
#pragma warning restore CA2201
}
}
}
}
public Update Current => _current!; // _current being null indicates MoveNextAsync was never called
public ValueTask DisposeAsync() {
_cts.Cancel();
_cts.Dispose();
return new();
}
}
}
}
#endif