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",
"version": "1.2.5",
"version": "1.2.7",
"description": "An internal TypeScript client library for transporting SMTP messages",
"main": "lib/index.js",
"module": "lib/index.esm.js",

View file

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