npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@alessiofrittoli/server-sent-events

v1.1.0

Published

Class Based implementation for Server-Sent events

Downloads

223

Readme

Server-Sent Events ✅

Version 1.1.0

Class Based implementation for Server-Sent events

Table of Contents

Getting started

Server-Side

Streaming events data

With server-sent events, it's possible for a server to send new data to a web page at any time, by pushing messages to the web page. These incoming messages can be treated as Events + data inside the web page.

By creating a new instance of ServerSentEvents class you will get access to methods required to stream data to the client.

Simple return ServerSentEvents.stream.readable in a Response instance to start streaming data.
⚠️ Do not forget to return the proper response headers.
⚠️ The streaming Response implementation depends on your back-end application.

import ServerSentEvents from '@alessiofrittoli/server-sent-events'

const sse = new ServerSentEvents()

return (
	new Response( sse.stream.readable, { headers: sse.headers } )
)
Writing into the stream

By using the method ServerSentEvents.push() you can write and push any serializeable data to the client.
This method will serialize your data before sending it to the client by using JSON.stringify().
The data is then read by the client by listening to the default message event on the EventSource instance (See Reading server-sent events data for more information about reading received data).

For this example we are going to execute a function which simulates an asynchronous long task.

import ServerSentEvents from '@alessiofrittoli/server-sent-events'

export const sleep = ( ms: number ) => (
	new Promise<void>( resolve => setTimeout( resolve, ms ) )
)

const longRunning = async ( stream: ServerSentEvents ) => {
	await stream.push( { message: 'Started' } )
	await sleep( 1000 )
	await stream.push( { message: 'Done 15%' } )
	await sleep( 1000 )
	await stream.push( { message: 'Done 35%' } )
	await sleep( 1000 )
	await stream.push( { message: 'Done 75%' } )
	await sleep( 1000 )
	await stream.push( { message: 'Final data' } )
}


const businessLogic = () => {

	const sse = new ServerSentEvents()

	longRunning( sse )

	return (
		new Response( sse.stream.readable )
	)

}
Custom events

By default our data is implicitly sent over the default message event.

However, you may need to push new data into the same connection stream under a custom event.
We can then specify its name as 2nd argument of the ServerSentEvents.push() method like so:

...

sse.push( { message: 'My data' }, 'customEvent' )

...

Notice that the client should now add a new listener to the EventSource instance referring to the name of the custom event.
Please refer to the Reading server-sent custom events data section for more information.

Closing the stream

The EventSource API has a built-in automatic reconnection mechanism. If the connection to the server is lost (due to network issues, server restart, etc.), the EventSource will try to reconnect after a delay if the client is not explicitly calling the EventSource.close() method. By default, this delay is 3 seconds, see Reconnection policies section for more information.

Due to the EventSource automatic recconnection mechanism the previous code will eventually end up in a sort of an infinite loop.

To tell the client that streaming is complete we can execute the ServerSentEvents.close() method. This method will push a custom event named "end" in the stream and close the ServerSentEvents.writer (See Custom events section to learn more about custom events).
⚠️ The client should listen for the "end" event and then close the EventSource connection with EventSource.close().

Since our previous function returns a void Promise, we can await it and then call the ServerSentEvents.close() method like so:

...

const businessLogic = () => {

	const sse = new ServerSentEvents()

	longRunning( sse )
		.then( () => {
			console.log( 'Streaming done.' )
			sse.close()
		} )

	return (
		new Response( sse.stream.readable )
	)

}
Reconnection policies

The EventSource API has a built-in automatic reconnection mechanism. If the connection to the server is lost (due to network issues, server restart, etc.), the EventSource will try to reconnect after a delay if the client is not explicitly calling the EventSource.close() method. By default, this delay is 3 seconds but the server can override this timing by setting the "retry" policy.

When creating a new instance of ServerSentEvents we can specify the reconnection delay value in milliseconds to the "retry" property of the constructor like so:

const sse = new ServerSentEvents( { retry: 5000 } )
Error handling

If an error occures in our longRunning example function we can use the ServerSentEvents.error() method to push a custom error event to the stream. The client should listen the default error event on the EventSource to handle errors client side.

Good to know - Since the ServerSentEvents.error() will push an event with the name error, the client could use a single listener to listen default and custom errors (See EventSource Error handling section to learn more about error handling on client).

...

const businessLogic = () => {

	...

	longRunning( sse )
		.then( () => {
			...
		} )
		.catch( error => {
			console.error( 'Failed', error )
			sse.error( { message: error.message } )
		} )

	...

}
Abort handling

Sometimes, error handling or awaiting a task to be finished before closing the stream could be not enough. Let's assume your task is an infinite task (like returning the time once a second) and the user abort the EventSource request by closing the client or by calling the EventSource.close() method: your infinite task will be still running.

Most back-end server applications will allow you to listen for an abort signal that will be fired when the request has been aborted by the client by forcibly shutting down the connection or by calling the EventSource.close() method. By listening for an abort signal, we can then execute the ServerSentEvents.abort() which will abort the ServerSentEvents.writer and prevent subsequent write events by setting the ServerSentEvents.closed flag to true.

For this example, we are going to desing a function that will push to the stream the current date in a ISO string format once a second. In our interval we check if ServerSentEvents.closed has been set to true before pushing new data into the stream and resolve the Promise if so.

...

const timer = async ( stream: ServerSentEvents ) => {
	await stream.push( { message: new Date().toISOString() } )
	await new Promise<void>( resolve => {
		const interval = setInterval( () => {
			if ( stream.closed ) {
				clearInterval( interval )
				return resolve()
			}
			stream.push( { message: new Date().toISOString() } )
		}, 1000 )
	} )
}

const businessLogic = request => {

	...

	request.signal.addEventListener( 'abort', event => {
		sse.abort( 'Request has been aborted from user.' )
	} )

	timer( sse )
		.then( () => {
			...
		} )
		.catch( error => {
			console.error( 'Failed', error )
			if ( error.name === 'AbortError' ) return
			sse.error( { message: error.message } )
		} )

	...

}

Client-Side

Reading server-sent events data

To listen server-sent events we can use the native EventSource Web API.
The connection remains open until closed by calling EventSource.close().

Once the connection is opened, incoming messages from the server are delivered to your code in the form of events. If there is an event field in the incoming message, the triggered event is the same as the event field value. If no event field is present, then a generic message event is fired.

const eventSource = new EventSource( new URL( ... ) )

eventSource.addEventListener( 'open', event => {
	console.log( 'Connection opened', event )
} )

eventSource.addEventListener( 'message', event => {
	const data = JSON.parse( event.data )
	console.log( '"message" event received data', data )
} )
Reading server-sent custom events data

To listen for incoming messages from the server sent over a custom event, we just add an event listener on the EventSource instance like we've done in the previous example.

...

eventSource.addEventListener( 'customEvent', event => {
	const data = JSON.parse( event.data )
	console.log( '"customEvent" event received data', data )
} )
Closing the EventSource

We can call the EventSource.close() method arbitrarily or listen to the "end" event sent by the server to close the EventSource connection.

...

const cancelRequestButton = document.querySelector( '#cancel' )

cancelRequestButton.addEventListener( 'click', e vent=> {
	eventSource.close()
	console.log( 'User aborted the request.', eventSource.readyState )
} )
...

eventSource.addEventListener( 'end', event => {
	eventSource.close()
	console.log( '"end" event received', eventSource.readyState, event )
} )
EventSource Error handling

By default, errors are handled by listening to the "error" event on the EventSource instance.
The server may use this event too to return handled errors occured on the server while running its tasks.

eventSource.addEventListener( 'error', event => {
	eventSource.close()
	console.log( '"error" event', eventSource.readyState, event )
} )

Security

If you believe you have found a security vulnerability, we encourage you to responsibly disclose this and NOT open a public issue. We will investigate all legitimate reports. Email [email protected] to disclose any security vulnerabilities.

Made with ☕