@bobfrankston/iflow-direct 0.1.30 → 0.1.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/imap-native.js +58 -8
- package/package.json +1 -1
package/imap-native.js
CHANGED
|
@@ -771,29 +771,79 @@ export class NativeImapClient {
|
|
|
771
771
|
if (this.verbose && !command.includes("LOGIN") && !command.includes("AUTHENTICATE")) {
|
|
772
772
|
console.log(` [imap] > ${command.trimEnd()}`);
|
|
773
773
|
}
|
|
774
|
+
// Snapshot transport diagnostics — used by both heartbeat and timeout.
|
|
775
|
+
const transportDiag = () => {
|
|
776
|
+
try {
|
|
777
|
+
const d = this.transport?.diagnostics;
|
|
778
|
+
if (!d)
|
|
779
|
+
return "";
|
|
780
|
+
const sinceRead = d.lastReadAt ? Date.now() - d.lastReadAt : -1;
|
|
781
|
+
return ` [conn#${d.connId} r=${d.bytesRead}B w=${d.bytesWritten}B writes=${d.writeCount} sinceLastRead=${sinceRead}ms]`;
|
|
782
|
+
}
|
|
783
|
+
catch {
|
|
784
|
+
return "";
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
// HEARTBEAT_INTERVAL_MS — periodic progress log while a command
|
|
788
|
+
// is pending. Catches wedges in flight rather than only at the
|
|
789
|
+
// 300s timeout edge. Key signal: if `sinceLastRead` keeps
|
|
790
|
+
// climbing, the socket is silently dead. If reads are happening
|
|
791
|
+
// but the tagged OK never arrives, the parser is stuck.
|
|
792
|
+
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
793
|
+
const cmdStart = Date.now();
|
|
794
|
+
const cmdSummary = command.split("\r")[0].substring(0, 80);
|
|
795
|
+
let heartbeatTimer = setInterval(() => {
|
|
796
|
+
const waited = Date.now() - cmdStart;
|
|
797
|
+
console.log(` [imap] still waiting for tag ${tag} after ${(waited / 1000).toFixed(1)}s — ${cmdSummary}${transportDiag()}`);
|
|
798
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
774
799
|
const onTimeout = () => {
|
|
775
800
|
this.commandTimer = null;
|
|
776
801
|
this.pendingCommand = null;
|
|
802
|
+
if (heartbeatTimer) {
|
|
803
|
+
clearInterval(heartbeatTimer);
|
|
804
|
+
heartbeatTimer = null;
|
|
805
|
+
}
|
|
777
806
|
// Kill the connection — a timed-out connection has stale data in the pipe.
|
|
778
807
|
// Mark disconnected so ensureConnected() reconnects on the next call —
|
|
779
808
|
// transport.close() may or may not fire onClose synchronously.
|
|
780
809
|
this._connected = false;
|
|
781
810
|
this.transport.close?.();
|
|
782
|
-
reject(new Error(`IMAP inactivity timeout (${this.inactivityTimeout / 1000}s): ${
|
|
811
|
+
reject(new Error(`IMAP inactivity timeout (${this.inactivityTimeout / 1000}s): ${cmdSummary}${transportDiag()}`));
|
|
783
812
|
};
|
|
784
813
|
this.commandTimer = setTimeout(onTimeout, this.inactivityTimeout);
|
|
814
|
+
const clearTimers = () => {
|
|
815
|
+
if (this.commandTimer) {
|
|
816
|
+
clearTimeout(this.commandTimer);
|
|
817
|
+
this.commandTimer = null;
|
|
818
|
+
}
|
|
819
|
+
if (heartbeatTimer) {
|
|
820
|
+
clearInterval(heartbeatTimer);
|
|
821
|
+
heartbeatTimer = null;
|
|
822
|
+
}
|
|
823
|
+
};
|
|
785
824
|
this.pendingCommand = {
|
|
786
825
|
tag, responses: [],
|
|
787
|
-
resolve: (responses) => {
|
|
788
|
-
|
|
789
|
-
reject: (err) => { if (this.commandTimer)
|
|
790
|
-
clearTimeout(this.commandTimer); this.commandTimer = null; reject(err); },
|
|
826
|
+
resolve: (responses) => { clearTimers(); resolve(responses); },
|
|
827
|
+
reject: (err) => { clearTimers(); reject(err); },
|
|
791
828
|
onUntagged,
|
|
792
829
|
};
|
|
830
|
+
// PRE-WRITE HEALTH CHECK — verify the socket is in a writable
|
|
831
|
+
// state BEFORE we burn the inactivityTimeout window waiting for
|
|
832
|
+
// a reply that can't come. socket-half-closed (peer FIN'd) and
|
|
833
|
+
// destroyed states both fail fast here instead of after 300s.
|
|
834
|
+
try {
|
|
835
|
+
const h = this.transport?.socketHealth;
|
|
836
|
+
if (h && (h.destroyed || h.writableEnded || !h.writable)) {
|
|
837
|
+
clearTimers();
|
|
838
|
+
this.pendingCommand = null;
|
|
839
|
+
this._connected = false;
|
|
840
|
+
reject(new Error(`IMAP socket dead before write: destroyed=${h.destroyed} writableEnded=${h.writableEnded} writable=${h.writable}${transportDiag()}`));
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
catch { /* transport may not expose socketHealth — fall through */ }
|
|
793
845
|
this.transport.write(command).catch((err) => {
|
|
794
|
-
|
|
795
|
-
clearTimeout(this.commandTimer);
|
|
796
|
-
this.commandTimer = null;
|
|
846
|
+
clearTimers();
|
|
797
847
|
// Write failure = dead socket. Clear pendingCommand + disconnect flag
|
|
798
848
|
// so the next ensureConnected() does a fresh connect instead of
|
|
799
849
|
// reusing a socket that the OS layer has already declared dead
|