mirror of
https://github.com/esiur/esiur-dotnet.git
synced 2026-07-30 17:30:40 +00:00
410 lines
11 KiB
C#
410 lines
11 KiB
C#
/*
|
|
|
|
Copyright (c) 2017-2026 Ahmed Kh. Zamil
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in all
|
|
copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
SOFTWARE.
|
|
|
|
*/
|
|
|
|
using Esiur.Data;
|
|
using Esiur.Data.Types;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace Esiur.Core;
|
|
|
|
/// <summary>
|
|
/// Represents a remotely executing stream and exposes its lifecycle controls.
|
|
/// </summary>
|
|
public class AsyncStreamReply : AsyncReply
|
|
{
|
|
readonly Func<AsyncReply> _pull;
|
|
readonly Func<AsyncReply> _terminate;
|
|
readonly Func<AsyncReply> _halt;
|
|
readonly Func<AsyncReply> _resume;
|
|
|
|
readonly object _streamLock = new object();
|
|
readonly TaskCompletionSource<bool> _started = NewCompletionSource();
|
|
|
|
Exception _streamException;
|
|
bool _streamStarted;
|
|
bool _streamCompleted;
|
|
bool _terminationSent;
|
|
|
|
/// <summary>
|
|
/// Gets the delivery mode declared by the remote function.
|
|
/// </summary>
|
|
public StreamMode StreamMode { get; }
|
|
|
|
/// <summary>
|
|
/// Gets whether the peer acknowledged that the stream has started.
|
|
/// </summary>
|
|
public bool Started
|
|
{
|
|
get
|
|
{
|
|
lock (_streamLock)
|
|
return _streamStarted;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets whether the stream completed or was terminated.
|
|
/// </summary>
|
|
public bool Completed
|
|
{
|
|
get
|
|
{
|
|
lock (_streamLock)
|
|
return _streamCompleted;
|
|
}
|
|
}
|
|
|
|
internal AsyncStreamReply(
|
|
StreamMode streamMode,
|
|
Func<AsyncReply> pull,
|
|
Func<AsyncReply> terminate,
|
|
Func<AsyncReply> halt,
|
|
Func<AsyncReply> resume)
|
|
{
|
|
StreamMode = streamMode;
|
|
_pull = pull;
|
|
_terminate = terminate;
|
|
_halt = halt;
|
|
_resume = resume;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Requests the next item of a pull stream.
|
|
/// </summary>
|
|
public AsyncReply Pull()
|
|
{
|
|
if (StreamMode != StreamMode.Pull)
|
|
throw new InvalidOperationException("Pull is only valid for a pull stream.");
|
|
|
|
lock (_streamLock)
|
|
{
|
|
if (_streamCompleted)
|
|
return new AsyncReply(null);
|
|
}
|
|
|
|
return _pull();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Terminates the remote stream execution and releases its enumerator.
|
|
/// </summary>
|
|
public AsyncReply Terminate()
|
|
{
|
|
lock (_streamLock)
|
|
{
|
|
if (_terminationSent || _streamCompleted)
|
|
return new AsyncReply(null);
|
|
|
|
_terminationSent = true;
|
|
_streamCompleted = true;
|
|
_started.TrySetResult(true);
|
|
}
|
|
|
|
OnStreamCompleted();
|
|
|
|
var reply = _terminate();
|
|
reply.Error(TriggerStreamError);
|
|
return reply;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Halts a pausable remote stream execution.
|
|
/// </summary>
|
|
public AsyncReply Halt()
|
|
{
|
|
lock (_streamLock)
|
|
{
|
|
if (_streamCompleted)
|
|
return new AsyncReply(null);
|
|
}
|
|
|
|
var reply = _halt();
|
|
reply.Error(TriggerStreamError);
|
|
return reply;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resumes a halted remote stream execution.
|
|
/// </summary>
|
|
public AsyncReply Resume()
|
|
{
|
|
lock (_streamLock)
|
|
{
|
|
if (_streamCompleted)
|
|
return new AsyncReply(null);
|
|
}
|
|
|
|
var reply = _resume();
|
|
reply.Error(TriggerStreamError);
|
|
return reply;
|
|
}
|
|
|
|
internal void TriggerStreamStarted()
|
|
{
|
|
lock (_streamLock)
|
|
{
|
|
if (_streamCompleted || _streamException != null)
|
|
return;
|
|
|
|
_streamStarted = true;
|
|
_started.TrySetResult(true);
|
|
}
|
|
|
|
OnStreamStarted();
|
|
}
|
|
|
|
internal void TriggerStreamCompleted()
|
|
{
|
|
lock (_streamLock)
|
|
{
|
|
if (_streamCompleted)
|
|
return;
|
|
|
|
_streamCompleted = true;
|
|
_started.TrySetResult(true);
|
|
}
|
|
|
|
OnStreamCompleted();
|
|
}
|
|
|
|
internal void TriggerStreamError(AsyncException exception)
|
|
{
|
|
lock (_streamLock)
|
|
{
|
|
if (_streamCompleted || _streamException != null)
|
|
return;
|
|
|
|
_streamException = exception;
|
|
_streamCompleted = true;
|
|
_started.TrySetException(exception);
|
|
}
|
|
|
|
OnStreamError(exception);
|
|
|
|
if (errorCallbacks != null)
|
|
base.TriggerError(exception);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void TriggerError(Exception exception)
|
|
{
|
|
TriggerStreamError(exception as AsyncException ?? new AsyncException(exception));
|
|
}
|
|
|
|
internal Task WaitUntilStartedAsync() => _started.Task;
|
|
|
|
internal Exception GetStreamException()
|
|
{
|
|
lock (_streamLock)
|
|
return _streamException;
|
|
}
|
|
|
|
internal bool IsStreamCompleted()
|
|
{
|
|
lock (_streamLock)
|
|
return _streamCompleted;
|
|
}
|
|
|
|
protected virtual void OnStreamStarted() { }
|
|
protected virtual void OnStreamCompleted() { }
|
|
protected virtual void OnStreamError(Exception exception) { }
|
|
|
|
protected static TaskCompletionSource<bool> NewCompletionSource()
|
|
=> new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A typed remote stream that can be consumed with <c>await foreach</c>.
|
|
/// Pull streams request exactly one remote item for each <c>MoveNextAsync</c> call.
|
|
/// </summary>
|
|
public sealed class AsyncStreamReply<T> : AsyncStreamReply, IAsyncEnumerable<T>
|
|
{
|
|
readonly object _itemsLock = new object();
|
|
readonly Queue<T> _items = new Queue<T>();
|
|
|
|
TaskCompletionSource<bool> _itemAvailable;
|
|
bool _enumeratorCreated;
|
|
bool _movePending;
|
|
|
|
internal AsyncStreamReply(
|
|
StreamMode streamMode,
|
|
Func<AsyncReply> pull,
|
|
Func<AsyncReply> terminate,
|
|
Func<AsyncReply> halt,
|
|
Func<AsyncReply> resume)
|
|
: base(streamMode, pull, terminate, halt, resume)
|
|
{
|
|
Chunk(value => ReceiveItem((T)RuntimeCaster.Cast(value, typeof(T))));
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default)
|
|
{
|
|
lock (_itemsLock)
|
|
{
|
|
if (_enumeratorCreated)
|
|
throw new InvalidOperationException("A remote stream can only be enumerated once.");
|
|
|
|
_enumeratorCreated = true;
|
|
}
|
|
|
|
return new Enumerator(this, cancellationToken);
|
|
}
|
|
|
|
void ReceiveItem(T item)
|
|
{
|
|
TaskCompletionSource<bool> itemAvailable;
|
|
|
|
lock (_itemsLock)
|
|
{
|
|
if (IsStreamCompleted() || GetStreamException() != null)
|
|
return;
|
|
|
|
_items.Enqueue(item);
|
|
itemAvailable = _itemAvailable;
|
|
_itemAvailable = null;
|
|
}
|
|
|
|
itemAvailable?.TrySetResult(true);
|
|
}
|
|
|
|
protected override void OnStreamCompleted()
|
|
{
|
|
TaskCompletionSource<bool> itemAvailable;
|
|
|
|
lock (_itemsLock)
|
|
{
|
|
itemAvailable = _itemAvailable;
|
|
_itemAvailable = null;
|
|
}
|
|
|
|
itemAvailable?.TrySetResult(true);
|
|
}
|
|
|
|
protected override void OnStreamError(Exception exception)
|
|
{
|
|
TaskCompletionSource<bool> itemAvailable;
|
|
|
|
lock (_itemsLock)
|
|
{
|
|
itemAvailable = _itemAvailable;
|
|
_itemAvailable = null;
|
|
}
|
|
|
|
itemAvailable?.TrySetException(exception);
|
|
}
|
|
|
|
async Task<(bool HasValue, T Value)> MoveNextAsync(CancellationToken cancellationToken)
|
|
{
|
|
lock (_itemsLock)
|
|
{
|
|
if (_movePending)
|
|
throw new InvalidOperationException("Concurrent MoveNextAsync calls are not supported.");
|
|
|
|
_movePending = true;
|
|
}
|
|
|
|
try
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
await WaitUntilStartedAsync();
|
|
|
|
while (true)
|
|
{
|
|
TaskCompletionSource<bool> itemAvailable;
|
|
|
|
lock (_itemsLock)
|
|
{
|
|
var exception = GetStreamException();
|
|
if (exception != null)
|
|
throw exception;
|
|
|
|
if (_items.Count > 0)
|
|
return (true, _items.Dequeue());
|
|
|
|
if (IsStreamCompleted())
|
|
return (false, default);
|
|
|
|
_itemAvailable = NewCompletionSource();
|
|
itemAvailable = _itemAvailable;
|
|
}
|
|
|
|
if (StreamMode == StreamMode.Pull)
|
|
{
|
|
var pull = Pull();
|
|
_ = pull.Error(TriggerStreamError);
|
|
}
|
|
|
|
using (cancellationToken.Register(() =>
|
|
{
|
|
itemAvailable.TrySetCanceled();
|
|
_ = Terminate();
|
|
}))
|
|
{
|
|
await itemAvailable.Task;
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
lock (_itemsLock)
|
|
_movePending = false;
|
|
}
|
|
}
|
|
|
|
async Task DisposeAsync()
|
|
{
|
|
if (IsStreamCompleted())
|
|
return;
|
|
|
|
await Terminate();
|
|
}
|
|
|
|
sealed class Enumerator : IAsyncEnumerator<T>
|
|
{
|
|
readonly AsyncStreamReply<T> _owner;
|
|
readonly CancellationToken _cancellationToken;
|
|
|
|
public T Current { get; private set; }
|
|
|
|
internal Enumerator(AsyncStreamReply<T> owner, CancellationToken cancellationToken)
|
|
{
|
|
_owner = owner;
|
|
_cancellationToken = cancellationToken;
|
|
}
|
|
|
|
public async ValueTask<bool> MoveNextAsync()
|
|
{
|
|
var result = await _owner.MoveNextAsync(_cancellationToken);
|
|
Current = result.Value;
|
|
return result.HasValue;
|
|
}
|
|
|
|
public ValueTask DisposeAsync() => new ValueTask(_owner.DisposeAsync());
|
|
}
|
|
}
|