Use hash privatE
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user