@bobfrankston/iflow-direct 0.1.18 → 0.1.20

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). Checks both the native client's connected
31
+ * flag AND the transport's connected state. The transport flag catches cases
32
+ * where the socket died without a close/error event reaching NativeImapClient
33
+ * (common on Android where the bridge may not relay all TCP events).
34
+ */
31
35
  private ensureConnected;
32
36
  logout(): Promise<void>;
33
37
  /** Get folder list */
package/imap-compat.js CHANGED
@@ -11,22 +11,27 @@ 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). Checks both the native client's connected
23
+ * flag AND the transport's connected state. The transport flag catches cases
24
+ * where the socket died without a close/error event reaching NativeImapClient
25
+ * (common on Android where the bridge may not relay all TCP events).
26
+ */
24
27
  async ensureConnected() {
25
- if (!this._connected)
28
+ if (!this.native.connected || !this.native.transportConnected) {
29
+ // Force a fresh transport — the old one may be in a bad state.
30
+ this.native.resetTransport();
26
31
  await this.connect();
32
+ }
27
33
  }
28
34
  async logout() {
29
- this._connected = false;
30
35
  await this.native.logout();
31
36
  }
32
37
  /** Get folder list */
package/imap-native.d.ts CHANGED
@@ -65,6 +65,11 @@ export declare class NativeImapClient {
65
65
  private continuationResolve;
66
66
  constructor(config: ImapClientConfig, transportFactory: TransportFactory);
67
67
  get connected(): boolean;
68
+ /** Check the underlying transport's connected state — catches silently dead sockets. */
69
+ get transportConnected(): boolean;
70
+ /** Replace the transport with a fresh one from the factory. Call before reconnect
71
+ * when the old transport is in a bad state (dead socket, stale bridge stream). */
72
+ resetTransport(): void;
68
73
  connect(): Promise<void>;
69
74
  private readGreeting;
70
75
  private authenticate;
package/imap-native.js CHANGED
@@ -36,13 +36,35 @@ export class NativeImapClient {
36
36
  this.fetchChunkSizeMax = config.fetchChunkSizeMax ?? 500;
37
37
  }
38
38
  get connected() { return this._connected; }
39
+ /** Check the underlying transport's connected state — catches silently dead sockets. */
40
+ get transportConnected() { return this.transport?.connected ?? false; }
41
+ /** Replace the transport with a fresh one from the factory. Call before reconnect
42
+ * when the old transport is in a bad state (dead socket, stale bridge stream). */
43
+ resetTransport() {
44
+ try {
45
+ this.transport?.close?.();
46
+ }
47
+ catch { /* ignore */ }
48
+ this.transport = this.transportFactory();
49
+ this._connected = false;
50
+ this.buffer = "";
51
+ this.pendingCommand = null;
52
+ this.selectedMailbox = null;
53
+ }
39
54
  // ── Connection ──
40
55
  async connect() {
41
56
  const useTls = this.config.port === 993;
42
57
  this.transport.onData((data) => this.handleData(data));
43
58
  this.transport.onClose(() => {
44
59
  this._connected = false;
45
- // Reject any pending command so it doesn't hang forever
60
+ // Drop stale IDLE state the socket is gone.
61
+ this.idleTag = null;
62
+ this.idleCallback = null;
63
+ this.idleStopped = true;
64
+ if (this.idleRefreshTimer) {
65
+ clearTimeout(this.idleRefreshTimer);
66
+ this.idleRefreshTimer = null;
67
+ }
46
68
  if (this.pendingCommand) {
47
69
  const { reject } = this.pendingCommand;
48
70
  this.pendingCommand = null;
@@ -52,7 +74,18 @@ export class NativeImapClient {
52
74
  this.transport.onError((err) => {
53
75
  if (this.verbose)
54
76
  console.error(` [imap] Transport error: ${err.message}`);
55
- // Reject any pending command on transport error
77
+ // Transport errors (DNS failure, ECONNRESET, TLS failure, etc.) mean
78
+ // the connection is dead — clear the flag so ensureConnected() will
79
+ // reconnect on the next call, and drop the stale IDLE state so we
80
+ // don't try to DONE a dead socket during auto-suspend.
81
+ this._connected = false;
82
+ this.idleTag = null;
83
+ this.idleCallback = null;
84
+ this.idleStopped = true;
85
+ if (this.idleRefreshTimer) {
86
+ clearTimeout(this.idleRefreshTimer);
87
+ this.idleRefreshTimer = null;
88
+ }
56
89
  if (this.pendingCommand) {
57
90
  const { reject } = this.pendingCommand;
58
91
  this.pendingCommand = null;
@@ -674,7 +707,10 @@ export class NativeImapClient {
674
707
  const onTimeout = () => {
675
708
  this.commandTimer = null;
676
709
  this.pendingCommand = null;
677
- // Kill the connection — a timed-out connection has stale data in the pipe
710
+ // Kill the connection — a timed-out connection has stale data in the pipe.
711
+ // Mark disconnected so ensureConnected() reconnects on the next call —
712
+ // transport.close() may or may not fire onClose synchronously.
713
+ this._connected = false;
678
714
  this.transport.close?.();
679
715
  reject(new Error(`IMAP inactivity timeout (${this.inactivityTimeout / 1000}s): ${command.split("\r")[0].substring(0, 80)}`));
680
716
  };
@@ -687,8 +723,18 @@ export class NativeImapClient {
687
723
  clearTimeout(this.commandTimer); this.commandTimer = null; reject(err); },
688
724
  onUntagged,
689
725
  };
690
- this.transport.write(command).catch((err) => { if (this.commandTimer)
691
- clearTimeout(this.commandTimer); this.commandTimer = null; reject(err); });
726
+ this.transport.write(command).catch((err) => {
727
+ if (this.commandTimer)
728
+ clearTimeout(this.commandTimer);
729
+ this.commandTimer = null;
730
+ // Write failure = dead socket. Clear pendingCommand + disconnect flag
731
+ // so the next ensureConnected() does a fresh connect instead of
732
+ // reusing a socket that the OS layer has already declared dead
733
+ // (the source of the "hostname nor servname provided" cascade).
734
+ this.pendingCommand = null;
735
+ this._connected = false;
736
+ reject(err);
737
+ });
692
738
  });
693
739
  }
694
740
  waitForContinuation(tag) {
@@ -718,6 +764,7 @@ export class NativeImapClient {
718
764
  if (this.pendingCommand) {
719
765
  const cmd = this.pendingCommand;
720
766
  this.pendingCommand = null;
767
+ this._connected = false;
721
768
  this.transport.close?.();
722
769
  cmd.reject(new Error(`IMAP inactivity timeout (${this.inactivityTimeout / 1000}s)`));
723
770
  }
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.20",
4
4
  "description": "Direct IMAP client — transport-agnostic, no Node.js dependencies, browser-ready",
5
5
  "main": "index.js",
6
6
  "types": "index.ts",