Server Sent Events with SvelteKit

How to use Server Sent Events with SvelteKit

Create a SSE endpoint (+server.ts)

// Set containing all the subscribers
const subscribers = new Set<ReadableStreamDefaultController>();

// Create a string from an object
function create_message_string(message: object) {
	return (
		Object.entries(message)
			.map(([key, value]) => `${key}: ${value}`)
			.join('\n') + '\n\n'
	);
}

export async function GET() {
	let _controller: ReadableStreamDefaultController;
	const stream = new ReadableStream({
    // the controller is the current connection
		start(controller) {
			_controller = controller;

      // Add the controller to the set of subscribers
			subscribers.add(_controller);

			// If you want to close the connection: controller.close();
		},
		cancel() {
      // Remove the controller from the set of subscribers
			subscribers.delete(_controller);
		}
	});

	return new Response(stream, {
		headers: {
			'Content-Type': 'text/event-stream',
			'Cache-Control': 'no-cache'
		}
	});
}

export const POST = async ({ request }) => {
	const body = await request.json();

  // create the string to send to the client
	const data = create_message_string({
		data: JSON.stringify(body)
	});

  // send the data to all the subscribers
	for (const item of subscribers) {
		item.enqueue(data);
	}

	return new Response('Message sent to subscribers', { status: 200 });
};

Client-side code (+page.svelte or component)

<script>
	import { onMount } from 'svelte';

  // list of messages and the value of the input
	let messages: string[] = [];
	let value = '';

  // subscribe to the SSE endpoint on mount
	onMount(subscribe);

	function subscribe() {
    // create event source and listen to messages
		const event_source = new EventSource('/sse');
		event_source.addEventListener('message', (event) => {
			const data = JSON.parse(event.data);
      // push the message to the list
			messages = [...messages, data.value];
		});

		return () => event_source.close();
	}

  // send message to the server
	async function send() {
		const res = await fetch(`/sse`, {
			method: 'POST',
			headers: {
				'Content-Type': 'application/json'
			},
			body: JSON.stringify({
				value
			})
		});
		if (!res.ok) throw Error('Error');
		value = '';
	}
</script>

<form on:submit|preventDefault={send}>
	<input type="text" bind:value />
	<button type="submit">Send</button>
</form>

<ol>
	{#each messages as message}
		<li>{message}</li>
	{/each}
</ol>

Keep message history on the server

// Add to the top of +server.ts
const messageHistory: string[] = [];

// Add right after subscribers.add()
messageHistory.forEach((element) => {
	controller.enqueue(element);
});

// Add before "for" inside POST
messageHistory.push(data);

Create rooms

const rooms = new Map(); // [{roomId: '123', users: [1,2,3]}]

// User room1
let connection1 = "connectionData1"

const room1 = rooms.get('room1') || new Set()
room1.add(connection1)
rooms.set('room1', room1)

// User 1 disconnect
const room11 = rooms.get('room1') || new Set()
room11.delete(connection1)
if(room11.size === 0) rooms.delete('room1')

Auth

  1. Return a connection id when the user connects
  2. send a post request to an endpoint with the following data: user credentials (session id), connection id, events to subscribe to
type Subscriber = {
	uuid: string;
	controller: ReadableStreamDefaultController;
};
const subscribers = new Set<Subscriber>();

// on connect
subscriber = { uuid: crypto.randomUUID(), controller };
subscribers.add(subscriber);

// on cancel
subscribers.delete(subscriber);

// send data
const subscriber = Array.from(subscribers).find((s) => s.uuid === body.id);
subscriber.controller.enqueue(`data: hello\n\n`);

Subscribe to a specific event

On the server specify an event name (id)

controller.enqueue('event: id\n');
controller.enqueue(`data: hello\n\n`);

// you can also additionaly send over a unique id for every message
controller.enqueue(`id: some uuid\n\n`);

On the client listen to it like this

const sse = new EventSource('/sse');
sse.addEventListener('id', (event) => {
	// do something with event.data
});

(default type is message)

Cloudflare Issues

If you face any issues with timeouts read this:

Are Server-sent events SSE supported, or will they trigger HTTP 524 timeouts?

You can send :ping\n\n if you need to send a ping/heartbeat every couple of seconds