@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.
- package/dist/stream/ndjson/createReadStreamAsNDJson.js +3 -3
- package/dist/stream/pipeline.js +3 -3
- package/dist/stream/transform/transformSplit.d.ts +5 -4
- package/dist/stream/transform/transformSplit.js +74 -105
- package/package.json +2 -2
- package/src/stream/ndjson/createReadStreamAsNDJson.ts +3 -3
- package/src/stream/pipeline.ts +3 -3
- package/src/stream/transform/transformSplit.ts +79 -108
|
@@ -19,17 +19,17 @@ export function createReadStreamAsNDJson(inputPath) {
|
|
|
19
19
|
fs2.requireFileToExist(inputPath);
|
|
20
20
|
let stream = fs2
|
|
21
21
|
.createReadStream(inputPath, {
|
|
22
|
-
highWaterMark:
|
|
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:
|
|
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:
|
|
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));
|
package/dist/stream/pipeline.js
CHANGED
|
@@ -73,7 +73,7 @@ export class Pipeline {
|
|
|
73
73
|
}
|
|
74
74
|
static fromFile(sourceFilePath) {
|
|
75
75
|
return new Pipeline(fs2.createReadStream(sourceFilePath, {
|
|
76
|
-
highWaterMark:
|
|
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:
|
|
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:
|
|
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
|
|
8
|
-
* (+5-10%
|
|
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
|
-
*
|
|
16
|
-
*
|
|
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
|
|
9
|
-
* (+5-10%
|
|
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
|
-
|
|
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
|
-
*
|
|
53
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
40
|
+
writableObjectMode: false,
|
|
60
41
|
writableHighWaterMark: 64 * 1024,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
let
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
125
|
-
|
|
83
|
+
start = idx + matcherLength;
|
|
84
|
+
idx = chunk.indexOf(needle, start);
|
|
126
85
|
}
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
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:
|
|
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:
|
|
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:
|
|
39
|
+
chunkSize: 256 * 1024, // tested: 256KB is +10-12% over 64KB on ~9KB rows, neutral on ~180B rows
|
|
40
40
|
}),
|
|
41
41
|
)
|
|
42
42
|
}
|
package/src/stream/pipeline.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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
|
|
12
|
-
* (+5-10%
|
|
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
|
-
|
|
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
|
-
*
|
|
60
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
46
|
+
writableObjectMode: false,
|
|
68
47
|
writableHighWaterMark: 64 * 1024,
|
|
48
|
+
readableObjectMode: true,
|
|
69
49
|
|
|
70
|
-
transform(
|
|
71
|
-
let
|
|
72
|
-
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
89
|
-
break
|
|
74
|
+
idx = straddleIdx - edge!.length
|
|
90
75
|
}
|
|
91
|
-
}
|
|
92
76
|
|
|
93
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
}
|