All files / http / server_sent_event_stream.ts

100.00% Branches 20/20
100.00% Functions 4/4
100.00% Lines 48/48
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
103
104
105
 
 
 
x4
x4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x24
x24
x9
x9
 
x9
x24
 
 
 
 
 
 
x4
x26
x26
x7
x7
x7
x7
 
x7
x7
x26
x8
x8
x8
x8
 
x8
x8
x26
x10
x10
 
x10
x26
x9
x9
x9
x9
 
x9
x9
x26
x17
x26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x4
x4
x4
x15
x15
x26
x26
x15
x15
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) {
    assertHasNoNewline(
      message.comment,
      "`message.comment`",
      "Cannot serialize message",
    );
    lines.push(`:${message.comment}`);
  }
  if (message.event) {
    assertHasNoNewline(
      message.event,
      "`message.event`",
      "Cannot serialize message",
    );
    lines.push(`event:${message.event}`);
  }
  if (message.data) {
    message.data.split(NEWLINE_REGEXP).forEach((line) =>
      lines.push(`data:${line}`)
    );
  }
  if (message.id) {
    assertHasNoNewline(
      message.id.toString(),
      "`message.id`",
      "Cannot serialize message",
    );
    lines.push(`id:${message.id}`);
  }
  if (message.retry) 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));
      },
    });
  }
}