@launchdarkly/eventsource
v0.2.1
Published
W3C compliant EventSource (server-sent events) client built on fetch, used by LaunchDarkly SDKs
Maintainers
Readme
LaunchDarkly EventSource for JavaScript SDKs
[!CAUTION] This package is in pre-release and not subject to backwards compatibility guarantees. The API may change based on feedback.
Pin to a specific minor version and review the changelog before upgrading.
This package contains a W3C-compliant EventSource (server-sent events) client built on fetch(),
ReadableStream and AbortController.
This package is intended to be used by LaunchDarkly SDKs and not as a general EventSource implementation.
This package is derived from LaunchDarkly's
launchdarkly-eventsource package,
which is a fork of the eventsource npm package.
See LICENSE for the original license terms.
The stream parser at src/parser is a fork of the
eventsource-parser npm package, version
4.1.1. It is maintained in this repository; see the third-party notices in LICENSE for
its original MIT license terms.
Install
npm install @launchdarkly/eventsourceQuick usage
import { createEventSource } from '@launchdarkly/eventsource';
const es = createEventSource('https://example.com/stream', {
initialRetryDelayMillis: 1000,
maxBackoffMillis: 30000,
jitterRatio: 0.5,
});
// The connection attempt starts before createEventSource returns. Attach
// listeners immediately after the call, before any other code runs, to avoid
// missing an event.
es.onopen = (event) => {
console.log('connected', event.headers);
};
es.onerror = (event) => {
if (event?.status === 401) {
console.log('not authorized');
}
};
es.addEventListener('message', (event) => {
console.log('received', event.data);
});
// Later, when the stream is no longer needed:
es.close();The EventSource instance
createEventSource returns an object that satisfies the exported EventSource interface:
readyState-- the connection state:CONNECTING(0),OPEN(1), orCLOSED(2). The package exports these constants.url-- the URL of the most recent connection attempt. With aurlBuilderit can change between attempts.reconnectInterval-- mirrors the most recent serverretry:field, in milliseconds; starts at 1000. The retry strategy owns the actual reconnect timing; this slot only supplies the fallback delay for a reconnect whose strategy call throws.close()-- permanently closes the stream, invokesonclose, and dispatches theclosedevent. Idempotent.
Unlike the standard EventSource, there is no onmessage slot; it is left out on purpose.
Register message listeners with addEventListener('message', ...).
Options reference
This section documents the behavior of createEventSource's second argument for maintainers of
this package and its consumers. The authoritative definitions, including any behavior not covered
here, are the TSDoc comments on EventSourceInitDict in src/types.ts.
Events
Listeners attach in two ways:
- The
onopen,onerror,onretrying, andonclosemembers are plain writable slots, one listener each. Assigning a slot never affects listeners registered throughaddEventListener. For one dispatched event, the matching slot runs first, then theaddEventListenerlisteners.oncloseis an exception. Only the publicclose()method invokes it, never event dispatch. addEventListener(type, listener)andremoveEventListener(type, listener)register and remove any number of listeners per event type, including the SSE event types a server names through theevent:field.
Listeners are typed by event type. For the event types this implementation itself dispatches,
the payload type comes from the exported EventSourceEventMap. A server-named SSE event carries
a MessageEvent.
Beyond the standard open/message/error events, this implementation dispatches:
closed: the stream has been permanently closed, either by a non-retryable error or by callingclose(). Whenclose()closes the stream, theoncloseslot is invoked at the same point, before theclosedlisteners; a non-retryable error dispatchesclosedwithout invokingonclose.end: the server ended the stream cleanly, with a complete response body. Not reported as anerror, but still passed toerrorFilterfor retry purposes. A mid-stream connection drop -- a reset, or a socket that closes without completing the response -- surfaces from the fetch transport as a read failure, so it dispatcheserrorand invokesonerror. A caller that treatsendanderrordifferently must expecterrorfor an incomplete close.retrying: after an error, indicates a reconnect is scheduled. The event'sdelayMillisproperty gives the delay.
The open event's headers property carries the HTTP response headers from the stream, as a
plain object:
{
'content-type': 'text/event-stream; charset=utf-8',
'transfer-encoding': 'chunked',
'cache-control': 'no-cache, no-store, must-revalidate',
}The error event carries status/message for HTTP errors:
es.onerror = (err) => {
if (err?.status === 401 || err?.status === 403) {
console.log('not authorized');
}
};A server-sent SSE frame named error also dispatches under the error type, with a
MessageEvent payload; only that frame carries a string data property.
An exception thrown by the onopen, onerror, or onretrying slot does not stop dispatch to
the addEventListener listeners for that event. An exception from a registered listener does
not stop the parse of the current chunk. Neither stops the stream's own reconnection logic; the
exception surfaces later, asynchronously, as an uncaught error.
Retry delay: backoff and jitter
initialRetryDelayMillis-- base delay before the first reconnect attempt (default 1000ms).maxBackoffMillis-- if set, the delay grows exponentially on each successive retry, up to this ceiling.jitterRatio-- if set, each computed delay is randomly reduced by up to this fraction.retryResetIntervalMillis-- how long the current connection must have been delivering events, measured from its first event, before the backoff counter resets to the initial delay. A connection that fails before it delivers an event does not count as healthy.
Without maxBackoffMillis and jitterRatio, every retry waits the same base delay. Set both, so
that clients which lose their connections at the same time -- for example, in a server outage --
do not all reconnect at the same time:
const es = createEventSource(url, {
initialRetryDelayMillis: 2000, // the first retry waits 2 seconds
maxBackoffMillis: 30000, // enables backoff, with a maximum of 30 seconds
retryResetIntervalMillis: 60000, // backoff resets after 60 seconds of delivered events
jitterRatio: 0.5, // each delay is reduced by a random amount of up to 50%
});Custom retry delay strategy
The retryDelayStrategy option replaces the built-in retry timing with a caller-supplied object.
The object must implement three methods:
nextRetryDelay(currentTimeMillis)-- called when a reconnect is about to be scheduled; returns the delay in milliseconds before the next attempt.setGoodSince(goodSinceTimeMillis)-- called when the first event of a connection arrives, to mark the connection as delivering data.setBaseDelay(delayMillis)-- called when the server sends aretry:field; the value reaches the strategy already validated as all ASCII digits and capped at one hour.
The timestamps the client passes come from one monotonic clock. They are not epoch times, and a
strategy must only compare them with each other, never with Date.now().
When retryDelayStrategy is set it fully replaces the built-in behavior, so
initialRetryDelayMillis, maxBackoffMillis, jitterRatio, and retryResetIntervalMillis have
no effect. When the option is absent, the built-in options above apply.
Error retry behavior
By default, connection failures and I/O errors are always retried; HTTP error responses are
retried only for 500, 502, 503, and 504. A 200 response that declares a Content-Type other than
text/event-stream, or whose headers cannot be read, is also treated as an error. Its report
carries no status, so the default filter retries it like a transient transport condition. A
response with no Content-Type header is accepted, so a minimal injected transport can report no
headers. Set errorFilter to override this -- it receives the error and returns true to retry
or false to close the stream and raise error. A filter that throws is treated as if it
returned false. The stream then closes cleanly and releases its connection, and the exception
surfaces asynchronously as an uncaught error, like a throwing listener's. Each status other than
200, including a redirect status that the transport did not follow, is an HTTP error response.
const es = createEventSource(url, {
// Retry every error except an unauthorized response.
errorFilter: (err) => err.status !== 401,
});Headers, method, and body
headers sets additional request headers, for example to send cookies or an initial
Last-Event-ID value. Normally Cache-Control: no-cache and Accept: text/event-stream are
also sent; skipDefaultHeaders: true sends only the headers you specify. This matters for a
cross-origin request, because CORS restricts the headers a request can carry. method overrides
the default GET; body sets a request body, for use with a non-GET method.
const es = createEventSource(url, {
method: 'REPORT',
body: JSON.stringify(context),
headers: { Authorization: sdkKey },
skipDefaultHeaders: true,
});Read timeout
readTimeoutMillis drops and retries the connection if that many milliseconds elapse with no data
received, guarding against a TCP connection that fails without an I/O error. When the server sends
heartbeat data at a known interval -- such as a : comment line, which SSE ignores -- set the
timeout longer than that interval.
Listener registry
Listener storage and dispatch for addEventListener registrations go through a registry created
once per instance. The createEventRegistry option supplies the factory; when absent, the
instance uses the internal Map-backed registry, whose factory is also exported as
createDefaultEventRegistry. A substitute must satisfy the exported EventListenerRegistry
interface -- for example, an SDK can supply one backed by Node's EventEmitter. The registry
never sees the on* slots; they are plain members of the instance. The registry is trusted
code. It observes every event the instance dispatches and controls what its listeners actually
receive.
Credentials
withCredentials: true maps to fetch()'s credentials: 'include', so a cross-origin stream is
opened with cookies and HTTP authentication.
Dynamic URLs
urlBuilder returns the URL to use for the next connection attempt and is called immediately
before every request, including the first. This is how a query parameter that changes between
attempts stays current across reconnects.
Injectable transport
The fetch option supplies the transport used to open the stream. When absent, the client uses
the global fetch. The option only requires the structural subset of the fetch API that the
client uses (see FetchLike in src/httpHelpers.ts), so a real fetch implementation satisfies it
without casts. This is how the Node-based SDKs connect this client to node:http/node:https
with their agent, proxy, and TLS configuration. The client itself knows nothing about any of
those.
Contributing
See Contributing.
Verifying SDK build provenance with the SLSA framework
LaunchDarkly uses the SLSA framework (Supply-chain Levels for Software Artifacts) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published SDK packages. To learn more, see the provenance guide.
About LaunchDarkly
- LaunchDarkly is a continuous delivery platform that provides feature flags as a service and allows developers to iterate quickly and safely. We allow you to easily flag your features and manage them from the LaunchDarkly dashboard. With LaunchDarkly, you can:
- Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases.
- Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?).
- Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file.
- Grant access to certain features based on user attributes, like payment plan (eg: users on the 'gold' plan get access to more features than users in the 'silver' plan).
- Disable parts of your application to facilitate maintenance, without taking everything offline.
- LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Read our documentation for a complete list.
- Explore LaunchDarkly
- launchdarkly.com for more information
- docs.launchdarkly.com for our documentation and SDK reference guides
- apidocs.launchdarkly.com for our API documentation
- blog.launchdarkly.com for the latest product updates
