Server-Sent Events with Deno.js
Server-Sent Events (SSE) is a way for a web server to send updates or messages directly to your browser in real-time. It’s like the server saying, “Hey, here’s some new information!” without the browser needing to ask for it.
- One-way communication: The server sends updates to the browser, but the browser doesn’t send anything back through SSE.
- Continuous connection: Once connected, the browser stays “listening” for new updates from the server.
- Good for real-time updates: Perfect for things like live news, stock prices, or notifications.
It’s simpler than WebSockets (another real-time tech), but it’s best for one-way data flow, not two-way chats.
SSE Headers
Sever-Sent Events is achieved by sending a continuous flow of events over a single HTTP request, similar to an HTTP response, but with a special type of encoding and specific response headers. Specifically, SSE responses require certain HTTP headers to be sent, including:
Content-Type: text/event-stream
: This tells the browser that the response is an event stream.Cache-Control: no-cache
: This tells the browser not to cache the response.Connection: keep-alive
: This tells the browser to keep the connection open.Content-Encoding: chunked
: This tells the browser that the response is sent in chunks.
export const sseRequiredHeaders = {
"content-type": "text/event-stream",
"cache-control": "no-cache",
"connection": "keep-alive",
"transfer-encoding": "chunked",
};
SSE Response Stream
An SSE response stream is the continuous data sent by the server to the client (browser) after the client establishes an SSE connection.
export class SSE {
trans: TransformStream<Event, Uint8Array>;
constructor() {
//...
}
response(source: ReadableStream<Event>): Response {
const reader = source.pipeThrough(this.trans);
return this.responseRaw(reader);
}
responseRaw(source: ReadableStream): Response {
return new Response(source, {
headers: sseRequiredHeaders,
status: 200,
});
}
}
Structure of SSE Stream
The data is sent as plain text, following a format with specific fields:
data
: The actual data being sent. This can span multiple lines if necessary.event
: (Optional) The type of the event, so the client can handle it differently.id
: (Optional) A unique identifier for the event, often used for reconnection.retry
: (Optional) Tells the client how long (in milliseconds) to wait before retrying if the connection is lost.A blank line (
\n\n`) signals the end of the event.
export interface Event {
data: string | boolean | number | object; //The data associated with the event, which can be an ArrayBuffer or ArrayBufferLike.
id?: number; //An optional identifier for the event.
event?: string; //The type of the event.
}
export class SSE {
trans: TransformStream<Event, Uint8Array>;
constructor() {
const encoder = new TextEncoder();
const trans = new TransformStream<Event, Uint8Array>({
transform(chunk, controller) {
const lines = [];
chunk.id && lines.push(`id: ${chunk.id}`);
chunk.event && lines.push(`event: ${chunk.event}`);
switch (typeof chunk.data) {
case "string":
case "boolean":
case "number":
lines.push(`data: ${chunk.data}`);
break;
case "object":
lines.push(`data: ${JSON.stringify(chunk.data)}`);
break;
default:
lines.push(`data: ${chunk.data || ""}`);
}
const message = encoder.encode(lines.join("\n") + "\n\n");
controller.enqueue(message);
},
});
this.trans = trans;
}
}
Usage of SSE Stream with Deno.js
Putting it all together, here's an example of an HTTP server that streams a response back to the client using Server-Sent Events (SSE).
/**
* @title HTTP server: SSE (Server-Sent Events)
* @difficulty intermediate
* @tags cli, deploy
* @run --allow-net <url>
* @resource {/examples/http-server-streaming} Example: HTTP server: Streaming
* @resource {https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream} MDN: Server-sent events
* @playground https://dash.deno.com/playground/server-sent-event
* @group Network
*
* An example HTTP Server-Sent Event streams a response back to the client.
*/
/**
* Represents an event in the Server-Sent Events (SSE) protocol.
*/
export interface Event {
data: string | boolean | number | object; //The data associated with the event, which can be an ArrayBuffer or ArrayBufferLike.
id?: number; //An optional identifier for the event.
event?: string; //The type of the event.
}
export const sseRequiredHeaders = {
"content-type": "text/event-stream",
"cache-control": "no-cache",
"connection": "keep-alive",
"transfer-encoding": "chunked",
};
/**
* The SSE class provides a way to create a server-sent events (SSE) stream.
* It uses a TransformStream to convert Event objects into Uint8Array chunks
* that can be read by a ReadableStream.
*/
export class SSE {
trans: TransformStream<Event, Uint8Array>;
/**
* Constructs a new SSE instance, initializing the TransformStream and its writer.
*/
constructor() {
const encoder = new TextEncoder();
const trans = new TransformStream<Event, Uint8Array>({
transform(chunk, controller) {
const lines = [];
chunk.id && lines.push(`id: ${chunk.id}`);
chunk.event && lines.push(`event: ${chunk.event}`);
switch (typeof chunk.data) {
case "string":
case "boolean":
case "number":
lines.push(`data: ${chunk.data}`);
break;
case "object":
lines.push(`data: ${JSON.stringify(chunk.data)}`);
break;
default:
lines.push(`data: ${chunk.data || ""}`);
}
const message = encoder.encode(lines.join("\n") + "\n\n");
controller.enqueue(message);
},
});
this.trans = trans;
}
/**
* Generates an HTTP response with the required headers for Server-Sent Events (SSE).
*
* @returns {Response} A new Response object with the readable stream and SSE headers.
*/
response(source: ReadableStream<Event>): Response {
const reader = source.pipeThrough(this.trans);
return this.responseRaw(reader);
}
/**
* Creates an HTTP response with the given readable stream as the body.
* The response is configured with headers required for Server-Sent Events (SSE).
*
* @param source - The readable stream to be used as the response body.
* @returns A Response object with the provided stream and SSE headers.
*/
responseRaw(source: ReadableStream): Response {
return new Response(source, {
headers: sseRequiredHeaders,
status: 200,
});
}
}
/**
* An example HTTP server-sent-event that streams a response back to the client.
*/
function handler(_req: Request): Response {
// instantiate the SSE class
const sse = new SSE();
// set up a demo Server-Sent Events stream
let timer: number | undefined = undefined;
let counter = 0;
// Create a ReadableStream that emits an event every second
const body = new ReadableStream<Event>({
start(controller) {
timer = setInterval(() => {
counter++;
const message = `It is ${new Date().toString()}`;
if (!controller.desiredSize) return; // Check if the stream is still writable
controller.enqueue({ data: message });
controller.enqueue({
data: { deno: "land" },
id: counter,
event: "data",
});
}, 1000);
//stop timer in 5 seconds
setTimeout(() => {
clearInterval(timer);
controller.enqueue({ data: "[DONE]" }); //just like OPEN AI Server-Sent Event
if (!controller.desiredSize) return; // Check if the stream is still writable
controller.close();
}, 5000);
},
cancel() {
if (timer !== undefined) {
clearInterval(timer);
}
},
});
return sse.response(body);
}
// To start the server on the default port, call `Deno.serve` with the handler.
Deno.serve(handler);
Was this article helpful to you?
Provide feedback
Last edited on November 20, 2024.
Edit this page