All files / http / server_sent_event_stream.ts

100.00% Branches 11/11
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x28
x28
x37
x37
 
x37
x28
 
 
 
 
 
 
x4
x30
x30
x37
x37
x37
x37
 
x37
x37
x30
x38
x38
x38
x38
 
x38
x38
x30
x40
x40
 
x40
x30
x39
x39
x39
x39
 
x39
x39
x30
x47
x30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x4
x4
x4
x19
x19
x45
x45
x19
x19
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));
      },
    });
  }
}