using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
namespace EvtSource
{
public class EventSourceReader : IDisposable
{
private const string DefaultEventType = "message";
public delegate void MessageReceivedHandler(object sender, EventSourceMessageEventArgs e);
public delegate void DisconnectEventHandler(object sender, DisconnectEventArgs e);
private HttpClient Hc;
private Stream Stream = null;
private readonly Uri Uri;
private volatile bool _IsDisposed = false;
public bool IsDisposed => _IsDisposed;
private volatile bool IsReading = false;
private readonly object StartLock = new object();
private int ReconnectDelay = 3000;
private string LastEventId = string.Empty;
public event MessageReceivedHandler MessageReceived;
public event DisconnectEventHandler Disconnected;
///
/// An instance of EventSourceReader
///
/// URL to listen from
/// An optional custom handler for HttpClient
public EventSourceReader(Uri url, HttpMessageHandler handler = null)
{
Uri = url;
Hc = new HttpClient(handler ?? new HttpClientHandler());
}
///
/// Returns instantly and starts listening
///
/// current instance
/// Dispose() has been called
public EventSourceReader Start()
{
if (_IsDisposed)
{
throw new ObjectDisposedException("EventSourceReader");
}
lock (StartLock)
{
if (IsReading == false)
{
IsReading = true;
// Only start a new one if one isn't already running
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
ReaderAsync();
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
}
}
return this;
}
///
/// Stop and dispose of the EventSourceReader
///
public void Dispose()
{
_IsDisposed = true;
Stream?.Dispose();
Hc.CancelPendingRequests();
Hc.Dispose();
}
private async Task ReaderAsync()
{
try
{
Hc.DefaultRequestHeaders.TryAddWithoutValidation("Accept", "text/event-stream");
if (string.Empty != LastEventId)
{
if (Hc.DefaultRequestHeaders.Contains("Last-Event-Id"))
{
Hc.DefaultRequestHeaders.Remove("Last-Event-Id");
}
Hc.DefaultRequestHeaders.TryAddWithoutValidation("Last-Event-Id", LastEventId);
}
using (HttpResponseMessage response = await Hc.GetAsync(Uri, HttpCompletionOption.ResponseHeadersRead))
{
response.EnsureSuccessStatusCode();
if (response.Headers.TryGetValues("content-type", out IEnumerable ctypes) || ctypes?.Contains("text/event-stream") == false)
{
throw new ArgumentException("Specified URI does not return server-sent events");
}
Stream = await response.Content.ReadAsStreamAsync();
using (var sr = new StreamReader(Stream))
{
string evt = DefaultEventType;
string id = string.Empty;
var data = new StringBuilder(string.Empty);
while (true)
{
string line = await sr.ReadLineAsync();
if (line == string.Empty)
{
// double newline, dispatch message and reset for next
if (data.Length > 0)
{
MessageReceived?.Invoke(this, new EventSourceMessageEventArgs(data.ToString().Trim(), evt, id));
}
data.Clear();
id = string.Empty;
evt = DefaultEventType;
continue;
}
else if (line.First() == ':')
{
// Ignore comments
continue;
}
int dataIndex = line.IndexOf(':');
string field;
if (dataIndex == -1)
{
dataIndex = line.Length;
field = line;
}
else
{
field = line.Substring(0, dataIndex);
dataIndex += 1;
}
string value = line.Substring(dataIndex).Trim();
switch (field)
{
case "event":
// Set event type
evt = value;
break;
case "data":
// Append a line to data using a single \n as EOL
data.Append($"{value}\n");
break;
case "retry":
// Set reconnect delay for next disconnect
int.TryParse(value, out ReconnectDelay);
break;
case "id":
// Set ID
LastEventId = value;
id = LastEventId;
break;
default:
// Ignore other fields
break;
}
}
}
}
}
catch (Exception ex)
{
Disconnect(ex);
}
}
private void Disconnect(Exception ex)
{
IsReading = false;
Disconnected?.Invoke(this, new DisconnectEventArgs(ReconnectDelay, ex));
}
}
}