UNPKG

@ayonli/jsext

Version:

A JavaScript extension package for building strong and modern applications.

1,101 lines (1,006 loc) 37.8 kB
/** * This module provides tools for working with server-sent events. * * The {@link EventEndpoint} is used to handle SSE requests on the server * and send messages to the client, while the {@link EventSource} and the * {@link EventConsumer} are used to read and process messages sent by the * server. * * Despite their names, these classes can be used in both the browser/client * and the server environments. * * NOTE: This module depends on the Fetch API and Web Streams API, in Node.js, * it requires Node.js v18.0 or above. * * @module * @experimental */ import "./external/event-target-polyfill/index.ts"; import type { IncomingMessage, ServerResponse } from "node:http"; import type { Http2ServerRequest, Http2ServerResponse } from "node:http2"; import type { Constructor } from "./types.ts"; import { createCloseEvent, createErrorEvent } from "./event.ts"; import { isBun, isDeno } from "./env.ts"; import runtime, { customInspect } from "./runtime.ts"; import _try from "./try.ts"; if (typeof MessageEvent !== "function" || runtime().identity === "workerd") { // Worker environments does not implement or only partially implement the MessageEvent, // we need to implement it ourselves. globalThis.MessageEvent = class MessageEvent<T = any> extends Event implements globalThis.MessageEvent<T> { readonly data: T = undefined as T; readonly lastEventId: string = ""; readonly origin: string = ""; readonly ports: ReadonlyArray<MessagePort> = []; readonly source: MessageEventSource | null = null; constructor(type: string, eventInitDict: MessageEventInit<T> | undefined = undefined) { super(type, eventInitDict); if (eventInitDict) { this.data = eventInitDict.data as T; this.lastEventId = eventInitDict.lastEventId ?? ""; this.origin = eventInitDict.origin ?? ""; this.ports = eventInitDict.ports ?? []; } } initMessageEvent( type: string, bubbles = false, cancelable = false, data = null, origin = "", lastEventId = "", source: MessageEventSource | null = null, ports: MessagePort[] = [] ): void { this.initEvent(type, bubbles ?? false, cancelable ?? false); Object.assign(this, { data, origin, lastEventId, source, ports }); } }; } const encoder = new TextEncoder(); const decoder = new TextDecoder(); const SSEMarkClosed = new Set<string>(); const _closed = Symbol.for("closed"); const _request = Symbol.for("request"); const _response = Symbol.for("response"); const _writer = Symbol.for("writer"); const _lastEventId = Symbol.for("lastEventId"); const _reconnectionTime = Symbol.for("reconnectionTime"); const _retry = Symbol.for("retry"); const _timer = Symbol.for("timer"); const _controller = Symbol.for("controller"); const _onopen = Symbol.for("onopen"); const _onerror = Symbol.for("onerror"); const _onmessage = Symbol.for("onmessage"); function setReadonly<T>(obj: any, name: string | symbol, value: T) { Object.defineProperty(obj, name, { configurable: true, enumerable: false, writable: false, value, }); } function getReadonly<T>(obj: any, name: string | symbol): T | undefined { return Object.getOwnPropertyDescriptor(obj, name)?.value; } function fixStringTag(ctor: Constructor): void { setReadonly(ctor.prototype, Symbol.toStringTag, ctor.name); } /** * The options for the {@link EventEndpoint} constructor. */ export interface EventEndpointOptions { /** * The time in milliseconds that instructs the client to wait before * reconnecting. */ reconnectionTime?: number; } /** * An SSE (server-sent events) implementation that can be used to send messages * to the client. This implementation is based on the `EventTarget` interface * and conforms the web standard. * * **Events:** * * - `close` - Dispatched when the connection is closed. * * @example * ```ts * // with Web APIs * import { EventEndpoint } from "@ayonli/jsext/sse"; * * export default { * async fetch(req: Request) { * const events = new EventEndpoint(req); * * events.addEventListener("close", (ev) => { * console.log(`The connection is closed, reason: ${ev.reason}`); * }); * * setTimeout(() => { * events.dispatchEvent(new MessageEvent("my-event", { * data: "Hello, World!", * lastEventId: "1", * })); * }, 1_000); * * return events.response!; * } * } * ``` * * @example * ```ts * // with Node.js APIs * import * as http from "node:http"; * import { EventEndpoint } from "@ayonli/jsext/sse"; * * const server = http.createServer((req, res) => { * const events = new EventEndpoint(req, res); * * events.addEventListener("close", (ev) => { * console.log(`The connection is closed, reason: ${ev.reason}`); * }); * * setTimeout(() => { * events.dispatchEvent(new MessageEvent("my-event", { * data: "Hello, World!", * lastEventId: "1", * })); * }, 1_000); * }); * * server.listen(3000); * ``` */ export class EventEndpoint<T extends Request | IncomingMessage | Http2ServerRequest = Request | IncomingMessage | Http2ServerRequest> extends EventTarget { private [_writer]: WritableStreamDefaultWriter<Uint8Array>; private [_response]: Response | null; private [_lastEventId]: string; private [_reconnectionTime]: number; private [_closed]: boolean; constructor(request: T, options?: EventEndpointOptions); constructor( request: T, response: ServerResponse | Http2ServerResponse, options?: EventEndpointOptions ); constructor( request: Request | IncomingMessage | Http2ServerRequest, ...args: any[] ) { super(); const isNodeRequest = "socket" in request && "socket" in args[0]; let options: EventEndpointOptions; if (isNodeRequest) { const req = request as IncomingMessage | Http2ServerRequest; this[_lastEventId] = String(req.headers["last-event-id"] ?? ""); options = args[1] ?? {}; } else { this[_lastEventId] = (request as Request).headers.get("Last-Event-ID") ?? ""; options = args[0] ?? {}; } this[_reconnectionTime] = options.reconnectionTime ?? 0; this[_closed] = this[_lastEventId] ? SSEMarkClosed.has(this[_lastEventId]) : false; const resInit: ResponseInit = { status: this.closed ? 204 : 200, statusText: this.closed ? "No Content" : "OK", headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "Transfer-Encoding": "chunked", }, }; const _this = this; if (isNodeRequest) { this[_response] = null; const res = args[0] as ServerResponse | Http2ServerResponse; const writable = new WritableStream({ write(chunk) { (res as ServerResponse).write(chunk); }, close() { res.closed || res.end(); _this[_closed] = true; _this.dispatchEvent(createCloseEvent("close", { wasClean: true })); }, abort(err) { res.closed || res.destroy(err); }, }); this[_writer] = writable.getWriter(); res.once("close", () => { this[_writer].close().catch(() => { }); }).once("error", (err) => { this[_writer].abort(err).catch(() => { }); }); for (const [name, value] of Object.entries(resInit.headers!)) { // Use `setHeader` to set headers instead of passing them to `writeHead`, // it seems in Deno, the headers are not written to the response if they // are passed to `writeHead`. res.setHeader(name, value); } res.writeHead(resInit.status!, resInit.statusText!); (res as ServerResponse).write(new Uint8Array(0)); } else { const { writable, readable } = new TransformStream<Uint8Array, Uint8Array>(); const reader = readable.getReader(); const _readable = new ReadableStream<Uint8Array>({ async start(controller) { if (isBun) { // In Bun, the response will not be sent to the client // until the first non-empty chunk is written. May be a // bug, but we need to work around it now. controller.enqueue(encoder.encode(":ok\n\n")); } else { controller.enqueue(new Uint8Array(0)); } }, async pull(controller) { while (true) { const { done, value } = await reader.read(); if (done) { try { controller.close(); } catch { } _this[_closed] = true; _this.dispatchEvent(createCloseEvent("close", { wasClean: true })); break; } controller.enqueue(value); } }, async cancel(reason) { await reader.cancel(reason); } }); this[_writer] = writable.getWriter(); this[_response] = new Response(this.closed ? null : _readable, resInit); } this.closed && this.close(); } /** * The last event ID that the server has sent. */ get lastEventId(): string { return this[_lastEventId]; } /** * Indicates whether the connection has been closed. */ get closed(): boolean { return this[_closed]; } /** * The response that will be sent to the client, only available when the * instance is created with the `Request` API. */ get response(): T extends Request ? Response : null { return this[_response] as any; } /** * Adds an event listener that will be called when the connection is closed. */ override addEventListener( type: "close", listener: (this: EventEndpoint<T>, ev: CloseEvent) => void, options?: boolean | AddEventListenerOptions ): void; override addEventListener( type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions ): void; override addEventListener( event: string, listener: any, options: boolean | AddEventListenerOptions | undefined = undefined ): void { return super.addEventListener(event, listener, options); } override removeEventListener( type: "close", listener: (this: EventEndpoint<T>, ev: CloseEvent) => void, options?: boolean | EventListenerOptions | undefined ): void; override removeEventListener( type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions | undefined ): void; override removeEventListener( type: string, listener: any, options: boolean | EventListenerOptions | undefined = undefined ): void { return super.removeEventListener(type, listener, options); } /** * Dispatches an message event that will be sent to the client. */ override dispatchEvent(event: MessageEvent<string>): boolean; override dispatchEvent(event: CloseEvent | ErrorEvent | Event): boolean; override dispatchEvent(event: MessageEvent | CloseEvent | ErrorEvent | Event): boolean { if (event instanceof MessageEvent) { if (event.type === "message") { this.send(event.data, event.lastEventId).catch(() => { }); } else { this.sendEvent(event.type, event.data, event.lastEventId) .catch(() => { }); } return !event.cancelable || !event.defaultPrevented; } else { return super.dispatchEvent(event); } } private buildMessage(data: string, options: { id?: string | undefined; event?: string | undefined; } = {}): Uint8Array { let message = ""; if (options.id) { this[_lastEventId] = options.id; message += `id: ${options.id}\n`; } if (options.event) { message += `event: ${options.event}\n`; } if (this[_reconnectionTime]) { message += `retry: ${this[_reconnectionTime]}\n`; } message += data.split(/\r\n|\n/).map((line) => `data: ${line}\n`).join(""); message += "\n"; return encoder.encode(message); } /** * Sends a message to the client. * * The client (`EventSource` or {@link EventConsumer}) will receive the * message as a `MessageEvent`, which can be listened to using the * `message` event. * * @param eventId If specified, the client will remember the value as the * last event ID and will send it back to the server when reconnecting. */ async send(data: string, eventId: string | undefined = undefined): Promise<void> { await this[_writer].write(this.buildMessage(data, { id: eventId })); } /** * Sends a custom event to the client. * * The client (`EventSource` or {@link EventConsumer}) will receive the * event as a `MessageEvent`, which can be listened to using the custom * event name. * * @param eventId If specified, the client will remember the value as the * last event ID and will send it back to the server when reconnecting. */ async sendEvent( event: string, data: string, eventId: string | undefined = undefined ): Promise<void> { await this[_writer].write(this.buildMessage(data, { id: eventId, event })); } /** * Closes the connection. * * By default, when the connection is closed, the client will try to * reconnect after a certain period of time, which is specified by the * `reconnectionTime` option when creating the instance. * * However, if the `noReconnect` parameter is set, this method will mark * the client as closed based on the last event ID. When the client * reconnects, the server will send a `204 No Content` response to the * client to instruct it to terminate the connection. * * It is important to note that the server depends on the last event ID to * identify the client for this purpose, so the server must send a globally * unique `lastEventId` to the client when sending messages. */ close(noReconnect = false): void { this[_writer].close().catch(() => { }).finally(() => { this[_closed] = true; if (this.lastEventId) { if (!SSEMarkClosed.has(this.lastEventId)) { noReconnect && SSEMarkClosed.add(this.lastEventId); } else { SSEMarkClosed.delete(this.lastEventId); } } }); } } fixStringTag(EventEndpoint); async function readAndProcessResponse( response: Response, handlers: { onId: (id: string) => void; onRetry: (time: number) => void; onData: (data: string, event: string) => void; onError: (error: unknown) => void; onEnd: () => void; } ): Promise<void> { const reader = response.body!.getReader(); let buffer: string = ""; try { while (true) { const { done, value } = await reader.read(); if (done) { handlers.onEnd(); break; } buffer += decoder.decode(value); const chunks = buffer.split(/\r\n\r\n|\n\n/); if (chunks.length === 1) { continue; } else { buffer = chunks.pop()!; } for (const chunk of chunks) { const lines = chunk.split(/\r\n|\n/); let data = ""; let type = "message"; let isMessage = false; for (const line of lines) { if (line.startsWith("data:") || line === "data") { let value = line.slice(5); if (value[0] === " ") { value = value.slice(1); } if (data) { data += "\n" + value; } else { data = value; } isMessage = true; } else if (line.startsWith("event:") || line === "event") { type = line.slice(6).trim(); isMessage = true; } else if (line.startsWith("id:") || line === "id") { handlers.onId(line.slice(3).trim()); isMessage = true; } else if (line.startsWith("retry:")) { const time = parseInt(line.slice(6).trim()); if (!isNaN(time) && time >= 0) { handlers.onRetry(time); isMessage = true; } } } if (isMessage) { handlers.onData(data, type || "message"); } } } } catch (error) { handlers.onError(error); } } /** * @deprecated Use {@link EventEndpoint} instead. */ export const SSE = EventEndpoint; /** * @deprecated Use {@link EventEndpointOptions} instead. */ export type SSEOptions = EventEndpointOptions; /** * This is an implementation of the * [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) * API that serves as a polyfill in environments that do not have native support, * such as Node.js. * * NOTE: This API depends on the Fetch API and Web Streams API, in Node.js, it * requires Node.js v18.0 or above. * * @example * ```ts * import { EventSource } from "@ayonli/jsext/sse"; * * globalThis.EventSource ??= EventSource; * * const events = new EventSource("http://localhost:3000"); * * events.addEventListener("open", () => { * console.log("The connection is open."); * }); * * events.addEventListener("error", (ev) => { * console.error("An error occurred:", ev.error); * }); * * events.addEventListener("message", (ev) => { * console.log("Received message from the server:", ev.data); * }); * * events.addEventListener("my-event", (ev) => { * console.log("Received custom event from the server:", ev.data); * }); * ``` */ export class EventSource extends EventTarget { private [_controller]: AbortController = new AbortController(); private [_request]: Request | null = null; private [_lastEventId]: string = ""; private [_reconnectionTime]: number = 0; private [_retry]: number = 0; private [_timer]: number | NodeJS.Timeout | null = null; private [_onopen]: ((this: EventSource, ev: Event) => any) | null = null; private [_onmessage]: ((this: EventSource, ev: MessageEvent<string>) => any) | null = null; private [_onerror]: ((this: EventSource, ev: ErrorEvent) => any) | null = null; readonly CONNECTING = EventSource.CONNECTING; readonly OPEN = EventSource.OPEN; readonly CLOSED = EventSource.CLOSED; static CONNECTING = 0 as const; static OPEN = 1 as const; static CLOSED = 2 as const; get url(): string { return getReadonly(this, "url") ?? ""; } get withCredentials(): boolean { return getReadonly(this, "withCredentials") ?? false; } get readyState(): number { return getReadonly(this, "readyState") ?? this.CONNECTING; } get onopen(): ((this: EventSource, ev: Event) => any) | null { return this[_onopen] ?? null; } set onopen(value: ((this: EventSource, ev: Event) => any) | null) { this[_onopen] = value; } get onmessage(): ((this: EventSource, ev: MessageEvent<string>) => any) | null { return this[_onmessage] ?? null; } set onmessage(value: ((this: EventSource, ev: MessageEvent<string>) => any) | null) { this[_onmessage] = value; } get onerror(): ((this: EventSource, ev: ErrorEvent) => any) | null { return this[_onerror] ?? null; } set onerror(value: ((this: EventSource, ev: ErrorEvent) => any) | null) { this[_onerror] = value; } constructor(url: string | URL, options: EventSourceInit = {}) { super(); url = typeof url === "object" ? url.href : new URL(url, typeof location === "object" ? location.href : undefined).href; setReadonly(this, "url", url); setReadonly(this, "withCredentials", options.withCredentials ?? false); setReadonly(this, "readyState", this.CONNECTING); setReadonly(this, "CONNECTING", EventSource.CONNECTING); setReadonly(this, "OPEN", EventSource.OPEN); setReadonly(this, "CLOSED", EventSource.CLOSED); this.connect().catch(() => { }); } private async connect() { if (this.readyState === this.CLOSED) { return; } setReadonly(this, "readyState", this.CONNECTING); const headers: HeadersInit = { "Accept": "text/event-stream", }; if (this[_lastEventId]) { headers["Last-Event-ID"] = this[_lastEventId]; } this[_request] = new Request(this.url, { headers, credentials: this.withCredentials ? "include" : "same-origin", cache: "no-store", signal: this[_controller].signal, }); const [err, res] = await _try(fetch(this[_request])); if (err || res?.type === "error") { const event = createErrorEvent("error", { error: err || new Error(`Failed to fetch '${this.url}'`), }); this.dispatchEvent(event); this.onerror?.call(this, event); this.tryReconnect(); return; } else if (res.status === 204) { // No more data, close the connection setReadonly(this, "readyState", this.CLOSED); return; } else if (res.status !== 200) { setReadonly(this, "readyState", this.CLOSED); const event = createErrorEvent("error", { error: new TypeError(`The server responded with status ${res.status}.`), }); this.dispatchEvent(event); this.onerror?.call(this, event); return; } else if (!res.headers.get("Content-Type")?.startsWith("text/event-stream")) { setReadonly(this, "readyState", this.CLOSED); const event = createErrorEvent("error", { error: new TypeError("The response is not an event stream."), }); this.dispatchEvent(event); this.onerror?.call(this, event); return; } else if (!res.body) { setReadonly(this, "readyState", this.CLOSED); const event = createErrorEvent("error", { error: new TypeError("The response does not have a body."), }); this.dispatchEvent(event); this.onerror?.call(this, event); return; } setReadonly(this, "readyState", this.OPEN); this[_retry] = 0; const event = new Event("open"); this.dispatchEvent(event); this.onopen?.call(this, event); const origin = new URL(res.url || this.url).origin; await readAndProcessResponse(res, { onId: (id) => { this[_lastEventId] = id; }, onRetry: (time) => { this[_reconnectionTime] = time; }, onData: (data, event) => { const _event = new MessageEvent(event, { lastEventId: this[_lastEventId], data, origin, }); this.dispatchEvent(_event); this.onmessage?.call(this, _event); }, onError: (error) => { if (this.readyState !== this.CLOSED) { const event = createErrorEvent("error", { error }); this.dispatchEvent(event); this.onerror?.call(this, event); this.tryReconnect(); } }, onEnd: () => { if (this.readyState !== this.CLOSED) { const event = createErrorEvent("error", { error: new Error("The connection is interrupted."), }); this.dispatchEvent(event); this.onerror?.call(this, event); this.tryReconnect(); } }, }); } private tryReconnect() { if (this[_timer]) { clearTimeout(this[_timer]); this[_timer] = null; } this[_timer] = setTimeout(() => { this.connect().catch(() => { }); }, this[_reconnectionTime] || 1000 * Math.min(30, Math.pow(2, this[_retry]++))); } /** * Closes the connection. */ close(): void { if (this[_timer]) { clearTimeout(this[_timer]); this[_timer] = null; } setReadonly(this, "readyState", this.CLOSED); this[_controller].abort(); } /** * Adds an event listener that will be called when the connection is open. */ override addEventListener( type: "open", listener: (this: EventSource, ev: Event) => void, options?: boolean | AddEventListenerOptions ): void; /** * Adds an event listener that will be called when the connection is * interrupted. */ override addEventListener( type: "error", listener: (this: EventSource, ev: ErrorEvent) => void, options?: boolean | AddEventListenerOptions ): void; /** * Adds an event listener that will be called when a message with the * default event type is received. */ override addEventListener( type: "message", listener: (this: EventSource, ev: MessageEvent<string>) => void, options?: boolean | AddEventListenerOptions ): void; /** * Adds an event listener that will be called when a message with a custom * event type is received. */ override addEventListener( type: string, listener: (this: EventSource, event: MessageEvent<string>) => void, options?: boolean | AddEventListenerOptions ): void; override addEventListener( type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions ): void; override addEventListener( event: string, listener: any, options: boolean | AddEventListenerOptions | undefined = undefined ): void { return super.addEventListener(event, listener, options); } override removeEventListener( type: "open", listener: (this: EventSource, ev: Event) => void, options?: boolean | EventListenerOptions ): void; override removeEventListener( type: "error", listener: (this: EventSource, ev: ErrorEvent) => void, options?: boolean | EventListenerOptions ): void; override removeEventListener( type: "message", listener: (this: EventSource, ev: MessageEvent<string>) => void, options?: boolean | EventListenerOptions ): void; override removeEventListener( type: string, listener: (this: EventSource, event: MessageEvent<string>) => void, options?: boolean | EventListenerOptions ): void; override removeEventListener( type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions ): void; override removeEventListener( type: string, listener: any, options: boolean | EventListenerOptions | undefined = undefined ): void { return super.removeEventListener(type, listener, options); } [customInspect](): object | string { const _this = this; if (isDeno) { return "EventSource " + Deno.inspect({ readyState: _this.readyState, url: _this.url, withCredentials: _this.withCredentials, onopen: _this.onopen, onmessage: _this.onmessage, onerror: _this.onerror, }, { colors: true }); } else { return new class EventSource { readyState = _this.readyState; url = _this.url; withCredentials = _this.withCredentials; onopen = _this.onopen; onmessage = _this.onmessage; onerror = _this.onerror; }; } } } fixStringTag(EventSource); /** * Unlike the {@link EventSource} API, which takes a URL and only supports GET * request, the {@link EventConsumer} API accepts a `Response` object and reads * the messages from its body, the response can be generated from any type of * request, usually returned from the `fetch` function. * * This API doesn't support active closure and reconnection, however, we can * use `AbortController` in the request to terminate the connection, and add * a event listener to the close event and reestablish the connection manually. * * **Events:** * * - `error` - Dispatched when an error occurs, such as network failure. After * this event is dispatched, the connection will be closed and the `close` * event will be dispatched. * - `close` - Dispatched when the connection is closed. If the connection is * closed due to some error, the `error` event will be dispatched before this * event, and the close event will have the `wasClean` set to `false`, and the * `reason` property contains the error message, if any. * - `message` - Dispatched when a message with the default event type is * received. * - custom events - Dispatched when a message with a custom event type is * received. * * NOTE: This API depends on the Fetch API and Web Streams API, in Node.js, it * requires Node.js v18.0 or above. * * @example * ```ts * import { EventConsumer } from "@ayonli/jsext/sse"; * * const response = await fetch("http://localhost:3000", { * method: "POST", * headers: { * "Accept": "text/event-stream", * }, * }); * const events = new EventConsumer(response); * * events.addEventListener("close", (ev) => { * console.log(`The connection is closed, reason: ${ev.reason}`); * * if (!ev.wasClean) { * // perhaps to reestablish the connection * } * }); * * events.addEventListener("my-event", (ev) => { * console.log(`Received message from the server: ${ev.data}`); * }); * ``` */ export class EventConsumer extends EventTarget { private [_lastEventId]: string = ""; private [_reconnectionTime]: number = 0; private [_closed] = false; constructor(response: Response) { super(); if (!response.body) { throw new TypeError("The response does not have a body."); } else if (response.bodyUsed) { throw new TypeError("The response body has already been used."); } else if (response.body.locked) { throw new TypeError("The response body is locked."); } else if (!response.headers.get("Content-Type")?.startsWith("text/event-stream")) { throw new TypeError("The response is not an event stream."); } const origin = response.url ? new URL(response.url).origin : ""; readAndProcessResponse(response, { onId: (id) => { this[_lastEventId] = id; }, onRetry: (time) => { this[_reconnectionTime] = time; }, onData: (data, event) => { this.dispatchEvent(new MessageEvent(event, { lastEventId: this[_lastEventId], data, origin, })); }, onError: (error) => { this[_closed] = true; this.dispatchEvent(createErrorEvent("error", { error })); this.dispatchEvent(createCloseEvent("close", { reason: error instanceof Error ? error.message : String(error), wasClean: false, })); }, onEnd: () => { this[_closed] = true; this.dispatchEvent(createCloseEvent("close", { wasClean: true })); }, }).catch(() => { }); } /** * The last event ID that the server has sent. */ get lastEventId(): string { return this[_lastEventId]; } /** * The time in milliseconds that instructs the client to wait before * reconnecting. * * NOTE: The {@link EventConsumer} API does not support auto-reconnection, * this value is only used when we want to reestablish the connection * manually. */ get retry(): number { return this[_reconnectionTime]; } /** * Indicates whether the connection has been closed. */ get closed(): boolean { return this[_closed]; } /** * Adds an event listener that will be called when the connection is * interrupted. After this event is dispatched, the connection will be * closed and the `close` event will be dispatched. */ override addEventListener( type: "error", listener: (this: EventConsumer, ev: ErrorEvent) => void, options?: boolean | AddEventListenerOptions ): void; /** * Adds an event listener that will be called when the connection is closed. * If the connection is closed due to some error, the `error` event will be * dispatched before this event, and the close event will have the `wasClean` * set to `false`, and the `reason` property contains the error message, if * any. */ override addEventListener( type: "close", listener: (this: EventConsumer, ev: CloseEvent) => void, options?: boolean | AddEventListenerOptions ): void; /** * Adds an event listener that will be called when a message with the * default event type is received. */ override addEventListener( type: "message", listener: (this: EventConsumer, ev: MessageEvent<string>) => void, options?: boolean | AddEventListenerOptions ): void; /** * Adds an event listener that will be called when a message with a custom * event type is received. */ override addEventListener( type: string, listener: (this: EventConsumer, event: MessageEvent<string>) => void, options?: boolean | AddEventListenerOptions ): void; override addEventListener( type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions ): void; override addEventListener( event: string, listener: any, options: boolean | AddEventListenerOptions | undefined = undefined ): void { return super.addEventListener(event, listener, options); } override removeEventListener( type: "error", listener: (this: EventConsumer, ev: ErrorEvent) => void, options?: boolean | EventListenerOptions ): void; override removeEventListener( type: "close", listener: (this: EventConsumer, ev: CloseEvent) => void, options?: boolean | EventListenerOptions ): void; override removeEventListener( type: "message", listener: (this: EventConsumer, ev: MessageEvent<string>) => void, options?: boolean | EventListenerOptions ): void; override removeEventListener( type: string, listener: (this: EventConsumer, event: MessageEvent<string>) => void, options?: boolean | EventListenerOptions ): void; override removeEventListener( type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions ): void; override removeEventListener( type: string, listener: any, options: boolean | EventListenerOptions | undefined = undefined ): void { return super.removeEventListener(type, listener, options); } } fixStringTag(EventConsumer); /** * @deprecated Use {@link EventConsumer} instead. */ export const EventClient = EventConsumer;