diff --git a/crates/bindings-typescript/src/sdk/db_connection_impl.ts b/crates/bindings-typescript/src/sdk/db_connection_impl.ts index cf73bc270bb..7f276683aa2 100644 --- a/crates/bindings-typescript/src/sdk/db_connection_impl.ts +++ b/crates/bindings-typescript/src/sdk/db_connection_impl.ts @@ -173,6 +173,21 @@ export class DbConnectionImpl */ isDisconnectRequested = false; + /** + * Whether the initial connection handshake completed, i.e. the + * `InitialConnection` message was received and `onConnect` was invoked. + * Used to route websocket errors on an established connection to the + * `disconnect` path instead of `connectError`. + */ + #everConnected = false; + + /** + * The websocket error that ended an established connection, if any. + * Passed to the `disconnect` emit so `onDisconnect` callbacks receive + * the error that caused the disconnect, per their documented contract. + */ + #connectionError?: ErrorEvent = undefined; + /** * Whether the underlying websocket has entered `CLOSING` (2) or `CLOSED` * (3). This becomes true even when the browser never delivered an @@ -378,10 +393,19 @@ export class DbConnectionImpl this.ws.onclose = () => { this.isActive = false; - this.#emitter.emit('disconnect', this); + this.#emitter.emit('disconnect', this, this.#connectionError); }; this.ws.onerror = (e: ErrorEvent) => { this.isActive = false; + if (this.#everConnected) { + // An error on an established connection is not a connect + // failure. Record it and close the socket so the `onclose` -> + // 'disconnect' path handles teardown, per the documented + // `onDisconnect` contract. + this.#connectionError = e; + this.ws?.close(); + return; + } this.#emitter.emit('connectError', this, e); }; this.ws.onopen = this.#handleOnOpen.bind(this); @@ -909,6 +933,7 @@ export class DbConnectionImpl this.token = serverMessage.value.token; } this.#setConnectionId(serverMessage.value.connectionId); + this.#everConnected = true; this.#emitter.emit('connect', this, this.identity, this.token); break; }