UNPKG

@ayonli/jsext

Version:

A JavaScript extension package for building strong and modern applications.

389 lines (388 loc) 16.1 kB
/// <reference types="node" /> /// <reference types="node" /> /** * 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 { customInspect } from "./runtime.ts"; declare const _closed: unique symbol; declare const _request: unique symbol; declare const _response: unique symbol; declare const _writer: unique symbol; declare const _lastEventId: unique symbol; declare const _reconnectionTime: unique symbol; declare const _retry: unique symbol; declare const _timer: unique symbol; declare const _controller: unique symbol; declare const _onopen: unique symbol; declare const _onerror: unique symbol; declare const _onmessage: unique symbol; /** * 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 declare class EventEndpoint<T extends Request | IncomingMessage | Http2ServerRequest = Request | IncomingMessage | Http2ServerRequest> extends EventTarget { private [_writer]; private [_response]; private [_lastEventId]; private [_reconnectionTime]; private [_closed]; constructor(request: T, options?: EventEndpointOptions); constructor(request: T, response: ServerResponse | Http2ServerResponse, options?: EventEndpointOptions); /** * The last event ID that the server has sent. */ get lastEventId(): string; /** * Indicates whether the connection has been closed. */ get closed(): boolean; /** * 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; /** * Adds an event listener that will be called when the connection is closed. */ addEventListener(type: "close", listener: (this: EventEndpoint<T>, ev: CloseEvent) => void, options?: boolean | AddEventListenerOptions): void; addEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void; removeEventListener(type: "close", listener: (this: EventEndpoint<T>, ev: CloseEvent) => void, options?: boolean | EventListenerOptions | undefined): void; removeEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions | undefined): void; /** * Dispatches an message event that will be sent to the client. */ dispatchEvent(event: MessageEvent<string>): boolean; dispatchEvent(event: CloseEvent | ErrorEvent | Event): boolean; private buildMessage; /** * 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. */ send(data: string, eventId?: string | undefined): Promise<void>; /** * 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. */ sendEvent(event: string, data: string, eventId?: string | undefined): Promise<void>; /** * 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?: boolean): void; } /** * @deprecated Use {@link EventEndpoint} instead. */ export declare const SSE: typeof 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 declare class EventSource extends EventTarget { private [_controller]; private [_request]; private [_lastEventId]; private [_reconnectionTime]; private [_retry]; private [_timer]; private [_onopen]; private [_onmessage]; private [_onerror]; readonly CONNECTING: 0; readonly OPEN: 1; readonly CLOSED: 2; static CONNECTING: 0; static OPEN: 1; static CLOSED: 2; get url(): string; get withCredentials(): boolean; get readyState(): number; get onopen(): ((this: EventSource, ev: Event) => any) | null; set onopen(value: ((this: EventSource, ev: Event) => any) | null); get onmessage(): ((this: EventSource, ev: MessageEvent<string>) => any) | null; set onmessage(value: ((this: EventSource, ev: MessageEvent<string>) => any) | null); get onerror(): ((this: EventSource, ev: ErrorEvent) => any) | null; set onerror(value: ((this: EventSource, ev: ErrorEvent) => any) | null); constructor(url: string | URL, options?: EventSourceInit); private connect; private tryReconnect; /** * Closes the connection. */ close(): void; /** * Adds an event listener that will be called when the connection is open. */ 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. */ 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. */ 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. */ addEventListener(type: string, listener: (this: EventSource, event: MessageEvent<string>) => void, options?: boolean | AddEventListenerOptions): void; addEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void; removeEventListener(type: "open", listener: (this: EventSource, ev: Event) => void, options?: boolean | EventListenerOptions): void; removeEventListener(type: "error", listener: (this: EventSource, ev: ErrorEvent) => void, options?: boolean | EventListenerOptions): void; removeEventListener(type: "message", listener: (this: EventSource, ev: MessageEvent<string>) => void, options?: boolean | EventListenerOptions): void; removeEventListener(type: string, listener: (this: EventSource, event: MessageEvent<string>) => void, options?: boolean | EventListenerOptions): void; removeEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions): void; [customInspect](): object | string; } /** * 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 declare class EventConsumer extends EventTarget { private [_lastEventId]; private [_reconnectionTime]; private [_closed]; constructor(response: Response); /** * The last event ID that the server has sent. */ get lastEventId(): string; /** * 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; /** * Indicates whether the connection has been closed. */ get closed(): boolean; /** * 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. */ 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. */ 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. */ 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. */ addEventListener(type: string, listener: (this: EventConsumer, event: MessageEvent<string>) => void, options?: boolean | AddEventListenerOptions): void; addEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void; removeEventListener(type: "error", listener: (this: EventConsumer, ev: ErrorEvent) => void, options?: boolean | EventListenerOptions): void; removeEventListener(type: "close", listener: (this: EventConsumer, ev: CloseEvent) => void, options?: boolean | EventListenerOptions): void; removeEventListener(type: "message", listener: (this: EventConsumer, ev: MessageEvent<string>) => void, options?: boolean | EventListenerOptions): void; removeEventListener(type: string, listener: (this: EventConsumer, event: MessageEvent<string>) => void, options?: boolean | EventListenerOptions): void; removeEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions): void; } /** * @deprecated Use {@link EventConsumer} instead. */ export declare const EventClient: typeof EventConsumer; export {};