All files / http / server_sent_event_stream.ts

100.00% Branches 18/18
100.00% Functions 4/4
100.00% Lines 45/45
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
 
 
 
x4
x4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x19
x19
x6
x6
 
x6
x19
 
 
 
 
 
 
x4
x29
x29
x6
x6
 
x6
x29
x9
x9
x9
x9
 
x9
x9
x29
x11
x11
 
x11
x29
x10
x10
x10
x10
 
x10
x10
x29
x23
x29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x4
x4
x4
x14
x14
x29
x29
x14
x14
x4




































































































// Copyright 2018-2026 the Deno authors. MIT license.
// This module is browser compatible.

const NEWLINE_REGEXP = /\r\n|\r|\n/;
const encoder = new TextEncoder();

/**
 * Represents a message in the Server-Sent Event (SSE) protocol.
 *
 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#fields}
 */
export interface ServerSentEventMessage {
  /** Ignored by the client. */
  comment?: string;
  /** A string identifying the type of event described. */
  event?: string;
  /** The data field for the message. Split by new lines. */
  data?: string;
  /** The event ID to set the {@linkcode EventSource} object's last event ID value. */
  id?: string | number;
  /** The reconnection time. */
  retry?: number;
}

function assertHasNoNewline(value: string, varName: string, errPrefix: string) {
  if (value.match(NEWLINE_REGEXP) !== null) {
    throw new SyntaxError(
      `${errPrefix}: ${varName} cannot contain a newline`,
    );
  }
}

/**
 * Converts a server-sent message object into a string for the client.
 *
 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format}
 */
function stringify(message: ServerSentEventMessage): Uint8Array {
  const lines = [];
  if (message.comment !== undefined) {
    message.comment.split(NEWLINE_REGEXP).forEach((line) =>
      lines.push(`:${line}`)
    );
  }
  if (message.event !== undefined) {
    assertHasNoNewline(
      message.event,
      "`message.event`",
      "Cannot serialize message",
    );
    lines.push(`event:${message.event}`);
  }
  if (message.data !== undefined) {
    message.data.split(NEWLINE_REGEXP).forEach((line) =>
      lines.push(`data:${line}`)
    );
  }
  if (message.id !== undefined) {
    assertHasNoNewline(
      message.id.toString(),
      "`message.id`",
      "Cannot serialize message",
    );
    lines.push(`id:${message.id}`);
  }
  if (message.retry !== undefined) lines.push(`retry:${message.retry}`);
  return encoder.encode(lines.join("\n") + "\n\n");
}

/**
 * Transforms server-sent message objects into strings for the client.
 *
 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events}
 *
 * @example Usage
 * ```ts no-assert
 * import {
 *   type ServerSentEventMessage,
 *   ServerSentEventStream,
 * } from "@std/http/server-sent-event-stream";
 *
 * const stream = ReadableStream.from<ServerSentEventMessage>([
 *   { data: "hello there" }
 * ]).pipeThrough(new ServerSentEventStream());
 * new Response(stream, {
 *   headers: {
 *     "content-type": "text/event-stream",
 *     "cache-control": "no-cache",
 *   },
 * });
 * ```
 */
export class ServerSentEventStream
  extends TransformStream<ServerSentEventMessage, Uint8Array> {
  constructor() {
    super({
      transform: (message, controller) => {
        controller.enqueue(stringify(message));
      },
    });
  }
}