From 8fb32ddd8b0f28349a1d37bdbadfffb95b1cdee4 Mon Sep 17 00:00:00 2001 From: CHOIJEWON Date: Wed, 10 Dec 2025 11:20:37 +0900 Subject: [PATCH 1/4] feat: add state getter to track connection lifecycle Adds a readonly state property to Connection that returns the current connection state. This enables users to monitor and debug connection lifecycle issues more effectively. The state property returns one of five possible states: - 'disconnected': Initial state or when connection is closed/destroyed - 'protocol_handshake': Connection established, handshake in progress - 'connected': Handshake completed but not yet authenticated - 'authenticated': Fully authenticated and ready for queries - 'error': Fatal or protocol error occurred State priority: error > closing/destroyed > authenticated > connected > protocol_handshake > disconnected --- lib/base/connection.js | 31 ++++ .../test-connection-state.test.cjs.js | 145 ++++++++++++++++++ typings/mysql/lib/Connection.d.ts | 9 ++ 3 files changed, 185 insertions(+) create mode 100644 test/unit/connection/test-connection-state.test.cjs.js 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.js b/test/unit/connection/test-connection-state.test.cjs.js new file mode 100644 index 0000000000..fce9ff66bd --- /dev/null +++ b/test/unit/connection/test-connection-state.test.cjs.js @@ -0,0 +1,145 @@ +'use strict'; +const { 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', + }); + + // 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 1: Initial state +const conn1 = createMockConnection(); +const initialState = conn1.state; +assert.ok( + initialState === 'disconnected' || initialState === 'protocol_handshake', + `Initial state should be disconnected or protocol_handshake, got: ${initialState}` +); + +// Test 2: Error state when fatal error occurs +const conn2 = createMockConnection(); +conn2._fatalError = new Error('Fatal error'); +assert.strictEqual( + conn2.state, + 'error', + 'State should be "error" when _fatalError is set' +); + +// Test 3: Error state when protocol error occurs +const conn3 = createMockConnection(); +conn3._protocolError = new Error('Protocol error'); +assert.strictEqual( + conn3.state, + 'error', + 'State should be "error" when _protocolError is set' +); + +// Test 4: Disconnected state when closing +const conn4 = createMockConnection(); +conn4._closing = true; +assert.strictEqual( + conn4.state, + 'disconnected', + 'State should be "disconnected" when _closing is true' +); + +// Test 5: Disconnected state when stream is destroyed +const conn5 = createMockConnection(); +conn5.stream.destroy(); // Call destroy() method instead of setting destroyed property +assert.strictEqual( + conn5.state, + 'disconnected', + 'State should be "disconnected" when stream is destroyed' +); + +// Test 6: Connected state when handshake is complete but not authorized +const conn6 = createMockConnection(); +conn6._handshakePacket = { connectionId: 123 }; // Simulate handshake completion +assert.strictEqual( + conn6.state, + 'connected', + 'State should be "connected" when handshake is complete but not authorized' +); + +// Test 7: Authenticated state when authorized +const conn7 = createMockConnection(); +conn7.authorized = true; +assert.strictEqual( + conn7.state, + 'authenticated', + 'State should be "authenticated" when authorized is true' +); + +// Test 8: Error state has highest priority (over authenticated and closing) +const conn8 = createMockConnection(); +conn8.authorized = true; +conn8._closing = true; +conn8._fatalError = new Error('Fatal error'); +assert.strictEqual( + conn8.state, + 'error', + 'State should be "error" even when authorized and closing (error has highest priority)' +); + +// Test 9: Closing state has higher priority than authenticated +const conn9 = createMockConnection(); +conn9.authorized = true; +conn9._closing = true; +assert.strictEqual( + conn9.state, + 'disconnected', + 'State should be "disconnected" even when authorized (closing has higher priority)' +); + +// Test 10: Protocol error has same priority as fatal error +const conn10 = createMockConnection(); +conn10.authorized = true; +conn10._protocolError = new Error('Protocol error'); +assert.strictEqual( + conn10.state, + 'error', + 'State should be "error" when _protocolError is set, regardless of authorization' +); + +// Test 11: Authenticated takes priority over connected +const conn11 = createMockConnection(); +conn11._handshakePacket = { connectionId: 123 }; +conn11.authorized = true; +assert.strictEqual( + conn11.state, + 'authenticated', + 'State should be "authenticated" when both handshake complete and authorized (authenticated has priority)' +); + +// Cleanup +[conn1, conn2, conn3, conn4, conn5, conn6, conn7, conn8, conn9, conn10, conn11].forEach( + (conn) => { + try { + conn.destroy(); + } catch (e) { + // Ignore cleanup errors + } + } +); diff --git a/typings/mysql/lib/Connection.d.ts b/typings/mysql/lib/Connection.d.ts index 6eb28338ae..2839cb2d47 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[][] From a4f46a6491302f3b50113a321dabe092c7200557 Mon Sep 17 00:00:00 2001 From: CHOIJEWON Date: Wed, 10 Dec 2025 11:51:14 +0900 Subject: [PATCH 2/4] chore(test): rename test file to test-connection-state.test.cjs --- ...onnection-state.test.cjs.js => test-connection-state.test.cjs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/unit/connection/{test-connection-state.test.cjs.js => test-connection-state.test.cjs} (100%) diff --git a/test/unit/connection/test-connection-state.test.cjs.js b/test/unit/connection/test-connection-state.test.cjs similarity index 100% rename from test/unit/connection/test-connection-state.test.cjs.js rename to test/unit/connection/test-connection-state.test.cjs From 09b74cee630623be9d240476d43a24c6e80a28a0 Mon Sep 17 00:00:00 2001 From: CHOIJEWON Date: Wed, 10 Dec 2025 12:12:54 +0900 Subject: [PATCH 3/4] chore(prettier): fix prettier format --- .../connection/test-connection-state.test.cjs | 26 +++++++++++++------ typings/mysql/lib/Connection.d.ts | 10 +++---- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/test/unit/connection/test-connection-state.test.cjs b/test/unit/connection/test-connection-state.test.cjs index fce9ff66bd..afc26d83c1 100644 --- a/test/unit/connection/test-connection-state.test.cjs +++ b/test/unit/connection/test-connection-state.test.cjs @@ -134,12 +134,22 @@ assert.strictEqual( ); // Cleanup -[conn1, conn2, conn3, conn4, conn5, conn6, conn7, conn8, conn9, conn10, conn11].forEach( - (conn) => { - try { - conn.destroy(); - } catch (e) { - // Ignore cleanup errors - } +[ + conn1, + conn2, + conn3, + conn4, + conn5, + conn6, + conn7, + conn8, + conn9, + conn10, + conn11, +].forEach((conn) => { + try { + conn.destroy(); + } catch (e) { + // Ignore cleanup errors } -); +}); diff --git a/typings/mysql/lib/Connection.d.ts b/typings/mysql/lib/Connection.d.ts index 2839cb2d47..c2c0edb233 100644 --- a/typings/mysql/lib/Connection.d.ts +++ b/typings/mysql/lib/Connection.d.ts @@ -340,11 +340,11 @@ export interface ConnectionOptions { } export type ConnectionState = - | "disconnected" - | "protocol_handshake" - | "connected" - | "authenticated" - | "error"; + | 'disconnected' + | 'protocol_handshake' + | 'connected' + | 'authenticated' + | 'error'; declare class Connection extends QueryableBase(ExecutableBase(EventEmitter)) { config: ConnectionOptions; From c23f11ff59df7d3d9ed47ce322e94f3bc9f9e561 Mon Sep 17 00:00:00 2001 From: CHOIJEWON Date: Tue, 16 Dec 2025 12:11:00 +0900 Subject: [PATCH 4/4] refactor(test): wrap tests in test() function and improve state validation - Wrap all test cases with test() function for better isolation - Split initial state test to properly verify both disconnected and protocol_handshake states - Remove cleanup code as each test has its own scope - Improve test descriptions with BDD-style naming --- .../connection/test-connection-state.test.cjs | 247 +++++++++--------- 1 file changed, 124 insertions(+), 123 deletions(-) diff --git a/test/unit/connection/test-connection-state.test.cjs b/test/unit/connection/test-connection-state.test.cjs index afc26d83c1..664f489bd3 100644 --- a/test/unit/connection/test-connection-state.test.cjs +++ b/test/unit/connection/test-connection-state.test.cjs @@ -1,5 +1,5 @@ 'use strict'; -const { assert } = require('poku'); +const { test, assert } = require('poku'); const BaseConnection = require('../../../lib/base/connection.js'); const ConnectionConfig = require('../../../lib/connection_config.js'); const EventEmitter = require('events'); @@ -11,6 +11,7 @@ function createMockConnection() { user: 'test', password: 'test', database: 'test', + connectTimeout: 0, }); // Create a minimal mock stream @@ -30,126 +31,126 @@ function createMockConnection() { return new BaseConnection({ config }); } -// Test 1: Initial state -const conn1 = createMockConnection(); -const initialState = conn1.state; -assert.ok( - initialState === 'disconnected' || initialState === 'protocol_handshake', - `Initial state should be disconnected or protocol_handshake, got: ${initialState}` -); - -// Test 2: Error state when fatal error occurs -const conn2 = createMockConnection(); -conn2._fatalError = new Error('Fatal error'); -assert.strictEqual( - conn2.state, - 'error', - 'State should be "error" when _fatalError is set' -); - -// Test 3: Error state when protocol error occurs -const conn3 = createMockConnection(); -conn3._protocolError = new Error('Protocol error'); -assert.strictEqual( - conn3.state, - 'error', - 'State should be "error" when _protocolError is set' -); - -// Test 4: Disconnected state when closing -const conn4 = createMockConnection(); -conn4._closing = true; -assert.strictEqual( - conn4.state, - 'disconnected', - 'State should be "disconnected" when _closing is true' -); - -// Test 5: Disconnected state when stream is destroyed -const conn5 = createMockConnection(); -conn5.stream.destroy(); // Call destroy() method instead of setting destroyed property -assert.strictEqual( - conn5.state, - 'disconnected', - 'State should be "disconnected" when stream is destroyed' -); - -// Test 6: Connected state when handshake is complete but not authorized -const conn6 = createMockConnection(); -conn6._handshakePacket = { connectionId: 123 }; // Simulate handshake completion -assert.strictEqual( - conn6.state, - 'connected', - 'State should be "connected" when handshake is complete but not authorized' -); - -// Test 7: Authenticated state when authorized -const conn7 = createMockConnection(); -conn7.authorized = true; -assert.strictEqual( - conn7.state, - 'authenticated', - 'State should be "authenticated" when authorized is true' -); - -// Test 8: Error state has highest priority (over authenticated and closing) -const conn8 = createMockConnection(); -conn8.authorized = true; -conn8._closing = true; -conn8._fatalError = new Error('Fatal error'); -assert.strictEqual( - conn8.state, - 'error', - 'State should be "error" even when authorized and closing (error has highest priority)' -); - -// Test 9: Closing state has higher priority than authenticated -const conn9 = createMockConnection(); -conn9.authorized = true; -conn9._closing = true; -assert.strictEqual( - conn9.state, - 'disconnected', - 'State should be "disconnected" even when authorized (closing has higher priority)' -); - -// Test 10: Protocol error has same priority as fatal error -const conn10 = createMockConnection(); -conn10.authorized = true; -conn10._protocolError = new Error('Protocol error'); -assert.strictEqual( - conn10.state, - 'error', - 'State should be "error" when _protocolError is set, regardless of authorization' -); - -// Test 11: Authenticated takes priority over connected -const conn11 = createMockConnection(); -conn11._handshakePacket = { connectionId: 123 }; -conn11.authorized = true; -assert.strictEqual( - conn11.state, - 'authenticated', - 'State should be "authenticated" when both handshake complete and authorized (authenticated has priority)' -); - -// Cleanup -[ - conn1, - conn2, - conn3, - conn4, - conn5, - conn6, - conn7, - conn8, - conn9, - conn10, - conn11, -].forEach((conn) => { - try { - conn.destroy(); - } catch (e) { - // Ignore cleanup errors - } +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)' + ); });