Files

225 lines
9.5 KiB
C#

using System;
namespace LaunchDarkly.EventSource
{
/// <summary>
/// Represents the Server-Sent Event message received from a stream.
/// </summary>
/// <remarks>
/// <para>
/// An SSE event consists of an event name (defaulting to "message" if not specified),
/// a data string, and an optional "ID" string that the server may provide.
/// </para>
/// <para>
/// The event name and ID properties are always stored as strings. By default, the
/// data property is also stored as a string. However, in some applications, it may
/// be desirable to represent the data as a UTF-8 byte array (for instance, if you are
/// using the <c>System.Text.Json</c> API to parse JSON data).
/// </para>
/// <para>
/// Since strings in .NET use two-byte UTF-16 characters, if you have a large block of
/// UTF-8 data it is considerably more efficient to process it in its original form
/// rather than converting it to or from a string. <see cref="EventSource"/> stores
/// data as strings by default, but you set <see cref="ConfigurationBuilder.PreferDataAsUtf8Bytes(bool)"/>
/// it can store the raw UTF-8 data instead. In either case, <c>MessageEvent</c> will
/// convert types transparently so that you can read either <see cref="MessageEvent.Data"/>
/// or <see cref="MessageEvent.DataUtf8Bytes"/>.
/// </para>
/// </remarks>
public struct MessageEvent
{
/// <summary>
/// The default value of <see cref="Name"/> if the SSE stream did not specify an
/// <c>event:</c> field.
/// </summary>
public const string DefaultName = "message";
#region Private Fields
private readonly string _name;
private readonly string _dataString;
private readonly Utf8ByteSpan _dataUtf8Bytes;
private readonly string _lastEventId;
private readonly Uri _origin;
#endregion
#region Public Constructors
/// <summary>
/// Initializes a new instance of the <see cref="MessageEvent"/> class.
/// </summary>
/// <param name="name">the event name</param>
/// <param name="data">the data received in the server-sent event</param>
/// <param name="lastEventId">the last event identifier, or null</param>
/// <param name="origin">the origin URI of the stream</param>
public MessageEvent(string name, string data, string lastEventId, Uri origin)
{
_name = name;
_dataString = data;
_dataUtf8Bytes = new Utf8ByteSpan();
_lastEventId = lastEventId;
_origin = origin;
}
/// <summary>
/// Initializes a new instance of the <see cref="MessageEvent"/> class,
/// providing the data as a UTF-8 byte span.
/// </summary>
/// <param name="name">the event name</param>
/// <param name="dataUtf8Bytes">the data received in the server-sent event;
/// the <c>MessageEvent</c> will store a reference to the byte array, rather than
/// copying it, so it should not be modified afterward by the caller
/// </param>
/// <param name="lastEventId">the last event identifier, or null</param>
/// <param name="origin">the origin URI of the stream</param>
public MessageEvent(string name, Utf8ByteSpan dataUtf8Bytes, string lastEventId, Uri origin)
{
_name = name;
_dataString = null;
_dataUtf8Bytes = dataUtf8Bytes;
_lastEventId = lastEventId;
_origin = origin;
}
/// <summary>
/// Initializes a new instance of the <see cref="MessageEvent" /> class.
/// </summary>
/// <param name="name">the event name</param>
/// <param name="data">the data received in the server-sent event</param>
/// <param name="origin">the origin URI of the stream</param>
/// <remarks>
/// The <see cref="LastEventId" /> will be initialized to null.
/// </remarks>
public MessageEvent(string name, string data, Uri origin) : this(name, data, null, origin) { }
#endregion
#region Public Properties
/// <summary>
/// The event name.
/// </summary>
/// <remarks>
/// This can be specified by the server in the <c>event:</c> field in the SSE data, as in
/// <c>event: my-event-name</c>. If there is no <c>event:</c> field, the default name
/// is <see cref="DefaultName"/>.
/// </remarks>
public string Name => _name;
/// <summary>
/// Gets the data received in the event as a string.
/// </summary>
/// <remarks>
/// <para>
/// This is the value of the <c>data:</c> field in the SSE data; if there are multiple
/// <c>data:</c> lines for a single event, they are concatenated with <c>"\n"</c>.
/// </para>
/// <para>
/// If the data was originally stored as a string, the same string is returned.
/// If it was stored as a UTF-8 byte array, the bytes are copied to a new string.
/// </para>
/// </remarks>
public string Data => _dataString is null ? _dataUtf8Bytes.GetString() : _dataString;
/// <summary>
/// Gets the data received in the event as a UTF-8 byte span.
/// </summary>
/// <remarks>
/// <para>
/// This is the value of the <c>data:</c> field in the SSE data; if there are multiple
/// <c>data:</c> lines for a single event, they are concatenated with <c>"\n"</c>.
/// </para>
/// <para>
/// If the data was originally stored as UTF-8 bytes, the returned value refers to
/// the same array, offset, and length (it is the caller's responsibility not to
/// modify the byte array). If it was originally stored as a string, the string
/// is copied to a new byte array.
/// </para>
/// </remarks>
public Utf8ByteSpan DataUtf8Bytes => _dataString is null ? _dataUtf8Bytes :
new Utf8ByteSpan(_dataString);
/// <summary>
/// Gets the last event identifier received in the server-sent event.
/// </summary>
/// <remarks>
/// This is the value of the <c>id:</c> field in the SSE data. If there is no such
/// field, it is null. You can use a previously received <see cref="LastEventId"/>
/// value with <see cref="ConfigurationBuilder.LastEventId(string)"/> when starting
/// a new <see cref="EventSource"/> to tell the server what the last event you
/// received was, although not all servers support this.
/// </remarks>
public string LastEventId => _lastEventId;
/// <summary>
/// Gets the origin URI of the stream that generated the server-sent event.
/// </summary>
public Uri Origin => _origin;
/// <summary>
/// True if the event data is stored internally as UTF-8 bytes.
/// </summary>
/// <remarks>
/// The data can be accessed with either <see cref="Data"/> or <see cref="DataUtf8Bytes"/>
/// regardless of the value of this property. The property only indicates the <i>original</i>
/// format of the data, so, for instance, if it is <see langword="true"/> then
/// reading <see cref="Data"/> will have more overhead (due to copying) than
/// reading <see cref="DataUtf8Bytes"/>.
/// </remarks>
public bool IsDataUtf8Bytes => _dataString is null;
#endregion
#region Public Methods
/// <summary>
/// Determines whether the specified <see cref="System.Object" /> is equal to this instance.
/// </summary>
/// <remarks>
/// This method is potentially inefficient and should be used only in testing.
/// </remarks>
/// <param name="obj">the <see cref="System.Object" /> to compare with this instance</param>
/// <returns><see langword="true"/> if the instances are equal</returns>
public override bool Equals(object obj)
{
if (!(obj is MessageEvent that) ||
!object.Equals(Name, that.Name) ||
!object.Equals(LastEventId, that.LastEventId) ||
!object.Equals(Origin, that.Origin))
{
return false;
}
if (this._dataString != null)
{
return that._dataString != null ?
this._dataString == that._dataString :
that._dataUtf8Bytes.Equals(this._dataString);
}
return that._dataString != null ?
this._dataUtf8Bytes.Equals(that._dataString) :
this._dataUtf8Bytes.Equals(that._dataUtf8Bytes);
}
/// <summary>
/// Returns a hash code for this instance.
/// </summary>
/// <returns>
/// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
/// This method is potentially inefficient and should be used only in testing.
/// </returns>
public override int GetHashCode()
{
int hash = 17;
hash = hash * 31 + (_dataString != null ? _dataString.GetHashCode() : _dataUtf8Bytes.GetString().GetHashCode());
hash = hash * 31 + (_lastEventId != null ? _lastEventId.GetHashCode() : 0);
hash = hash * 31 + (_origin != null ? _origin.GetHashCode() : 0);
return hash;
}
#endregion
}
}