diff --git a/Libraries/Esiur/Core/AsyncReply.cs b/Libraries/Esiur/Core/AsyncReply.cs
index 30dc518..872c2e1 100644
--- a/Libraries/Esiur/Core/AsyncReply.cs
+++ b/Libraries/Esiur/Core/AsyncReply.cs
@@ -271,7 +271,7 @@ public class AsyncReply
return;
}
- public void TriggerError(Exception exception)
+ public virtual void TriggerError(Exception exception)
{
//timeout?.Dispose();
diff --git a/Libraries/Esiur/Core/AsyncStreamReply.cs b/Libraries/Esiur/Core/AsyncStreamReply.cs
new file mode 100644
index 0000000..7dfb47b
--- /dev/null
+++ b/Libraries/Esiur/Core/AsyncStreamReply.cs
@@ -0,0 +1,409 @@
+/*
+
+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;
+
+///
+/// Represents a remotely executing stream and exposes its lifecycle controls.
+///
+public class AsyncStreamReply : AsyncReply
+{
+ readonly Func _pull;
+ readonly Func _terminate;
+ readonly Func _halt;
+ readonly Func _resume;
+
+ readonly object _streamLock = new object();
+ readonly TaskCompletionSource _started = NewCompletionSource();
+
+ Exception _streamException;
+ bool _streamStarted;
+ bool _streamCompleted;
+ bool _terminationSent;
+
+ ///
+ /// Gets the delivery mode declared by the remote function.
+ ///
+ public StreamMode StreamMode { get; }
+
+ ///
+ /// Gets whether the peer acknowledged that the stream has started.
+ ///
+ public bool Started
+ {
+ get
+ {
+ lock (_streamLock)
+ return _streamStarted;
+ }
+ }
+
+ ///
+ /// Gets whether the stream completed or was terminated.
+ ///
+ public bool Completed
+ {
+ get
+ {
+ lock (_streamLock)
+ return _streamCompleted;
+ }
+ }
+
+ internal AsyncStreamReply(
+ StreamMode streamMode,
+ Func pull,
+ Func terminate,
+ Func halt,
+ Func resume)
+ {
+ StreamMode = streamMode;
+ _pull = pull;
+ _terminate = terminate;
+ _halt = halt;
+ _resume = resume;
+ }
+
+ ///
+ /// Requests the next item of a pull stream.
+ ///
+ 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();
+ }
+
+ ///
+ /// Terminates the remote stream execution and releases its enumerator.
+ ///
+ 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;
+ }
+
+ ///
+ /// Halts a pausable remote stream execution.
+ ///
+ public AsyncReply Halt()
+ {
+ lock (_streamLock)
+ {
+ if (_streamCompleted)
+ return new AsyncReply(null);
+ }
+
+ var reply = _halt();
+ reply.Error(TriggerStreamError);
+ return reply;
+ }
+
+ ///
+ /// Resumes a halted remote stream execution.
+ ///
+ 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);
+ }
+
+ ///
+ 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 NewCompletionSource()
+ => new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+}
+
+///
+/// A typed remote stream that can be consumed with await foreach.
+/// Pull streams request exactly one remote item for each MoveNextAsync call.
+///
+public sealed class AsyncStreamReply : AsyncStreamReply, IAsyncEnumerable
+{
+ readonly object _itemsLock = new object();
+ readonly Queue _items = new Queue();
+
+ TaskCompletionSource _itemAvailable;
+ bool _enumeratorCreated;
+ bool _movePending;
+
+ internal AsyncStreamReply(
+ StreamMode streamMode,
+ Func pull,
+ Func terminate,
+ Func halt,
+ Func resume)
+ : base(streamMode, pull, terminate, halt, resume)
+ {
+ Chunk(value => ReceiveItem((T)RuntimeCaster.Cast(value, typeof(T))));
+ }
+
+ ///
+ public IAsyncEnumerator 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 itemAvailable;
+
+ lock (_itemsLock)
+ {
+ if (IsStreamCompleted() || GetStreamException() != null)
+ return;
+
+ _items.Enqueue(item);
+ itemAvailable = _itemAvailable;
+ _itemAvailable = null;
+ }
+
+ itemAvailable?.TrySetResult(true);
+ }
+
+ protected override void OnStreamCompleted()
+ {
+ TaskCompletionSource itemAvailable;
+
+ lock (_itemsLock)
+ {
+ itemAvailable = _itemAvailable;
+ _itemAvailable = null;
+ }
+
+ itemAvailable?.TrySetResult(true);
+ }
+
+ protected override void OnStreamError(Exception exception)
+ {
+ TaskCompletionSource 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 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
+ {
+ readonly AsyncStreamReply _owner;
+ readonly CancellationToken _cancellationToken;
+
+ public T Current { get; private set; }
+
+ internal Enumerator(AsyncStreamReply owner, CancellationToken cancellationToken)
+ {
+ _owner = owner;
+ _cancellationToken = cancellationToken;
+ }
+
+ public async ValueTask MoveNextAsync()
+ {
+ var result = await _owner.MoveNextAsync(_cancellationToken);
+ Current = result.Value;
+ return result.HasValue;
+ }
+
+ public ValueTask DisposeAsync() => new ValueTask(_owner.DisposeAsync());
+ }
+}
diff --git a/Libraries/Esiur/Core/ExceptionCode.cs b/Libraries/Esiur/Core/ExceptionCode.cs
index b5624fa..90323d4 100644
--- a/Libraries/Esiur/Core/ExceptionCode.cs
+++ b/Libraries/Esiur/Core/ExceptionCode.cs
@@ -46,5 +46,6 @@ public enum ExceptionCode : ushort
Timeout,
NotSupported,
NotImplemented,
- NotAllowed
+ NotAllowed,
+ RateLimitExceeded
}
diff --git a/Libraries/Esiur/Core/InvocationContext.cs b/Libraries/Esiur/Core/InvocationContext.cs
index 9c5e847..73dbe73 100644
--- a/Libraries/Esiur/Core/InvocationContext.cs
+++ b/Libraries/Esiur/Core/InvocationContext.cs
@@ -1,15 +1,47 @@
-using Esiur.Protocol;
+using Esiur.Data.Types;
+using Esiur.Protocol;
using System;
+using System.Collections;
using System.Collections.Generic;
-using System.Text;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
namespace Esiur.Core
{
public class InvocationContext
{
- private uint CallbackId;
+ readonly uint CallbackId;
+ readonly object _stateLock = new object();
+ readonly CancellationTokenSource _cancellation = new CancellationTokenSource();
+ readonly SemaphoreSlim _moveLock = new SemaphoreSlim(1, 1);
+ TaskCompletionSource _resumeSignal = CompletedSignal();
- internal bool Ended;
+ IAsyncEnumeratorAdapter _asyncEnumerator;
+ IEnumerator _enumerator;
+ bool _halted;
+
+ internal StreamMode StreamMode { get; private set; }
+ internal bool Pausable { get; private set; }
+
+ internal volatile bool Ended;
+
+ ///
+ /// Gets a token that is cancelled when the caller terminates this execution.
+ ///
+ public CancellationToken CancellationToken => _cancellation.Token;
+
+ ///
+ /// Gets whether a pausable execution is currently halted.
+ ///
+ public bool IsHalted
+ {
+ get
+ {
+ lock (_stateLock)
+ return _halted;
+ }
+ }
public void Chunk(object value)
{
@@ -19,8 +51,8 @@ namespace Esiur.Core
Connection.SendChunk(CallbackId, value);
}
- public void Progress(uint value, uint max) {
-
+ public void Progress(uint value, uint max)
+ {
if (Ended)
throw new Exception("Execution has ended.");
@@ -29,7 +61,6 @@ namespace Esiur.Core
public void Warning(byte level, string message)
{
-
if (Ended)
throw new Exception("Execution has ended.");
@@ -38,12 +69,192 @@ namespace Esiur.Core
public EpConnection Connection { get; internal set; }
-
internal InvocationContext(EpConnection connection, uint callbackId)
{
Connection = connection;
CallbackId = callbackId;
+ }
+ internal void InitializeStream(StreamMode streamMode, bool pausable)
+ {
+ StreamMode = streamMode;
+ Pausable = pausable;
+ }
+
+ internal bool SetAsyncEnumerable(object enumerable)
+ {
+ var asyncEnumerableInterface = enumerable.GetType()
+ .GetInterfaces()
+ .Concat(new[] { enumerable.GetType() })
+ .FirstOrDefault(x => x.IsGenericType &&
+ x.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>));
+
+ if (asyncEnumerableInterface == null)
+ return false;
+
+ var itemType = asyncEnumerableInterface.GetGenericArguments()[0];
+ var adapterType = typeof(AsyncEnumeratorAdapter<>).MakeGenericType(itemType);
+ _asyncEnumerator = (IAsyncEnumeratorAdapter)Activator.CreateInstance(
+ adapterType, enumerable, _cancellation.Token);
+ return true;
+ }
+
+ internal void SetEnumerable(IEnumerable enumerable)
+ {
+ _enumerator = enumerable.GetEnumerator();
+ }
+
+ internal async Task<(bool HasValue, object Value)> PullAsync()
+ {
+ if (StreamMode != StreamMode.Pull || _asyncEnumerator == null)
+ throw new InvalidOperationException("Execution is not an asynchronous pull stream.");
+
+ await _moveLock.WaitAsync(_cancellation.Token);
+ try
+ {
+ await WaitWhileHaltedAsync();
+ _cancellation.Token.ThrowIfCancellationRequested();
+
+ var hasValue = await _asyncEnumerator.MoveNextAsync();
+ return (hasValue, hasValue ? _asyncEnumerator.Current : null);
+ }
+ finally
+ {
+ _moveLock.Release();
+ }
+ }
+
+ internal async Task<(bool HasValue, object Value)> MoveNextAsync()
+ {
+ if (_enumerator == null)
+ throw new InvalidOperationException("Execution is not a synchronous stream.");
+
+ await WaitWhileHaltedAsync();
+ _cancellation.Token.ThrowIfCancellationRequested();
+
+ var hasValue = _enumerator.MoveNext();
+ return (hasValue, hasValue ? _enumerator.Current : null);
+ }
+
+ internal bool Halt()
+ {
+ lock (_stateLock)
+ {
+ if (Ended || !Pausable || _halted)
+ return false;
+
+ _halted = true;
+ _resumeSignal = NewSignal();
+ return true;
+ }
+ }
+
+ internal bool Resume()
+ {
+ TaskCompletionSource resumeSignal;
+
+ lock (_stateLock)
+ {
+ if (Ended || !Pausable || !_halted)
+ return false;
+
+ _halted = false;
+ resumeSignal = _resumeSignal;
+ }
+
+ resumeSignal.TrySetResult(true);
+ return true;
+ }
+
+ ///
+ /// Asynchronously waits until a halted execution is resumed.
+ ///
+ public async Task WaitWhileHaltedAsync()
+ {
+ Task resumeTask;
+
+ lock (_stateLock)
+ resumeTask = _resumeSignal.Task;
+
+ await resumeTask;
+ _cancellation.Token.ThrowIfCancellationRequested();
+ }
+
+ internal async Task TerminateAsync()
+ {
+ TaskCompletionSource resumeSignal;
+
+ lock (_stateLock)
+ {
+ if (Ended)
+ return;
+
+ Ended = true;
+ _halted = false;
+ resumeSignal = _resumeSignal;
+ }
+
+ resumeSignal.TrySetResult(true);
+ _cancellation.Cancel();
+
+ if (_asyncEnumerator != null)
+ await _asyncEnumerator.DisposeAsync();
+
+ (_enumerator as IDisposable)?.Dispose();
+ }
+
+ internal async Task EndAsync()
+ {
+ TaskCompletionSource resumeSignal;
+
+ lock (_stateLock)
+ {
+ if (Ended)
+ return;
+
+ Ended = true;
+ _halted = false;
+ resumeSignal = _resumeSignal;
+ }
+
+ resumeSignal.TrySetResult(true);
+
+ if (_asyncEnumerator != null)
+ await _asyncEnumerator.DisposeAsync();
+
+ (_enumerator as IDisposable)?.Dispose();
+ }
+
+ static TaskCompletionSource NewSignal()
+ => new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ static TaskCompletionSource CompletedSignal()
+ {
+ var signal = NewSignal();
+ signal.SetResult(true);
+ return signal;
}
}
+
+ interface IAsyncEnumeratorAdapter
+ {
+ object Current { get; }
+ ValueTask MoveNextAsync();
+ ValueTask DisposeAsync();
+ }
+
+ sealed class AsyncEnumeratorAdapter : IAsyncEnumeratorAdapter
+ {
+ readonly IAsyncEnumerator _enumerator;
+
+ public object Current => _enumerator.Current;
+
+ public AsyncEnumeratorAdapter(IAsyncEnumerable enumerable, CancellationToken cancellationToken)
+ {
+ _enumerator = enumerable.GetAsyncEnumerator(cancellationToken);
+ }
+
+ public ValueTask MoveNextAsync() => _enumerator.MoveNextAsync();
+ public ValueTask DisposeAsync() => _enumerator.DisposeAsync();
+ }
}
diff --git a/Libraries/Esiur/Data/Types/ConstantDef.cs b/Libraries/Esiur/Data/Types/ConstantDef.cs
index af003b1..17c72c7 100644
--- a/Libraries/Esiur/Data/Types/ConstantDef.cs
+++ b/Libraries/Esiur/Data/Types/ConstantDef.cs
@@ -14,7 +14,6 @@ public class ConstantDef : MemberDef
{
public object Value { get; set; }
- public Map Annotations { get; set; }
public Tru ValueType { get; set; }
public FieldInfo FieldInfo { get; set; }
@@ -128,7 +127,7 @@ public class ConstantDef : MemberDef
- return new ConstantDef()
+ return DefinitionAttributeReader.Apply(ci, new ConstantDef()
{
Name = customName,
Index = index,
@@ -137,7 +136,7 @@ public class ConstantDef : MemberDef
Value = value,
FieldInfo = ci,
Annotations = annotations,
- };
+ });
}
diff --git a/Libraries/Esiur/Data/Types/DefinitionAttributeReader.cs b/Libraries/Esiur/Data/Types/DefinitionAttributeReader.cs
new file mode 100644
index 0000000..a6ce8fe
--- /dev/null
+++ b/Libraries/Esiur/Data/Types/DefinitionAttributeReader.cs
@@ -0,0 +1,74 @@
+using Esiur.Resource;
+using System;
+using System.Linq;
+using System.Reflection;
+
+namespace Esiur.Data.Types;
+
+internal static class DefinitionAttributeReader
+{
+ internal static void Apply(Type source, TypeDef target)
+ {
+ target.Usage = source.GetCustomAttribute(true)?.Value;
+ target.Description = source.GetCustomAttribute(true)?.Value;
+ target.Example = source.GetCustomAttributes(true)
+ .Select(x => x.Value)
+ .FirstOrDefault();
+ target.Category = source.GetCustomAttribute(true)?.Value;
+ target.Since = source.GetCustomAttribute(true)?.Value;
+ }
+
+ internal static T Apply(MemberInfo source, T target) where T : MemberDef
+ {
+ target.Description = source.GetCustomAttribute(true)?.Value;
+ target.Usage = source.GetCustomAttribute(true)?.Value;
+
+ var examples = source.GetCustomAttributes(true)
+ .Select(x => x.Value)
+ .ToList();
+ target.Examples = examples.Count == 0 ? null : examples;
+
+ var tags = source.GetCustomAttribute(true)?.Values;
+ target.Tags = tags == null || tags.Length == 0 ? null : tags.ToList();
+ target.Unit = source.GetCustomAttribute(true)?.Value;
+ target.Minimum = source.GetCustomAttribute(true)?.Value;
+ target.Maximum = source.GetCustomAttribute(true)?.Value;
+
+ var allowedValues = source.GetCustomAttributes(true)
+ .Select(x => x.Value)
+ .ToList();
+ target.AllowedValues = allowedValues.Count == 0 ? null : allowedValues;
+
+ target.Pattern = source.GetCustomAttribute(true)?.Value;
+ target.Format = source.GetCustomAttribute(true)?.Value;
+
+ var preconditions = source.GetCustomAttributes(true)
+ .Select(x => x.Value)
+ .ToList();
+ target.Preconditions = preconditions.Count == 0 ? null : preconditions;
+
+ var postconditions = source.GetCustomAttributes(true)
+ .Select(x => x.Value)
+ .ToList();
+ target.Postconditions = postconditions.Count == 0 ? null : postconditions;
+
+ target.Effects = source.GetCustomAttribute(true)?.Value
+ ?? OperationEffects.None;
+
+ var warnings = source.GetCustomAttributes(true)
+ .Select(x => x.Value)
+ .ToList();
+ target.Warnings = warnings.Count == 0 ? null : warnings;
+
+ var relatedMembers = source.GetCustomAttribute(true)?.Indexes;
+ target.RelatedMembers = relatedMembers == null || relatedMembers.Length == 0
+ ? null
+ : relatedMembers.ToList();
+
+ var obsolete = source.GetCustomAttribute(true);
+ target.Deprecated = obsolete != null;
+ target.DeprecationMessage = obsolete?.Message;
+
+ return target;
+ }
+}
diff --git a/Libraries/Esiur/Data/Types/EventDef.cs b/Libraries/Esiur/Data/Types/EventDef.cs
index 2710e72..537c56f 100644
--- a/Libraries/Esiur/Data/Types/EventDef.cs
+++ b/Libraries/Esiur/Data/Types/EventDef.cs
@@ -15,12 +15,6 @@ namespace Esiur.Data.Types;
public class EventDef : MemberDef
{
- public Map Annotations
- {
- get;
- set;
- }
-
public override string ToString()
{
return $"{Name}: {ArgumentType}";
@@ -35,7 +29,9 @@ public class EventDef : MemberDef
set => AutoDelivered = !value;
}
- public bool Deprecated { get; set; }
+ public bool Historical { get; set; }
+
+ public OrderingControl OrderingControl { get; set; }
public EventInfo EventInfo { get; set; }
@@ -166,7 +162,9 @@ public class EventDef : MemberDef
throw new Exception($"Unsupported type `{argType}` in event `{type.Name}.{ei.Name}`");
var annotationAttrs = ei.GetCustomAttributes(true);
- var autoDeliveredAttr = ei.GetCustomAttribute(true);
+ var autoDeliveryAttr = ei.GetCustomAttribute(true);
+ var historicalAttr = ei.GetCustomAttribute(true);
+ var orderingAttr = ei.GetCustomAttribute(true);
//evtType.Nullable = new NullabilityInfoContext().Create(ei).ReadState is NullabilityState.Nullable;
@@ -209,7 +207,7 @@ public class EventDef : MemberDef
}
- return new EventDef()
+ return DefinitionAttributeReader.Apply(ei, new EventDef()
{
Name = name,
ArgumentType = evtType,
@@ -217,8 +215,10 @@ public class EventDef : MemberDef
Inherited = ei.DeclaringType != type,
Annotations = annotations,
EventInfo = ei,
- AutoDelivered = autoDeliveredAttr != null
- };
+ AutoDelivered = autoDeliveryAttr != null,
+ Historical = historicalAttr != null,
+ OrderingControl = orderingAttr?.Control ?? OrderingControl.Strict,
+ });
}
}
diff --git a/Libraries/Esiur/Data/Types/FunctionDef.cs b/Libraries/Esiur/Data/Types/FunctionDef.cs
index cdeda32..b3f8de3 100644
--- a/Libraries/Esiur/Data/Types/FunctionDef.cs
+++ b/Libraries/Esiur/Data/Types/FunctionDef.cs
@@ -15,12 +15,6 @@ namespace Esiur.Data.Types;
public class FunctionDef : MemberDef
{
- public Map Annotations
- {
- get;
- set;
- }
-
//public bool IsVoid
//{
// get;
@@ -33,7 +27,7 @@ public class FunctionDef : MemberDef
public bool ReadOnly { get; set; }
public bool Idempotent { get; set; }
public bool Cancellable { get; set; }
- public bool Deprecated { get; set; }
+ public bool Pausable { get; set; }
//public FunctionDefFlags Flags { get; set; }
public StreamMode StreamMode { get; set; }
@@ -175,28 +169,61 @@ public class FunctionDef : MemberDef
{
var genericRtType = mi.ReturnType.IsGenericType ? mi.ReturnType.GetGenericTypeDefinition() : null;
-
+ var streamAttribute = mi.GetCustomAttribute(true);
+ var streamMode = StreamMode.None;
+ var pausable = false;
Tru rtType;
- if (genericRtType == typeof(AsyncReply<>))
+ if (streamAttribute != null &&
+ genericRtType != typeof(AsyncReply<>) &&
+ genericRtType != typeof(AsyncStreamReply<>))
+ throw new Exception($"Method `{type.Name}.{mi.Name}` uses StreamAttribute and must return AsyncReply or AsyncStreamReply.");
+
+ if (genericRtType == typeof(IAsyncEnumerable<>))
{
+ streamMode = StreamMode.Pull;
rtType = Tru.FromType(mi.ReturnType.GetGenericArguments()[0], warehouse);
}
- else if (genericRtType == typeof(Task<>))
+ else if (genericRtType == typeof(IEnumerable<>))
{
+ streamMode = StreamMode.Push;
rtType = Tru.FromType(mi.ReturnType.GetGenericArguments()[0], warehouse);
}
- else if (genericRtType == typeof(IEnumerable<>) || genericRtType == typeof(IAsyncEnumerable<>))
+ else if (genericRtType == typeof(AsyncStreamReply<>))
+ {
+ if (streamAttribute == null)
+ throw new Exception($"Method `{type.Name}.{mi.Name}` returning AsyncStreamReply must declare StreamAttribute.");
+
+ streamMode = streamAttribute.Mode;
+ rtType = Tru.FromType(mi.ReturnType.GetGenericArguments()[0], warehouse);
+ }
+ else if (streamAttribute != null)
+ {
+ streamMode = streamAttribute.Mode;
+ rtType = Tru.FromType(mi.ReturnType.GetGenericArguments()[0], warehouse);
+ }
+ else if (genericRtType == typeof(AsyncReply<>) || genericRtType == typeof(Task<>))
{
- // get export
rtType = Tru.FromType(mi.ReturnType.GetGenericArguments()[0], warehouse);
}
else
{
- if (mi.ReturnType == typeof(Task))
- rtType = Tru.FromType(null, warehouse);
- else
- rtType = Tru.FromType(mi.ReturnType, warehouse);
+ rtType = mi.ReturnType == typeof(Task)
+ ? Tru.FromType(null, warehouse)
+ : Tru.FromType(mi.ReturnType, warehouse);
+ }
+
+ if (streamAttribute != null)
+ {
+ if (streamAttribute.Mode != StreamMode.Push && streamAttribute.Mode != StreamMode.Pull)
+ throw new Exception($"Stream method `{type.Name}.{mi.Name}` must use Push or Pull mode.");
+
+ if (streamMode != streamAttribute.Mode)
+ throw new Exception($"Stream mode `{streamAttribute.Mode}` conflicts with return type `{mi.ReturnType}` in method `{type.Name}.{mi.Name}`.");
+
+ pausable = streamAttribute.Pausable;
+ if (pausable && streamMode != StreamMode.Push)
+ throw new Exception($"Only push stream method `{type.Name}.{mi.Name}` can be pausable.");
}
if (rtType == null)
@@ -237,7 +264,12 @@ public class FunctionDef : MemberDef
//var rtFlags = rtNullableAttr?.Flags?.ToList() ?? new List();
//var rtFlags = ((byte[])rtNullableAttr?.NullableFlags ?? new byte[0]).ToList();
- if (rtNullableAttrFlags.Count > 0 && genericRtType == typeof(AsyncReply<>))
+ if (rtNullableAttrFlags.Count > 0 &&
+ (genericRtType == typeof(AsyncReply<>) ||
+ genericRtType == typeof(Task<>) ||
+ genericRtType == typeof(IEnumerable<>) ||
+ genericRtType == typeof(IAsyncEnumerable<>) ||
+ genericRtType == typeof(AsyncStreamReply<>)))
rtNullableAttrFlags.RemoveAt(0);
if (rtNullableContextAttrFlag == 2)
@@ -333,7 +365,7 @@ public class FunctionDef : MemberDef
}
- return new FunctionDef()
+ return DefinitionAttributeReader.Apply(mi, new FunctionDef()
{
Name = name,
Index = index,
@@ -342,8 +374,14 @@ public class FunctionDef : MemberDef
ReturnType = rtType,
Arguments = arguments,
MethodInfo = mi,
- Annotations = annotations
- };
+ Annotations = annotations,
+ ReadOnly = mi.GetCustomAttribute(true) != null,
+ Idempotent = mi.GetCustomAttribute(true) != null,
+ Cancellable = mi.GetCustomAttribute(true) != null,
+ RatePolicyName = mi.GetCustomAttribute(true)?.PolicyName,
+ StreamMode = streamMode,
+ Pausable = pausable,
+ });
}
diff --git a/Libraries/Esiur/Data/Types/FunctionDefFlags.cs b/Libraries/Esiur/Data/Types/FunctionDefFlags.cs
index 20a57e7..e9e9ddc 100644
--- a/Libraries/Esiur/Data/Types/FunctionDefFlags.cs
+++ b/Libraries/Esiur/Data/Types/FunctionDefFlags.cs
@@ -15,5 +15,6 @@ namespace Esiur.Data.Types
ReadOnly = 0x08,
Idempotent = 0x10,
Cancellable = 0x20,
+ Pausable = 0x40,
}
-}
\ No newline at end of file
+}
diff --git a/Libraries/Esiur/Data/Types/LocalTypeDef.cs b/Libraries/Esiur/Data/Types/LocalTypeDef.cs
index 663cfd0..5b1b2eb 100644
--- a/Libraries/Esiur/Data/Types/LocalTypeDef.cs
+++ b/Libraries/Esiur/Data/Types/LocalTypeDef.cs
@@ -61,6 +61,9 @@ public class LocalTypeDef:TypeDef
if (genericType == typeof(List<>)
|| genericType == typeof(PropertyContext<>)
|| genericType == typeof(AsyncReply<>)
+ || genericType == typeof(Task<>)
+ || genericType == typeof(IEnumerable<>)
+ || genericType == typeof(IAsyncEnumerable<>)
|| genericType == typeof(ResourceLink<>))
{
return GetDistributedTypes(genericTypeArgs[0]);
@@ -273,6 +276,8 @@ public class LocalTypeDef:TypeDef
_typeName = GetTypeName(type);
+ DefinitionAttributeReader.Apply(type, this);
+
warehouse.TryRegisterLocalTypeDef(this);
diff --git a/Libraries/Esiur/Data/Types/MemberDef.cs b/Libraries/Esiur/Data/Types/MemberDef.cs
index a93daa2..8bdcb7a 100644
--- a/Libraries/Esiur/Data/Types/MemberDef.cs
+++ b/Libraries/Esiur/Data/Types/MemberDef.cs
@@ -16,6 +16,14 @@ public class MemberDef
public bool Inherited { get; set; }
+ public bool Deprecated { get; set; }
+
+ ///
+ /// Name of the server-side Warehouse rate policy applied to this member.
+ /// This is local execution metadata and is not sent to remote clients.
+ ///
+ public string? RatePolicyName { get; set; }
+
public TypeDef Definition { get; set; } = null!;
// Human-readable metadata
@@ -54,6 +62,8 @@ public class MemberDef
// Compatibility guidance
public string? DeprecationMessage { get; set; }
+ public Map? Annotations { get; set; }
+
public string Fullname =>
Definition is null || string.IsNullOrEmpty(Definition.Name)
? Name
diff --git a/Libraries/Esiur/Data/Types/MemberDefField.cs b/Libraries/Esiur/Data/Types/MemberDefField.cs
index 0fb550f..ac46d67 100644
--- a/Libraries/Esiur/Data/Types/MemberDefField.cs
+++ b/Libraries/Esiur/Data/Types/MemberDefField.cs
@@ -34,5 +34,8 @@ namespace Esiur.Data.Types
// Compatibility guidance
DeprecationMessage = 0x2F, // string
+
+ // Extensible application metadata
+ Annotations = 0x30, // Map
}
}
diff --git a/Libraries/Esiur/Data/Types/MemberDefInfo.cs b/Libraries/Esiur/Data/Types/MemberDefInfo.cs
index e075640..c57047b 100644
--- a/Libraries/Esiur/Data/Types/MemberDefInfo.cs
+++ b/Libraries/Esiur/Data/Types/MemberDefInfo.cs
@@ -68,4 +68,7 @@ public class MemberDefInfo : IndexedStructure
[Index((int)MemberDefField.DeprecationMessage)]
public string? DeprecationMessage { get; set; }
+
+ [Index((int)MemberDefField.Annotations)]
+ public Map? Annotations { get; set; }
}
diff --git a/Libraries/Esiur/Data/Types/PropertyDef.cs b/Libraries/Esiur/Data/Types/PropertyDef.cs
index feabe22..4512396 100644
--- a/Libraries/Esiur/Data/Types/PropertyDef.cs
+++ b/Libraries/Esiur/Data/Types/PropertyDef.cs
@@ -14,11 +14,6 @@ namespace Esiur.Data.Types;
public class PropertyDef : MemberDef
{
- public Map Annotations { get; set; }
-
-
-
-
public PropertyInfo PropertyInfo
{
get;
@@ -52,10 +47,15 @@ public class PropertyDef : MemberDef
}
public bool Constant { get; set; }
+ public bool Volatile { get; set; }
//public bool IsNullable { get; set; }
public bool Historical { get; set; }
+ public OrderingControl OrderingControl { get; set; }
+
+ public object? DefaultValue { get; set; }
+
public bool HasHistory
{
get => Historical;
@@ -258,6 +258,10 @@ public class PropertyDef : MemberDef
var annotationAttrs = pi.GetCustomAttributes(true);
var historicalAttr = pi.GetCustomAttribute(true);
+ var readOnlyAttr = pi.GetCustomAttribute(true);
+ var volatileAttr = pi.GetCustomAttribute(true);
+ var orderingAttr = pi.GetCustomAttribute(true);
+ var defaultValueAttr = pi.GetCustomAttribute(true);
//var nullabilityContext = new NullabilityInfoContext();
//propType.Nullable = nullabilityContext.Create(pi).ReadState is NullabilityState.Nullable;
@@ -306,16 +310,21 @@ public class PropertyDef : MemberDef
}
- return new PropertyDef()
+ return DefinitionAttributeReader.Apply(pi, new PropertyDef()
{
Name = name,
Index = index,
Inherited = pi.DeclaringType != type,
ValueType = propType,
PropertyInfo = pi,
- Historical = historicalAttr == null,
+ RatePolicyName = pi.GetCustomAttribute(true)?.PolicyName,
+ ReadOnly = readOnlyAttr != null,
+ Volatile = volatileAttr != null,
+ Historical = historicalAttr != null,
+ OrderingControl = orderingAttr?.Control ?? OrderingControl.Strict,
+ DefaultValue = defaultValueAttr?.Value,
Annotations = annotations,
- };
+ });
}
@@ -325,7 +334,7 @@ public class PropertyDef : MemberDef
PropertyPermission permission, TypeDef typeDef)
{
var definition = MakePropertyDef(warehouse, type, pi, name, index, typeDef);
- definition.Permission = permission;
+ definition.ReadOnly = definition.ReadOnly || permission == PropertyPermission.Read;
return definition;
}
diff --git a/Libraries/Esiur/Data/Types/RemoteTypeDef.cs b/Libraries/Esiur/Data/Types/RemoteTypeDef.cs
index 6e6fc5a..d70c69d 100644
--- a/Libraries/Esiur/Data/Types/RemoteTypeDef.cs
+++ b/Libraries/Esiur/Data/Types/RemoteTypeDef.cs
@@ -194,6 +194,11 @@ public class RemoteTypeDef:TypeDef
: $"{info.Namespace}.{info.Name}";
definition._typeDefKind = info.Kind;
definition._version = info.Version;
+ definition.Usage = info.Usage;
+ definition.Description = info.Description;
+ definition.Example = info.Example;
+ definition.Category = info.Category;
+ definition.Since = info.Since;
definition.Annotations = info.Annotations;
definition._properties.Clear();
@@ -218,6 +223,7 @@ public class RemoteTypeDef:TypeDef
member.Index = info.Index;
member.Name = info.Name;
member.Inherited = (info.Flags & (byte)MemberDefFlags.Inherited) != 0;
+ member.Deprecated = (info.Flags & (byte)MemberDefFlags.Deprecated) != 0;
member.Description = info.Description;
member.Usage = info.Usage;
member.Examples = info.Examples;
@@ -234,6 +240,7 @@ public class RemoteTypeDef:TypeDef
member.Warnings = info.Warnings;
member.RelatedMembers = info.RelatedMembers;
member.DeprecationMessage = info.DeprecationMessage;
+ member.Annotations = info.Annotations;
return member;
}
@@ -245,7 +252,10 @@ public class RemoteTypeDef:TypeDef
ValueType = info.ValueType,
ReadOnly = flags.HasFlag(PropertyDefFlags.ReadOnly),
Constant = flags.HasFlag(PropertyDefFlags.Constant),
+ Volatile = flags.HasFlag(PropertyDefFlags.Volatile),
Historical = flags.HasFlag(PropertyDefFlags.Historical) || info.HistoryControl != 0,
+ OrderingControl = info.OrderingControl,
+ DefaultValue = info.DefaultValue,
});
}
@@ -260,7 +270,7 @@ public class RemoteTypeDef:TypeDef
ReadOnly = flags.HasFlag(FunctionDefFlags.ReadOnly),
Idempotent = flags.HasFlag(FunctionDefFlags.Idempotent),
Cancellable = flags.HasFlag(FunctionDefFlags.Cancellable),
- Deprecated = flags.HasFlag(FunctionDefFlags.Deprecated),
+ Pausable = flags.HasFlag(FunctionDefFlags.Pausable),
Arguments = info.Arguments?.Select(ToArgument).ToArray() ?? Array.Empty(),
});
}
@@ -275,6 +285,7 @@ public class RemoteTypeDef:TypeDef
Optional = flags.HasFlag(ArgumentDefFlags.Optional),
Type = info.ValueType,
DefaultValue = info.DefaultValue,
+ Annotations = info.Annotations,
};
}
@@ -285,7 +296,8 @@ public class RemoteTypeDef:TypeDef
{
ArgumentType = info.ArgumentType,
AutoDelivered = flags.HasFlag(EventDefFlags.AutoDelivered),
- Deprecated = flags.HasFlag(EventDefFlags.Deprecated),
+ Historical = flags.HasFlag(EventDefFlags.Historical) || info.HistoryControl != 0,
+ OrderingControl = info.OrderingControl,
});
}
diff --git a/Libraries/Esiur/Data/Types/TypeDef.cs b/Libraries/Esiur/Data/Types/TypeDef.cs
index 6e5b75c..d23e1c6 100644
--- a/Libraries/Esiur/Data/Types/TypeDef.cs
+++ b/Libraries/Esiur/Data/Types/TypeDef.cs
@@ -23,6 +23,16 @@ public class TypeDef
public Map Annotations { get; set; }
+ public string? Usage { get; set; }
+
+ public string? Description { get; set; }
+
+ public object? Example { get; set; }
+
+ public string? Category { get; set; }
+
+ public string? Since { get; set; }
+
protected string _typeName;
protected List _functions = new List();
protected List _events = new List();
diff --git a/Libraries/Esiur/Data/Types/TypeDefInfo.cs b/Libraries/Esiur/Data/Types/TypeDefInfo.cs
index 8a82bc2..5d6ffe5 100644
--- a/Libraries/Esiur/Data/Types/TypeDefInfo.cs
+++ b/Libraries/Esiur/Data/Types/TypeDefInfo.cs
@@ -66,6 +66,11 @@ public class TypeDefInfo : IndexedStructure
Name = definition.Name,
Kind = definition.Kind,
Parent = definition.ParentTypeId,
+ Usage = definition.Usage,
+ Description = definition.Description,
+ Example = definition.Example,
+ Category = definition.Category,
+ Since = definition.Since,
Annotations = definition.Annotations,
Properties = definition.Properties.Select(FromProperty).ToList(),
Functions = definition.Functions.Select(FromFunction).ToList(),
@@ -94,21 +99,26 @@ public class TypeDefInfo : IndexedStructure
target.Warnings = source.Warnings;
target.RelatedMembers = source.RelatedMembers;
target.DeprecationMessage = source.DeprecationMessage;
+ target.Annotations = source.Annotations;
return target;
}
private static PropertyDefInfo FromProperty(PropertyDef source)
{
var flags = source.Inherited ? PropertyDefFlags.Inherited : PropertyDefFlags.None;
+ if (source.Deprecated) flags |= PropertyDefFlags.Deprecated;
if (source.ReadOnly) flags |= PropertyDefFlags.ReadOnly;
if (source.Constant) flags |= PropertyDefFlags.Constant;
+ if (source.Volatile) flags |= PropertyDefFlags.Volatile;
if (source.Historical) flags |= PropertyDefFlags.Historical;
return CopyMember(source, new PropertyDefInfo
{
Flags = (byte)flags,
ValueType = source.ValueType,
+ OrderingControl = source.OrderingControl,
HistoryControl = source.Historical ? (byte)1 : (byte)0,
+ DefaultValue = source.DefaultValue,
});
}
@@ -120,6 +130,7 @@ public class TypeDefInfo : IndexedStructure
if (source.ReadOnly) flags |= FunctionDefFlags.ReadOnly;
if (source.Idempotent) flags |= FunctionDefFlags.Idempotent;
if (source.Cancellable) flags |= FunctionDefFlags.Cancellable;
+ if (source.Pausable) flags |= FunctionDefFlags.Pausable;
return CopyMember(source, new FunctionDefInfo
{
@@ -139,6 +150,7 @@ public class TypeDefInfo : IndexedStructure
Flags = source.Optional ? (byte)ArgumentDefFlags.Optional : (byte)ArgumentDefFlags.None,
ValueType = source.Type,
DefaultValue = source.DefaultValue,
+ Annotations = source.Annotations,
};
}
@@ -147,17 +159,21 @@ public class TypeDefInfo : IndexedStructure
var flags = source.Inherited ? EventDefFlags.Inherited : EventDefFlags.None;
if (source.Deprecated) flags |= EventDefFlags.Deprecated;
if (source.AutoDelivered) flags |= EventDefFlags.AutoDelivered;
+ if (source.Historical) flags |= EventDefFlags.Historical;
return CopyMember(source, new EventDefInfo
{
Flags = (byte)flags,
ArgumentType = source.ArgumentType,
+ OrderingControl = source.OrderingControl,
+ HistoryControl = source.Historical ? (byte)1 : (byte)0,
});
}
private static ConstantDefInfo FromConstant(ConstantDef source)
{
var flags = source.Inherited ? ConstantDefFlags.Inherited : ConstantDefFlags.None;
+ if (source.Deprecated) flags |= ConstantDefFlags.Deprecated;
return CopyMember(source, new ConstantDefInfo
{
Flags = (byte)flags,
diff --git a/Libraries/Esiur/Net/NetworkServer.cs b/Libraries/Esiur/Net/NetworkServer.cs
index 27baf6c..09123a0 100644
--- a/Libraries/Esiur/Net/NetworkServer.cs
+++ b/Libraries/Esiur/Net/NetworkServer.cs
@@ -103,7 +103,7 @@ public abstract class NetworkServer : IDestructible where TConnecti
if (s == null)
{
- Global.Log("NetworkServer", LogType.Error, "sock == null");
+ //Global.Log("NetworkServer", LogType.Error, "sock == null");
return;
}
diff --git a/Libraries/Esiur/Net/Packets/EpPacketRequest.cs b/Libraries/Esiur/Net/Packets/EpPacketRequest.cs
index 516d335..1e01cf4 100644
--- a/Libraries/Esiur/Net/Packets/EpPacketRequest.cs
+++ b/Libraries/Esiur/Net/Packets/EpPacketRequest.cs
@@ -37,6 +37,7 @@ namespace Esiur.Net.Packets
PullStream = 0x1C,
TerminateExecution = 0x1D,
HaltExecution = 0x1E,
+ ResumeExecution = 0x1F,
}
}
diff --git a/Libraries/Esiur/Protocol/EpConnection.cs b/Libraries/Esiur/Protocol/EpConnection.cs
index 58ec131..25961aa 100644
--- a/Libraries/Esiur/Protocol/EpConnection.cs
+++ b/Libraries/Esiur/Protocol/EpConnection.cs
@@ -454,6 +454,7 @@ public partial class EpConnection : NetworkConnection, IStore
public override void Destroy()
{
+ TerminateInvocations();
UnsubscribeAll();
this.OnReady = null;
this.OnError = null;
@@ -477,7 +478,9 @@ public partial class EpConnection : NetworkConnection, IStore
{
offset += (uint)rt;
- if (_packet.Tdu == null)
+ if (_packet.Tdu == null &&
+ _packet.Method != EpPacketMethod.Reply &&
+ _packet.Method != EpPacketMethod.Extension)
return offset;
#if VERBOSE
@@ -514,6 +517,9 @@ public partial class EpConnection : NetworkConnection, IStore
}
else if (_packet.Method == EpPacketMethod.Request)
{
+ if (IsRateControlBlocked)
+ return offset;
+
var dt = _packet.Tdu.Value;
switch (_packet.Request)
@@ -582,37 +588,58 @@ public partial class EpConnection : NetworkConnection, IStore
case EpPacketRequest.StaticCall:
EpRequestStaticCall(_packet.CallbackId, dt);
break;
+ case EpPacketRequest.PullStream:
+ EpRequestPullStream(_packet.CallbackId, dt);
+ break;
+ case EpPacketRequest.TerminateExecution:
+ EpRequestTerminateExecution(_packet.CallbackId, dt);
+ break;
+ case EpPacketRequest.HaltExecution:
+ EpRequestHaltExecution(_packet.CallbackId, dt);
+ break;
+ case EpPacketRequest.ResumeExecution:
+ EpRequestResumeExecution(_packet.CallbackId, dt);
+ break;
}
}
else if (_packet.Method == EpPacketMethod.Reply)
{
- var dt = _packet.Tdu.Value;
+ var dt = _packet.Tdu;
switch (_packet.Reply)
{
case EpPacketReply.Completed:
EpReplyCompleted(_packet.CallbackId, dt);
break;
+ case EpPacketReply.Stream:
+ EpReplyStream(_packet.CallbackId);
+ break;
case EpPacketReply.Propagated:
- EpReplyPropagated(_packet.CallbackId, dt);
+ if (dt.HasValue)
+ EpReplyPropagated(_packet.CallbackId, dt.Value);
break;
case EpPacketReply.PermissionError:
- EpReplyError(_packet.CallbackId, dt, ErrorType.Management);
+ if (dt.HasValue)
+ EpReplyError(_packet.CallbackId, dt.Value, ErrorType.Management);
break;
case EpPacketReply.ExecutionError:
- EpReplyError(_packet.CallbackId, dt, ErrorType.Exception);
+ if (dt.HasValue)
+ EpReplyError(_packet.CallbackId, dt.Value, ErrorType.Exception);
break;
case EpPacketReply.Progress:
- EpReplyProgress(_packet.CallbackId, dt);
+ if (dt.HasValue)
+ EpReplyProgress(_packet.CallbackId, dt.Value);
break;
case EpPacketReply.Chunk:
- EpReplyChunk(_packet.CallbackId, dt);
+ if (dt.HasValue)
+ EpReplyChunk(_packet.CallbackId, dt.Value);
break;
case EpPacketReply.Warning:
- EpReplyWarning(_packet.Extension, dt);
+ if (dt.HasValue)
+ EpReplyWarning(_packet.CallbackId, dt.Value);
break;
}
@@ -2102,6 +2129,7 @@ public partial class EpConnection : NetworkConnection, IStore
protected override void Disconnected()
{
// clean up
+ TerminateInvocations();
_authenticated = false;
Status = EpConnectionStatus.Closed;
diff --git a/Libraries/Esiur/Protocol/EpConnectionProtocol.cs b/Libraries/Esiur/Protocol/EpConnectionProtocol.cs
index ac020a7..025e552 100644
--- a/Libraries/Esiur/Protocol/EpConnectionProtocol.cs
+++ b/Libraries/Esiur/Protocol/EpConnectionProtocol.cs
@@ -31,6 +31,7 @@ using Esiur.Net.Packets;
using Esiur.Resource;
using Esiur.Security.Authority;
using Esiur.Security.Permissions;
+using Esiur.Security.RateLimiting;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System;
using System.Collections;
@@ -47,6 +48,140 @@ namespace Esiur.Protocol;
partial class EpConnection
{
+ readonly object _rateControlDenialsLock = new object();
+ DateTime _rateControlDenialWindowStarted;
+ int _rateControlDenials;
+ int _rateControlBlocked;
+
+ internal bool IsRateControlBlocked => Volatile.Read(ref _rateControlBlocked) != 0;
+
+ bool TryApplyRateControl(
+ MemberDef member,
+ IResource? resource,
+ ActionType action,
+ uint callback,
+ out TimeSpan delay)
+ {
+ delay = TimeSpan.Zero;
+
+ if (string.IsNullOrWhiteSpace(member.RatePolicyName))
+ return true;
+
+ if (IsRateControlBlocked)
+ return false;
+
+ var warehouse = Instance.Warehouse;
+ var policy = warehouse.TryGetRatePolicy(member.RatePolicyName);
+
+ if (policy == null)
+ {
+ DenyRateControlledRequest(
+ callback,
+ $"Rate policy `{member.RatePolicyName}` is not registered.");
+ return false;
+ }
+
+ try
+ {
+ var context = new RateControlContext(
+ warehouse,
+ this,
+ _session,
+ resource,
+ member,
+ action);
+
+ if (policy.Applicable(context) == Ruling.Denied)
+ {
+ DenyRateControlledRequest(
+ callback,
+ $"Rate policy `{member.RatePolicyName}` denied `{member.Fullname}`.");
+ return false;
+ }
+
+ delay = context.Delay > TimeSpan.Zero ? context.Delay : TimeSpan.Zero;
+ return true;
+ }
+ catch (Exception exception)
+ {
+ DenyRateControlledRequest(
+ callback,
+ $"Rate policy `{member.RatePolicyName}` failed: {exception.Message}");
+ return false;
+ }
+ }
+
+ void DenyRateControlledRequest(uint callback, string message)
+ {
+ var configuration = Instance.Warehouse.Configuration.RateControl;
+ var now = DateTime.UtcNow;
+ var shouldBlock = false;
+
+ lock (_rateControlDenialsLock)
+ {
+ var window = configuration.DenialWindow > TimeSpan.Zero
+ ? configuration.DenialWindow
+ : TimeSpan.FromMinutes(1);
+
+ if (_rateControlDenialWindowStarted == default ||
+ now - _rateControlDenialWindowStarted > window)
+ {
+ _rateControlDenialWindowStarted = now;
+ _rateControlDenials = 0;
+ }
+
+ _rateControlDenials++;
+ shouldBlock = configuration.DenialsBeforeConnectionBlock > 0 &&
+ _rateControlDenials >= configuration.DenialsBeforeConnectionBlock &&
+ Interlocked.Exchange(ref _rateControlBlocked, 1) == 0;
+ }
+
+ SendError(
+ ErrorType.Management,
+ callback,
+ (ushort)ExceptionCode.RateLimitExceeded,
+ message);
+
+ if (shouldBlock)
+ _ = CloseRateControlledConnectionAsync(configuration.ConnectionBlockDelay);
+ }
+
+ async Task CloseRateControlledConnectionAsync(TimeSpan delay)
+ {
+ if (delay > TimeSpan.Zero)
+ await Task.Delay(delay).ConfigureAwait(false);
+
+ Close();
+ }
+
+ void ExecuteRateControlled(uint callback, TimeSpan delay, Action action)
+ {
+ if (delay <= TimeSpan.Zero)
+ {
+ action();
+ return;
+ }
+
+ _ = ExecuteRateControlledAsync(callback, delay, action);
+ }
+
+ async Task ExecuteRateControlledAsync(uint callback, TimeSpan delay, Action action)
+ {
+ await Task.Delay(delay).ConfigureAwait(false);
+
+ if (!IsConnected || IsRateControlBlocked)
+ return;
+
+ try
+ {
+ action();
+ }
+ catch (Exception exception)
+ {
+ SendError(ErrorType.Exception, callback, (ushort)ExceptionCode.GeneralFailure, exception.Message);
+ }
+ }
+
KeyList _neededTypeDefs = new KeyList();
KeyList _cachedTypeDefs = new KeyList();
KeyList> _typeDefRequests = new KeyList>();
@@ -94,6 +229,9 @@ partial class EpConnection
KeyList _requests = new KeyList();
+ readonly object _invocationsLock = new object();
+ readonly Dictionary _invocations = new Dictionary();
+
volatile int _callbackCounter = 0;
Dictionary> _subscriptions = new Dictionary>();
@@ -125,18 +263,25 @@ partial class EpConnection
//callbackCounter++; // avoid thread racing
_requests.Add(c, reply);
+ SendRequestPacket(action, c, args);
+ return reply;
+ }
+
+ void SendRequestPacket(EpPacketRequest action, uint callbackId, object[] args)
+ {
+
if (args.Length == 0)
{
var bl = new BinaryList();
bl.AddUInt8((byte)(0x40 | (byte)action))
- .AddUInt32(c);
+ .AddUInt32(callbackId);
Send(bl.ToArray());
}
if (args.Length == 1)
{
var bl = new BinaryList();
bl.AddUInt8((byte)(0x60 | (byte)action))
- .AddUInt32(c)
+ .AddUInt32(callbackId)
.AddUInt8Array(Codec.Compose(args[0], this.Instance?.Warehouse ?? _serverWarehouse, this));
Send(bl.ToArray());
}
@@ -144,11 +289,39 @@ partial class EpConnection
{
var bl = new BinaryList();
bl.AddUInt8((byte)(0x60 | (byte)action))
- .AddUInt32(c)
+ .AddUInt32(callbackId)
.AddUInt8Array(Codec.Compose(args, this.Instance?.Warehouse ?? _serverWarehouse, this));
Send(bl.ToArray());
}
+ }
+ AsyncStreamReply SendStreamRequest(StreamMode streamMode, EpPacketRequest action, params object[] args)
+ {
+ var callbackId = (uint)Interlocked.Increment(ref _callbackCounter);
+ var reply = new AsyncStreamReply(
+ streamMode,
+ () => SendRequest(EpPacketRequest.PullStream, callbackId),
+ () => SendRequest(EpPacketRequest.TerminateExecution, callbackId),
+ () => SendRequest(EpPacketRequest.HaltExecution, callbackId),
+ () => SendRequest(EpPacketRequest.ResumeExecution, callbackId));
+
+ _requests.Add(callbackId, reply);
+ SendRequestPacket(action, callbackId, args);
+ return reply;
+ }
+
+ AsyncStreamReply SendStreamRequest(StreamMode streamMode, EpPacketRequest action, params object[] args)
+ {
+ var callbackId = (uint)Interlocked.Increment(ref _callbackCounter);
+ var reply = new AsyncStreamReply(
+ streamMode,
+ () => SendRequest(EpPacketRequest.PullStream, callbackId),
+ () => SendRequest(EpPacketRequest.TerminateExecution, callbackId),
+ () => SendRequest(EpPacketRequest.HaltExecution, callbackId),
+ () => SendRequest(EpPacketRequest.ResumeExecution, callbackId));
+
+ _requests.Add(callbackId, reply);
+ SendRequestPacket(action, callbackId, args);
return reply;
}
@@ -304,6 +477,16 @@ partial class EpConnection
return SendRequest(EpPacketRequest.StaticCall, typeId, index, parameters);
}
+ public AsyncStreamReply StaticStreamCall(ulong typeId, byte index, object parameters, StreamMode streamMode)
+ {
+ return SendStreamRequest(streamMode, EpPacketRequest.StaticCall, typeId, index, parameters);
+ }
+
+ public AsyncStreamReply StaticStreamCall(ulong typeId, byte index, object parameters, StreamMode streamMode)
+ {
+ return SendStreamRequest(streamMode, EpPacketRequest.StaticCall, typeId, index, parameters);
+ }
+
public AsyncReply Call(string procedureCall, params object[] parameters)
{
//var args = new Map();
@@ -324,6 +507,16 @@ partial class EpConnection
return SendRequest(EpPacketRequest.InvokeFunction, instanceId, index, parameters);
}
+ internal AsyncStreamReply SendStreamInvoke(uint instanceId, byte index, object parameters, StreamMode streamMode)
+ {
+ return SendStreamRequest(streamMode, EpPacketRequest.InvokeFunction, instanceId, index, parameters);
+ }
+
+ internal AsyncStreamReply SendStreamInvoke(uint instanceId, byte index, object parameters, StreamMode streamMode)
+ {
+ return SendStreamRequest(streamMode, EpPacketRequest.InvokeFunction, instanceId, index, parameters);
+ }
+
internal AsyncReply SendSetProperty(uint instanceId, byte index, object value)
{
return SendRequest(EpPacketRequest.SetProperty, instanceId, index, value);
@@ -383,7 +576,7 @@ partial class EpConnection
SendReply(EpPacketReply.Chunk, callbackId, chunk);
}
- void EpReplyCompleted(uint callbackId, PlainTdu tdu)
+ void EpReplyCompleted(uint callbackId, PlainTdu? tdu)
{
var req = _requests.Take(callbackId);
@@ -395,7 +588,19 @@ partial class EpConnection
return;
}
- var pr = Codec.Parse(tdu, this, null);
+ if (req is AsyncStreamReply streamReply)
+ {
+ streamReply.TriggerStreamCompleted();
+ return;
+ }
+
+ if (tdu == null)
+ {
+ req.Trigger(null);
+ return;
+ }
+
+ var pr = Codec.Parse(tdu.Value, this, null);
if (pr is AsyncReply asyncReply)
{
@@ -421,6 +626,12 @@ partial class EpConnection
//}).Error(req.TriggerError);
}
+ void EpReplyStream(uint callbackId)
+ {
+ if (_requests[callbackId] is AsyncStreamReply streamReply)
+ streamReply.TriggerStreamStarted();
+ }
+
void EpExtensionAction(byte actionId, PlainTdu? tdu)
{
// nothing is supported now
@@ -1419,6 +1630,22 @@ partial class EpConnection
void InvokeFunction(FunctionDef ft, uint callback, object arguments, EpPacketRequest actionType, object target = null)
+ {
+ if (!TryApplyRateControl(
+ ft,
+ target as IResource,
+ ActionType.Execute,
+ callback,
+ out var delay))
+ return;
+
+ ExecuteRateControlled(
+ callback,
+ delay,
+ () => InvokeFunctionCore(ft, callback, arguments, actionType, target));
+ }
+
+ void InvokeFunctionCore(FunctionDef ft, uint callback, object arguments, EpPacketRequest actionType, object target = null)
{
// cast arguments
@@ -1559,59 +1786,44 @@ partial class EpConnection
return;
}
- if (rt is IAsyncEnumerable