feat: bump version to 1.2.7 and enhance connection handling in SMTPOverWSClient to prevent race conditions

This commit is contained in:
Siwat Sirichai 2025-08-20 21:43:08 +07:00
parent ad1424a79a
commit 3526902a72
2 changed files with 32 additions and 6 deletions

View file

@ -1,6 +1,6 @@
{ {
"name": "@siwats/mxrelay-consumer", "name": "@siwats/mxrelay-consumer",
"version": "1.2.5", "version": "1.2.7",
"description": "An internal TypeScript client library for transporting SMTP messages", "description": "An internal TypeScript client library for transporting SMTP messages",
"main": "lib/index.js", "main": "lib/index.js",
"module": "lib/index.esm.js", "module": "lib/index.esm.js",

View file

@ -82,10 +82,12 @@ export class SMTPOverWSClient extends EventEmitter {
private reconnectTimer: NodeJS.Timeout | null = null; private reconnectTimer: NodeJS.Timeout | null = null;
private authTimer: NodeJS.Timeout | null = null; private authTimer: NodeJS.Timeout | null = null;
private channelTimer: NodeJS.Timeout | null = null; private channelTimer: NodeJS.Timeout | null = null;
private channelCloseTimer: NodeJS.Timeout | null = null;
private heartbeatTimer: NodeJS.Timeout | null = null; private heartbeatTimer: NodeJS.Timeout | null = null;
private sessionTimer: NodeJS.Timeout | null = null; private sessionTimer: NodeJS.Timeout | null = null;
private isProcessingQueue = false; private isProcessingQueue = false;
private isShuttingDown = false; private isShuttingDown = false;
private connectionPromise: Promise<void> | null = null;
private logger: Logger; private logger: Logger;
private stats: ClientStats; private stats: ClientStats;
private connectionStartTime: number = 0; private connectionStartTime: number = 0;
@ -314,9 +316,17 @@ export class SMTPOverWSClient extends EventEmitter {
let failed = 0; let failed = 0;
try { try {
// Connect if not connected // Connect if not connected - use connection promise to prevent race conditions
if (this.state === ConnectionState.DISCONNECTED) { if (this.state === ConnectionState.DISCONNECTED || this.state === ConnectionState.FAILED) {
await this.connect(); if (!this.connectionPromise) {
this.connectionPromise = this.connect().finally(() => {
this.connectionPromise = null;
});
}
await this.connectionPromise;
} else if (this.connectionPromise) {
// Wait for ongoing connection attempt to complete
await this.connectionPromise;
} }
// Process sessions sequentially one at a time (SMTP sessions are mutex) // Process sessions sequentially one at a time (SMTP sessions are mutex)
@ -789,6 +799,11 @@ export class SMTPOverWSClient extends EventEmitter {
return new Promise((resolve) => { return new Promise((resolve) => {
if (this.state === ConnectionState.CHANNEL_READY) { if (this.state === ConnectionState.CHANNEL_READY) {
const onChannelClosed = () => { const onChannelClosed = () => {
// Clear the fallback timer since channel closed properly
if (this.channelCloseTimer) {
clearTimeout(this.channelCloseTimer);
this.channelCloseTimer = null;
}
this.setState(ConnectionState.AUTHENTICATED); this.setState(ConnectionState.AUTHENTICATED);
this.emit('channelClosed'); this.emit('channelClosed');
this.logger.debug('SMTP channel closed'); this.logger.debug('SMTP channel closed');
@ -798,9 +813,14 @@ export class SMTPOverWSClient extends EventEmitter {
this.once('smtp_channel_closed', onChannelClosed); this.once('smtp_channel_closed', onChannelClosed);
// Fallback timeout // Fallback timeout
setTimeout(() => { this.channelCloseTimer = setTimeout(() => {
this.removeListener('smtp_channel_closed', onChannelClosed); this.removeListener('smtp_channel_closed', onChannelClosed);
// Only set to authenticated if we're still connected
if (this.ws && this.ws.readyState === WebSocket.OPEN &&
this.state !== ConnectionState.DISCONNECTED) {
this.setState(ConnectionState.AUTHENTICATED); this.setState(ConnectionState.AUTHENTICATED);
}
this.channelCloseTimer = null;
resolve(); resolve();
}, 5000); }, 5000);
} else { } else {
@ -883,6 +903,7 @@ export class SMTPOverWSClient extends EventEmitter {
this.emit('disconnected', reason); this.emit('disconnected', reason);
this.stopHeartbeat(); this.stopHeartbeat();
this.clearTimers(); this.clearTimers();
this.connectionPromise = null;
// Always try to reconnect if we have queued sessions and not shutting down // Always try to reconnect if we have queued sessions and not shutting down
// NEVER reject emails - keep trying with periodic retries // NEVER reject emails - keep trying with periodic retries
@ -1010,6 +1031,11 @@ export class SMTPOverWSClient extends EventEmitter {
this.channelTimer = null; this.channelTimer = null;
} }
if (this.channelCloseTimer) {
clearTimeout(this.channelCloseTimer);
this.channelCloseTimer = null;
}
if (this.sessionTimer) { if (this.sessionTimer) {
clearTimeout(this.sessionTimer); clearTimeout(this.sessionTimer);
this.sessionTimer = null; this.sessionTimer = null;