@naturalcycles/nodejs-lib 15.18.1 → 15.20.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.
@@ -241,18 +241,30 @@ class Exec2 {
241
241
  logStart(cmd, opt) {
242
242
  if (!opt.logStart)
243
243
  return;
244
- console.log([
245
- dimGrey(...Object.entries(opt.env || {}).map(([k, v]) => [k, v].join('='))),
246
- white(opt.name || _substringAfterLast(cmd, '/')),
247
- ...((!opt.name && opt.args) || []),
248
- ]
249
- .filter(Boolean)
250
- .join(' '));
244
+ const envString = Object.entries(opt.env || {})
245
+ .map(([k, v]) => [k, v].join('='))
246
+ .join(' ');
247
+ if (opt.name) {
248
+ console.log([' ', dimGrey(envString), white(opt.name), dimGrey('started...')]
249
+ .filter(Boolean)
250
+ .join(' '));
251
+ }
252
+ else {
253
+ console.log([
254
+ ' ',
255
+ dimGrey(envString),
256
+ white(_substringAfterLast(cmd, '/')),
257
+ ...(opt.args || []),
258
+ ]
259
+ .filter(Boolean)
260
+ .join(' '));
261
+ }
251
262
  }
252
263
  logFinish(cmd, opt, started, isSuccessful) {
253
264
  if (isSuccessful && !opt.logFinish)
254
265
  return;
255
266
  console.log([
267
+ isSuccessful ? '✔️ ' : '❌',
256
268
  white(opt.name || _substringAfterLast(cmd, '/')),
257
269
  ...((!opt.name && opt.args) || []),
258
270
  dimGrey('took ' + _since(started)),
@@ -14,6 +14,7 @@ export * from './readable/readableToArray.js';
14
14
  export * from './stream.model.js';
15
15
  export * from './transform/transformChunk.js';
16
16
  export * from './transform/transformFilter.js';
17
+ export * from './transform/transformFlatten.js';
17
18
  export * from './transform/transformLimit.js';
18
19
  export * from './transform/transformLogProgress.js';
19
20
  export * from './transform/transformMap.js';
@@ -14,6 +14,7 @@ export * from './readable/readableToArray.js';
14
14
  export * from './stream.model.js';
15
15
  export * from './transform/transformChunk.js';
16
16
  export * from './transform/transformFilter.js';
17
+ export * from './transform/transformFlatten.js';
17
18
  export * from './transform/transformLimit.js';
18
19
  export * from './transform/transformLogProgress.js';
19
20
  export * from './transform/transformMap.js';
@@ -9,12 +9,6 @@ export interface NDJSONMapOptions<IN = any, OUT = IN> extends TransformMapOption
9
9
  * @default 100_000
10
10
  */
11
11
  logEveryOutput?: number;
12
- /**
13
- * Defaults to `true` for ndjsonMap
14
- *
15
- * @default true
16
- */
17
- flattenArrayOutput?: boolean;
18
12
  }
19
13
  /**
20
14
  * Unzips input file automatically, if it ends with `.gz`.
@@ -1,5 +1,5 @@
1
1
  import { ErrorMode } from '@naturalcycles/js-lib/error/errorMode.js';
2
- import { createReadStreamAsNDJSON, createWriteStreamAsNDJSON, } from '../index.js';
2
+ import { createReadStreamAsNDJSON, createWriteStreamAsNDJSON, transformFlatten, } from '../index.js';
3
3
  import { _pipeline, transformLimit, transformLogProgress, transformMap } from '../index.js';
4
4
  /**
5
5
  * Unzips input file automatically, if it ends with `.gz`.
@@ -16,10 +16,10 @@ export async function ndjsonMap(mapper, opt) {
16
16
  readable,
17
17
  transformLogProgress({ metric: 'read', ...opt }),
18
18
  transformMap(mapper, {
19
- flattenArrayOutput: true,
20
19
  errorMode: ErrorMode.SUPPRESS,
21
20
  ...opt,
22
21
  }),
22
+ transformFlatten(),
23
23
  transformLimit({ limit: limitOutput, sourceReadable: readable }),
24
24
  transformLogProgress({ metric: 'saved', logEvery: logEveryOutput }),
25
25
  ...createWriteStreamAsNDJSON(outputFilePath),
@@ -1,5 +1,5 @@
1
1
  import { inspect } from 'node:util';
2
- import { _mb } from '@naturalcycles/js-lib';
2
+ import { _hc, _mb } from '@naturalcycles/js-lib';
3
3
  import { _since, localTime } from '@naturalcycles/js-lib/datetime';
4
4
  import { SimpleMovingAverage } from '@naturalcycles/js-lib/math';
5
5
  import { boldWhite, dimGrey, hasColors, white, yellow } from '../colors/colors.js';
@@ -108,11 +108,8 @@ export class ProgressLogger {
108
108
  }
109
109
  }
110
110
  if (tenx) {
111
- let perHour = Math.round((batchedProgress * 1000 * 60 * 60) / (now - this.started)) || 0;
112
- if (perHour > 900) {
113
- perHour = Math.round(perHour / 1000) + 'K';
114
- }
115
- logger.log(`${dimGrey(localTime.now().toPretty())} ${white(metric)} took ${yellow(_since(this.started))} so far to process ${yellow(batchedProgress)} rows, ~${yellow(perHour)}/hour`);
111
+ const perHour = _hc((batchedProgress * 1000 * 60 * 60) / (now - this.started));
112
+ logger.log(`${dimGrey(localTime.now().toPretty())} ${white(metric)} took ${yellow(_since(this.started))} so far to process ${yellow(_hc(batchedProgress))} rows, ~${yellow(perHour)}/hour`);
116
113
  }
117
114
  else if (final) {
118
115
  logger.log(`${boldWhite(metric)} took ${yellow(_since(this.started))} to process ${yellow(batchedProgress)} rows with total RPS of ${yellow(rpsTotal)}`);
@@ -0,0 +1,2 @@
1
+ import type { TransformTyped } from '../stream.model.js';
2
+ export declare function transformFlatten<T>(): TransformTyped<T[], T>;
@@ -0,0 +1,18 @@
1
+ import { Transform } from 'node:stream';
2
+ export function transformFlatten() {
3
+ return new Transform({
4
+ objectMode: true,
5
+ transform(chunk, _, cb) {
6
+ if (!Array.isArray(chunk)) {
7
+ // As a safety precaution, to not crash the pipeline - push as is
8
+ this.push(chunk);
9
+ }
10
+ else {
11
+ for (const item of chunk) {
12
+ this.push(item);
13
+ }
14
+ }
15
+ cb(); // acknowledge
16
+ },
17
+ });
18
+ }
@@ -3,12 +3,6 @@ import type { CommonLogger } from '@naturalcycles/js-lib/log';
3
3
  import { type AbortableAsyncMapper, type AsyncPredicate, END, type Promisable, SKIP, type StringMap, type UnixTimestampMillis } from '@naturalcycles/js-lib/types';
4
4
  import type { TransformTyped } from '../stream.model.js';
5
5
  export interface TransformMapOptions<IN = any, OUT = IN> {
6
- /**
7
- * Set true to support "multiMap" - possibility to return [] and emit 1 result for each item in the array.
8
- *
9
- * @default false
10
- */
11
- flattenArrayOutput?: boolean;
12
6
  /**
13
7
  * Predicate to filter outgoing results (after mapper).
14
8
  * Allows to not emit all results.
@@ -1,7 +1,6 @@
1
1
  import { _hc } from '@naturalcycles/js-lib';
2
2
  import { _since } from '@naturalcycles/js-lib/datetime/time.util.js';
3
3
  import { _anyToError, ErrorMode } from '@naturalcycles/js-lib/error';
4
- import { pMap } from '@naturalcycles/js-lib/promise/pMap.js';
5
4
  import { _stringify } from '@naturalcycles/js-lib/string/stringify.js';
6
5
  import { END, SKIP, } from '@naturalcycles/js-lib/types';
7
6
  import through2Concurrent from 'through2-concurrent';
@@ -23,7 +22,7 @@ import { pipelineClose } from '../stream.util.js';
23
22
  */
24
23
  export function transformMap(mapper, opt = {}) {
25
24
  const { concurrency = 16, predicate, // we now default to "no predicate" (meaning pass-everything)
26
- errorMode = ErrorMode.THROW_IMMEDIATELY, flattenArrayOutput, onError, onDone, metric = 'stream', logger = console, } = opt;
25
+ errorMode = ErrorMode.THROW_IMMEDIATELY, onError, onDone, metric = 'stream', logger = console, } = opt;
27
26
  const started = Date.now();
28
27
  let index = -1;
29
28
  let countOut = 0;
@@ -80,40 +79,20 @@ export function transformMap(mapper, opt = {}) {
80
79
  // Check for isSettled again, as it may happen while mapper was running
81
80
  if (isSettled)
82
81
  return cb();
83
- // todo: consider retiring flattenArrayOutput from here
84
- // and implementing it as a separate .flat transform/operator
85
- const resInput = (flattenArrayOutput && Array.isArray(res) ? res : [res]);
86
- if (predicate) {
87
- await pMap(resInput, async (r) => {
88
- if (r === END) {
89
- isSettled = true; // will be checked later
90
- return END;
91
- }
92
- if (r === SKIP)
93
- return;
94
- if (await predicate(r, currentIndex)) {
95
- if (isSettled)
96
- return END; // isSettled could have happened in parallel
97
- countOut++;
98
- this.push(r);
99
- }
100
- });
101
- }
102
- else {
103
- for (const r of resInput) {
104
- if (r === END) {
105
- isSettled = true; // will be checked later
106
- break;
107
- }
108
- if (r === SKIP)
109
- continue;
110
- countOut++;
111
- this.push(r);
112
- }
113
- }
114
- if (isSettled) {
82
+ if (res === END) {
83
+ isSettled = true;
115
84
  logger.log(`transformMap END received at index ${currentIndex}`);
116
85
  pipelineClose('transformMap', this, this.sourceReadable, this.streamDone, logger);
86
+ return cb();
87
+ }
88
+ if (res === SKIP) {
89
+ // do nothing, don't push
90
+ return cb();
91
+ }
92
+ if (!predicate || ((await predicate(res, currentIndex)) && !isSettled)) {
93
+ // isSettled could have happened in parallel, hence the extra check
94
+ countOut++;
95
+ this.push(res);
117
96
  }
118
97
  cb(); // done processing
119
98
  }
@@ -10,11 +10,6 @@ export interface TransformMapSyncOptions<IN = any, OUT = IN> {
10
10
  * @default true
11
11
  */
12
12
  objectMode?: boolean;
13
- /**
14
- * @default false
15
- * Set true to support "multiMap" - possibility to return [] and emit 1 result for each item in the array.
16
- */
17
- flattenArrayOutput?: boolean;
18
13
  /**
19
14
  * Predicate to filter outgoing results (after mapper).
20
15
  * Allows to not emit all results.
@@ -11,7 +11,7 @@ export class TransformMapSync extends AbortableTransform {
11
11
  */
12
12
  export function transformMapSync(mapper, opt = {}) {
13
13
  const { predicate, // defaults to "no predicate" (pass everything)
14
- errorMode = ErrorMode.THROW_IMMEDIATELY, flattenArrayOutput = false, onError, onDone, metric = 'stream', objectMode = true, logger = console, } = opt;
14
+ errorMode = ErrorMode.THROW_IMMEDIATELY, onError, onDone, metric = 'stream', objectMode = true, logger = console, } = opt;
15
15
  const started = Date.now();
16
16
  let index = -1;
17
17
  let countOut = 0;
@@ -29,21 +29,19 @@ export function transformMapSync(mapper, opt = {}) {
29
29
  try {
30
30
  // map and pass through
31
31
  const v = mapper(chunk, currentIndex);
32
- // todo: consider retiring flattenArrayOutput option
33
- const vInput = (flattenArrayOutput && Array.isArray(v) ? v : [v]);
34
- for (const r of vInput) {
35
- if (r === END) {
36
- isSettled = true; // will be checked later
37
- break;
38
- }
39
- if (r !== SKIP && (!predicate || predicate(r, currentIndex))) {
40
- countOut++;
41
- this.push(r);
42
- }
43
- }
44
- if (isSettled) {
32
+ if (v === END) {
33
+ isSettled = true; // will be checked later
45
34
  logger.log(`transformMapSync END received at index ${currentIndex}`);
46
35
  pipelineClose('transformMapSync', this, this.sourceReadable, this.streamDone, logger);
36
+ return cb();
37
+ }
38
+ if (v === SKIP) {
39
+ // do nothing, don't push
40
+ return cb();
41
+ }
42
+ if (!predicate || predicate(v, currentIndex)) {
43
+ countOut++;
44
+ this.push(v);
47
45
  }
48
46
  cb(); // done processing
49
47
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/nodejs-lib",
3
3
  "type": "module",
4
- "version": "15.18.1",
4
+ "version": "15.20.0",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@types/js-yaml": "^4",
@@ -23,7 +23,7 @@
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/through2-concurrent": "^2",
26
- "@naturalcycles/dev-lib": "19.27.0"
26
+ "@naturalcycles/dev-lib": "19.31.0"
27
27
  },
28
28
  "exports": {
29
29
  ".": "./dist/index.js",
@@ -271,15 +271,28 @@ class Exec2 {
271
271
  private logStart(cmd: string, opt: SpawnOptions | ExecOptions): void {
272
272
  if (!opt.logStart) return
273
273
 
274
- console.log(
275
- [
276
- dimGrey(...Object.entries(opt.env || {}).map(([k, v]) => [k, v].join('='))),
277
- white(opt.name || _substringAfterLast(cmd, '/')),
278
- ...((!opt.name && (opt as SpawnOptions).args) || []),
279
- ]
280
- .filter(Boolean)
281
- .join(' '),
282
- )
274
+ const envString = Object.entries(opt.env || {})
275
+ .map(([k, v]) => [k, v].join('='))
276
+ .join(' ')
277
+
278
+ if (opt.name) {
279
+ console.log(
280
+ [' ', dimGrey(envString), white(opt.name), dimGrey('started...')]
281
+ .filter(Boolean)
282
+ .join(' '),
283
+ )
284
+ } else {
285
+ console.log(
286
+ [
287
+ ' ',
288
+ dimGrey(envString),
289
+ white(_substringAfterLast(cmd, '/')),
290
+ ...((opt as SpawnOptions).args || []),
291
+ ]
292
+ .filter(Boolean)
293
+ .join(' '),
294
+ )
295
+ }
283
296
  }
284
297
 
285
298
  private logFinish(
@@ -292,6 +305,7 @@ class Exec2 {
292
305
 
293
306
  console.log(
294
307
  [
308
+ isSuccessful ? '✔️ ' : '❌',
295
309
  white(opt.name || _substringAfterLast(cmd, '/')),
296
310
  ...((!opt.name && (opt as SpawnOptions).args) || []),
297
311
  dimGrey('took ' + _since(started)),
@@ -14,6 +14,7 @@ export * from './readable/readableToArray.js'
14
14
  export * from './stream.model.js'
15
15
  export * from './transform/transformChunk.js'
16
16
  export * from './transform/transformFilter.js'
17
+ export * from './transform/transformFlatten.js'
17
18
  export * from './transform/transformLimit.js'
18
19
  export * from './transform/transformLogProgress.js'
19
20
  export * from './transform/transformMap.js'
@@ -3,6 +3,7 @@ import type { AbortableAsyncMapper } from '@naturalcycles/js-lib/types'
3
3
  import {
4
4
  createReadStreamAsNDJSON,
5
5
  createWriteStreamAsNDJSON,
6
+ transformFlatten,
6
7
  type TransformLogProgressOptions,
7
8
  type TransformMapOptions,
8
9
  } from '../index.js'
@@ -21,13 +22,6 @@ export interface NDJSONMapOptions<IN = any, OUT = IN>
21
22
  * @default 100_000
22
23
  */
23
24
  logEveryOutput?: number
24
-
25
- /**
26
- * Defaults to `true` for ndjsonMap
27
- *
28
- * @default true
29
- */
30
- flattenArrayOutput?: boolean
31
25
  }
32
26
 
33
27
  /**
@@ -53,10 +47,10 @@ export async function ndjsonMap<IN = any, OUT = any>(
53
47
  readable,
54
48
  transformLogProgress({ metric: 'read', ...opt }),
55
49
  transformMap(mapper, {
56
- flattenArrayOutput: true,
57
50
  errorMode: ErrorMode.SUPPRESS,
58
51
  ...opt,
59
52
  }),
53
+ transformFlatten(),
60
54
  transformLimit({ limit: limitOutput, sourceReadable: readable }),
61
55
  transformLogProgress({ metric: 'saved', logEvery: logEveryOutput }),
62
56
  ...createWriteStreamAsNDJSON(outputFilePath),
@@ -1,6 +1,6 @@
1
1
  import type { InspectOptions } from 'node:util'
2
2
  import { inspect } from 'node:util'
3
- import { _mb } from '@naturalcycles/js-lib'
3
+ import { _hc, _mb } from '@naturalcycles/js-lib'
4
4
  import { _since, localTime } from '@naturalcycles/js-lib/datetime'
5
5
  import type { CommonLogger } from '@naturalcycles/js-lib/log'
6
6
  import { SimpleMovingAverage } from '@naturalcycles/js-lib/math'
@@ -284,16 +284,12 @@ export class ProgressLogger<T> implements Disposable {
284
284
  }
285
285
 
286
286
  if (tenx) {
287
- let perHour: number | string =
288
- Math.round((batchedProgress * 1000 * 60 * 60) / (now - this.started)) || 0
289
- if (perHour > 900) {
290
- perHour = Math.round(perHour / 1000) + 'K'
291
- }
287
+ const perHour = _hc((batchedProgress * 1000 * 60 * 60) / (now - this.started))
292
288
 
293
289
  logger.log(
294
290
  `${dimGrey(localTime.now().toPretty())} ${white(metric)} took ${yellow(
295
291
  _since(this.started),
296
- )} so far to process ${yellow(batchedProgress)} rows, ~${yellow(perHour)}/hour`,
292
+ )} so far to process ${yellow(_hc(batchedProgress))} rows, ~${yellow(perHour)}/hour`,
297
293
  )
298
294
  } else if (final) {
299
295
  logger.log(
@@ -0,0 +1,19 @@
1
+ import { Transform } from 'node:stream'
2
+ import type { TransformTyped } from '../stream.model.js'
3
+
4
+ export function transformFlatten<T>(): TransformTyped<T[], T> {
5
+ return new Transform({
6
+ objectMode: true,
7
+ transform(chunk: T[], _, cb) {
8
+ if (!Array.isArray(chunk)) {
9
+ // As a safety precaution, to not crash the pipeline - push as is
10
+ this.push(chunk)
11
+ } else {
12
+ for (const item of chunk) {
13
+ this.push(item)
14
+ }
15
+ }
16
+ cb() // acknowledge
17
+ },
18
+ })
19
+ }
@@ -2,7 +2,6 @@ import { _hc } from '@naturalcycles/js-lib'
2
2
  import { _since } from '@naturalcycles/js-lib/datetime/time.util.js'
3
3
  import { _anyToError, ErrorMode } from '@naturalcycles/js-lib/error'
4
4
  import type { CommonLogger } from '@naturalcycles/js-lib/log'
5
- import { pMap } from '@naturalcycles/js-lib/promise/pMap.js'
6
5
  import { _stringify } from '@naturalcycles/js-lib/string/stringify.js'
7
6
  import {
8
7
  type AbortableAsyncMapper,
@@ -20,13 +19,6 @@ import type { TransformTyped } from '../stream.model.js'
20
19
  import { pipelineClose } from '../stream.util.js'
21
20
 
22
21
  export interface TransformMapOptions<IN = any, OUT = IN> {
23
- /**
24
- * Set true to support "multiMap" - possibility to return [] and emit 1 result for each item in the array.
25
- *
26
- * @default false
27
- */
28
- flattenArrayOutput?: boolean
29
-
30
22
  /**
31
23
  * Predicate to filter outgoing results (after mapper).
32
24
  * Allows to not emit all results.
@@ -133,7 +125,6 @@ export function transformMap<IN = any, OUT = IN>(
133
125
  concurrency = 16,
134
126
  predicate, // we now default to "no predicate" (meaning pass-everything)
135
127
  errorMode = ErrorMode.THROW_IMMEDIATELY,
136
- flattenArrayOutput,
137
128
  onError,
138
129
  onDone,
139
130
  metric = 'stream',
@@ -206,42 +197,23 @@ export function transformMap<IN = any, OUT = IN>(
206
197
  const res: OUT | typeof SKIP | typeof END = await mapper(chunk, currentIndex)
207
198
  // Check for isSettled again, as it may happen while mapper was running
208
199
  if (isSettled) return cb()
209
- // todo: consider retiring flattenArrayOutput from here
210
- // and implementing it as a separate .flat transform/operator
211
- const resInput = (flattenArrayOutput && Array.isArray(res) ? res : [res]) as (
212
- | OUT
213
- | typeof SKIP
214
- | typeof END
215
- )[]
216
-
217
- if (predicate) {
218
- await pMap(resInput, async r => {
219
- if (r === END) {
220
- isSettled = true // will be checked later
221
- return END
222
- }
223
- if (r === SKIP) return
224
- if (await predicate(r, currentIndex)) {
225
- if (isSettled) return END // isSettled could have happened in parallel
226
- countOut++
227
- this.push(r)
228
- }
229
- })
230
- } else {
231
- for (const r of resInput) {
232
- if (r === END) {
233
- isSettled = true // will be checked later
234
- break
235
- }
236
- if (r === SKIP) continue
237
- countOut++
238
- this.push(r)
239
- }
240
- }
241
200
 
242
- if (isSettled) {
201
+ if (res === END) {
202
+ isSettled = true
243
203
  logger.log(`transformMap END received at index ${currentIndex}`)
244
204
  pipelineClose('transformMap', this, this.sourceReadable, this.streamDone, logger)
205
+ return cb()
206
+ }
207
+
208
+ if (res === SKIP) {
209
+ // do nothing, don't push
210
+ return cb()
211
+ }
212
+
213
+ if (!predicate || ((await predicate(res, currentIndex)) && !isSettled)) {
214
+ // isSettled could have happened in parallel, hence the extra check
215
+ countOut++
216
+ this.push(res)
245
217
  }
246
218
 
247
219
  cb() // done processing
@@ -14,12 +14,6 @@ export interface TransformMapSyncOptions<IN = any, OUT = IN> {
14
14
  */
15
15
  objectMode?: boolean
16
16
 
17
- /**
18
- * @default false
19
- * Set true to support "multiMap" - possibility to return [] and emit 1 result for each item in the array.
20
- */
21
- flattenArrayOutput?: boolean
22
-
23
17
  /**
24
18
  * Predicate to filter outgoing results (after mapper).
25
19
  * Allows to not emit all results.
@@ -75,7 +69,6 @@ export function transformMapSync<IN = any, OUT = IN>(
75
69
  const {
76
70
  predicate, // defaults to "no predicate" (pass everything)
77
71
  errorMode = ErrorMode.THROW_IMMEDIATELY,
78
- flattenArrayOutput = false,
79
72
  onError,
80
73
  onDone,
81
74
  metric = 'stream',
@@ -102,27 +95,22 @@ export function transformMapSync<IN = any, OUT = IN>(
102
95
  try {
103
96
  // map and pass through
104
97
  const v = mapper(chunk, currentIndex)
105
- // todo: consider retiring flattenArrayOutput option
106
- const vInput = (flattenArrayOutput && Array.isArray(v) ? v : [v]) as (
107
- | OUT
108
- | typeof SKIP
109
- | typeof END
110
- )[]
111
-
112
- for (const r of vInput) {
113
- if (r === END) {
114
- isSettled = true // will be checked later
115
- break
116
- }
117
- if (r !== SKIP && (!predicate || predicate(r, currentIndex))) {
118
- countOut++
119
- this.push(r)
120
- }
121
- }
122
98
 
123
- if (isSettled) {
99
+ if (v === END) {
100
+ isSettled = true // will be checked later
124
101
  logger.log(`transformMapSync END received at index ${currentIndex}`)
125
102
  pipelineClose('transformMapSync', this, this.sourceReadable, this.streamDone, logger)
103
+ return cb()
104
+ }
105
+
106
+ if (v === SKIP) {
107
+ // do nothing, don't push
108
+ return cb()
109
+ }
110
+
111
+ if (!predicate || predicate(v, currentIndex)) {
112
+ countOut++
113
+ this.push(v)
126
114
  }
127
115
 
128
116
  cb() // done processing