@naturalcycles/nodejs-lib 15.124.0 → 15.125.0
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.
|
@@ -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,90 @@
|
|
|
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 a separator arrives,
|
|
34
|
+
// so a long line spanning many chunks is copied once rather than on every chunk (linear, not quadratic)
|
|
35
|
+
let pending = [];
|
|
36
|
+
let pendingLength = 0;
|
|
37
|
+
let edge; // last `overlap` bytes of pending data, only tracked when overlap > 0
|
|
58
38
|
return new Transform({
|
|
59
|
-
|
|
39
|
+
writableObjectMode: false,
|
|
60
40
|
writableHighWaterMark: 64 * 1024,
|
|
61
|
-
|
|
41
|
+
readableObjectMode: true,
|
|
42
|
+
transform(chunk, _enc, cb) {
|
|
43
|
+
let buf = chunk;
|
|
62
44
|
let offset = 0;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (lastMatch !== idx) {
|
|
73
|
-
this.push(buf.slice(lastMatch, idx));
|
|
45
|
+
if (pendingLength > 0) {
|
|
46
|
+
const straddles = overlap > 0 && Buffer.concat([edge, chunk.subarray(0, overlap)]).includes(matcher);
|
|
47
|
+
const idxInChunk = straddles ? -1 : chunk.indexOf(needle);
|
|
48
|
+
if (!straddles && idxInChunk === -1) {
|
|
49
|
+
// No separator yet: keep accumulating without copying
|
|
50
|
+
pending.push(chunk);
|
|
51
|
+
pendingLength += chunk.length;
|
|
52
|
+
if (overlap > 0) {
|
|
53
|
+
edge = Buffer.concat([edge, chunk.subarray(-overlap)]).subarray(-overlap);
|
|
74
54
|
}
|
|
75
|
-
|
|
76
|
-
|
|
55
|
+
cb();
|
|
56
|
+
return;
|
|
77
57
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
58
|
+
pending.push(chunk);
|
|
59
|
+
buf = Buffer.concat(pending, pendingLength + chunk.length);
|
|
60
|
+
// Start scanning where the first separator can possibly be
|
|
61
|
+
offset = straddles ? Math.max(0, pendingLength - overlap) : pendingLength + idxInChunk;
|
|
62
|
+
pending = [];
|
|
63
|
+
pendingLength = 0;
|
|
64
|
+
}
|
|
65
|
+
let start = 0;
|
|
66
|
+
let idx = buf.indexOf(needle, offset);
|
|
67
|
+
while (idx !== -1) {
|
|
68
|
+
if (idx > start) {
|
|
69
|
+
this.push(buf.subarray(start, idx));
|
|
81
70
|
}
|
|
71
|
+
start = idx + matcherLength;
|
|
72
|
+
idx = buf.indexOf(needle, start);
|
|
82
73
|
}
|
|
83
|
-
|
|
74
|
+
if (start < buf.length) {
|
|
75
|
+
const rest = buf.subarray(start);
|
|
76
|
+
pending.push(rest);
|
|
77
|
+
pendingLength = rest.length;
|
|
78
|
+
if (overlap > 0)
|
|
79
|
+
edge = rest.subarray(-overlap);
|
|
80
|
+
}
|
|
81
|
+
cb();
|
|
84
82
|
},
|
|
85
|
-
flush(
|
|
86
|
-
if (
|
|
87
|
-
this.push(
|
|
88
|
-
|
|
83
|
+
flush(cb) {
|
|
84
|
+
if (pendingLength > 0) {
|
|
85
|
+
this.push(Buffer.concat(pending, pendingLength));
|
|
86
|
+
}
|
|
87
|
+
cb();
|
|
89
88
|
},
|
|
90
89
|
});
|
|
91
90
|
}
|
|
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
|
-
}
|
|
123
|
-
}
|
|
124
|
-
if (fullMatch)
|
|
125
|
-
return j - matcher.length;
|
|
126
|
-
}
|
|
127
|
-
else {
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
return i + matcher.length - 1;
|
|
133
|
-
}
|
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.0",
|
|
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": {
|
|
@@ -2,142 +2,101 @@ 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 a separator arrives,
|
|
39
|
+
// so a long line spanning many chunks is copied once rather than on every chunk (linear, not quadratic)
|
|
40
|
+
let pending: Buffer[] = []
|
|
41
|
+
let pendingLength = 0
|
|
42
|
+
let edge: Buffer | undefined // last `overlap` bytes of pending data, only tracked when overlap > 0
|
|
65
43
|
|
|
66
44
|
return new Transform({
|
|
67
|
-
|
|
45
|
+
writableObjectMode: false,
|
|
68
46
|
writableHighWaterMark: 64 * 1024,
|
|
47
|
+
readableObjectMode: true,
|
|
69
48
|
|
|
70
|
-
transform(
|
|
49
|
+
transform(chunk: Buffer, _enc, cb) {
|
|
50
|
+
let buf = chunk
|
|
71
51
|
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
52
|
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
53
|
+
if (pendingLength > 0) {
|
|
54
|
+
const straddles =
|
|
55
|
+
overlap > 0 && Buffer.concat([edge!, chunk.subarray(0, overlap)]).includes(matcher)
|
|
56
|
+
const idxInChunk = straddles ? -1 : chunk.indexOf(needle)
|
|
57
|
+
if (!straddles && idxInChunk === -1) {
|
|
58
|
+
// No separator yet: keep accumulating without copying
|
|
59
|
+
pending.push(chunk)
|
|
60
|
+
pendingLength += chunk.length
|
|
61
|
+
if (overlap > 0) {
|
|
62
|
+
edge = Buffer.concat([edge!, chunk.subarray(-overlap)]).subarray(-overlap)
|
|
84
63
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
} else {
|
|
88
|
-
buffered = buf.slice(lastMatch)
|
|
89
|
-
break
|
|
64
|
+
cb()
|
|
65
|
+
return
|
|
90
66
|
}
|
|
67
|
+
pending.push(chunk)
|
|
68
|
+
buf = Buffer.concat(pending, pendingLength + chunk.length)
|
|
69
|
+
// Start scanning where the first separator can possibly be
|
|
70
|
+
offset = straddles ? Math.max(0, pendingLength - overlap) : pendingLength + idxInChunk
|
|
71
|
+
pending = []
|
|
72
|
+
pendingLength = 0
|
|
91
73
|
}
|
|
92
74
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
75
|
+
let start = 0
|
|
76
|
+
let idx = buf.indexOf(needle, offset)
|
|
77
|
+
while (idx !== -1) {
|
|
78
|
+
if (idx > start) {
|
|
79
|
+
this.push(buf.subarray(start, idx))
|
|
80
|
+
}
|
|
81
|
+
start = idx + matcherLength
|
|
82
|
+
idx = buf.indexOf(needle, start)
|
|
83
|
+
}
|
|
102
84
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
85
|
+
if (start < buf.length) {
|
|
86
|
+
const rest = buf.subarray(start)
|
|
87
|
+
pending.push(rest)
|
|
88
|
+
pendingLength = rest.length
|
|
89
|
+
if (overlap > 0) edge = rest.subarray(-overlap)
|
|
90
|
+
}
|
|
106
91
|
|
|
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
|
-
}
|
|
92
|
+
cb()
|
|
93
|
+
},
|
|
120
94
|
|
|
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
|
|
95
|
+
flush(cb) {
|
|
96
|
+
if (pendingLength > 0) {
|
|
97
|
+
this.push(Buffer.concat(pending, pendingLength))
|
|
138
98
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return i + matcher.length - 1
|
|
99
|
+
cb()
|
|
100
|
+
},
|
|
101
|
+
})
|
|
143
102
|
}
|