@naturalcycles/nodejs-lib 15.124.0 → 15.125.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.
@@ -19,17 +19,17 @@ export function createReadStreamAsNDJson(inputPath) {
19
19
  fs2.requireFileToExist(inputPath);
20
20
  let stream = fs2
21
21
  .createReadStream(inputPath, {
22
- highWaterMark: 64 * 1024, // no observed speedup
22
+ highWaterMark: 256 * 1024, // 64KB had no observed speedup over the default; 256KB is +6-12% end-to-end on ~9KB rows, neutral on ~180B rows, 1MB brings nothing more
23
23
  })
24
24
  .on('error', err => stream.destroy(err));
25
25
  if (inputPath.endsWith('.gz')) {
26
26
  stream = stream.pipe(createUnzip({
27
- chunkSize: 64 * 1024, // speedup from ~3200 to 3800 rps!
27
+ chunkSize: 256 * 1024, // 64KB: speedup from ~3200 to 3800 rps! 256KB: +6-8% more on ~9KB rows, neutral on ~180B rows
28
28
  }));
29
29
  }
30
30
  else if (inputPath.endsWith('.zst')) {
31
31
  stream = stream.pipe(createZstdDecompress({
32
- chunkSize: 64 * 1024, // todo: test it
32
+ chunkSize: 256 * 1024, // tested: 256KB is +10-12% over 64KB on ~9KB rows, neutral on ~180B rows
33
33
  }));
34
34
  }
35
35
  return stream.pipe(transformSplitOnNewline()).map(line => JSON.parse(line));
@@ -73,7 +73,7 @@ export class Pipeline {
73
73
  }
74
74
  static fromFile(sourceFilePath) {
75
75
  return new Pipeline(fs2.createReadStream(sourceFilePath, {
76
- highWaterMark: 64 * 1024, // no observed speedup
76
+ highWaterMark: 256 * 1024, // 64KB had no observed speedup over the default; 256KB is +6-12% end-to-end on ~9KB rows, neutral on ~180B rows, 1MB brings nothing more
77
77
  }), false);
78
78
  }
79
79
  /**
@@ -254,7 +254,7 @@ export class Pipeline {
254
254
  }
255
255
  gunzip(opt) {
256
256
  this.transforms.push(createUnzip({
257
- chunkSize: 64 * 1024, // speedup from ~3200 to 3800 rps!
257
+ chunkSize: 256 * 1024, // 64KB: speedup from ~3200 to 3800 rps! 256KB: +6-8% more on ~9KB rows, neutral on ~180B rows
258
258
  ...opt,
259
259
  }));
260
260
  this.objectMode = false;
@@ -268,7 +268,7 @@ export class Pipeline {
268
268
  }
269
269
  zstdDecompress(opt) {
270
270
  this.transforms.push(createZstdDecompress({
271
- chunkSize: 64 * 1024, // todo: test it
271
+ chunkSize: 256 * 1024, // tested: 256KB is +10-12% over 64KB on ~9KB rows, neutral on ~180B rows
272
272
  ...opt,
273
273
  }));
274
274
  this.objectMode = false;
@@ -4,15 +4,16 @@ import type { TransformTyped } from '../stream.model.js';
4
4
  *
5
5
  * Useful for reading NDJSON files from fs.
6
6
  *
7
- * Same as binarySplit, but optimized (hard-coded) to split on NEWLINE (aka `\n`).
8
- * (+5-10% _pipeline speedup measured, compared to generic `binarySplit` on variable length delimiter)
7
+ * Same as `transformSplit('\n')`.
8
+ * (It used to be a separate hard-coded implementation, +5-10% faster than the generic one,
9
+ * but since the generic one searches 1-byte separators by byte value there's no difference anymore)
9
10
  */
10
11
  export declare function transformSplitOnNewline(): TransformTyped<Buffer, Buffer>;
11
12
  /**
12
13
  * Input: stream (objectMode=false) of arbitrary string|Buffer chunks, like when read from fs
13
14
  * Output: stream (objectMode=true) or string|Buffer chunks split by `separator` (@default to `\n`)
14
15
  *
15
- * Please use slightly more optimized `transformSplitOnNewline` for NDJSON file parsing.
16
- * (+5-10% _pipeline speedup measured!)
16
+ * Empty lines (consecutive separators) are skipped.
17
+ * Trailing data without a separator is emitted on flush.
17
18
  */
18
19
  export declare function transformSplit(separator?: string): TransformTyped<Buffer, Buffer>;
@@ -1,133 +1,102 @@
1
1
  import { Transform } from 'node:stream';
2
2
  // The code below is carefully adopted from: https://github.com/max-mapper/binary-split
3
+ // (including the v2.0.0 performance improvements: native Buffer#indexOf and linear-time long lines)
3
4
  /**
4
5
  * Transforms input Buffer/string stream into Buffer chunks (objectMode: true) split by newLine.
5
6
  *
6
7
  * Useful for reading NDJSON files from fs.
7
8
  *
8
- * Same as binarySplit, but optimized (hard-coded) to split on NEWLINE (aka `\n`).
9
- * (+5-10% _pipeline speedup measured, compared to generic `binarySplit` on variable length delimiter)
9
+ * Same as `transformSplit('\n')`.
10
+ * (It used to be a separate hard-coded implementation, +5-10% faster than the generic one,
11
+ * but since the generic one searches 1-byte separators by byte value there's no difference anymore)
10
12
  */
11
13
  export function transformSplitOnNewline() {
12
- let buffered;
13
- return new Transform({
14
- writableObjectMode: false,
15
- writableHighWaterMark: 64 * 1024,
16
- readableObjectMode: true,
17
- transform(buf, _enc, cb) {
18
- let offset = 0;
19
- let lastMatch = 0;
20
- if (buffered) {
21
- buf = Buffer.concat([buffered, buf]);
22
- offset = buffered.length;
23
- buffered = undefined;
24
- }
25
- while (true) {
26
- const idx = firstNewlineMatch(buf, offset);
27
- if (idx !== -1 && idx < buf.length) {
28
- if (lastMatch !== idx) {
29
- this.push(buf.slice(lastMatch, idx));
30
- }
31
- offset = idx + 1;
32
- lastMatch = offset;
33
- }
34
- else {
35
- buffered = buf.slice(lastMatch);
36
- break;
37
- }
38
- }
39
- cb();
40
- },
41
- flush(done) {
42
- if (buffered && buffered.length > 0)
43
- this.push(buffered);
44
- done();
45
- },
46
- });
14
+ return transformSplit('\n');
47
15
  }
48
16
  /**
49
17
  * Input: stream (objectMode=false) of arbitrary string|Buffer chunks, like when read from fs
50
18
  * Output: stream (objectMode=true) or string|Buffer chunks split by `separator` (@default to `\n`)
51
19
  *
52
- * Please use slightly more optimized `transformSplitOnNewline` for NDJSON file parsing.
53
- * (+5-10% _pipeline speedup measured!)
20
+ * Empty lines (consecutive separators) are skipped.
21
+ * Trailing data without a separator is emitted on flush.
54
22
  */
55
23
  export function transformSplit(separator = '\n') {
56
24
  const matcher = Buffer.from(separator);
57
- let buffered;
25
+ if (matcher.length === 0) {
26
+ throw new Error('transformSplit: separator must not be empty');
27
+ }
28
+ const matcherLength = matcher.length;
29
+ // indexOf with a byte value is much faster than with a 1-byte Buffer, which matters for short lines
30
+ const needle = matcherLength === 1 ? matcher[0] : matcher;
31
+ // a multi-byte separator may straddle a chunk boundary, so we keep that many trailing bytes around
32
+ const overlap = matcherLength - 1;
33
+ // Unterminated data is collected as a list of chunks and concatenated only once its separator arrives,
34
+ // so a line spanning many chunks is copied once rather than on every chunk (linear, not quadratic).
35
+ // Only that one line is copied: the rest of the chunk is scanned in place and emitted as views into it.
36
+ let pending = [];
37
+ let pendingLength = 0;
38
+ let edge; // last `overlap` bytes of pending data, only tracked when overlap > 0
58
39
  return new Transform({
59
- readableObjectMode: true,
40
+ writableObjectMode: false,
60
41
  writableHighWaterMark: 64 * 1024,
61
- transform(buf, _enc, done) {
62
- let offset = 0;
63
- let lastMatch = 0;
64
- if (buffered) {
65
- buf = Buffer.concat([buffered, buf]);
66
- offset = buffered.length;
67
- buffered = undefined;
68
- }
69
- while (true) {
70
- const idx = firstMatch(buf, offset - matcher.length + 1, matcher);
71
- if (idx !== -1 && idx < buf.length) {
72
- if (lastMatch !== idx) {
73
- this.push(buf.slice(lastMatch, idx));
42
+ readableObjectMode: true,
43
+ transform(chunk, _enc, cb) {
44
+ let start = 0;
45
+ if (pendingLength > 0) {
46
+ // A multi-byte separator may straddle the chunk boundary: look for it in the last `overlap` bytes
47
+ // of pending data followed by the first `overlap` bytes of this chunk
48
+ const straddleIdx = overlap > 0 ? Buffer.concat([edge, chunk.subarray(0, overlap)]).indexOf(matcher) : -1;
49
+ // Position of the first separator in chunk coordinates, negative when it started inside the pending data
50
+ let idx;
51
+ if (straddleIdx === -1) {
52
+ idx = chunk.indexOf(needle);
53
+ if (idx === -1) {
54
+ // No separator yet: keep accumulating without copying
55
+ pending.push(chunk);
56
+ pendingLength += chunk.length;
57
+ if (overlap > 0) {
58
+ edge = Buffer.concat([edge, chunk.subarray(-overlap)]).subarray(-overlap);
59
+ }
60
+ cb();
61
+ return;
74
62
  }
75
- offset = idx + matcher.length;
76
- lastMatch = offset;
77
63
  }
78
64
  else {
79
- buffered = buf.slice(lastMatch);
80
- break;
65
+ idx = straddleIdx - edge.length;
66
+ }
67
+ // Complete the pending line with the head of this chunk
68
+ if (idx > 0)
69
+ pending.push(chunk.subarray(0, idx));
70
+ const lineLength = pendingLength + idx;
71
+ if (lineLength > 0) {
72
+ this.push(Buffer.concat(pending, lineLength));
81
73
  }
74
+ pending = [];
75
+ pendingLength = 0;
76
+ start = idx + matcherLength;
82
77
  }
83
- done();
84
- },
85
- flush(done) {
86
- if (buffered && buffered.length > 0)
87
- this.push(buffered);
88
- done();
89
- },
90
- });
91
- }
92
- // const NEWLINE = Buffer.from('\n')
93
- // const NEWLINE_CODE = NEWLINE[0]! // it is `10`
94
- const NEWLINE_CODE = 10;
95
- /**
96
- * Same as firstMatch, but optimized (hard-coded) to find NEWLINE (aka `\n`).
97
- */
98
- function firstNewlineMatch(buf, offset) {
99
- const bufLength = buf.length;
100
- if (offset >= bufLength)
101
- return -1;
102
- for (let i = offset; i < bufLength; i++) {
103
- if (buf[i] === NEWLINE_CODE) {
104
- return i;
105
- }
106
- }
107
- return -1; // this code is unreachable, because i is guaranteed to be found in the loop above
108
- }
109
- function firstMatch(buf, offset, matcher) {
110
- if (offset >= buf.length)
111
- return -1;
112
- let i;
113
- for (i = offset; i < buf.length; i++) {
114
- if (buf[i] === matcher[0]) {
115
- if (matcher.length > 1) {
116
- let fullMatch = true;
117
- let j = i;
118
- for (let k = 0; j < i + matcher.length; j++, k++) {
119
- if (buf[j] !== matcher[k]) {
120
- fullMatch = false;
121
- break;
122
- }
78
+ let idx = chunk.indexOf(needle, start);
79
+ while (idx !== -1) {
80
+ if (idx > start) {
81
+ this.push(chunk.subarray(start, idx));
123
82
  }
124
- if (fullMatch)
125
- return j - matcher.length;
83
+ start = idx + matcherLength;
84
+ idx = chunk.indexOf(needle, start);
126
85
  }
127
- else {
128
- break;
86
+ if (start < chunk.length) {
87
+ const rest = chunk.subarray(start);
88
+ pending.push(rest);
89
+ pendingLength = rest.length;
90
+ if (overlap > 0)
91
+ edge = rest.subarray(-overlap);
129
92
  }
130
- }
131
- }
132
- return i + matcher.length - 1;
93
+ cb();
94
+ },
95
+ flush(cb) {
96
+ if (pendingLength > 0) {
97
+ this.push(Buffer.concat(pending, pendingLength));
98
+ }
99
+ cb();
100
+ },
101
+ });
133
102
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/nodejs-lib",
3
3
  "type": "module",
4
- "version": "15.124.0",
4
+ "version": "15.125.1",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@standard-schema/spec": "^1",
@@ -14,7 +14,7 @@
14
14
  "yaml": "^2"
15
15
  },
16
16
  "devDependencies": {
17
- "@naturalcycles/dev-lib": "20.64.0",
17
+ "@naturalcycles/dev-lib": "0.0.0",
18
18
  "typescript": "^7"
19
19
  },
20
20
  "exports": {
@@ -23,20 +23,20 @@ export function createReadStreamAsNDJson<ROW = any>(inputPath: string): Readable
23
23
 
24
24
  let stream: ReadableTyped<ROW> = fs2
25
25
  .createReadStream(inputPath, {
26
- highWaterMark: 64 * 1024, // no observed speedup
26
+ highWaterMark: 256 * 1024, // 64KB had no observed speedup over the default; 256KB is +6-12% end-to-end on ~9KB rows, neutral on ~180B rows, 1MB brings nothing more
27
27
  })
28
28
  .on('error', err => stream.destroy(err))
29
29
 
30
30
  if (inputPath.endsWith('.gz')) {
31
31
  stream = stream.pipe(
32
32
  createUnzip({
33
- chunkSize: 64 * 1024, // speedup from ~3200 to 3800 rps!
33
+ chunkSize: 256 * 1024, // 64KB: speedup from ~3200 to 3800 rps! 256KB: +6-8% more on ~9KB rows, neutral on ~180B rows
34
34
  }),
35
35
  )
36
36
  } else if (inputPath.endsWith('.zst')) {
37
37
  stream = stream.pipe(
38
38
  createZstdDecompress({
39
- chunkSize: 64 * 1024, // todo: test it
39
+ chunkSize: 256 * 1024, // tested: 256KB is +10-12% over 64KB on ~9KB rows, neutral on ~180B rows
40
40
  }),
41
41
  )
42
42
  }
@@ -114,7 +114,7 @@ export class Pipeline<T = unknown> {
114
114
  static fromFile(sourceFilePath: string): Pipeline<Uint8Array> {
115
115
  return new Pipeline(
116
116
  fs2.createReadStream(sourceFilePath, {
117
- highWaterMark: 64 * 1024, // no observed speedup
117
+ highWaterMark: 256 * 1024, // 64KB had no observed speedup over the default; 256KB is +6-12% end-to-end on ~9KB rows, neutral on ~180B rows, 1MB brings nothing more
118
118
  }),
119
119
  false,
120
120
  )
@@ -347,7 +347,7 @@ export class Pipeline<T = unknown> {
347
347
  gunzip(this: Pipeline<Uint8Array>, opt?: ZlibOptions): Pipeline<Uint8Array> {
348
348
  this.transforms.push(
349
349
  createUnzip({
350
- chunkSize: 64 * 1024, // speedup from ~3200 to 3800 rps!
350
+ chunkSize: 256 * 1024, // 64KB: speedup from ~3200 to 3800 rps! 256KB: +6-8% more on ~9KB rows, neutral on ~180B rows
351
351
  ...opt,
352
352
  }),
353
353
  )
@@ -368,7 +368,7 @@ export class Pipeline<T = unknown> {
368
368
  zstdDecompress(this: Pipeline<Uint8Array>, opt?: ZstdOptions): Pipeline<Uint8Array> {
369
369
  this.transforms.push(
370
370
  createZstdDecompress({
371
- chunkSize: 64 * 1024, // todo: test it
371
+ chunkSize: 256 * 1024, // tested: 256KB is +10-12% over 64KB on ~9KB rows, neutral on ~180B rows
372
372
  ...opt,
373
373
  }),
374
374
  )
@@ -2,142 +2,113 @@ import { Transform } from 'node:stream'
2
2
  import type { TransformTyped } from '../stream.model.js'
3
3
 
4
4
  // The code below is carefully adopted from: https://github.com/max-mapper/binary-split
5
+ // (including the v2.0.0 performance improvements: native Buffer#indexOf and linear-time long lines)
5
6
 
6
7
  /**
7
8
  * Transforms input Buffer/string stream into Buffer chunks (objectMode: true) split by newLine.
8
9
  *
9
10
  * Useful for reading NDJSON files from fs.
10
11
  *
11
- * Same as binarySplit, but optimized (hard-coded) to split on NEWLINE (aka `\n`).
12
- * (+5-10% _pipeline speedup measured, compared to generic `binarySplit` on variable length delimiter)
12
+ * Same as `transformSplit('\n')`.
13
+ * (It used to be a separate hard-coded implementation, +5-10% faster than the generic one,
14
+ * but since the generic one searches 1-byte separators by byte value there's no difference anymore)
13
15
  */
14
16
  export function transformSplitOnNewline(): TransformTyped<Buffer, Buffer> {
15
- let buffered: Buffer | undefined
16
-
17
- return new Transform({
18
- writableObjectMode: false,
19
- writableHighWaterMark: 64 * 1024,
20
- readableObjectMode: true,
21
-
22
- transform(buf: Buffer, _enc, cb) {
23
- let offset = 0
24
- let lastMatch = 0
25
- if (buffered) {
26
- buf = Buffer.concat([buffered, buf])
27
- offset = buffered.length
28
- buffered = undefined
29
- }
30
-
31
- while (true) {
32
- const idx = firstNewlineMatch(buf, offset)
33
- if (idx !== -1 && idx < buf.length) {
34
- if (lastMatch !== idx) {
35
- this.push(buf.slice(lastMatch, idx))
36
- }
37
- offset = idx + 1
38
- lastMatch = offset
39
- } else {
40
- buffered = buf.slice(lastMatch)
41
- break
42
- }
43
- }
44
-
45
- cb()
46
- },
47
-
48
- flush(done) {
49
- if (buffered && buffered.length > 0) this.push(buffered)
50
- done()
51
- },
52
- })
17
+ return transformSplit('\n')
53
18
  }
54
19
 
55
20
  /**
56
21
  * Input: stream (objectMode=false) of arbitrary string|Buffer chunks, like when read from fs
57
22
  * Output: stream (objectMode=true) or string|Buffer chunks split by `separator` (@default to `\n`)
58
23
  *
59
- * Please use slightly more optimized `transformSplitOnNewline` for NDJSON file parsing.
60
- * (+5-10% _pipeline speedup measured!)
24
+ * Empty lines (consecutive separators) are skipped.
25
+ * Trailing data without a separator is emitted on flush.
61
26
  */
62
27
  export function transformSplit(separator = '\n'): TransformTyped<Buffer, Buffer> {
63
28
  const matcher = Buffer.from(separator)
64
- let buffered: Buffer | undefined
29
+ if (matcher.length === 0) {
30
+ throw new Error('transformSplit: separator must not be empty')
31
+ }
32
+ const matcherLength = matcher.length
33
+ // indexOf with a byte value is much faster than with a 1-byte Buffer, which matters for short lines
34
+ const needle = matcherLength === 1 ? matcher[0]! : matcher
35
+ // a multi-byte separator may straddle a chunk boundary, so we keep that many trailing bytes around
36
+ const overlap = matcherLength - 1
37
+
38
+ // Unterminated data is collected as a list of chunks and concatenated only once its separator arrives,
39
+ // so a line spanning many chunks is copied once rather than on every chunk (linear, not quadratic).
40
+ // Only that one line is copied: the rest of the chunk is scanned in place and emitted as views into it.
41
+ let pending: Buffer[] = []
42
+ let pendingLength = 0
43
+ let edge: Buffer | undefined // last `overlap` bytes of pending data, only tracked when overlap > 0
65
44
 
66
45
  return new Transform({
67
- readableObjectMode: true,
46
+ writableObjectMode: false,
68
47
  writableHighWaterMark: 64 * 1024,
48
+ readableObjectMode: true,
69
49
 
70
- transform(buf: Buffer, _enc, done) {
71
- let offset = 0
72
- let lastMatch = 0
73
- if (buffered) {
74
- buf = Buffer.concat([buffered, buf])
75
- offset = buffered.length
76
- buffered = undefined
77
- }
78
-
79
- while (true) {
80
- const idx = firstMatch(buf, offset - matcher.length + 1, matcher)
81
- if (idx !== -1 && idx < buf.length) {
82
- if (lastMatch !== idx) {
83
- this.push(buf.slice(lastMatch, idx))
50
+ transform(chunk: Buffer, _enc, cb) {
51
+ let start = 0
52
+
53
+ if (pendingLength > 0) {
54
+ // A multi-byte separator may straddle the chunk boundary: look for it in the last `overlap` bytes
55
+ // of pending data followed by the first `overlap` bytes of this chunk
56
+ const straddleIdx =
57
+ overlap > 0 ? Buffer.concat([edge!, chunk.subarray(0, overlap)]).indexOf(matcher) : -1
58
+
59
+ // Position of the first separator in chunk coordinates, negative when it started inside the pending data
60
+ let idx: number
61
+ if (straddleIdx === -1) {
62
+ idx = chunk.indexOf(needle)
63
+ if (idx === -1) {
64
+ // No separator yet: keep accumulating without copying
65
+ pending.push(chunk)
66
+ pendingLength += chunk.length
67
+ if (overlap > 0) {
68
+ edge = Buffer.concat([edge!, chunk.subarray(-overlap)]).subarray(-overlap)
69
+ }
70
+ cb()
71
+ return
84
72
  }
85
- offset = idx + matcher.length
86
- lastMatch = offset
87
73
  } else {
88
- buffered = buf.slice(lastMatch)
89
- break
74
+ idx = straddleIdx - edge!.length
90
75
  }
91
- }
92
76
 
93
- done()
94
- },
77
+ // Complete the pending line with the head of this chunk
78
+ if (idx > 0) pending.push(chunk.subarray(0, idx))
79
+ const lineLength = pendingLength + idx
80
+ if (lineLength > 0) {
81
+ this.push(Buffer.concat(pending, lineLength))
82
+ }
83
+ pending = []
84
+ pendingLength = 0
85
+ start = idx + matcherLength
86
+ }
95
87
 
96
- flush(done) {
97
- if (buffered && buffered.length > 0) this.push(buffered)
98
- done()
99
- },
100
- })
101
- }
88
+ let idx = chunk.indexOf(needle, start)
89
+ while (idx !== -1) {
90
+ if (idx > start) {
91
+ this.push(chunk.subarray(start, idx))
92
+ }
93
+ start = idx + matcherLength
94
+ idx = chunk.indexOf(needle, start)
95
+ }
102
96
 
103
- // const NEWLINE = Buffer.from('\n')
104
- // const NEWLINE_CODE = NEWLINE[0]! // it is `10`
105
- const NEWLINE_CODE = 10
97
+ if (start < chunk.length) {
98
+ const rest = chunk.subarray(start)
99
+ pending.push(rest)
100
+ pendingLength = rest.length
101
+ if (overlap > 0) edge = rest.subarray(-overlap)
102
+ }
106
103
 
107
- /**
108
- * Same as firstMatch, but optimized (hard-coded) to find NEWLINE (aka `\n`).
109
- */
110
- function firstNewlineMatch(buf: Buffer, offset: number): number {
111
- const bufLength = buf.length
112
- if (offset >= bufLength) return -1
113
- for (let i = offset; i < bufLength; i++) {
114
- if (buf[i] === NEWLINE_CODE) {
115
- return i
116
- }
117
- }
118
- return -1 // this code is unreachable, because i is guaranteed to be found in the loop above
119
- }
104
+ cb()
105
+ },
120
106
 
121
- function firstMatch(buf: Buffer, offset: number, matcher: Buffer): number {
122
- if (offset >= buf.length) return -1
123
- let i: number
124
- for (i = offset; i < buf.length; i++) {
125
- if (buf[i] === matcher[0]) {
126
- if (matcher.length > 1) {
127
- let fullMatch = true
128
- let j = i
129
- for (let k = 0; j < i + matcher.length; j++, k++) {
130
- if (buf[j] !== matcher[k]) {
131
- fullMatch = false
132
- break
133
- }
134
- }
135
- if (fullMatch) return j - matcher.length
136
- } else {
137
- break
107
+ flush(cb) {
108
+ if (pendingLength > 0) {
109
+ this.push(Buffer.concat(pending, pendingLength))
138
110
  }
139
- }
140
- }
141
-
142
- return i + matcher.length - 1
111
+ cb()
112
+ },
113
+ })
143
114
  }