@push.rocks/smartrust 2.5.0 → 2.5.1

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.
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartrust',
6
- version: '2.5.0',
6
+ version: '2.5.1',
7
7
  description: 'A type-safe bridge between JavaScript engines and Rust binaries.'
8
8
  };
9
9
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSx1QkFBdUI7SUFDN0IsT0FBTyxFQUFFLE9BQU87SUFDaEIsV0FBVyxFQUFFLGtFQUFrRTtDQUNoRixDQUFBIn0=
@@ -3,9 +3,16 @@ import type { IRustBridgeLogger } from './interfaces/index.js';
3
3
  * Buffer-based newline scanner for streaming binary data.
4
4
  * Accumulates chunks and emits complete lines via callback.
5
5
  * Used by both StdioTransport and SocketTransport.
6
+ *
7
+ * Each byte is scanned once and copied at most once into its line, so a line that arrives in many
8
+ * chunks costs time linear in its length.
6
9
  */
7
10
  export declare class LineScanner {
8
- private buffer;
11
+ /** Chunks of the line being received; none of them contains a newline. */
12
+ private pending;
13
+ private pendingLength;
14
+ /** Bytes after a line whose callback threw; they may contain newlines and are scanned first. */
15
+ private unscanned;
9
16
  private maxPayloadSize;
10
17
  private logger;
11
18
  constructor(maxPayloadSize: number, logger: IRustBridgeLogger);
@@ -15,4 +22,9 @@ export declare class LineScanner {
15
22
  push(chunk: Buffer, onLine: (line: string) => void): void;
16
23
  /** Reset the internal buffer. */
17
24
  clear(): void;
25
+ /** Joins the pending chunks with the line's last piece; null when the line is too large. */
26
+ private takeLine;
27
+ private appendPartial;
28
+ /** A subarray retains its entire backing allocation; copy a small piece of a large one. */
29
+ private static compact;
18
30
  }
@@ -2,9 +2,16 @@
2
2
  * Buffer-based newline scanner for streaming binary data.
3
3
  * Accumulates chunks and emits complete lines via callback.
4
4
  * Used by both StdioTransport and SocketTransport.
5
+ *
6
+ * Each byte is scanned once and copied at most once into its line, so a line that arrives in many
7
+ * chunks costs time linear in its length.
5
8
  */
6
9
  export class LineScanner {
7
- buffer = Buffer.alloc(0);
10
+ /** Chunks of the line being received; none of them contains a newline. */
11
+ pending = [];
12
+ pendingLength = 0;
13
+ /** Bytes after a line whose callback threw; they may contain newlines and are scanned first. */
14
+ unscanned = Buffer.alloc(0);
8
15
  maxPayloadSize;
9
16
  logger;
10
17
  constructor(maxPayloadSize, logger) {
@@ -15,14 +22,15 @@ export class LineScanner {
15
22
  * Feed a chunk of data. Calls onLine for each complete newline-terminated line found.
16
23
  */
17
24
  push(chunk, onLine) {
18
- this.buffer = Buffer.concat([this.buffer, chunk]);
25
+ const input = this.unscanned.length > 0 ? Buffer.concat([this.unscanned, chunk]) : chunk;
26
+ this.unscanned = Buffer.alloc(0);
27
+ let start = 0;
19
28
  try {
20
29
  let newlineIndex;
21
- while ((newlineIndex = this.buffer.indexOf(0x0A)) !== -1) {
22
- const lineBuffer = this.buffer.subarray(0, newlineIndex);
23
- this.buffer = this.buffer.subarray(newlineIndex + 1);
24
- if (lineBuffer.length > this.maxPayloadSize) {
25
- this.logger.log('error', `Inbound message exceeds maxPayloadSize (${lineBuffer.length} bytes), dropping`);
30
+ while ((newlineIndex = input.indexOf(0x0A, start)) !== -1) {
31
+ const lineBuffer = this.takeLine(input.subarray(start, newlineIndex));
32
+ start = newlineIndex + 1;
33
+ if (!lineBuffer) {
26
34
  continue;
27
35
  }
28
36
  const line = lineBuffer.toString('utf8').trim();
@@ -30,27 +38,52 @@ export class LineScanner {
30
38
  onLine(line);
31
39
  }
32
40
  }
33
- // Prevent OOM if sender never sends newline
34
- if (this.buffer.length > this.maxPayloadSize) {
35
- this.logger.log('error', `Buffer exceeded maxPayloadSize (${this.buffer.length} bytes) without newline, clearing`);
36
- this.buffer = Buffer.alloc(0);
37
- }
41
+ this.appendPartial(input.subarray(start));
38
42
  }
39
- finally {
40
- // A subarray retains its entire backing allocation, even when empty.
41
- // Compact once per chunk, including when a callback throws, without
42
- // repeatedly copying the unread suffix for each parsed line.
43
- if (this.buffer.length === 0) {
44
- this.buffer = Buffer.alloc(0);
45
- }
46
- else if (this.buffer.buffer.byteLength > Math.max(Buffer.poolSize, this.buffer.length * 2)) {
47
- this.buffer = Buffer.from(this.buffer);
48
- }
43
+ catch (error) {
44
+ // Keep what follows the failed line for the next push, without retaining what was consumed.
45
+ this.unscanned = LineScanner.compact(input.subarray(start));
46
+ throw error;
49
47
  }
50
48
  }
51
49
  /** Reset the internal buffer. */
52
50
  clear() {
53
- this.buffer = Buffer.alloc(0);
51
+ this.pending = [];
52
+ this.pendingLength = 0;
53
+ this.unscanned = Buffer.alloc(0);
54
+ }
55
+ /** Joins the pending chunks with the line's last piece; null when the line is too large. */
56
+ takeLine(tail) {
57
+ const length = this.pendingLength + tail.length;
58
+ const pending = this.pending;
59
+ this.pending = [];
60
+ this.pendingLength = 0;
61
+ if (length > this.maxPayloadSize) {
62
+ this.logger.log('error', `Inbound message exceeds maxPayloadSize (${length} bytes), dropping`);
63
+ return null;
64
+ }
65
+ return pending.length === 0 ? tail : Buffer.concat([...pending, tail], length);
66
+ }
67
+ appendPartial(partial) {
68
+ if (partial.length === 0) {
69
+ return;
70
+ }
71
+ // Prevent OOM if sender never sends newline
72
+ if (this.pendingLength + partial.length > this.maxPayloadSize) {
73
+ this.logger.log('error', `Buffer exceeded maxPayloadSize (${this.pendingLength + partial.length} bytes) without newline, clearing`);
74
+ this.pending = [];
75
+ this.pendingLength = 0;
76
+ return;
77
+ }
78
+ this.pending.push(LineScanner.compact(partial));
79
+ this.pendingLength += partial.length;
80
+ }
81
+ /** A subarray retains its entire backing allocation; copy a small piece of a large one. */
82
+ static compact(piece) {
83
+ if (piece.length === 0) {
84
+ return Buffer.alloc(0);
85
+ }
86
+ return piece.buffer.byteLength > Math.max(Buffer.poolSize, piece.length * 2) ? Buffer.from(piece) : piece;
54
87
  }
55
88
  }
56
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5saW5lc2Nhbm5lci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL2NsYXNzZXMubGluZXNjYW5uZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBRUE7Ozs7R0FJRztBQUNILE1BQU0sT0FBTyxXQUFXO0lBQ2QsTUFBTSxHQUFXLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDakMsY0FBYyxDQUFTO0lBQ3ZCLE1BQU0sQ0FBb0I7SUFFbEMsWUFBWSxjQUFzQixFQUFFLE1BQXlCO1FBQzNELElBQUksQ0FBQyxjQUFjLEdBQUcsY0FBYyxDQUFDO1FBQ3JDLElBQUksQ0FBQyxNQUFNLEdBQUcsTUFBTSxDQUFDO0lBQ3ZCLENBQUM7SUFFRDs7T0FFRztJQUNJLElBQUksQ0FBQyxLQUFhLEVBQUUsTUFBOEI7UUFDdkQsSUFBSSxDQUFDLE1BQU0sR0FBRyxNQUFNLENBQUMsTUFBTSxDQUFDLENBQUMsSUFBSSxDQUFDLE1BQU0sRUFBRSxLQUFLLENBQUMsQ0FBQyxDQUFDO1FBRWxELElBQUksQ0FBQztZQUNILElBQUksWUFBb0IsQ0FBQztZQUN6QixPQUFPLENBQUMsWUFBWSxHQUFHLElBQUksQ0FBQyxNQUFNLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLEVBQUUsQ0FBQztnQkFDekQsTUFBTSxVQUFVLEdBQUcsSUFBSSxDQUFDLE1BQU0sQ0FBQyxRQUFRLENBQUMsQ0FBQyxFQUFFLFlBQVksQ0FBQyxDQUFDO2dCQUN6RCxJQUFJLENBQUMsTUFBTSxHQUFHLElBQUksQ0FBQyxNQUFNLENBQUMsUUFBUSxDQUFDLFlBQVksR0FBRyxDQUFDLENBQUMsQ0FBQztnQkFFckQsSUFBSSxVQUFVLENBQUMsTUFBTSxHQUFHLElBQUksQ0FBQyxjQUFjLEVBQUUsQ0FBQztvQkFDNUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsT0FBTyxFQUFFLDJDQUEyQyxVQUFVLENBQUMsTUFBTSxtQkFBbUIsQ0FBQyxDQUFDO29CQUMxRyxTQUFTO2dCQUNYLENBQUM7Z0JBRUQsTUFBTSxJQUFJLEdBQUcsVUFBVSxDQUFDLFFBQVEsQ0FBQyxNQUFNLENBQUMsQ0FBQyxJQUFJLEVBQUUsQ0FBQztnQkFDaEQsSUFBSSxJQUFJLEVBQUUsQ0FBQztvQkFDVCxNQUFNLENBQUMsSUFBSSxDQUFDLENBQUM7Z0JBQ2YsQ0FBQztZQUNILENBQUM7WUFFRCw0Q0FBNEM7WUFDNUMsSUFBSSxJQUFJLENBQUMsTUFBTSxDQUFDLE1BQU0sR0FBRyxJQUFJLENBQUMsY0FBYyxFQUFFLENBQUM7Z0JBQzdDLElBQUksQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLE9BQU8sRUFBRSxtQ0FBbUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxNQUFNLG1DQUFtQyxDQUFDLENBQUM7Z0JBQ25ILElBQUksQ0FBQyxNQUFNLEdBQUcsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQztZQUNoQyxDQUFDO1FBQ0gsQ0FBQztnQkFBUyxDQUFDO1lBQ1QscUVBQXFFO1lBQ3JFLG9FQUFvRTtZQUNwRSw2REFBNkQ7WUFDN0QsSUFBSSxJQUFJLENBQUMsTUFBTSxDQUFDLE1BQU0sS0FBSyxDQUFDLEVBQUUsQ0FBQztnQkFDN0IsSUFBSSxDQUFDLE1BQU0sR0FBRyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDO1lBQ2hDLENBQUM7aUJBQU0sSUFBSSxJQUFJLENBQUMsTUFBTSxDQUFDLE1BQU0sQ0FBQyxVQUFVLEdBQUcsSUFBSSxDQUFDLEdBQUcsQ0FBQyxNQUFNLENBQUMsUUFBUSxFQUFFLElBQUksQ0FBQyxNQUFNLENBQUMsTUFBTSxHQUFHLENBQUMsQ0FBQyxFQUFFLENBQUM7Z0JBQzdGLElBQUksQ0FBQyxNQUFNLEdBQUcsTUFBTSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsTUFBTSxDQUFDLENBQUM7WUFDekMsQ0FBQztRQUNILENBQUM7SUFDSCxDQUFDO0lBRUQsaUNBQWlDO0lBQzFCLEtBQUs7UUFDVixJQUFJLENBQUMsTUFBTSxHQUFHLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDaEMsQ0FBQztDQUNGIn0=
89
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5saW5lc2Nhbm5lci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL2NsYXNzZXMubGluZXNjYW5uZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBRUE7Ozs7Ozs7R0FPRztBQUNILE1BQU0sT0FBTyxXQUFXO0lBQ3RCLDBFQUEwRTtJQUNsRSxPQUFPLEdBQWEsRUFBRSxDQUFDO0lBQ3ZCLGFBQWEsR0FBRyxDQUFDLENBQUM7SUFDMUIsZ0dBQWdHO0lBQ3hGLFNBQVMsR0FBVyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDO0lBQ3BDLGNBQWMsQ0FBUztJQUN2QixNQUFNLENBQW9CO0lBRWxDLFlBQVksY0FBc0IsRUFBRSxNQUF5QjtRQUMzRCxJQUFJLENBQUMsY0FBYyxHQUFHLGNBQWMsQ0FBQztRQUNyQyxJQUFJLENBQUMsTUFBTSxHQUFHLE1BQU0sQ0FBQztJQUN2QixDQUFDO0lBRUQ7O09BRUc7SUFDSSxJQUFJLENBQUMsS0FBYSxFQUFFLE1BQThCO1FBQ3ZELE1BQU0sS0FBSyxHQUFHLElBQUksQ0FBQyxTQUFTLENBQUMsTUFBTSxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLE1BQU0sQ0FBQyxDQUFDLElBQUksQ0FBQyxTQUFTLEVBQUUsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDO1FBQ3pGLElBQUksQ0FBQyxTQUFTLEdBQUcsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQztRQUNqQyxJQUFJLEtBQUssR0FBRyxDQUFDLENBQUM7UUFFZCxJQUFJLENBQUM7WUFDSCxJQUFJLFlBQW9CLENBQUM7WUFDekIsT0FBTyxDQUFDLFlBQVksR0FBRyxLQUFLLENBQUMsT0FBTyxDQUFDLElBQUksRUFBRSxLQUFLLENBQUMsQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUFFLENBQUM7Z0JBQzFELE1BQU0sVUFBVSxHQUFHLElBQUksQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFDLFFBQVEsQ0FBQyxLQUFLLEVBQUUsWUFBWSxDQUFDLENBQUMsQ0FBQztnQkFDdEUsS0FBSyxHQUFHLFlBQVksR0FBRyxDQUFDLENBQUM7Z0JBQ3pCLElBQUksQ0FBQyxVQUFVLEVBQUUsQ0FBQztvQkFDaEIsU0FBUztnQkFDWCxDQUFDO2dCQUNELE1BQU0sSUFBSSxHQUFHLFVBQVUsQ0FBQyxRQUFRLENBQUMsTUFBTSxDQUFDLENBQUMsSUFBSSxFQUFFLENBQUM7Z0JBQ2hELElBQUksSUFBSSxFQUFFLENBQUM7b0JBQ1QsTUFBTSxDQUFDLElBQUksQ0FBQyxDQUFDO2dCQUNmLENBQUM7WUFDSCxDQUFDO1lBQ0QsSUFBSSxDQUFDLGFBQWEsQ0FBQyxLQUFLLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUM7UUFDNUMsQ0FBQztRQUFDLE9BQU8sS0FBSyxFQUFFLENBQUM7WUFDZiw0RkFBNEY7WUFDNUYsSUFBSSxDQUFDLFNBQVMsR0FBRyxXQUFXLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQztZQUM1RCxNQUFNLEtBQUssQ0FBQztRQUNkLENBQUM7SUFDSCxDQUFDO0lBRUQsaUNBQWlDO0lBQzFCLEtBQUs7UUFDVixJQUFJLENBQUMsT0FBTyxHQUFHLEVBQUUsQ0FBQztRQUNsQixJQUFJLENBQUMsYUFBYSxHQUFHLENBQUMsQ0FBQztRQUN2QixJQUFJLENBQUMsU0FBUyxHQUFHLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDbkMsQ0FBQztJQUVELDRGQUE0RjtJQUNwRixRQUFRLENBQUMsSUFBWTtRQUMzQixNQUFNLE1BQU0sR0FBRyxJQUFJLENBQUMsYUFBYSxHQUFHLElBQUksQ0FBQyxNQUFNLENBQUM7UUFDaEQsTUFBTSxPQUFPLEdBQUcsSUFBSSxDQUFDLE9BQU8sQ0FBQztRQUM3QixJQUFJLENBQUMsT0FBTyxHQUFHLEVBQUUsQ0FBQztRQUNsQixJQUFJLENBQUMsYUFBYSxHQUFHLENBQUMsQ0FBQztRQUN2QixJQUFJLE1BQU0sR0FBRyxJQUFJLENBQUMsY0FBYyxFQUFFLENBQUM7WUFDakMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsT0FBTyxFQUFFLDJDQUEyQyxNQUFNLG1CQUFtQixDQUFDLENBQUM7WUFDL0YsT0FBTyxJQUFJLENBQUM7UUFDZCxDQUFDO1FBQ0QsT0FBTyxPQUFPLENBQUMsTUFBTSxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDLENBQUMsR0FBRyxPQUFPLEVBQUUsSUFBSSxDQUFDLEVBQUUsTUFBTSxDQUFDLENBQUM7SUFDakYsQ0FBQztJQUVPLGFBQWEsQ0FBQyxPQUFlO1FBQ25DLElBQUksT0FBTyxDQUFDLE1BQU0sS0FBSyxDQUFDLEVBQUUsQ0FBQztZQUN6QixPQUFPO1FBQ1QsQ0FBQztRQUNELDRDQUE0QztRQUM1QyxJQUFJLElBQUksQ0FBQyxhQUFhLEdBQUcsT0FBTyxDQUFDLE1BQU0sR0FBRyxJQUFJLENBQUMsY0FBYyxFQUFFLENBQUM7WUFDOUQsSUFBSSxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQ2IsT0FBTyxFQUNQLG1DQUFtQyxJQUFJLENBQUMsYUFBYSxHQUFHLE9BQU8sQ0FBQyxNQUFNLG1DQUFtQyxDQUMxRyxDQUFDO1lBQ0YsSUFBSSxDQUFDLE9BQU8sR0FBRyxFQUFFLENBQUM7WUFDbEIsSUFBSSxDQUFDLGFBQWEsR0FBRyxDQUFDLENBQUM7WUFDdkIsT0FBTztRQUNULENBQUM7UUFDRCxJQUFJLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxXQUFXLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUM7UUFDaEQsSUFBSSxDQUFDLGFBQWEsSUFBSSxPQUFPLENBQUMsTUFBTSxDQUFDO0lBQ3ZDLENBQUM7SUFFRCwyRkFBMkY7SUFDbkYsTUFBTSxDQUFDLE9BQU8sQ0FBQyxLQUFhO1FBQ2xDLElBQUksS0FBSyxDQUFDLE1BQU0sS0FBSyxDQUFDLEVBQUUsQ0FBQztZQUN2QixPQUFPLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUM7UUFDekIsQ0FBQztRQUNELE9BQU8sS0FBSyxDQUFDLE1BQU0sQ0FBQyxVQUFVLEdBQUcsSUFBSSxDQUFDLEdBQUcsQ0FBQyxNQUFNLENBQUMsUUFBUSxFQUFFLEtBQUssQ0FBQyxNQUFNLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQztJQUM1RyxDQUFDO0NBQ0YifQ==
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@push.rocks/smartrust",
3
- "version": "2.5.0",
3
+ "version": "2.5.1",
4
4
  "private": false,
5
5
  "description": "A type-safe bridge between JavaScript engines and Rust binaries.",
6
6
  "main": "dist_ts/index.js",
@@ -9,10 +9,10 @@
9
9
  "author": "Task Venture Capital GmbH",
10
10
  "license": "MIT",
11
11
  "devDependencies": {
12
- "@git.zone/tsbuild": "^5.0.0",
13
- "@git.zone/tsrun": "^3.0.0",
14
- "@git.zone/tstest": "^6.2.0",
15
- "@types/node": "^26.6.1"
12
+ "@git.zone/tsbuild": "^5.1.2",
13
+ "@git.zone/tsrun": "^3.0.2",
14
+ "@git.zone/tstest": "^7.1.0",
15
+ "@types/node": "^26.6.3"
16
16
  },
17
17
  "dependencies": {
18
18
  "@push.rocks/smartpath": "^6.0.0"
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartrust',
6
- version: '2.5.0',
6
+ version: '2.5.1',
7
7
  description: 'A type-safe bridge between JavaScript engines and Rust binaries.'
8
8
  }
@@ -4,9 +4,16 @@ import type { IRustBridgeLogger } from './interfaces/index.js';
4
4
  * Buffer-based newline scanner for streaming binary data.
5
5
  * Accumulates chunks and emits complete lines via callback.
6
6
  * Used by both StdioTransport and SocketTransport.
7
+ *
8
+ * Each byte is scanned once and copied at most once into its line, so a line that arrives in many
9
+ * chunks costs time linear in its length.
7
10
  */
8
11
  export class LineScanner {
9
- private buffer: Buffer = Buffer.alloc(0);
12
+ /** Chunks of the line being received; none of them contains a newline. */
13
+ private pending: Buffer[] = [];
14
+ private pendingLength = 0;
15
+ /** Bytes after a line whose callback threw; they may contain newlines and are scanned first. */
16
+ private unscanned: Buffer = Buffer.alloc(0);
10
17
  private maxPayloadSize: number;
11
18
  private logger: IRustBridgeLogger;
12
19
 
@@ -19,44 +26,74 @@ export class LineScanner {
19
26
  * Feed a chunk of data. Calls onLine for each complete newline-terminated line found.
20
27
  */
21
28
  public push(chunk: Buffer, onLine: (line: string) => void): void {
22
- this.buffer = Buffer.concat([this.buffer, chunk]);
29
+ const input = this.unscanned.length > 0 ? Buffer.concat([this.unscanned, chunk]) : chunk;
30
+ this.unscanned = Buffer.alloc(0);
31
+ let start = 0;
23
32
 
24
33
  try {
25
34
  let newlineIndex: number;
26
- while ((newlineIndex = this.buffer.indexOf(0x0A)) !== -1) {
27
- const lineBuffer = this.buffer.subarray(0, newlineIndex);
28
- this.buffer = this.buffer.subarray(newlineIndex + 1);
29
-
30
- if (lineBuffer.length > this.maxPayloadSize) {
31
- this.logger.log('error', `Inbound message exceeds maxPayloadSize (${lineBuffer.length} bytes), dropping`);
35
+ while ((newlineIndex = input.indexOf(0x0A, start)) !== -1) {
36
+ const lineBuffer = this.takeLine(input.subarray(start, newlineIndex));
37
+ start = newlineIndex + 1;
38
+ if (!lineBuffer) {
32
39
  continue;
33
40
  }
34
-
35
41
  const line = lineBuffer.toString('utf8').trim();
36
42
  if (line) {
37
43
  onLine(line);
38
44
  }
39
45
  }
40
-
41
- // Prevent OOM if sender never sends newline
42
- if (this.buffer.length > this.maxPayloadSize) {
43
- this.logger.log('error', `Buffer exceeded maxPayloadSize (${this.buffer.length} bytes) without newline, clearing`);
44
- this.buffer = Buffer.alloc(0);
45
- }
46
- } finally {
47
- // A subarray retains its entire backing allocation, even when empty.
48
- // Compact once per chunk, including when a callback throws, without
49
- // repeatedly copying the unread suffix for each parsed line.
50
- if (this.buffer.length === 0) {
51
- this.buffer = Buffer.alloc(0);
52
- } else if (this.buffer.buffer.byteLength > Math.max(Buffer.poolSize, this.buffer.length * 2)) {
53
- this.buffer = Buffer.from(this.buffer);
54
- }
46
+ this.appendPartial(input.subarray(start));
47
+ } catch (error) {
48
+ // Keep what follows the failed line for the next push, without retaining what was consumed.
49
+ this.unscanned = LineScanner.compact(input.subarray(start));
50
+ throw error;
55
51
  }
56
52
  }
57
53
 
58
54
  /** Reset the internal buffer. */
59
55
  public clear(): void {
60
- this.buffer = Buffer.alloc(0);
56
+ this.pending = [];
57
+ this.pendingLength = 0;
58
+ this.unscanned = Buffer.alloc(0);
59
+ }
60
+
61
+ /** Joins the pending chunks with the line's last piece; null when the line is too large. */
62
+ private takeLine(tail: Buffer): Buffer | null {
63
+ const length = this.pendingLength + tail.length;
64
+ const pending = this.pending;
65
+ this.pending = [];
66
+ this.pendingLength = 0;
67
+ if (length > this.maxPayloadSize) {
68
+ this.logger.log('error', `Inbound message exceeds maxPayloadSize (${length} bytes), dropping`);
69
+ return null;
70
+ }
71
+ return pending.length === 0 ? tail : Buffer.concat([...pending, tail], length);
72
+ }
73
+
74
+ private appendPartial(partial: Buffer): void {
75
+ if (partial.length === 0) {
76
+ return;
77
+ }
78
+ // Prevent OOM if sender never sends newline
79
+ if (this.pendingLength + partial.length > this.maxPayloadSize) {
80
+ this.logger.log(
81
+ 'error',
82
+ `Buffer exceeded maxPayloadSize (${this.pendingLength + partial.length} bytes) without newline, clearing`,
83
+ );
84
+ this.pending = [];
85
+ this.pendingLength = 0;
86
+ return;
87
+ }
88
+ this.pending.push(LineScanner.compact(partial));
89
+ this.pendingLength += partial.length;
90
+ }
91
+
92
+ /** A subarray retains its entire backing allocation; copy a small piece of a large one. */
93
+ private static compact(piece: Buffer): Buffer {
94
+ if (piece.length === 0) {
95
+ return Buffer.alloc(0);
96
+ }
97
+ return piece.buffer.byteLength > Math.max(Buffer.poolSize, piece.length * 2) ? Buffer.from(piece) : piece;
61
98
  }
62
99
  }