mirror of
https://github.com/JKorf/CryptoExchange.Net
synced 2026-04-07 10:11:10 +00:00
wip
This commit is contained in:
parent
08d361b259
commit
aed98140aa
@ -6,9 +6,9 @@
|
|||||||
<PackageId>CryptoExchange.Net</PackageId>
|
<PackageId>CryptoExchange.Net</PackageId>
|
||||||
<Authors>JKorf</Authors>
|
<Authors>JKorf</Authors>
|
||||||
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
||||||
<PackageVersion>11.0.3</PackageVersion>
|
<PackageVersion>11.0.4</PackageVersion>
|
||||||
<AssemblyVersion>11.0.3</AssemblyVersion>
|
<AssemblyVersion>11.0.4</AssemblyVersion>
|
||||||
<FileVersion>11.0.3</FileVersion>
|
<FileVersion>11.0.4</FileVersion>
|
||||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||||
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
|
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
|
|||||||
@ -11,6 +11,7 @@ using System;
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Data;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
@ -489,6 +490,14 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected internal virtual void HandleStreamMessage2(WebSocketMessageType type, ReadOnlySpan<byte> data)
|
protected internal virtual void HandleStreamMessage2(WebSocketMessageType type, ReadOnlySpan<byte> data)
|
||||||
{
|
{
|
||||||
|
// Forward message rules:
|
||||||
|
// | Message Topic | Route Topic Filter | Topics Match | Forward | Description
|
||||||
|
// | N | N | - | Y | No topic filter applied
|
||||||
|
// | N | Y | - | N | Route only listens to specific topic
|
||||||
|
// | Y | N | - | Y | Route listens to all message regardless of topic
|
||||||
|
// | Y | Y | Y | Y | Route listens to specific message topic
|
||||||
|
// | Y | Y | N | N | Route listens to different topic
|
||||||
|
|
||||||
var receiveTime = DateTime.UtcNow;
|
var receiveTime = DateTime.UtcNow;
|
||||||
|
|
||||||
// 1. Decrypt/Preprocess if necessary
|
// 1. Decrypt/Preprocess if necessary
|
||||||
@ -525,10 +534,9 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
foreach (var listener in _listeners)
|
foreach (var listener in _listeners)
|
||||||
{
|
{
|
||||||
var routes = listener.MessageRouter[typeIdentifier];
|
var routes = listener.MessageRouter[typeIdentifier];
|
||||||
if (routes.Count > 0) {
|
deserializationType = routes?.RouteType;
|
||||||
deserializationType = routes[0].DeserializationType;
|
if (deserializationType != null)
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deserializationType == null)
|
if (deserializationType == null)
|
||||||
@ -577,55 +585,18 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
bool processed = false;
|
bool processed = false;
|
||||||
foreach (var listener in _listeners)
|
foreach (var listener in _listeners)
|
||||||
{
|
{
|
||||||
var routes = listener.MessageRouter[typeIdentifier];
|
var routeMap = listener.MessageRouter[typeIdentifier];
|
||||||
|
if (routeMap != null)
|
||||||
bool isQuery = false;
|
|
||||||
Query? query = null;
|
|
||||||
if (listener is Query cquery)
|
|
||||||
{
|
{
|
||||||
isQuery = true;
|
// Could be null if listeners was updated while handling message
|
||||||
query = cquery;
|
var handled = listener.Handle(topicFilter, this, receiveTime, originalData, result, routeMap);
|
||||||
}
|
if (!processed)
|
||||||
|
processed = handled;
|
||||||
|
|
||||||
var complete = false;
|
if (!routeMap.MultipleReaders)
|
||||||
|
// If this was a response to a query and there are no other routes expecting this message we can break here
|
||||||
foreach (var route in routes)
|
|
||||||
{
|
|
||||||
// Forward message rules:
|
|
||||||
// | Message Topic | Route Topic Filter | Topics Match | Forward | Description
|
|
||||||
// | N | N | - | Y | No topic filter applied
|
|
||||||
// | N | Y | - | N | Route only listens to specific topic
|
|
||||||
// | Y | N | - | Y | Route listens to all message regardless of topic
|
|
||||||
// | Y | Y | Y | Y | Route listens to specific message topic
|
|
||||||
// | Y | Y | N | N | Route listens to different topic
|
|
||||||
if (topicFilter == null)
|
|
||||||
{
|
|
||||||
if (route.TopicFilter != null)
|
|
||||||
// No topic on message, but route is filtering on topic
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (route.TopicFilter != null && !route.TopicFilter.Equals(topicFilter, StringComparison.Ordinal))
|
|
||||||
// Message has a topic, and the route has a filter for another topic
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
processed = true;
|
|
||||||
|
|
||||||
if (isQuery && query!.Completed)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
listener.Handle(this, receiveTime, originalData, result, route);
|
|
||||||
if (isQuery && !route.MultipleReaders)
|
|
||||||
{
|
|
||||||
complete = true;
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (complete)
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!processed)
|
if (!processed)
|
||||||
|
|||||||
@ -180,10 +180,11 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handle an update message
|
/// Handle an update message
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CallResult? Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object data, MessageRoute route)
|
public bool Handle(string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object data, RouteMapEntry routeMap)
|
||||||
{
|
{
|
||||||
ConnectionInvocations++;
|
ConnectionInvocations++;
|
||||||
TotalInvocations++;
|
TotalInvocations++;
|
||||||
|
|
||||||
if (SubscriptionQuery != null && !SubscriptionQuery.Completed && SubscriptionQuery.TimeoutBehavior == TimeoutBehavior.Succeed)
|
if (SubscriptionQuery != null && !SubscriptionQuery.Completed && SubscriptionQuery.TimeoutBehavior == TimeoutBehavior.Succeed)
|
||||||
{
|
{
|
||||||
// The subscription query is one where it is successful if there is no error returned
|
// The subscription query is one where it is successful if there is no error returned
|
||||||
@ -192,7 +193,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
SubscriptionQuery.Timeout();
|
SubscriptionQuery.Timeout();
|
||||||
}
|
}
|
||||||
|
|
||||||
return route.Handle(connection, receiveTime, originalData, data);
|
return routeMap.Handle(topicFilter, connection, receiveTime, originalData, data, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -21,6 +21,6 @@ namespace CryptoExchange.Net.Sockets.Interfaces
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handle a message
|
/// Handle a message
|
||||||
/// </summary>
|
/// </summary>
|
||||||
CallResult? Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object result, MessageRoute route);
|
bool Handle(string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object result, RouteMapEntry route);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,82 @@ using System.Linq;
|
|||||||
|
|
||||||
namespace CryptoExchange.Net.Sockets
|
namespace CryptoExchange.Net.Sockets
|
||||||
{
|
{
|
||||||
|
public record RouteMapEntry
|
||||||
|
{
|
||||||
|
public Type RouteType { get; }
|
||||||
|
public bool MultipleReaders { get; private set; }
|
||||||
|
|
||||||
|
private List<MessageRoute> _routesWithoutTopicFilter;
|
||||||
|
private Dictionary<string, List<MessageRoute>> _routesWithTopicFilter;
|
||||||
|
|
||||||
|
public RouteMapEntry(Type routeType)
|
||||||
|
{
|
||||||
|
_routesWithoutTopicFilter = new List<MessageRoute>();
|
||||||
|
_routesWithTopicFilter = new Dictionary<string, List<MessageRoute>>();
|
||||||
|
|
||||||
|
RouteType = routeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddRoute(string? topicFilter, MessageRoute route)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(topicFilter))
|
||||||
|
{
|
||||||
|
_routesWithoutTopicFilter.Add(route);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!_routesWithTopicFilter.TryGetValue(topicFilter!, out var list))
|
||||||
|
{
|
||||||
|
list = new List<MessageRoute>();
|
||||||
|
_routesWithTopicFilter.Add(topicFilter!, list);
|
||||||
|
}
|
||||||
|
|
||||||
|
list.Add(route);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.MultipleReaders)
|
||||||
|
MultipleReaders = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool Handle(string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object data, out CallResult? result)
|
||||||
|
{
|
||||||
|
result = null;
|
||||||
|
|
||||||
|
// Routes without topic filter handle both when the message topic is empty and when it is not, so we always call them
|
||||||
|
var handled = false;
|
||||||
|
foreach (var route in _routesWithoutTopicFilter)
|
||||||
|
{
|
||||||
|
var thisResult = route.Handle(connection, receiveTime, originalData, data);
|
||||||
|
if (thisResult != null)
|
||||||
|
result ??= thisResult;
|
||||||
|
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward to routes with matching topic filter, if any
|
||||||
|
if (topicFilter != null)
|
||||||
|
{
|
||||||
|
_routesWithTopicFilter.TryGetValue(topicFilter, out var matchingTopicRoutes);
|
||||||
|
foreach (var route in matchingTopicRoutes ?? [])
|
||||||
|
{
|
||||||
|
var thisResult = route.Handle(connection, receiveTime, originalData, data);
|
||||||
|
handled = true;
|
||||||
|
|
||||||
|
if (thisResult != null)
|
||||||
|
{
|
||||||
|
result ??= thisResult;
|
||||||
|
|
||||||
|
if (!MultipleReaders)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return handled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Message router
|
/// Message router
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -20,9 +96,10 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
public MessageRoute[] Routes { get; }
|
public MessageRoute[] Routes { get; }
|
||||||
|
|
||||||
#if NET8_0_OR_GREATER
|
#if NET8_0_OR_GREATER
|
||||||
private FrozenDictionary<string, List<MessageRoute>>? _routeMap;
|
// Used for mapping a type identifier to the routes matching it
|
||||||
|
private FrozenDictionary<string, RouteMapEntry>? _routeMap;
|
||||||
#else
|
#else
|
||||||
private Dictionary<string, List<MessageRoute>>? _routeMap;
|
private Dictionary<string, RouteMapEntry>? _routeMap;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -38,12 +115,16 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void BuildRouteMap()
|
public void BuildRouteMap()
|
||||||
{
|
{
|
||||||
var newMap = new Dictionary<string, List<MessageRoute>>();
|
var newMap = new Dictionary<string, RouteMapEntry>();
|
||||||
foreach (var route in Routes)
|
foreach (var route in Routes)
|
||||||
{
|
{
|
||||||
if (!newMap.ContainsKey(route.TypeIdentifier))
|
if (!newMap.TryGetValue(route.TypeIdentifier, out var typeMap))
|
||||||
newMap.Add(route.TypeIdentifier, new List<MessageRoute>());
|
{
|
||||||
newMap[route.TypeIdentifier].Add(route);
|
typeMap = new RouteMapEntry(route.DeserializationType);
|
||||||
|
newMap.Add(route.TypeIdentifier, typeMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
typeMap.AddRoute(route.TopicFilter, route);
|
||||||
}
|
}
|
||||||
|
|
||||||
#if NET8_0_OR_GREATER
|
#if NET8_0_OR_GREATER
|
||||||
@ -56,9 +137,9 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get routes matching the type identifier
|
/// Get routes matching the type identifier
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<MessageRoute> this[string identifier]
|
public RouteMapEntry? this[string identifier]
|
||||||
{
|
{
|
||||||
get => (_routeMap ?? throw new InvalidOperationException("Route map not initialized before use")).TryGetValue(identifier, out var routes) ? routes: [];
|
get => (_routeMap ?? throw new InvalidOperationException("Route map not initialized before use")).TryGetValue(identifier, out var routes) ? routes: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -164,7 +164,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handle a response message
|
/// Handle a response message
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract CallResult Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object message, MessageRoute route);
|
public abstract bool Handle(string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object message, RouteMapEntry routeMap);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,7 +194,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override CallResult Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object message, MessageRoute route)
|
public override bool Handle(string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object message, RouteMapEntry routeMap)
|
||||||
{
|
{
|
||||||
CurrentResponses++;
|
CurrentResponses++;
|
||||||
if (CurrentResponses == RequiredResponses)
|
if (CurrentResponses == RequiredResponses)
|
||||||
@ -203,7 +203,8 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
if (Result?.Success != false)
|
if (Result?.Success != false)
|
||||||
{
|
{
|
||||||
// If an error result is already set don't override that
|
// If an error result is already set don't override that
|
||||||
Result = route.Handle(connection, receiveTime, originalData, message);
|
routeMap.Handle(topicFilter, connection, receiveTime, originalData, message, out var result);
|
||||||
|
Result = result;
|
||||||
if (Result == null)
|
if (Result == null)
|
||||||
// Null from Handle means it wasn't actually for this query
|
// Null from Handle means it wasn't actually for this query
|
||||||
CurrentResponses -= 1;
|
CurrentResponses -= 1;
|
||||||
@ -216,7 +217,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
OnComplete?.Invoke();
|
OnComplete?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result ?? CallResult.SuccessResult;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user