@bobfrankston/iflow-direct 0.1.33 → 0.1.34

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.d.ts CHANGED
@@ -48,7 +48,15 @@ export declare class NativeImapClient {
48
48
  private transport;
49
49
  private transportFactory;
50
50
  private config;
51
+ /** Receive buffer + read offset. CRITICAL: never reassign with
52
+ * `this.buffer = this.buffer.substring(N)` — that copies the whole
53
+ * remainder per line and is O(n²) over a multi-line response (a 457KB
54
+ * LIST response actually blocked the daemon's event loop for 10 minutes
55
+ * in production). Use bufferOffset to advance the read cursor; only
56
+ * compact the buffer string when the consumed portion is >= half its
57
+ * length, keeping per-line work O(1). */
51
58
  private buffer;
59
+ private bufferOffset;
52
60
  private pendingCommand;
53
61
  private capabilities;
54
62
  private _connected;
@@ -203,6 +211,11 @@ export declare class NativeImapClient {
203
211
  private waitForContinuation;
204
212
  private waitForTagged;
205
213
  private handleData;
214
+ /** Compact the buffer when bufferOffset has advanced past half. Cheap
215
+ * amortized cost; eliminates the O(n²) substring-each-line antipattern
216
+ * that synchronously blocked the daemon for 10 minutes on a large LIST
217
+ * response. */
218
+ private compactBufferIfNeeded;
206
219
  private processBuffer;
207
220
  private handleUntaggedResponse;
208
221
  private parseFetchResponses;
package/imap-native.js CHANGED
@@ -11,7 +11,15 @@ export class NativeImapClient {
11
11
  transport;
12
12
  transportFactory;
13
13
  config;
14
+ /** Receive buffer + read offset. CRITICAL: never reassign with
15
+ * `this.buffer = this.buffer.substring(N)` — that copies the whole
16
+ * remainder per line and is O(n²) over a multi-line response (a 457KB
17
+ * LIST response actually blocked the daemon's event loop for 10 minutes
18
+ * in production). Use bufferOffset to advance the read cursor; only
19
+ * compact the buffer string when the consumed portion is >= half its
20
+ * length, keeping per-line work O(1). */
14
21
  buffer = "";
22
+ bufferOffset = 0;
15
23
  pendingCommand = null;
16
24
  capabilities = new Set();
17
25
  _connected = false;
@@ -921,6 +929,16 @@ export class NativeImapClient {
921
929
  this.buffer += data;
922
930
  this.processBuffer();
923
931
  }
932
+ /** Compact the buffer when bufferOffset has advanced past half. Cheap
933
+ * amortized cost; eliminates the O(n²) substring-each-line antipattern
934
+ * that synchronously blocked the daemon for 10 minutes on a large LIST
935
+ * response. */
936
+ compactBufferIfNeeded() {
937
+ if (this.bufferOffset > 0 && this.bufferOffset >= this.buffer.length / 2) {
938
+ this.buffer = this.buffer.substring(this.bufferOffset);
939
+ this.bufferOffset = 0;
940
+ }
941
+ }
924
942
  processBuffer() {
925
943
  while (true) {
926
944
  // Check for literal {size}\r\n — reading exact BYTE count of literal data
@@ -929,16 +947,20 @@ export class NativeImapClient {
929
947
  // We must use byte-accurate extraction (TextEncoder for browser, Buffer for Node).
930
948
  if (this.pendingCommand?.literalBytes != null) {
931
949
  const neededBytes = this.pendingCommand.literalBytes;
932
- const encoded = new TextEncoder().encode(this.buffer);
950
+ // Encode only the unread portion — was encoding the whole
951
+ // buffer (including already-consumed bytes) on every literal,
952
+ // which on a multi-MB buffer is a non-trivial chunk of work.
953
+ const unread = this.bufferOffset === 0 ? this.buffer : this.buffer.substring(this.bufferOffset);
954
+ const encoded = new TextEncoder().encode(unread);
933
955
  const bufferBytes = encoded.byteLength;
934
956
  if (bufferBytes >= neededBytes) {
935
957
  const literal_bytes = encoded.subarray(0, neededBytes);
936
958
  const rest_bytes = encoded.subarray(neededBytes);
937
959
  let literal = new TextDecoder().decode(literal_bytes);
960
+ // Reset buffer to just the rest — bufferOffset goes back
961
+ // to 0 since we replaced the buffer content.
938
962
  this.buffer = new TextDecoder().decode(rest_bytes);
939
- // if (neededBytes !== literal.length) {
940
- // console.log(`iflow-direct: [imap] literal: ${neededBytes} bytes → ${literal.length} chars (multi-byte corrected)`);
941
- // }
963
+ this.bufferOffset = 0;
942
964
  // For non-BODY literals (e.g. display names in ENVELOPE), wrap in quotes
943
965
  // so tokenizeParenList treats them as a single token
944
966
  if (!this.pendingCommand.currentLiteralKey) {
@@ -966,11 +988,23 @@ export class NativeImapClient {
966
988
  }
967
989
  break; // Wait for more data
968
990
  }
969
- const lineEnd = this.buffer.indexOf("\r\n");
970
- if (lineEnd < 0)
991
+ // Find next CRLF starting from the read offset (NOT from 0 —
992
+ // the bytes before bufferOffset are already consumed and
993
+ // searching them is dead work that scales with response size).
994
+ const lineEnd = this.buffer.indexOf("\r\n", this.bufferOffset);
995
+ if (lineEnd < 0) {
996
+ // No complete line yet. Compact the buffer if we've advanced
997
+ // far enough that the unconsumed-prefix is dragging.
998
+ this.compactBufferIfNeeded();
971
999
  break;
972
- const line = this.buffer.substring(0, lineEnd + 2);
973
- this.buffer = this.buffer.substring(lineEnd + 2);
1000
+ }
1001
+ // Extract the line and advance the read cursor — no substring
1002
+ // copy of the remainder, no buffer reallocation.
1003
+ const line = this.buffer.substring(this.bufferOffset, lineEnd + 2);
1004
+ this.bufferOffset = lineEnd + 2;
1005
+ // Periodic compaction so the consumed prefix doesn't keep
1006
+ // growing forever in long-running connections (IDLE, big sync).
1007
+ this.compactBufferIfNeeded();
974
1008
  // Check for literal announcement {size}\r\n at end of line
975
1009
  const literalMatch = line.match(/\{(\d+)\}\r\n$/);
976
1010
  if (literalMatch && this.pendingCommand) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/iflow-direct",
3
- "version": "0.1.33",
3
+ "version": "0.1.34",
4
4
  "description": "Direct IMAP client — transport-agnostic, no Node.js dependencies, browser-ready",
5
5
  "main": "index.js",
6
6
  "types": "index.ts",