using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading;
namespace LaunchDarkly.EventSource
{
///
/// A standard Builder pattern for constructing a instance.
///
///
///
/// Initialize a builder by calling new ConfigurationBuilder(uri) or
/// Configuration.Builder(uri). The URI is always required; all other properties
/// are set to defaults. Use the builder's setter methods to modify any desired properties;
/// setter methods can be chained. Then call Build() to construct the final immutable
/// Configuration.
///
///
/// All setter methods will throw ArgumentException if called with an invalid value,
/// so it is never possible for Build() to fail.
///
///
public class ConfigurationBuilder
{
#region Private Fields
internal readonly Uri _uri;
internal TimeSpan _initialRetryDelay = Configuration.DefaultInitialRetryDelay;
internal TimeSpan _backoffResetThreshold = Configuration.DefaultBackoffResetThreshold;
internal string _lastEventId;
internal IDictionary _requestHeaders = new Dictionary();
internal HttpMessageHandler _httpMessageHandler;
internal HttpClient _httpClient;
internal TimeSpan _maxRetryDelay = Configuration.DefaultMaxRetryDelay;
internal HttpMethod _method = HttpMethod.Get;
internal bool _preferDataAsUtf8Bytes = false;
internal TimeSpan _readTimeout = Configuration.DefaultReadTimeout;
internal Func _requestBodyFactory;
internal TimeSpan _responseStartTimeout = Configuration.DefaultResponseStartTimeout;
internal Action _httpRequestModifier;
#endregion
#region Constructor
internal ConfigurationBuilder(Uri uri)
{
if (uri == null)
{
throw new ArgumentNullException(nameof(uri));
}
this._uri = uri;
}
#endregion
#region Public Methods
///
/// Constructs a instance based on the current builder properies.
///
/// the configuration
public Configuration Build() =>
new Configuration(this);
///
/// Obsolete name for .
///
/// the timeout
/// the builder
[Obsolete("Use ResponseStartTimeout")]
public ConfigurationBuilder ConnectionTimeout(TimeSpan responseStartTimeout) =>
ResponseStartTimeout(responseStartTimeout);
///
/// Sets a delegate hook invoked before an HTTP request is performed. This may be useful if you
/// want to modify some properties of the request that EventSource doesn't already have an option for.
///
/// code that will be called with the request before it is sent
/// the builder
public ConfigurationBuilder HttpRequestModifier(Action httpRequestModifier)
{
this._httpRequestModifier = httpRequestModifier;
return this;
}
///
/// Sets the initial amount of time to wait before attempting to reconnect to the EventSource API.
///
///
///
/// If the connection fails more than once, the retry delay will increase from this value using
/// a backoff algorithm.
///
///
/// A server-directed reconnection time received via the SSE retry: field supersedes
/// this value, as do bounds installed by
/// .
///
///
/// The default value is . Negative values
/// are changed to zero.
///
///
/// The actual duration of each delay will vary slightly because there is a random jitter
/// factor to avoid clients all reconnecting at once.
///
///
/// the initial retry delay
/// the builder
///
public ConfigurationBuilder InitialRetryDelay(TimeSpan initialRetryDelay)
{
_initialRetryDelay = FiniteTimeSpan(initialRetryDelay);
return this;
}
///
/// Sets the maximum amount of time to wait before attempting to reconnect.
///
///
///
/// EventSource uses an exponential backoff algorithm (with random jitter) so that
/// the delay between reconnections starts at but
/// increases with each subsequent attempt. MaxRetryDelay sets a limit on how long
/// the delay can be.
///
///
/// The default value is . Negative values
/// are changed to zero.
///
///
/// the maximum retry delay
/// the builder
///
public ConfigurationBuilder MaxRetryDelay(TimeSpan maxRetryDelay)
{
_maxRetryDelay = FiniteTimeSpan(maxRetryDelay);
return this;
}
///
/// Sets the amount of time a connection must stay open before the EventSource resets its backoff delay.
///
///
///
/// If a connection fails before the threshold has elapsed, the delay before reconnecting will be greater
/// than the last delay; if it fails after the threshold, the delay will start over at the initial minimum
/// value. This prevents long delays from occurring on connections that are only rarely restarted.
///
///
/// A connection that stays open for at least this long also discards any bounds set by
/// , restoring
/// and . A
/// server-directed reconnection time received via the SSE retry: field is not
/// discarded.
///
///
/// The default value is . Negative
/// values are changed to zero.
///
///
/// the threshold time
/// the builder
public ConfigurationBuilder BackoffResetThreshold(TimeSpan backoffResetThreshold)
{
_backoffResetThreshold = FiniteTimeSpan(backoffResetThreshold);
return this;
}
///
/// Sets the maximum amount of time EventSource will wait between starting an HTTP request and
/// receiving the response headers.
///
///
///
/// This is the same as the Timeout property in .NET's HttpClient. The default value is
/// .
///
///
/// It is not the same as a TCP connection timeout. A connection timeout would include only the
/// time of establishing the connection, not the time it takes for the server to prepare the beginning
/// of the response. .NET does not consistently support a connection timeout, but if you are using .NET
/// Core or .NET 5+ you can implement it by using SocketsHttpHandler as your
/// and setting the
/// ConnectTimeout property there.
///
///
/// the timeout
///
public ConfigurationBuilder ResponseStartTimeout(TimeSpan responseStartTimeout)
{
_responseStartTimeout = TimeSpanCanBeInfinite(responseStartTimeout);
return this;
}
///
/// Sets the timeout when reading from the EventSource API.
///
///
///
/// The connection will be automatically dropped and restarted if the server sends no data within
/// this interval. This prevents keeping a stale connection that may no longer be working. It is common
/// for SSE servers to send a simple comment line (":") as a heartbeat to prevent timeouts.
///
///
/// The default value is .
///
///
public ConfigurationBuilder ReadTimeout(TimeSpan readTimeout)
{
_readTimeout = TimeSpanCanBeInfinite(readTimeout);
return this;
}
///
/// Sets the last event identifier.
///
///
/// Setting this value will cause EventSource to add a "Last-Event-ID" header in its HTTP request.
/// This normally corresponds to the field of a previously
/// received event.
///
/// the event identifier
/// the builder
public ConfigurationBuilder LastEventId(string lastEventId)
{
_lastEventId = lastEventId;
return this;
}
///
/// Sets the logging implementation to be used for all EventSource log output.
///
///
///
/// This uses the ILogAdapter abstraction from the LaunchDarkly.Logging library,
/// which provides several basic implementations such as Logs.ToConsole and an integration
/// with the .NET Core logging framework. For more about this and about adapters to other logging
/// frameworks, see LaunchDarkly.Logging.
///
///
/// LaunchDarkly.Logging defines logging levels of Debug, Info, Warn, and Error. If you do not
/// want detailed Debug-level logging, use the Level() modifier to set a minimum level of Info
/// or above, as shown in the code example (unless you are using an adapter to another logging
/// framework that has its own way of doing log filtering).
///
///
/// Log messages will use as the logger name. If you
/// want to specify a different logger name, use .
///
///
/// If you don't specify or ,
/// EventSource will not do any logging.
///
///
///
/// using LaunchDarkly.Logging;
///
/// // Send log output to the console (standard error), suppressing Debug messages
/// var config = new ConfigurationBuilder(uri).
/// LogAdapter(Logs.ToConsole.Level(LogLevel.Info)).
/// Build();
///
/// a LaunchDarkly.Logging.ILogAdapter
/// the builder
public ConfigurationBuilder LogAdapter()
{
return this;
}
///
/// Sets a custom logger to be used for all EventSource log output.
///
///
///
/// This uses the Logger type from the LaunchDarkly.Logging library,
/// which provides several basic implementations such as Logs.ToConsole and an integration
/// with the .NET Core logging framework. For more about this and about adapters to other logging
/// frameworks, see LaunchDarkly.Logging.
///
///
/// If you don't specify or ,
/// EventSource will not do any logging.
///
///
///
/// using LaunchDarkly.Logging;
///
/// // Define a logger that sends output to the console (standard output), suppressing
/// // Debug messages, and using a logger name of "EventStream"
/// var logger = Logs.ToConsole.Level(LogLevel.Info).Logger("EventStream");
/// var config = new ConfigurationBuilder(uri).
/// Logger(logger).
/// Build();
///
/// a LaunchDarkly.Logging.Logger instance
/// the builder
public ConfigurationBuilder Logger()
{
return this;
}
///
/// Specifies whether to use UTF-8 byte arrays internally if possible when
/// reading the stream.
///
///
/// As described in , in some applications it may be
/// preferable to store and process event data as UTF-8 byte arrays rather than
/// strings. By default, EventSource will use the string type when
/// processing the event stream; if you then use
/// to get the data, it will be converted to a byte array as needed. However, if
/// you set PreferDataAsUtf8Bytes to , the event data
/// will be stored internally as a UTF-8 byte array so that if you read
/// , you will get the same array with no
/// extra copying or conversion. Therefore, for greatest efficiency you should set
/// this to if you intend to process the data as UTF-8. Note
/// that Server-Sent Event streams always use UTF-8 encoding, as required by the
/// SSE specification.
///
/// true if you intend to request the event
/// data as UTF-8 bytes
/// the builder
public ConfigurationBuilder PreferDataAsUtf8Bytes(bool preferDataAsUtf8Bytes)
{
_preferDataAsUtf8Bytes = preferDataAsUtf8Bytes;
return this;
}
///
/// Sets the request headers to be sent with each EventSource HTTP request.
///
/// the headers (null is equivalent to an empty dictionary)
/// the builder
public ConfigurationBuilder RequestHeaders(IDictionary headers)
{
_requestHeaders = headers is null ? new Dictionary() :
new Dictionary(headers);
return this;
}
///
/// Adds a request header to be sent with each EventSource HTTP request.
///
/// the header name
/// the header value
/// the builder
public ConfigurationBuilder RequestHeader(string name, string value)
{
if (name != null)
{
_requestHeaders[name] = value;
}
return this;
}
///
/// Sets the HttpMessageHandler that will be used for the HTTP client, or null for the default handler.
///
///
/// If you have specified a custom HTTP client instance with , then
/// is ignored.
///
/// the message handler implementation
/// the builder
public ConfigurationBuilder HttpMessageHandler(HttpMessageHandler handler)
{
this._httpMessageHandler = handler;
return this;
}
///
/// Specifies that EventSource should use a specific HttpClient instance for HTTP requests.
///
///
///
/// Normally, EventSource creates its own HttpClient and disposes of it when you dispose of the
/// EventSource. If you provide your own HttpClient using this method, you are responsible for
/// managing the HttpClient's lifecycle-- EventSource will not dispose of it.
///
///
/// EventSource will not modify this client's properties, so if you call
/// or , those methods will be ignored.
///
///
/// an HttpClient instance, or null to use the default behavior
/// the builder
public ConfigurationBuilder HttpClient(HttpClient client)
{
this._httpClient = client;
return this;
}
///
/// Sets the HTTP method that will be used when connecting to the EventSource API.
///
///
/// By default, this is .
///
public ConfigurationBuilder Method(HttpMethod method)
{
this._method = method ?? throw new ArgumentNullException(nameof(method));
return this;
}
///
/// Sets a factory for HTTP request body content, if the HTTP method is one that allows a request body.
///
///
/// This is in the form of a factory function because the request may need to be sent more than once.
///
/// the factory function, or null for none
/// the builder
public ConfigurationBuilder RequestBodyFactory(Func factory)
{
this._requestBodyFactory = factory;
return this;
}
///
/// Equivalent , but for content
/// that is a simple string.
///
/// the content
/// the Content-Type header
/// the builder
public ConfigurationBuilder RequestBody(string bodyString, string contentType)
{
return RequestBodyFactory(() => new StringContent(bodyString, Encoding.UTF8, contentType));
}
#endregion
#region Private methods
// Canonicalizes the value so all negative numbers become InfiniteTimeSpan
private static TimeSpan TimeSpanCanBeInfinite(TimeSpan t) =>
t < TimeSpan.Zero ? Timeout.InfiniteTimeSpan : t;
// Replaces all negative times with zero
private static TimeSpan FiniteTimeSpan(TimeSpan t) =>
t < TimeSpan.Zero ? TimeSpan.Zero : t;
#endregion
}
}