add all
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
|
||||
namespace Best.HTTP.Shared.Extensions
|
||||
{
|
||||
public sealed class CircularBuffer<T>
|
||||
{
|
||||
public int Capacity { get; private set; }
|
||||
public int Count { get; private set; }
|
||||
public int StartIdx { get { return this.startIdx; } }
|
||||
public int EndIdx { get { return this.endIdx; } }
|
||||
|
||||
public T this[int idx]
|
||||
{
|
||||
get
|
||||
{
|
||||
int realIdx = (this.startIdx + idx) % this.Capacity;
|
||||
|
||||
return this.buffer[realIdx];
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
int realIdx = (this.startIdx + idx) % this.Capacity;
|
||||
|
||||
this.buffer[realIdx] = value;
|
||||
}
|
||||
}
|
||||
|
||||
private T[] buffer;
|
||||
private int startIdx;
|
||||
private int endIdx;
|
||||
|
||||
public CircularBuffer(int capacity)
|
||||
{
|
||||
this.Capacity = capacity;
|
||||
}
|
||||
|
||||
public void Add(T element)
|
||||
{
|
||||
if (this.buffer == null)
|
||||
this.buffer = new T[this.Capacity];
|
||||
|
||||
this.buffer[this.endIdx] = element;
|
||||
|
||||
this.endIdx = (this.endIdx + 1) % this.Capacity;
|
||||
if (this.endIdx == this.startIdx)
|
||||
this.startIdx = (this.startIdx + 1) % this.Capacity;
|
||||
|
||||
this.Count = Math.Min(this.Count + 1, this.Capacity);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
this.Count = this.startIdx = this.endIdx = 0;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = PlatformSupport.Text.StringBuilderPool.Get(2);
|
||||
sb.Append("[");
|
||||
|
||||
int idx = this.startIdx;
|
||||
while (idx != this.endIdx)
|
||||
{
|
||||
sb.Append(this.buffer[idx].ToString());
|
||||
|
||||
idx = (idx + 1) % this.Capacity;
|
||||
if (idx != this.endIdx)
|
||||
sb.Append("; ");
|
||||
}
|
||||
sb.Append("]");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d7e67aa2e8ef9344cb04e85bfff0aed5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,593 @@
|
||||
using Best.HTTP.Shared.PlatformSupport.Memory;
|
||||
using Best.HTTP.Shared.PlatformSupport.Text;
|
||||
using Best.HTTP.Shared.Streams;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
using static Best.HTTP.Hosts.Connections.HTTP1.Constants;
|
||||
|
||||
using Cryptography = System.Security.Cryptography;
|
||||
|
||||
namespace Best.HTTP.Shared.Extensions
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
#region ASCII Encoding (These are required because Windows Phone doesn't supports the Encoding.ASCII class.)
|
||||
|
||||
/// <summary>
|
||||
/// On WP8 platform there are no ASCII encoding.
|
||||
/// </summary>
|
||||
public static string AsciiToString(this byte[] bytes)
|
||||
{
|
||||
StringBuilder sb = StringBuilderPool.Get(bytes.Length); //new StringBuilder(bytes.Length);
|
||||
foreach (byte b in bytes)
|
||||
sb.Append(b <= 0x7f ? (char)b : '?');
|
||||
return StringBuilderPool.ReleaseAndGrab(sb);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// On WP8 platform there are no ASCII encoding.
|
||||
/// </summary>
|
||||
public static BufferSegment GetASCIIBytes(this string str)
|
||||
{
|
||||
byte[] result = BufferPool.Get(str.Length, true);
|
||||
for (int i = 0; i < str.Length; ++i)
|
||||
{
|
||||
char ch = str[i];
|
||||
result[i] = (byte)((ch < (char)0x80) ? ch : '?');
|
||||
}
|
||||
|
||||
return new BufferSegment(result, 0, str.Length);
|
||||
}
|
||||
|
||||
public static void SendAsASCII(this BinaryWriter stream, string str)
|
||||
{
|
||||
for (int i = 0; i < str.Length; ++i)
|
||||
{
|
||||
char ch = str[i];
|
||||
|
||||
stream.Write((byte)((ch < (char)0x80) ? ch : '?'));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Headers
|
||||
|
||||
public static Dictionary<string, List<string>> AddHeader(this Dictionary<string, List<string>> headers, string name, string value)
|
||||
{
|
||||
if (headers == null)
|
||||
headers = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
List<string> values;
|
||||
if (!headers.TryGetValue(name, out values))
|
||||
headers.Add(name, values = new List<string>(1));
|
||||
|
||||
values.Add(value);
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
public static Dictionary<string, List<string>> SetHeader(this Dictionary<string, List<string>> headers, string name, string value)
|
||||
{
|
||||
if (headers == null)
|
||||
headers = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
List<string> values;
|
||||
if (!headers.TryGetValue(name, out values))
|
||||
headers.Add(name, values = new List<string>(1));
|
||||
|
||||
values.Clear();
|
||||
values.Add(value);
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
public static bool RemoveHeader(this Dictionary<string, List<string>> headers, string name) => headers != null && headers.Remove(name);
|
||||
|
||||
public static void RemoveHeaders(this Dictionary<string, List<string>> headers) => headers?.Clear();
|
||||
|
||||
public static List<string> GetHeaderValues(this Dictionary<string, List<string>> headers, string name)
|
||||
{
|
||||
if (headers == null)
|
||||
return null;
|
||||
|
||||
List<string> values;
|
||||
if (!headers.TryGetValue(name, out values) || values.Count == 0)
|
||||
return null;
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
public static string GetFirstHeaderValue(this Dictionary<string, List<string>> headers, string name)
|
||||
{
|
||||
if (headers == null)
|
||||
return null;
|
||||
|
||||
List<string> values;
|
||||
if (!headers.TryGetValue(name, out values) || values.Count == 0)
|
||||
return null;
|
||||
|
||||
return values[0];
|
||||
}
|
||||
|
||||
public static bool HasHeaderWithValue(this Dictionary<string, List<string>> headers, string headerName, string value)
|
||||
{
|
||||
var values = headers.GetHeaderValues(headerName);
|
||||
if (values == null)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < values.Count; ++i)
|
||||
if (string.Compare(values[i], value, StringComparison.OrdinalIgnoreCase) == 0)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool HasHeader(this Dictionary<string, List<string>> headers, string headerName)
|
||||
=> headers != null && headers.ContainsKey(headerName);
|
||||
|
||||
#endregion
|
||||
|
||||
#region FileSystem WriteLine function support
|
||||
|
||||
public static void WriteString(this Stream fs, string value)
|
||||
{
|
||||
int count = System.Text.Encoding.UTF8.GetByteCount(value);
|
||||
var buffer = BufferPool.Get(count, true);
|
||||
try
|
||||
{
|
||||
System.Text.Encoding.UTF8.GetBytes(value, 0, value.Length, buffer, 0);
|
||||
fs.Write(buffer, 0, count);
|
||||
}
|
||||
finally
|
||||
{
|
||||
BufferPool.Release(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
public static void WriteLine(this Stream fs)
|
||||
{
|
||||
fs.Write(EOL, 0, 2);
|
||||
}
|
||||
|
||||
public static void WriteLine(this Stream fs, string line)
|
||||
{
|
||||
var buff = line.GetASCIIBytes();
|
||||
try
|
||||
{
|
||||
fs.Write(buff.Data, buff.Offset, buff.Count);
|
||||
fs.WriteLine();
|
||||
}
|
||||
finally
|
||||
{
|
||||
BufferPool.Release(buff);
|
||||
}
|
||||
}
|
||||
|
||||
public static void WriteLine(this Stream fs, string format, params object[] values)
|
||||
{
|
||||
var buff = string.Format(format, values).GetASCIIBytes();
|
||||
try
|
||||
{
|
||||
fs.Write(buff.Data, buff.Offset, buff.Count);
|
||||
fs.WriteLine();
|
||||
}
|
||||
finally
|
||||
{
|
||||
BufferPool.Release(buff);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Other Extensions
|
||||
|
||||
public static AutoReleaseBuffer AsAutoRelease(this byte[] buffer) => new AutoReleaseBuffer(buffer);
|
||||
|
||||
public static BufferSegment AsBuffer(this byte[] bytes)
|
||||
{
|
||||
return new BufferSegment(bytes, 0, bytes.Length);
|
||||
}
|
||||
|
||||
public static BufferSegment AsBuffer(this byte[] bytes, int length)
|
||||
{
|
||||
return new BufferSegment(bytes, 0, length);
|
||||
}
|
||||
|
||||
public static BufferSegment AsBuffer(this byte[] bytes, int offset, int length)
|
||||
{
|
||||
return new BufferSegment(bytes, offset, length);
|
||||
}
|
||||
|
||||
public static BufferSegment CopyAsBuffer(this byte[] bytes, int offset, int length)
|
||||
{
|
||||
var newBuff = BufferPool.Get(length, true);
|
||||
|
||||
Array.Copy(bytes, offset, newBuff, 0, length);
|
||||
|
||||
return newBuff.AsBuffer(0, length);
|
||||
}
|
||||
|
||||
public static string GetRequestPathAndQueryURL(this Uri uri)
|
||||
{
|
||||
string requestPathAndQuery = uri.GetComponents(UriComponents.PathAndQuery, UriFormat.UriEscaped);
|
||||
|
||||
// http://forum.unity3d.com/threads/best-http-released.200006/page-26#post-2723250
|
||||
if (string.IsNullOrEmpty(requestPathAndQuery))
|
||||
requestPathAndQuery = "/";
|
||||
|
||||
return requestPathAndQuery;
|
||||
}
|
||||
|
||||
public static string[] FindOption(this string str, string option)
|
||||
{
|
||||
//s-maxage=2678400, must-revalidate, max-age=0
|
||||
string[] options = str.ToLowerInvariant().Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
option = option.ToLowerInvariant();
|
||||
|
||||
for (int i = 0; i < options.Length; ++i)
|
||||
if (options[i].Contains(option))
|
||||
return options[i].Split(new char[] { '=' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string[] FindOption(this string[] options, string option)
|
||||
{
|
||||
for (int i = 0; i < options.Length; ++i)
|
||||
if (options[i].Contains(option))
|
||||
return options[i].Split(new char[] { '=' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void WriteArray(this Stream stream, byte[] array)
|
||||
{
|
||||
stream.Write(array, 0, array.Length);
|
||||
}
|
||||
|
||||
public static void WriteBufferSegment(this Stream stream, BufferSegment buffer)
|
||||
{
|
||||
stream.Write(buffer.Data, buffer.Offset, buffer.Count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the Uri's host is a valid IPv4 or IPv6 address.
|
||||
/// </summary>
|
||||
public static bool IsHostIsAnIPAddress(this Uri uri)
|
||||
{
|
||||
if (uri == null)
|
||||
return false;
|
||||
|
||||
return IsIpV4AddressValid(uri.Host) || IsIpV6AddressValid(uri.Host);
|
||||
}
|
||||
|
||||
// Original idea from: https://www.code4copy.com/csharp/c-validate-ip-address-string/
|
||||
// Working regex: https://www.regular-expressions.info/ip.html
|
||||
private static readonly System.Text.RegularExpressions.Regex validIpV4AddressRegex = new System.Text.RegularExpressions.Regex("\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Validates an IPv4 address.
|
||||
/// </summary>
|
||||
public static bool IsIpV4AddressValid(string address)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(address))
|
||||
return validIpV4AddressRegex.IsMatch(address.Trim());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates an IPv6 address.
|
||||
/// </summary>
|
||||
public static bool IsIpV6AddressValid(string address)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(address))
|
||||
{
|
||||
System.Net.IPAddress ip;
|
||||
if (System.Net.IPAddress.TryParse(address, out ip))
|
||||
return ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region String Conversions
|
||||
|
||||
public static int ToInt32(this string str, int defaultValue = default(int)) => int.TryParse(str, out var value) ? value : defaultValue;
|
||||
public static uint ToUInt32(this string str, uint defaultValue = default) => uint.TryParse(str, out var value) ? value : defaultValue;
|
||||
|
||||
public static long ToInt64(this string str, long defaultValue = default(long)) => long.TryParse(str, out var value) ? value : defaultValue;
|
||||
|
||||
public static ulong ToUInt64(this string str, ulong defaultValue = default(ulong)) => ulong.TryParse(str, out var value) ? value : defaultValue;
|
||||
|
||||
public static DateTime ToDateTime(this string str, DateTime defaultValue = default(DateTime))
|
||||
{
|
||||
if (str == null)
|
||||
return defaultValue;
|
||||
|
||||
if (DateTime.TryParse(str, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out var value))
|
||||
return value;
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public static string ToStrOrEmpty(this string str)
|
||||
{
|
||||
if (str == null)
|
||||
return String.Empty;
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
public static string ToStr(this string str, string defaultVale)
|
||||
{
|
||||
if (str == null)
|
||||
return defaultVale;
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
public static string ToBinaryStr(this byte value)
|
||||
{
|
||||
return Convert.ToString(value, 2).PadLeft(8, '0');
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region MD5 Hashing
|
||||
|
||||
public static string CalculateMD5Hash(this string input)
|
||||
{
|
||||
var asciiBuff = input.GetASCIIBytes();
|
||||
var hash = asciiBuff.CalculateMD5Hash();
|
||||
BufferPool.Release(asciiBuff);
|
||||
return hash;
|
||||
}
|
||||
|
||||
public static string CalculateMD5Hash(this BufferSegment input)
|
||||
{
|
||||
using (var md5 = Cryptography.MD5.Create())
|
||||
{
|
||||
var hash = md5.ComputeHash(input.Data, input.Offset, input.Count);
|
||||
var sb = StringBuilderPool.Get(hash.Length); //new StringBuilder(hash.Length);
|
||||
for (int i = 0; i < hash.Length; ++i)
|
||||
sb.Append(hash[i].ToString("x2"));
|
||||
BufferPool.Release(hash);
|
||||
return StringBuilderPool.ReleaseAndGrab(sb);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Efficient String Parsing Helpers
|
||||
|
||||
internal static string Read(this string str, ref int pos, char block, bool needResult = true)
|
||||
{
|
||||
return str.Read(ref pos, (ch) => ch != block, needResult);
|
||||
}
|
||||
|
||||
internal static string Read(this string str, ref int pos, Func<char, bool> block, bool needResult = true)
|
||||
{
|
||||
if (pos >= str.Length)
|
||||
return string.Empty;
|
||||
|
||||
str.SkipWhiteSpace(ref pos);
|
||||
|
||||
int startPos = pos;
|
||||
|
||||
while (pos < str.Length && block(str[pos]))
|
||||
pos++;
|
||||
|
||||
string result = needResult ? str.Substring(startPos, pos - startPos) : null;
|
||||
|
||||
// set position to the next char
|
||||
pos++;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static string ReadPossibleQuotedText(this string str, ref int pos)
|
||||
{
|
||||
string result = string.Empty;
|
||||
if (str == null)
|
||||
return result;
|
||||
|
||||
// It's a quoted text?
|
||||
if (str[pos] == '\"')
|
||||
{
|
||||
// Skip the starting quote
|
||||
str.Read(ref pos, '\"', false);
|
||||
|
||||
// Read the text until the ending quote
|
||||
result = str.Read(ref pos, '\"');
|
||||
|
||||
// Next option
|
||||
str.Read(ref pos, (ch) => ch != ',' && ch != ';', false);
|
||||
}
|
||||
else
|
||||
// It's not a quoted text, so we will read until the next option
|
||||
result = str.Read(ref pos, (ch) => ch != ',' && ch != ';');
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static void SkipWhiteSpace(this string str, ref int pos)
|
||||
{
|
||||
if (pos >= str.Length)
|
||||
return;
|
||||
|
||||
while (pos < str.Length && char.IsWhiteSpace(str[pos]))
|
||||
pos++;
|
||||
}
|
||||
|
||||
internal static string TrimAndLower(this string str)
|
||||
{
|
||||
if (str == null)
|
||||
return null;
|
||||
|
||||
char[] buffer = new char[str.Length];
|
||||
int length = 0;
|
||||
|
||||
for (int i = 0; i < str.Length; ++i)
|
||||
{
|
||||
char ch = str[i];
|
||||
if (!char.IsWhiteSpace(ch) && !char.IsControl(ch))
|
||||
buffer[length++] = char.ToLowerInvariant(ch);
|
||||
}
|
||||
|
||||
return new string(buffer, 0, length);
|
||||
}
|
||||
|
||||
internal static char? Peek(this string str, int pos)
|
||||
{
|
||||
if (pos < 0 || pos >= str.Length)
|
||||
return null;
|
||||
|
||||
return str[pos];
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Specialized String Parsers
|
||||
|
||||
//public, max-age=2592000
|
||||
internal static List<HeaderValue> ParseOptionalHeader(this string str)
|
||||
{
|
||||
List<HeaderValue> result = new List<HeaderValue>();
|
||||
|
||||
if (str == null)
|
||||
return result;
|
||||
|
||||
int idx = 0;
|
||||
|
||||
// process the rest of the text
|
||||
while (idx < str.Length)
|
||||
{
|
||||
// Read key
|
||||
string key = str.Read(ref idx, (ch) => ch != '=' && ch != ',').TrimAndLower();
|
||||
HeaderValue qp = new HeaderValue(key);
|
||||
|
||||
if (str[idx - 1] == '=')
|
||||
qp.Value = str.ReadPossibleQuotedText(ref idx);
|
||||
|
||||
result.Add(qp);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
//deflate, gzip, x-gzip, identity, *;q=0
|
||||
internal static List<HeaderValue> ParseQualityParams(this string str)
|
||||
{
|
||||
List<HeaderValue> result = new List<HeaderValue>();
|
||||
|
||||
if (str == null)
|
||||
return result;
|
||||
|
||||
int idx = 0;
|
||||
while (idx < str.Length)
|
||||
{
|
||||
string key = str.Read(ref idx, (ch) => ch != ',' && ch != ';').TrimAndLower();
|
||||
|
||||
HeaderValue qp = new HeaderValue(key);
|
||||
|
||||
if (str[idx - 1] == ';')
|
||||
{
|
||||
str.Read(ref idx, '=', false);
|
||||
qp.Value = str.Read(ref idx, ',');
|
||||
}
|
||||
|
||||
result.Add(qp);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Buffer Filling
|
||||
|
||||
/// <summary>
|
||||
/// Will fill the entire buffer from the stream. Will throw an exception when the underlying stream is closed.
|
||||
/// </summary>
|
||||
public static void ReadBuffer(this Stream stream, byte[] buffer)
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
do
|
||||
{
|
||||
int read = stream.Read(buffer, count, buffer.Length - count);
|
||||
|
||||
if (read <= 0)
|
||||
throw ExceptionHelper.ServerClosedTCPStream();
|
||||
|
||||
count += read;
|
||||
} while (count < buffer.Length);
|
||||
}
|
||||
|
||||
public static void ReadBuffer(this Stream stream, byte[] buffer, int length)
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
do
|
||||
{
|
||||
int read = stream.Read(buffer, count, length - count);
|
||||
|
||||
if (read <= 0)
|
||||
throw ExceptionHelper.ServerClosedTCPStream();
|
||||
|
||||
count += read;
|
||||
} while (count < length);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region BufferPoolMemoryStream
|
||||
|
||||
public static void WriteString(this BufferPoolMemoryStream ms, string str)
|
||||
{
|
||||
var byteCount = Encoding.UTF8.GetByteCount(str);
|
||||
byte[] buffer = BufferPool.Get(byteCount, true);
|
||||
Encoding.UTF8.GetBytes(str, 0, str.Length, buffer, 0);
|
||||
ms.Write(buffer, 0, byteCount);
|
||||
BufferPool.Release(buffer);
|
||||
}
|
||||
|
||||
public static void WriteLine(this BufferPoolMemoryStream ms)
|
||||
{
|
||||
ms.Write(EOL, 0, EOL.Length);
|
||||
}
|
||||
|
||||
public static void WriteLine(this BufferPoolMemoryStream ms, string str)
|
||||
{
|
||||
ms.WriteString(str);
|
||||
ms.Write(EOL, 0, EOL.Length);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#if NET_STANDARD_2_0 || NET_4_6
|
||||
public static void Clear<T>(this System.Collections.Concurrent.ConcurrentQueue<T> queue)
|
||||
{
|
||||
T result;
|
||||
while (queue.TryDequeue(out result))
|
||||
;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public static class ExceptionHelper
|
||||
{
|
||||
public static Exception ServerClosedTCPStream()
|
||||
{
|
||||
return new Exception("TCP Stream closed unexpectedly by the remote server");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d1436ef67cdb4ca409385774293711b2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,399 @@
|
||||
// Based on https://github.com/nickgravelyn/UnityToolbag/blob/master/Future/Future.cs
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017, Nick Gravelyn
|
||||
|
||||
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 System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Best.HTTP.Futures
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes the state of a future.
|
||||
/// </summary>
|
||||
public enum FutureState
|
||||
{
|
||||
/// <summary>
|
||||
/// The future hasn't begun to resolve a value.
|
||||
/// </summary>
|
||||
Pending,
|
||||
|
||||
/// <summary>
|
||||
/// The future is working on resolving a value.
|
||||
/// </summary>
|
||||
Processing,
|
||||
|
||||
/// <summary>
|
||||
/// The future has a value ready.
|
||||
/// </summary>
|
||||
Success,
|
||||
|
||||
/// <summary>
|
||||
/// The future failed to resolve a value.
|
||||
/// </summary>
|
||||
Error
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines the interface of an object that can be used to track a future value.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of object being retrieved.</typeparam>
|
||||
public interface IFuture<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the state of the future.
|
||||
/// </summary>
|
||||
FutureState state { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value if the State is Success.
|
||||
/// </summary>
|
||||
T value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the failure exception if the State is Error.
|
||||
/// </summary>
|
||||
Exception error { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new callback to invoke when an intermediate result is known.
|
||||
/// </summary>
|
||||
/// <param name="callback">The callback to invoke.</param>
|
||||
/// <returns>The future so additional calls can be chained together.</returns>
|
||||
IFuture<T> OnItem(FutureValueCallback<T> callback);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new callback to invoke if the future value is retrieved successfully.
|
||||
/// </summary>
|
||||
/// <param name="callback">The callback to invoke.</param>
|
||||
/// <returns>The future so additional calls can be chained together.</returns>
|
||||
IFuture<T> OnSuccess(FutureValueCallback<T> callback);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new callback to invoke if the future has an error.
|
||||
/// </summary>
|
||||
/// <param name="callback">The callback to invoke.</param>
|
||||
/// <returns>The future so additional calls can be chained together.</returns>
|
||||
IFuture<T> OnError(FutureErrorCallback callback);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new callback to invoke if the future value is retrieved successfully or has an error.
|
||||
/// </summary>
|
||||
/// <param name="callback">The callback to invoke.</param>
|
||||
/// <returns>The future so additional calls can be chained together.</returns>
|
||||
IFuture<T> OnComplete(FutureCallback<T> callback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines the signature for callbacks used by the future.
|
||||
/// </summary>
|
||||
/// <param name="future">The future.</param>
|
||||
public delegate void FutureCallback<T>(IFuture<T> future);
|
||||
|
||||
public delegate void FutureValueCallback<T>(T value);
|
||||
public delegate void FutureErrorCallback(Exception error);
|
||||
|
||||
/// <summary>
|
||||
/// An implementation of <see cref="IFuture{T}"/> that can be used internally by methods that return futures.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Methods should always return the <see cref="IFuture{T}"/> interface when calling code requests a future.
|
||||
/// This class is intended to be constructed internally in the method to provide a simple implementation of
|
||||
/// the interface. By returning the interface instead of the class it ensures the implementation can change
|
||||
/// later on if requirements change, without affecting the calling code.
|
||||
/// </remarks>
|
||||
/// <typeparam name="T">The type of object being retrieved.</typeparam>
|
||||
public class Future<T> : IFuture<T>
|
||||
{
|
||||
private volatile FutureState _state;
|
||||
private T _value;
|
||||
private Exception _error;
|
||||
private Func<T> _processFunc;
|
||||
|
||||
private readonly List<FutureValueCallback<T>> _itemCallbacks = new List<FutureValueCallback<T>>();
|
||||
private readonly List<FutureValueCallback<T>> _successCallbacks = new List<FutureValueCallback<T>>();
|
||||
private readonly List<FutureErrorCallback> _errorCallbacks = new List<FutureErrorCallback>();
|
||||
private readonly List<FutureCallback<T>> _complationCallbacks = new List<FutureCallback<T>>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the state of the future.
|
||||
/// </summary>
|
||||
public FutureState state { get { return _state; } }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value if the State is Success.
|
||||
/// </summary>
|
||||
public T value
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_state != FutureState.Success && _state != FutureState.Processing)
|
||||
{
|
||||
throw new InvalidOperationException("value is not available unless state is Success or Processing.");
|
||||
}
|
||||
|
||||
return _value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the failure exception if the State is Error.
|
||||
/// </summary>
|
||||
public Exception error
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_state != FutureState.Error)
|
||||
{
|
||||
throw new InvalidOperationException("error is not available unless state is Error.");
|
||||
}
|
||||
|
||||
return _error;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Future{T}"/> class.
|
||||
/// </summary>
|
||||
public Future()
|
||||
{
|
||||
_state = FutureState.Pending;
|
||||
}
|
||||
|
||||
public IFuture<T> OnItem(FutureValueCallback<T> callback)
|
||||
{
|
||||
if (_state < FutureState.Success && !_itemCallbacks.Contains(callback))
|
||||
_itemCallbacks.Add(callback);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new callback to invoke if the future value is retrieved successfully.
|
||||
/// </summary>
|
||||
/// <param name="callback">The callback to invoke.</param>
|
||||
/// <returns>The future so additional calls can be chained together.</returns>
|
||||
public IFuture<T> OnSuccess(FutureValueCallback<T> callback)
|
||||
{
|
||||
if (_state == FutureState.Success)
|
||||
{
|
||||
callback(this.value);
|
||||
}
|
||||
else if (_state != FutureState.Error && !_successCallbacks.Contains(callback))
|
||||
{
|
||||
_successCallbacks.Add(callback);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new callback to invoke if the future has an error.
|
||||
/// </summary>
|
||||
/// <param name="callback">The callback to invoke.</param>
|
||||
/// <returns>The future so additional calls can be chained together.</returns>
|
||||
public IFuture<T> OnError(FutureErrorCallback callback)
|
||||
{
|
||||
if (_state == FutureState.Error)
|
||||
{
|
||||
callback(this.error);
|
||||
}
|
||||
else if (_state != FutureState.Success && !_errorCallbacks.Contains(callback))
|
||||
{
|
||||
_errorCallbacks.Add(callback);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new callback to invoke if the future value is retrieved successfully or has an error.
|
||||
/// </summary>
|
||||
/// <param name="callback">The callback to invoke.</param>
|
||||
/// <returns>The future so additional calls can be chained together.</returns>
|
||||
public IFuture<T> OnComplete(FutureCallback<T> callback)
|
||||
{
|
||||
if (_state == FutureState.Success || _state == FutureState.Error)
|
||||
{
|
||||
callback(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_complationCallbacks.Contains(callback))
|
||||
_complationCallbacks.Add(callback);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
#pragma warning disable 1998
|
||||
|
||||
/// <summary>
|
||||
/// Begins running a given function on a background thread to resolve the future's value, as long
|
||||
/// as it is still in the Pending state.
|
||||
/// </summary>
|
||||
/// <param name="func">The function that will retrieve the desired value.</param>
|
||||
public IFuture<T> Process(Func<T> func)
|
||||
{
|
||||
if (_state != FutureState.Pending)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot process a future that isn't in the Pending state.");
|
||||
}
|
||||
|
||||
BeginProcess();
|
||||
_processFunc = func;
|
||||
|
||||
System.Threading.ThreadPool.QueueUserWorkItem(ThreadFunc);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private void ThreadFunc(object param)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Directly call the Impl version to avoid the state validation of the public method
|
||||
AssignImpl(_processFunc());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Directly call the Impl version to avoid the state validation of the public method
|
||||
FailImpl(e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_processFunc = null;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning restore 1998
|
||||
|
||||
/// <summary>
|
||||
/// Allows manually assigning a value to a future, as long as it is still in the pending state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// There are times where you may not need to do background processing for a value. For example,
|
||||
/// you may have a cache of values and can just hand one out. In those cases you still want to
|
||||
/// return a future for the method signature, but can just call this method to fill in the future.
|
||||
/// </remarks>
|
||||
/// <param name="value">The value to assign the future.</param>
|
||||
public void Assign(T value)
|
||||
{
|
||||
if (_state != FutureState.Pending && _state != FutureState.Processing)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot assign a value to a future that isn't in the Pending or Processing state.");
|
||||
}
|
||||
|
||||
AssignImpl(value);
|
||||
}
|
||||
|
||||
public void BeginProcess(T initialItem = default(T))
|
||||
{
|
||||
_state = FutureState.Processing;
|
||||
_value = initialItem;
|
||||
}
|
||||
|
||||
public void AssignItem(T value)
|
||||
{
|
||||
_value = value;
|
||||
_error = null;
|
||||
|
||||
foreach (var callback in _itemCallbacks)
|
||||
callback(this.value);
|
||||
}
|
||||
|
||||
public void Finish()
|
||||
{
|
||||
_state = FutureState.Success;
|
||||
|
||||
FlushSuccessCallbacks();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allows manually failing a future, as long as it is still in the pending state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// As with the Assign method, there are times where you may know a future value is a failure without
|
||||
/// doing any background work. In those cases you can simply fail the future manually and return it.
|
||||
/// </remarks>
|
||||
/// <param name="error">The exception to use to fail the future.</param>
|
||||
public void Fail(Exception error)
|
||||
{
|
||||
if (_state != FutureState.Pending && _state != FutureState.Processing)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot fail future that isn't in the Pending or Processing state.");
|
||||
}
|
||||
|
||||
FailImpl(error);
|
||||
}
|
||||
|
||||
private void AssignImpl(T value)
|
||||
{
|
||||
_value = value;
|
||||
_error = null;
|
||||
_state = FutureState.Success;
|
||||
|
||||
FlushSuccessCallbacks();
|
||||
}
|
||||
|
||||
private void FailImpl(Exception error)
|
||||
{
|
||||
_value = default(T);
|
||||
_error = error;
|
||||
_state = FutureState.Error;
|
||||
|
||||
FlushErrorCallbacks();
|
||||
}
|
||||
|
||||
private void FlushSuccessCallbacks()
|
||||
{
|
||||
foreach (var callback in _successCallbacks)
|
||||
callback(this.value);
|
||||
|
||||
FlushComplationCallbacks();
|
||||
}
|
||||
|
||||
private void FlushErrorCallbacks()
|
||||
{
|
||||
foreach (var callback in _errorCallbacks)
|
||||
callback(this.error);
|
||||
|
||||
FlushComplationCallbacks();
|
||||
}
|
||||
|
||||
private void FlushComplationCallbacks()
|
||||
{
|
||||
foreach (var callback in _complationCallbacks)
|
||||
callback(this);
|
||||
ClearCallbacks();
|
||||
}
|
||||
|
||||
private void ClearCallbacks()
|
||||
{
|
||||
_itemCallbacks.Clear();
|
||||
_successCallbacks.Clear();
|
||||
_errorCallbacks.Clear();
|
||||
_complationCallbacks.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 356c46c35b6c6df45abc494c61a7039a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Best.HTTP.Shared.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Will parse a comma-separeted header value
|
||||
/// </summary>
|
||||
public sealed class HeaderParser : KeyValuePairList
|
||||
{
|
||||
public HeaderParser(string headerStr)
|
||||
{
|
||||
base.Values = Parse(headerStr);
|
||||
}
|
||||
|
||||
private List<HeaderValue> Parse(string headerStr)
|
||||
{
|
||||
List<HeaderValue> result = new List<HeaderValue>();
|
||||
|
||||
int pos = 0;
|
||||
|
||||
try
|
||||
{
|
||||
while (pos < headerStr.Length)
|
||||
{
|
||||
HeaderValue current = new HeaderValue();
|
||||
|
||||
current.Parse(headerStr, ref pos);
|
||||
|
||||
result.Add(current);
|
||||
}
|
||||
}
|
||||
catch(System.Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("HeaderParser - Parse", headerStr, ex);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 35bb5ccf89a1dc545a7be69cc867d08d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,133 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
using Best.HTTP.Shared.PlatformSupport.Text;
|
||||
|
||||
namespace Best.HTTP.Shared.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Used in string parsers. Its Value is optional.
|
||||
/// </summary>
|
||||
public sealed class HeaderValue
|
||||
{
|
||||
#region Public Properties
|
||||
|
||||
public string Key { get; set; }
|
||||
public string Value { get; set; }
|
||||
public List<HeaderValue> Options { get; set; }
|
||||
|
||||
public bool HasValue { get { return !string.IsNullOrEmpty(this.Value); } }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
public HeaderValue()
|
||||
{ }
|
||||
|
||||
public HeaderValue(string key)
|
||||
{
|
||||
this.Key = key;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Helper Functions
|
||||
|
||||
public void Parse(string headerStr, ref int pos)
|
||||
{
|
||||
ParseImplementation(headerStr, ref pos, true);
|
||||
}
|
||||
|
||||
public bool TryGetOption(string key, out HeaderValue option)
|
||||
{
|
||||
option = null;
|
||||
|
||||
if (Options == null || Options.Count == 0)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < Options.Count; ++i)
|
||||
if (String.Equals(Options[i].Key, key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
option = Options[i];
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Helper Functions
|
||||
|
||||
private void ParseImplementation(string headerStr, ref int pos, bool isOptionIsAnOption)
|
||||
{
|
||||
// According to https://tools.ietf.org/html/rfc7234#section-5.2.1.1
|
||||
// Max-Age has a form "max-age=5", but some (imgur.com specifically) sends it as "max-age:5"
|
||||
string key = headerStr.Read(ref pos, (ch) => ch != ';' && ch != '=' && ch != ':' && ch != ',', true);
|
||||
this.Key = key;
|
||||
|
||||
char? skippedChar = headerStr.Peek(pos - 1);
|
||||
bool isValue = skippedChar == '=' || skippedChar == ':';
|
||||
bool isOption = isOptionIsAnOption && skippedChar == ';';
|
||||
|
||||
while ((skippedChar != null && isValue || isOption) && pos < headerStr.Length)
|
||||
{
|
||||
|
||||
if (isValue)
|
||||
{
|
||||
string value = headerStr.ReadPossibleQuotedText(ref pos);
|
||||
this.Value = value;
|
||||
}
|
||||
else if (isOption)
|
||||
{
|
||||
HeaderValue option = new HeaderValue();
|
||||
option.ParseImplementation(headerStr, ref pos, false);
|
||||
|
||||
if (this.Options == null)
|
||||
this.Options = new List<HeaderValue>();
|
||||
|
||||
this.Options.Add(option);
|
||||
}
|
||||
|
||||
if (!isOptionIsAnOption)
|
||||
return;
|
||||
|
||||
skippedChar = headerStr.Peek(pos - 1);
|
||||
isValue = skippedChar == '=';
|
||||
isOption = isOptionIsAnOption && skippedChar == ';';
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Overrides
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (this.Options != null && this.Options.Count > 0)
|
||||
{
|
||||
StringBuilder sb = StringBuilderPool.Get(4); //new StringBuilder(4);
|
||||
sb.Append(Key);
|
||||
sb.Append("=");
|
||||
sb.Append(Value);
|
||||
|
||||
foreach(var option in Options)
|
||||
{
|
||||
sb.Append(";");
|
||||
sb.Append(option.ToString());
|
||||
}
|
||||
|
||||
return StringBuilderPool.ReleaseAndGrab(sb);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(Value))
|
||||
return Key + '=' + Value;
|
||||
else
|
||||
return Key;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 01b6bb7a29a6e7447baf8768ab665143
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,124 @@
|
||||
using Best.HTTP.Shared.Logger;
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
namespace Best.HTTP.Shared.Extensions
|
||||
{
|
||||
public sealed class RunOnceOnMainThread : IHeartbeat
|
||||
{
|
||||
private Action _action;
|
||||
private int _subscribed;
|
||||
private LoggingContext _context;
|
||||
|
||||
public RunOnceOnMainThread(Action action, LoggingContext context)
|
||||
{
|
||||
this._action = action;
|
||||
this._context = context;
|
||||
}
|
||||
|
||||
public void Subscribe()
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref this._subscribed, 1, 0) == 0)
|
||||
HTTPManager.Heartbeats.Subscribe(this);
|
||||
}
|
||||
|
||||
public void OnHeartbeatUpdate(DateTime now, TimeSpan dif)
|
||||
{
|
||||
try
|
||||
{
|
||||
this._action?.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception(nameof(RunOnceOnMainThread.OnHeartbeatUpdate), $"{nameof(_action)}", ex, this._context);
|
||||
}
|
||||
finally
|
||||
{
|
||||
HTTPManager.Heartbeats.Unsubscribe(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface IHeartbeat
|
||||
{
|
||||
void OnHeartbeatUpdate(DateTime utcNow, TimeSpan dif);
|
||||
}
|
||||
|
||||
enum HeartbeatUpdateEventType
|
||||
{
|
||||
Subscribe,
|
||||
Unsubscribe,
|
||||
Clear
|
||||
}
|
||||
|
||||
struct HeartbeatUpdateEvent
|
||||
{
|
||||
public HeartbeatUpdateEventType Event;
|
||||
public IHeartbeat Heartbeat;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A manager class that can handle subscribing and unsubscribeing in the same update.
|
||||
/// </summary>
|
||||
public sealed class HeartbeatManager
|
||||
{
|
||||
private List<IHeartbeat> _heartbeats = new List<IHeartbeat>();
|
||||
private DateTime _lastUpdate = DateTime.MinValue;
|
||||
|
||||
private ConcurrentQueue<HeartbeatUpdateEvent> _updates = new();
|
||||
|
||||
public void Subscribe(IHeartbeat heartbeat)
|
||||
{
|
||||
this._updates.Enqueue(new HeartbeatUpdateEvent { Event = HeartbeatUpdateEventType.Subscribe, Heartbeat = heartbeat });
|
||||
}
|
||||
|
||||
public void Unsubscribe(IHeartbeat heartbeat)
|
||||
{
|
||||
this._updates.Enqueue(new HeartbeatUpdateEvent { Event = HeartbeatUpdateEventType.Unsubscribe, Heartbeat = heartbeat });
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
var now = HTTPManager.CurrentFrameDateTime;
|
||||
|
||||
if (_lastUpdate == DateTime.MinValue)
|
||||
_lastUpdate = now;
|
||||
else
|
||||
{
|
||||
TimeSpan dif = now - _lastUpdate;
|
||||
_lastUpdate = now;
|
||||
|
||||
while (this._updates.TryDequeue(out var updateEvent))
|
||||
{
|
||||
switch (updateEvent.Event)
|
||||
{
|
||||
case HeartbeatUpdateEventType.Subscribe: this._heartbeats.Add(updateEvent.Heartbeat); break;
|
||||
case HeartbeatUpdateEventType.Unsubscribe: this._heartbeats.Remove(updateEvent.Heartbeat); break;
|
||||
case HeartbeatUpdateEventType.Clear: this._heartbeats.Clear(); break;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < this._heartbeats.Count; ++i)
|
||||
{
|
||||
var heartbeat = this._heartbeats[i];
|
||||
try
|
||||
{
|
||||
heartbeat.OnHeartbeatUpdate(now, dif);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("HeartbeatManager", heartbeat.GetType().Name, ex, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
this._updates.Enqueue(new HeartbeatUpdateEvent { Event = HeartbeatUpdateEventType.Clear, Heartbeat = null });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 169261907c72c004eae68f0a8ae6a8a8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Best.HTTP.Shared.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for specialized parsers
|
||||
/// </summary>
|
||||
public class KeyValuePairList
|
||||
{
|
||||
public List<HeaderValue> Values { get; protected set; }
|
||||
|
||||
public bool TryGet(string valueKeyName, out HeaderValue @param)
|
||||
{
|
||||
@param = null;
|
||||
for (int i = 0; i < Values.Count; ++i)
|
||||
if (string.CompareOrdinal(Values[i].Key, valueKeyName) == 0)
|
||||
{
|
||||
@param = Values[i];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4ccc301b8038cb64b9e489bafa17d2bc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.PlatformSupport.Threading;
|
||||
|
||||
namespace Best.HTTP.Shared.Extensions
|
||||
{
|
||||
public readonly struct TimerData
|
||||
{
|
||||
public readonly DateTime Created;
|
||||
public readonly TimeSpan Interval;
|
||||
public readonly object Context;
|
||||
|
||||
public readonly Func<DateTime, object, bool> OnTimer;
|
||||
|
||||
public bool IsOnTime(DateTime now)
|
||||
{
|
||||
return now >= this.Created + this.Interval;
|
||||
}
|
||||
|
||||
public TimerData(TimeSpan interval, object context, Func<DateTime, object, bool> onTimer)
|
||||
{
|
||||
this.Created = DateTime.UtcNow;
|
||||
this.Interval = interval;
|
||||
this.Context = context;
|
||||
this.OnTimer = onTimer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new TimerData but the Created field will be set to the current time.
|
||||
/// </summary>
|
||||
public TimerData CreateNew()
|
||||
{
|
||||
return new TimerData(this.Interval, this.Context, this.OnTimer);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("[TimerData Created: {0}, Interval: {1}, IsOnTime: {2}]", this.Created.ToString(System.Globalization.CultureInfo.InvariantCulture), this.Interval, this.IsOnTime(DateTime.UtcNow));
|
||||
}
|
||||
}
|
||||
|
||||
public static class Timer
|
||||
{
|
||||
private static List<TimerData> _timers = new List<TimerData>(1);
|
||||
private static ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
|
||||
|
||||
private static int _isSubscribed;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
public static void ResetSetup()
|
||||
{
|
||||
HTTPManager.Logger.Information(nameof(Timer), "Reset called!");
|
||||
Interlocked.Exchange(ref _isSubscribed, 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
public static void Add(TimerData timer)
|
||||
{
|
||||
using (var _ = new WriteLock(_lock))
|
||||
_timers.Add(timer);
|
||||
|
||||
if (Interlocked.CompareExchange(ref _isSubscribed, 1, 0) == 0)
|
||||
{
|
||||
HTTPManager.Logger.Information(nameof(Timer), "Subscribing timer to heartbeats!");
|
||||
HTTPManager.Heartbeats.Subscribe(new TimerImplementation());
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TimerImplementation : IHeartbeat
|
||||
{
|
||||
public void OnHeartbeatUpdate(DateTime now, TimeSpan dif)
|
||||
{
|
||||
using var __ = new Unity.Profiling.ProfilerMarker(nameof(Timer)).Auto();
|
||||
|
||||
using (var _ = new WriteLock(_lock))
|
||||
{
|
||||
if (_timers.Count == 0)
|
||||
{
|
||||
HTTPManager.Heartbeats.Unsubscribe(this);
|
||||
|
||||
Interlocked.Exchange(ref _isSubscribed, 0);
|
||||
|
||||
HTTPManager.Logger.Information(nameof(Timer), "Unsubscribing timer from heartbeats!");
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < _timers.Count; ++i)
|
||||
{
|
||||
TimerData timer = _timers[i];
|
||||
|
||||
if (timer.IsOnTime(now))
|
||||
{
|
||||
try
|
||||
{
|
||||
bool repeat = timer.OnTimer(now, timer.Context);
|
||||
|
||||
if (repeat)
|
||||
_timers[i] = timer.CreateNew();
|
||||
else
|
||||
_timers.RemoveAt(i--);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception(nameof(Timer), "OnTimer", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 70785d1a06f961348acdef24b845ba0c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user