@ayonli/jsext
Version:
A JavaScript extension package for building strong and modern applications.
389 lines (388 loc) • 16.1 kB
TypeScript
/// <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 {};