diff --git a/lib/base/connection.js b/lib/base/connection.js index 70cec19c97..77a4b85054 100644 --- a/lib/base/connection.js +++ b/lib/base/connection.js @@ -412,6 +412,37 @@ class BaseConnection extends EventEmitter { this.emit('error', err); } + get state() { + // Error state has highest priority + if (this._fatalError || this._protocolError) { + return 'error'; + } + + // Closing state has second priority + if (this._closing || (this.stream && this.stream.destroyed)) { + return 'disconnected'; + } + + // Authenticated state has third priority + if (this.authorized) { + return 'authenticated'; + } + + // Connected state: handshake completed but not yet authorized + // This matches the original mysql driver's 'connected' state + if (this._handshakePacket) { + return 'connected'; + } + + // Protocol handshake state: connection established, handshake in progress + if (this.stream && !this.stream.destroyed) { + return 'protocol_handshake'; + } + + // Default: not connected + return 'disconnected'; + } + get fatalError() { return this._fatalError; } diff --git a/test/unit/connection/test-connection-state.test.cjs b/test/unit/connection/test-connection-state.test.cjs new file mode 100644 index 0000000000..664f489bd3 --- /dev/null +++ b/test/unit/connection/test-connection-state.test.cjs @@ -0,0 +1,156 @@ +'use strict'; +const { test, assert } = require('poku'); +const BaseConnection = require('../../../lib/base/connection.js'); +const ConnectionConfig = require('../../../lib/connection_config.js'); +const EventEmitter = require('events'); + +// Helper to create a mock connection without actually connecting +function createMockConnection() { + const config = new ConnectionConfig({ + host: 'localhost', + user: 'test', + password: 'test', + database: 'test', + connectTimeout: 0, + }); + + // Create a minimal mock stream + const mockStream = new EventEmitter(); + mockStream.write = () => true; + mockStream.end = () => {}; + mockStream.destroy = () => { + mockStream.destroyed = true; + }; + mockStream.destroyed = false; + mockStream.setKeepAlive = () => {}; + mockStream.setNoDelay = () => {}; + + config.stream = mockStream; + config.isServer = true; // Prevent handshake command + + return new BaseConnection({ config }); +} + +test('should return disconnected state when no stream exists', () => { + const conn = createMockConnection(); + conn.stream = null; + assert.strictEqual( + conn.state, + 'disconnected', + 'State should be "disconnected" when stream is null' + ); +}); + +test('should return protocol_handshake state when stream exists but handshake not complete', () => { + const conn = createMockConnection(); + assert.strictEqual( + conn.state, + 'protocol_handshake', + 'State should be "protocol_handshake" when stream exists but handshake not complete' + ); +}); + +test('should return error state when fatal error occurs', () => { + const conn = createMockConnection(); + conn._fatalError = new Error('Fatal error'); + assert.strictEqual( + conn.state, + 'error', + 'State should be "error" when _fatalError is set' + ); +}); + +test('should return error state when protocol error occurs', () => { + const conn = createMockConnection(); + conn._protocolError = new Error('Protocol error'); + assert.strictEqual( + conn.state, + 'error', + 'State should be "error" when _protocolError is set' + ); +}); + +test('should return disconnected state when closing', () => { + const conn = createMockConnection(); + conn._closing = true; + assert.strictEqual( + conn.state, + 'disconnected', + 'State should be "disconnected" when _closing is true' + ); +}); + +test('should return disconnected state when stream is destroyed', () => { + const conn = createMockConnection(); + conn.stream.destroy(); + assert.strictEqual( + conn.state, + 'disconnected', + 'State should be "disconnected" when stream is destroyed' + ); +}); + +test('should return connected state when handshake is complete but not authorized', () => { + const conn = createMockConnection(); + conn._handshakePacket = { connectionId: 123 }; + assert.strictEqual( + conn.state, + 'connected', + 'State should be "connected" when handshake is complete but not authorized' + ); +}); + +test('should return authenticated state when authorized', () => { + const conn = createMockConnection(); + conn.authorized = true; + assert.strictEqual( + conn.state, + 'authenticated', + 'State should be "authenticated" when authorized is true' + ); +}); + +test('should return error state even when authorized and closing (error has highest priority)', () => { + const conn = createMockConnection(); + conn.authorized = true; + conn._closing = true; + conn._fatalError = new Error('Fatal error'); + assert.strictEqual( + conn.state, + 'error', + 'State should be "error" even when authorized and closing (error has highest priority)' + ); +}); + +test('should return disconnected state even when authorized (closing has higher priority)', () => { + const conn = createMockConnection(); + conn.authorized = true; + conn._closing = true; + assert.strictEqual( + conn.state, + 'disconnected', + 'State should be "disconnected" even when authorized (closing has higher priority)' + ); +}); + +test('should return error state when protocol error is set, regardless of authorization', () => { + const conn = createMockConnection(); + conn.authorized = true; + conn._protocolError = new Error('Protocol error'); + assert.strictEqual( + conn.state, + 'error', + 'State should be "error" when _protocolError is set, regardless of authorization' + ); +}); + +test('should return authenticated state when both handshake complete and authorized (authenticated has priority)', () => { + const conn = createMockConnection(); + conn._handshakePacket = { connectionId: 123 }; + conn.authorized = true; + assert.strictEqual( + conn.state, + 'authenticated', + 'State should be "authenticated" when both handshake complete and authorized (authenticated has priority)' + ); +}); diff --git a/typings/mysql/lib/Connection.d.ts b/typings/mysql/lib/Connection.d.ts index 6eb28338ae..c2c0edb233 100644 --- a/typings/mysql/lib/Connection.d.ts +++ b/typings/mysql/lib/Connection.d.ts @@ -339,6 +339,13 @@ export interface ConnectionOptions { gracefulEnd?: boolean; } +export type ConnectionState = + | 'disconnected' + | 'protocol_handshake' + | 'connected' + | 'authenticated' + | 'error'; + declare class Connection extends QueryableBase(ExecutableBase(EventEmitter)) { config: ConnectionOptions; @@ -346,6 +353,8 @@ declare class Connection extends QueryableBase(ExecutableBase(EventEmitter)) { authorized: boolean; + readonly state: ConnectionState; + static createQuery< T extends | RowDataPacket[][]