Use hash privatE

This commit is contained in:
2026-08-07 06:02:17 +00:00
parent 20a6c328fa
commit a799299633
+44 -44
View File
@@ -165,7 +165,7 @@ export class SSESession extends EventEmitter<SSESessionEventMap> {
} }
/** SSE endpoint URL for this session. */ /** SSE endpoint URL for this session. */
private readonly url: string; readonly #url: string;
/** /**
* Per-instance configuration. * Per-instance configuration.
@@ -203,7 +203,7 @@ export class SSESession extends EventEmitter<SSESessionEventMap> {
}; };
/** AbortController for the currently active fetch, if any. */ /** AbortController for the currently active fetch, if any. */
private controller: AbortController | null = null; #controller: AbortController | null = null;
/** /**
* Asynchronous stream of parsed SSE events for the active connection. * Asynchronous stream of parsed SSE events for the active connection.
@@ -227,7 +227,7 @@ export class SSESession extends EventEmitter<SSESessionEventMap> {
public constructor(url: string, options: Partial<SSESessionOptions> = {}) { public constructor(url: string, options: Partial<SSESessionOptions> = {}) {
super(); super();
this.url = url; this.#url = url;
this.options = { this.options = {
...this.options, ...this.options,
...options, ...options,
@@ -241,7 +241,7 @@ export class SSESession extends EventEmitter<SSESessionEventMap> {
* *
* Resolves once the HTTP stream is established and `"connected"` has been * Resolves once the HTTP stream is established and `"connected"` has been
* emitted. Body reading continues asynchronously in the background via * emitted. Body reading continues asynchronously in the background via
* {@link readStream}. * {@link #readStream}.
* *
* @throws When the fetch retry policy exhausts attempts or the connection * @throws When the fetch retry policy exhausts attempts or the connection
* is superseded before the reader is handed off (in the latter case the * is superseded before the reader is handed off (in the latter case the
@@ -249,16 +249,16 @@ export class SSESession extends EventEmitter<SSESessionEventMap> {
*/ */
public async connect(): Promise<void> { public async connect(): Promise<void> {
// If there is already a controller present, we are already connected. // If there is already a controller present, we are already connected.
if (this.controller) return; if (this.#controller) return;
// Prepare for a fresh transport. Parser state from an abandoned connection // Prepare for a fresh transport. Parser state from an abandoned connection
// must not bleed into the next one; reopen messages if a prior terminal // must not bleed into the next one; reopen messages if a prior terminal
// close ended the consumer's iteration loop. // close ended the consumer's iteration loop.
this.resetEventParser(); this.#resetEventParser();
this.ensureMessageStreamOpen(); this.#ensureMessageStreamOpen();
const controller = new AbortController(); const controller = new AbortController();
this.controller = controller; this.#controller = controller;
const { method, headers, body } = this.options; const { method, headers, body } = this.options;
@@ -275,22 +275,22 @@ export class SSESession extends EventEmitter<SSESessionEventMap> {
let reader: ReadableStreamDefaultReader<Uint8Array>; let reader: ReadableStreamDefaultReader<Uint8Array>;
try { try {
reader = await this.options.retry.run(() => this.createReader(fetchOptions)); reader = await this.options.retry.run(() => this.#createReader(fetchOptions));
} catch (error) { } catch (error) {
// A newer abort/connect superseded this attempt — leave state to the winner. // A newer abort/connect superseded this attempt — leave state to the winner.
if (this.controller !== controller) return; if (this.#controller !== controller) return;
this.controller = null; this.#controller = null;
await this.notifyDisconnected(); await this.#notifyDisconnected();
await this.notifyError(error); await this.#notifyError(error);
this.closeMessageStream(); this.#closeMessageStream();
throw error; throw error;
} }
// Connection succeeded but was already replaced (for example abort during fetch). // Connection succeeded but was already replaced (for example abort during fetch).
if (this.controller !== controller) { if (this.#controller !== controller) {
await reader.cancel(); await reader.cancel();
return; return;
@@ -303,7 +303,7 @@ export class SSESession extends EventEmitter<SSESessionEventMap> {
this.emit('connected', undefined); this.emit('connected', undefined);
// Fire-and-forget: connect() resolves while the stream is consumed. // Fire-and-forget: connect() resolves while the stream is consumed.
this.readStream(reader, controller).catch((error) => { this.#readStream(reader, controller).catch((error) => {
this.options.onError(error); this.options.onError(error);
}); });
} }
@@ -318,17 +318,17 @@ export class SSESession extends EventEmitter<SSESessionEventMap> {
* Emits `"disconnected"` but not `"closed"`. * Emits `"disconnected"` but not `"closed"`.
*/ */
public async abort(): Promise<void> { public async abort(): Promise<void> {
if (!this.controller) return; if (!this.#controller) return;
// Grab the current controller to ensure we are aborting the correct one. // Grab the current controller to ensure we are aborting the correct one.
const controller = this.controller; const controller = this.#controller;
this.controller = null; this.#controller = null;
// Invalidate any in-flight read loop and fetch for this transport. // Invalidate any in-flight read loop and fetch for this transport.
controller.abort(); controller.abort();
this.resetEventParser(); this.#resetEventParser();
await this.notifyDisconnected(); await this.#notifyDisconnected();
} }
/** /**
@@ -338,13 +338,13 @@ export class SSESession extends EventEmitter<SSESessionEventMap> {
* Closes {@link messages} and emits `"closed"`. * Closes {@link messages} and emits `"closed"`.
*/ */
public async disconnect(): Promise<void> { public async disconnect(): Promise<void> {
this.closeMessageStream(); this.#closeMessageStream();
this.emit('closed', undefined); this.emit('closed', undefined);
if (this.controller) { if (this.#controller) {
await this.abort(); await this.abort();
} else { } else {
this.resetEventParser(); this.#resetEventParser();
} }
} }
@@ -354,22 +354,22 @@ export class SSESession extends EventEmitter<SSESessionEventMap> {
* {@link SSESessionOptions.onRequest} may mutate headers (for example auth * {@link SSESessionOptions.onRequest} may mutate headers (for example auth
* tokens or `Last-Event-ID`) before the fetch runs. * tokens or `Last-Event-ID`) before the fetch runs.
*/ */
private async createReader(fetchOptions: RequestInit): Promise<ReadableStreamDefaultReader<Uint8Array>> { async #createReader(fetchOptions: RequestInit): Promise<ReadableStreamDefaultReader<Uint8Array>> {
const requestOptions = await this.options.onRequest(fetchOptions); const requestOptions = await this.options.onRequest(fetchOptions);
const response = await this.options.fetch(this.url, requestOptions); const response = await this.options.fetch(this.#url, requestOptions);
if (!response.ok) { if (!response.ok) {
const responseCode = response.status; const responseCode = response.status;
const responseText = await response.text(); const responseText = await response.text();
const error = new HTTPError(responseCode, responseText); const error = new HTTPError(responseCode, responseText);
void this.notifyError(error); void this.#notifyError(error);
throw error; throw error;
} }
if (!response.body) { if (!response.body) {
const error = new ResponseBodyNullError(); const error = new ResponseBodyNullError();
void this.notifyError(error); void this.#notifyError(error);
throw error; throw error;
} }
@@ -380,24 +380,24 @@ export class SSESession extends EventEmitter<SSESessionEventMap> {
* Reads bytes from an established stream until it ends, errors, or is * Reads bytes from an established stream until it ends, errors, or is
* superseded by a newer connection. * superseded by a newer connection.
*/ */
private async readStream(reader: ReadableStreamDefaultReader<Uint8Array>, controller: AbortController): Promise<void> { async #readStream(reader: ReadableStreamDefaultReader<Uint8Array>, controller: AbortController): Promise<void> {
try { try {
while (this.controller === controller) { while (this.#controller === controller) {
const { done, value } = await reader.read(); const { done, value } = await reader.read();
// abort() or a newer connect() may have landed while we were awaiting. // abort() or a newer connect() may have landed while we were awaiting.
if (this.controller !== controller) return; if (this.#controller !== controller) return;
if (done) { if (done) {
this.controller = null; this.#controller = null;
await this.notifyDisconnected(); await this.#notifyDisconnected();
if (this.options.persistent) { if (this.options.persistent) {
// Server closed gracefully — reopen unless the consumer opted out. // Server closed gracefully — reopen unless the consumer opted out.
await this.connect(); await this.connect();
} else { } else {
this.closeMessageStream(); this.#closeMessageStream();
} }
return; return;
@@ -413,28 +413,28 @@ export class SSESession extends EventEmitter<SSESessionEventMap> {
} }
} catch (error) { } catch (error) {
// If the controller is different, we already started a new connection and it would be confusing to handle this error. // If the controller is different, we already started a new connection and it would be confusing to handle this error.
if (controller !== this.controller) return; if (controller !== this.#controller) return;
// Invalidate the current controller to allow for reconnection if needed // Invalidate the current controller to allow for reconnection if needed
this.controller = null; this.#controller = null;
await this.notifyDisconnected(); await this.#notifyDisconnected();
// Expected path for abort() — do not treat as an error or reconnect. // Expected path for abort() — do not treat as an error or reconnect.
if (controller.signal.aborted) return; if (controller.signal.aborted) return;
await this.notifyError(error); await this.#notifyError(error);
if (this.options.attemptReconnect) { if (this.options.attemptReconnect) {
await this.connect(); await this.connect();
} else { } else {
this.closeMessageStream(); this.#closeMessageStream();
} }
} }
} }
/** Clears partial SSE frames left over from an abandoned transport. */ /** Clears partial SSE frames left over from an abandoned transport. */
private resetEventParser(): void { #resetEventParser(): void {
this.options.eventParser.reset(); this.options.eventParser.reset();
} }
@@ -442,21 +442,21 @@ export class SSESession extends EventEmitter<SSESessionEventMap> {
* Creates a new {@link messages} iterator when the previous one was closed * Creates a new {@link messages} iterator when the previous one was closed
* by a terminal disconnect or server stream end. * by a terminal disconnect or server stream end.
*/ */
private ensureMessageStreamOpen(): void { #ensureMessageStreamOpen(): void {
if (!this.messages.closed) return; if (!this.messages.closed) return;
this.messages = new AsyncPushIterator<SSEvent>(); this.messages = new AsyncPushIterator<SSEvent>();
} }
/** Ends the message iteration loop for the current connection span. */ /** Ends the message iteration loop for the current connection span. */
private closeMessageStream(): void { #closeMessageStream(): void {
if (this.messages.closed) return; if (this.messages.closed) return;
this.messages.close(); this.messages.close();
} }
/** Invokes {@link SSESessionOptions.onDisconnected} and emits `"disconnected"`. */ /** Invokes {@link SSESessionOptions.onDisconnected} and emits `"disconnected"`. */
private async notifyDisconnected(): Promise<void> { async #notifyDisconnected(): Promise<void> {
await tryAsync( await tryAsync(
() => this.options.onDisconnected(), () => this.options.onDisconnected(),
(error) => this.options.onError(error), (error) => this.options.onError(error),
@@ -465,7 +465,7 @@ export class SSESession extends EventEmitter<SSESessionEventMap> {
} }
/** Invokes {@link SSESessionOptions.onError} and emits `"error"`. */ /** Invokes {@link SSESessionOptions.onError} and emits `"error"`. */
private async notifyError(error: unknown): Promise<void> { async #notifyError(error: unknown): Promise<void> {
const errorInstance = error instanceof Error ? error : new Error(String(error)); const errorInstance = error instanceof Error ? error : new Error(String(error));
await tryAsync( await tryAsync(