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 } }