@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 +13 -0
- package/imap-native.js +42 -8
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
970
|
-
|
|
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
|
-
|
|
973
|
-
|
|
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) {
|