@bobfrankston/iflow-direct 0.1.18 → 0.1.19

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/README.md CHANGED
@@ -106,6 +106,16 @@ the same connection). Suspend sends DONE, awaits the tagged OK, runs the
106
106
  command, then re-enters IDLE. If the caller stops IDLE (via the `startIdle`
107
107
  stop function) during the command, IDLE is not re-entered.
108
108
 
109
+ ## Connection liveness
110
+
111
+ `NativeImapClient.connected` (and `CompatImapClient`'s lazy `ensureConnected`
112
+ path, which now delegates to it) is authoritative: it gets cleared on transport
113
+ error, transport close, inactivity-timeout socket kill, and write failure. Any
114
+ of those paths also drops IDLE state so auto-suspend doesn't try to DONE a dead
115
+ socket. Callers that detect connection-style errors (e.g. DNS "hostname not
116
+ known" from a dead Android socket) can safely retry — the next operation will
117
+ reconnect from scratch rather than reusing the dead socket.
118
+
109
119
  ## Configuration
110
120
 
111
121
  `ImapClientConfig` fields relevant to throughput/resilience:
package/imap-compat.d.ts CHANGED
@@ -23,11 +23,15 @@ export interface SpecialFolders {
23
23
  */
24
24
  export declare class CompatImapClient {
25
25
  private native;
26
- private _connected;
27
26
  constructor(config: ImapClientConfig, transportFactory: TransportFactory);
28
27
  /** Connect and authenticate */
29
28
  connect(): Promise<void>;
30
- /** Ensure connected (lazy connect) */
29
+ /**
30
+ * Ensure connected (lazy connect). Delegates to the native client's
31
+ * `connected` flag, which is cleared by transport close/error, inactivity
32
+ * timeout, and write failures — so a silently dead socket reconnects on the
33
+ * next call instead of cascading ENOTFOUND across every folder.
34
+ */
31
35
  private ensureConnected;
32
36
  logout(): Promise<void>;
33
37
  /** Get folder list */
package/imap-compat.js CHANGED
@@ -11,22 +11,24 @@ import { FetchedMessage } from "./fetched-message.js";
11
11
  */
12
12
  export class CompatImapClient {
13
13
  native;
14
- _connected = false;
15
14
  constructor(config, transportFactory) {
16
15
  this.native = new NativeImapClient(config, transportFactory);
17
16
  }
18
17
  /** Connect and authenticate */
19
18
  async connect() {
20
19
  await this.native.connect();
21
- this._connected = true;
22
20
  }
23
- /** Ensure connected (lazy connect) */
21
+ /**
22
+ * Ensure connected (lazy connect). Delegates to the native client's
23
+ * `connected` flag, which is cleared by transport close/error, inactivity
24
+ * timeout, and write failures — so a silently dead socket reconnects on the
25
+ * next call instead of cascading ENOTFOUND across every folder.
26
+ */
24
27
  async ensureConnected() {
25
- if (!this._connected)
28
+ if (!this.native.connected)
26
29
  await this.connect();
27
30
  }
28
31
  async logout() {
29
- this._connected = false;
30
32
  await this.native.logout();
31
33
  }
32
34
  /** Get folder list */
package/imap-native.js CHANGED
@@ -42,7 +42,14 @@ export class NativeImapClient {
42
42
  this.transport.onData((data) => this.handleData(data));
43
43
  this.transport.onClose(() => {
44
44
  this._connected = false;
45
- // Reject any pending command so it doesn't hang forever
45
+ // Drop stale IDLE state the socket is gone.
46
+ this.idleTag = null;
47
+ this.idleCallback = null;
48
+ this.idleStopped = true;
49
+ if (this.idleRefreshTimer) {
50
+ clearTimeout(this.idleRefreshTimer);
51
+ this.idleRefreshTimer = null;
52
+ }
46
53
  if (this.pendingCommand) {
47
54
  const { reject } = this.pendingCommand;
48
55
  this.pendingCommand = null;
@@ -52,7 +59,18 @@ export class NativeImapClient {
52
59
  this.transport.onError((err) => {
53
60
  if (this.verbose)
54
61
  console.error(` [imap] Transport error: ${err.message}`);
55
- // Reject any pending command on transport error
62
+ // Transport errors (DNS failure, ECONNRESET, TLS failure, etc.) mean
63
+ // the connection is dead — clear the flag so ensureConnected() will
64
+ // reconnect on the next call, and drop the stale IDLE state so we
65
+ // don't try to DONE a dead socket during auto-suspend.
66
+ this._connected = false;
67
+ this.idleTag = null;
68
+ this.idleCallback = null;
69
+ this.idleStopped = true;
70
+ if (this.idleRefreshTimer) {
71
+ clearTimeout(this.idleRefreshTimer);
72
+ this.idleRefreshTimer = null;
73
+ }
56
74
  if (this.pendingCommand) {
57
75
  const { reject } = this.pendingCommand;
58
76
  this.pendingCommand = null;
@@ -674,7 +692,10 @@ export class NativeImapClient {
674
692
  const onTimeout = () => {
675
693
  this.commandTimer = null;
676
694
  this.pendingCommand = null;
677
- // Kill the connection — a timed-out connection has stale data in the pipe
695
+ // Kill the connection — a timed-out connection has stale data in the pipe.
696
+ // Mark disconnected so ensureConnected() reconnects on the next call —
697
+ // transport.close() may or may not fire onClose synchronously.
698
+ this._connected = false;
678
699
  this.transport.close?.();
679
700
  reject(new Error(`IMAP inactivity timeout (${this.inactivityTimeout / 1000}s): ${command.split("\r")[0].substring(0, 80)}`));
680
701
  };
@@ -687,8 +708,18 @@ export class NativeImapClient {
687
708
  clearTimeout(this.commandTimer); this.commandTimer = null; reject(err); },
688
709
  onUntagged,
689
710
  };
690
- this.transport.write(command).catch((err) => { if (this.commandTimer)
691
- clearTimeout(this.commandTimer); this.commandTimer = null; reject(err); });
711
+ this.transport.write(command).catch((err) => {
712
+ if (this.commandTimer)
713
+ clearTimeout(this.commandTimer);
714
+ this.commandTimer = null;
715
+ // Write failure = dead socket. Clear pendingCommand + disconnect flag
716
+ // so the next ensureConnected() does a fresh connect instead of
717
+ // reusing a socket that the OS layer has already declared dead
718
+ // (the source of the "hostname nor servname provided" cascade).
719
+ this.pendingCommand = null;
720
+ this._connected = false;
721
+ reject(err);
722
+ });
692
723
  });
693
724
  }
694
725
  waitForContinuation(tag) {
@@ -718,6 +749,7 @@ export class NativeImapClient {
718
749
  if (this.pendingCommand) {
719
750
  const cmd = this.pendingCommand;
720
751
  this.pendingCommand = null;
752
+ this._connected = false;
721
753
  this.transport.close?.();
722
754
  cmd.reject(new Error(`IMAP inactivity timeout (${this.inactivityTimeout / 1000}s)`));
723
755
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/iflow-direct",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "Direct IMAP client — transport-agnostic, no Node.js dependencies, browser-ready",
5
5
  "main": "index.js",
6
6
  "types": "index.ts",