@naturalcycles/js-lib 14.79.0 → 14.82.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.
@@ -6,6 +6,7 @@ Improvements:
6
6
  - Included Typescript typings (no need for @types/p-map)
7
7
  - Compatible with pProps (that had typings issues)
8
8
  */
9
+ import { __asyncValues } from "tslib";
9
10
  import { END, ErrorMode, SKIP } from '..';
10
11
  import { AggregatedError } from './AggregatedError';
11
12
  /**
@@ -35,28 +36,85 @@ import { AggregatedError } from './AggregatedError';
35
36
  * })();
36
37
  */
37
38
  export async function pMap(iterable, mapper, opt = {}) {
39
+ var e_1, _a;
40
+ const ret = [];
41
+ // const iterator = iterable[Symbol.iterator]()
42
+ const items = [...iterable];
43
+ const itemsLength = items.length;
44
+ if (itemsLength === 0)
45
+ return []; // short circuit
46
+ const { concurrency = itemsLength, errorMode = ErrorMode.THROW_IMMEDIATELY } = opt;
47
+ const errors = [];
48
+ let isSettled = false;
49
+ let resolvingCount = 0;
50
+ let currentIndex = 0;
51
+ // Special cases that are able to preserve async stack traces
52
+ if (concurrency === 1) {
53
+ try {
54
+ // Special case for concurrency == 1
55
+ for (var items_1 = __asyncValues(items), items_1_1; items_1_1 = await items_1.next(), !items_1_1.done;) {
56
+ const item = items_1_1.value;
57
+ try {
58
+ const r = await mapper(item, currentIndex++);
59
+ if (r === END)
60
+ break;
61
+ if (r !== SKIP)
62
+ ret.push(r);
63
+ }
64
+ catch (err) {
65
+ if (errorMode === ErrorMode.THROW_IMMEDIATELY)
66
+ throw err;
67
+ if (errorMode === ErrorMode.THROW_AGGREGATED) {
68
+ errors.push(err);
69
+ }
70
+ // otherwise, suppress completely
71
+ }
72
+ }
73
+ }
74
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
75
+ finally {
76
+ try {
77
+ if (items_1_1 && !items_1_1.done && (_a = items_1.return)) await _a.call(items_1);
78
+ }
79
+ finally { if (e_1) throw e_1.error; }
80
+ }
81
+ if (errors.length) {
82
+ throw new AggregatedError(errors, ret);
83
+ }
84
+ return ret;
85
+ }
86
+ else if (!opt.concurrency || items.length <= opt.concurrency) {
87
+ // Special case for concurrency == infinity or iterable.length < concurrency
88
+ if (errorMode === ErrorMode.THROW_IMMEDIATELY) {
89
+ return (await Promise.all(items.map((item, i) => mapper(item, i)))).filter(r => r !== SKIP && r !== END);
90
+ }
91
+ ;
92
+ (await Promise.allSettled(items.map((item, i) => mapper(item, i)))).forEach(r => {
93
+ if (r.status === 'fulfilled') {
94
+ if (r.value !== SKIP && r.value !== END)
95
+ ret.push(r.value);
96
+ }
97
+ else if (errorMode === ErrorMode.THROW_AGGREGATED) {
98
+ errors.push(r.reason);
99
+ }
100
+ });
101
+ if (errors.length) {
102
+ throw new AggregatedError(errors, ret);
103
+ }
104
+ return ret;
105
+ }
38
106
  return new Promise((resolve, reject) => {
39
- const { concurrency = Number.POSITIVE_INFINITY, errorMode = ErrorMode.THROW_IMMEDIATELY } = opt;
40
- const ret = [];
41
- const iterator = iterable[Symbol.iterator]();
42
- const errors = [];
43
- let isSettled = false;
44
- let isIterableDone = false;
45
- let resolvingCount = 0;
46
- let currentIndex = 0;
47
- const next = (skipped = false) => {
107
+ const next = () => {
48
108
  if (isSettled) {
49
109
  return;
50
110
  }
51
- const nextItem = iterator.next();
52
- const i = currentIndex;
53
- if (!skipped)
54
- currentIndex++;
55
- if (nextItem.done) {
56
- isIterableDone = true;
111
+ const nextItem = items[currentIndex];
112
+ const i = currentIndex++;
113
+ if (currentIndex > itemsLength) {
57
114
  if (resolvingCount === 0) {
115
+ isSettled = true;
58
116
  const r = ret.filter(r => r !== SKIP);
59
- if (errors.length && errorMode === ErrorMode.THROW_AGGREGATED) {
117
+ if (errors.length) {
60
118
  reject(new AggregatedError(errors, r));
61
119
  }
62
120
  else {
@@ -66,7 +124,7 @@ export async function pMap(iterable, mapper, opt = {}) {
66
124
  return;
67
125
  }
68
126
  resolvingCount++;
69
- Promise.resolve(nextItem.value)
127
+ Promise.resolve(nextItem)
70
128
  .then(async (element) => await mapper(element, i))
71
129
  .then(value => {
72
130
  if (value === END) {
@@ -82,7 +140,9 @@ export async function pMap(iterable, mapper, opt = {}) {
82
140
  reject(err);
83
141
  }
84
142
  else {
85
- errors.push(err);
143
+ if (errorMode === ErrorMode.THROW_AGGREGATED) {
144
+ errors.push(err);
145
+ }
86
146
  resolvingCount--;
87
147
  next();
88
148
  }
@@ -90,7 +150,7 @@ export async function pMap(iterable, mapper, opt = {}) {
90
150
  };
91
151
  for (let i = 0; i < concurrency; i++) {
92
152
  next();
93
- if (isIterableDone) {
153
+ if (isSettled) {
94
154
  break;
95
155
  }
96
156
  }
@@ -1,54 +1,85 @@
1
1
  import { _since, _stringifyAny } from '..';
2
+ import { TimeoutError } from './pTimeout';
2
3
  /**
3
4
  * Returns a Function (!), enhanced with retry capabilities.
4
5
  * Implements "Exponential back-off strategy" by multiplying the delay by `delayMultiplier` with each try.
5
6
  */
6
- // eslint-disable-next-line @typescript-eslint/ban-types
7
- export function pRetry(fn, opt = {}) {
8
- const { maxAttempts = 4, delay: initialDelay = 1000, delayMultiplier = 2, predicate, logger = console, name = fn.name, } = opt;
7
+ export function pRetryFn(fn, opt = {}) {
8
+ return async function pRetryFunction(...args) {
9
+ return await pRetry(() => fn.call(this, ...args), opt);
10
+ };
11
+ }
12
+ export async function pRetry(fn, opt = {}) {
13
+ const { maxAttempts = 4, delay: initialDelay = 1000, delayMultiplier = 2, predicate, logger = console, name, keepStackTrace = true, timeout, } = opt;
14
+ const fakeError = keepStackTrace ? new Error('RetryError') : undefined;
9
15
  let { logFirstAttempt = false, logRetries = true, logFailures = false, logSuccess = false } = opt;
10
16
  if (opt.logAll) {
11
- logFirstAttempt = logRetries = logFailures = true;
17
+ logSuccess = logFirstAttempt = logRetries = logFailures = true;
12
18
  }
13
19
  if (opt.logNone) {
14
20
  logSuccess = logFirstAttempt = logRetries = logFailures = false;
15
21
  }
16
- const fname = ['pRetry', name].filter(Boolean).join('.');
17
- return async function (...args) {
18
- let delay = initialDelay;
19
- let attempt = 0;
20
- return await new Promise((resolve, reject) => {
21
- const next = async () => {
22
- const started = Date.now();
23
- try {
24
- attempt++;
25
- if ((attempt === 1 && logFirstAttempt) || (attempt > 1 && logRetries)) {
26
- logger.log(`${fname} attempt #${attempt}...`);
27
- }
28
- const r = await fn.apply(this, args);
29
- if (logSuccess) {
30
- logger.log(`${fname} attempt #${attempt} succeeded in ${_since(started)}`);
31
- }
32
- resolve(r);
22
+ const fname = name || fn.name || 'pRetry function';
23
+ let delay = initialDelay;
24
+ let attempt = 0;
25
+ let timer;
26
+ let timedOut = false;
27
+ return await new Promise((resolve, reject) => {
28
+ const rejectWithTimeout = () => {
29
+ timedOut = true; // to prevent more tries
30
+ const err = new TimeoutError(`"${fname}" timed out after ${timeout} ms`);
31
+ if (fakeError) {
32
+ // keep original stack
33
+ err.stack = fakeError.stack.replace('Error: RetryError', 'TimeoutError');
34
+ }
35
+ reject(err);
36
+ };
37
+ const next = async () => {
38
+ if (timedOut)
39
+ return;
40
+ if (timeout) {
41
+ timer = setTimeout(rejectWithTimeout, timeout);
42
+ }
43
+ const started = Date.now();
44
+ try {
45
+ attempt++;
46
+ if ((attempt === 1 && logFirstAttempt) || (attempt > 1 && logRetries)) {
47
+ logger.log(`${fname} attempt #${attempt}...`);
33
48
  }
34
- catch (err) {
35
- if (logFailures) {
36
- logger.warn(`${fname} attempt #${attempt} error in ${_since(started)}:`, _stringifyAny(err, {
37
- includeErrorData: true,
38
- }));
39
- }
40
- if (attempt >= maxAttempts || (predicate && !predicate(err, attempt, maxAttempts))) {
41
- // Give up
42
- reject(err);
43
- }
44
- else {
45
- // Retry after delay
46
- delay *= delayMultiplier;
47
- setTimeout(next, delay);
49
+ const r = await fn(attempt);
50
+ clearTimeout(timer);
51
+ if (logSuccess) {
52
+ logger.log(`${fname} attempt #${attempt} succeeded in ${_since(started)}`);
53
+ }
54
+ resolve(r);
55
+ }
56
+ catch (err) {
57
+ clearTimeout(timer);
58
+ if (logFailures) {
59
+ logger.warn(`${fname} attempt #${attempt} error in ${_since(started)}:`, _stringifyAny(err, {
60
+ includeErrorData: true,
61
+ }));
62
+ }
63
+ if (attempt >= maxAttempts ||
64
+ (predicate && !predicate(err, attempt, maxAttempts))) {
65
+ // Give up
66
+ if (fakeError) {
67
+ // Preserve the original call stack
68
+ Object.defineProperty(err, 'stack', {
69
+ value: err.stack +
70
+ '\n --' +
71
+ fakeError.stack.replace('Error: RetryError', ''),
72
+ });
48
73
  }
74
+ reject(err);
49
75
  }
50
- };
51
- void next();
52
- });
53
- };
76
+ else {
77
+ // Retry after delay
78
+ delay *= delayMultiplier;
79
+ setTimeout(next, delay);
80
+ }
81
+ }
82
+ };
83
+ void next();
84
+ });
54
85
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naturalcycles/js-lib",
3
- "version": "14.79.0",
3
+ "version": "14.82.0",
4
4
  "scripts": {
5
5
  "prepare": "husky install",
6
6
  "build-prod": "build-prod-esm-cjs",
@@ -1,10 +1,10 @@
1
- import { pRetry, PRetryOptions } from '..'
1
+ import { pRetryFn, PRetryOptions } from '..'
2
2
 
3
3
  // eslint-disable-next-line @typescript-eslint/naming-convention
4
4
  export function _Retry(opt: PRetryOptions = {}): MethodDecorator {
5
5
  return (target, key, descriptor) => {
6
6
  const originalFn = descriptor.value
7
- descriptor.value = pRetry(originalFn as any, opt)
7
+ descriptor.value = pRetryFn(originalFn as any, opt)
8
8
  return descriptor
9
9
  }
10
10
  }
package/src/index.ts CHANGED
@@ -67,14 +67,13 @@ export * from './object/object.util'
67
67
  export * from './object/sortObject'
68
68
  export * from './object/sortObjectDeep'
69
69
  import { AggregatedError } from './promise/AggregatedError'
70
- export * from './promise/pBatch'
71
70
  import { DeferredPromise, pDefer } from './promise/pDefer'
72
71
  export * from './promise/pDelay'
73
72
  export * from './promise/pFilter'
74
73
  export * from './promise/pHang'
75
74
  import { pMap, PMapOptions } from './promise/pMap'
76
75
  export * from './promise/pProps'
77
- import { pRetry, PRetryOptions } from './promise/pRetry'
76
+ import { pRetry, pRetryFn, PRetryOptions } from './promise/pRetry'
78
77
  export * from './promise/pState'
79
78
  import { pTimeout, pTimeoutFn, PTimeoutOptions } from './promise/pTimeout'
80
79
  export * from './promise/pTuple'
@@ -250,6 +249,7 @@ export {
250
249
  pDefer,
251
250
  AggregatedError,
252
251
  pRetry,
252
+ pRetryFn,
253
253
  pTimeout,
254
254
  pTimeoutFn,
255
255
  _tryCatch,
@@ -365,9 +365,9 @@ export class JsonSchemaObjectBuilder<T extends AnyObject> extends JsonSchemaAnyB
365
365
  return this
366
366
  }
367
367
 
368
- baseDBEntity(): JsonSchemaObjectBuilder<T & BaseDBEntity> {
368
+ baseDBEntity<ID = string>(idType = 'string'): JsonSchemaObjectBuilder<T & BaseDBEntity<ID>> {
369
369
  Object.assign(this.schema.properties, {
370
- id: { type: 'string' },
370
+ id: { type: idType },
371
371
  created: { type: 'number', format: 'unixTimestamp' },
372
372
  updated: { type: 'number', format: 'unixTimestamp' },
373
373
  })
@@ -375,8 +375,8 @@ export class JsonSchemaObjectBuilder<T extends AnyObject> extends JsonSchemaAnyB
375
375
  return this
376
376
  }
377
377
 
378
- savedDBEntity(): JsonSchemaObjectBuilder<T & SavedDBEntity> {
379
- return this.baseDBEntity().addRequired(['id', 'created', 'updated']) as any
378
+ savedDBEntity<ID = string>(idType = 'string'): JsonSchemaObjectBuilder<T & SavedDBEntity<ID>> {
379
+ return this.baseDBEntity(idType).addRequired(['id', 'created', 'updated']) as any
380
380
  }
381
381
 
382
382
  extend<T2 extends AnyObject>(s2: JsonSchemaObjectBuilder<T2>): JsonSchemaObjectBuilder<T & T2> {
@@ -7,20 +7,15 @@ export class AggregatedError<RESULT = any> extends Error {
7
7
  errors!: Error[]
8
8
  results!: RESULT[]
9
9
 
10
- constructor(errors: (Error | string)[], results: RESULT[] = []) {
11
- const mappedErrors = errors.map(e => {
12
- if (typeof e === 'string') return new Error(e)
13
- return e
14
- })
15
-
10
+ constructor(errors: Error[], results: RESULT[] = []) {
16
11
  const message = [
17
12
  `${errors.length} errors:`,
18
- ...mappedErrors.map((e, i) => `${i + 1}. ${e.message}`),
13
+ ...errors.map((e, i) => `${i + 1}. ${e.message}`),
19
14
  ].join('\n')
20
15
 
21
16
  super(message)
22
17
 
23
- this.errors = mappedErrors
18
+ this.errors = errors
24
19
  this.results = results
25
20
 
26
21
  Object.defineProperty(this, 'name', {
@@ -1,16 +1,7 @@
1
- import { AbortableAsyncPredicate } from '../types'
2
- import { pMap, PMapOptions } from './pMap'
1
+ import { AsyncPredicate } from '../types'
3
2
 
4
- export async function pFilter<T>(
5
- iterable: Iterable<T | PromiseLike<T>>,
6
- filterFn: AbortableAsyncPredicate<T>,
7
- opt?: PMapOptions,
8
- ): Promise<T[]> {
9
- const values = await pMap(
10
- iterable,
11
- async (item, index) => await Promise.all([filterFn(item, index), item]),
12
- opt,
13
- )
14
-
15
- return values.filter(value => Boolean(value[0])).map(value => value[1])
3
+ export async function pFilter<T>(iterable: Iterable<T>, filterFn: AsyncPredicate<T>): Promise<T[]> {
4
+ const items = [...iterable]
5
+ const predicates = await Promise.all(items.map((item, i) => filterFn(item, i)))
6
+ return items.filter((item, i) => predicates[i])
16
7
  }
@@ -55,36 +55,85 @@ export interface PMapOptions {
55
55
  * })();
56
56
  */
57
57
  export async function pMap<IN, OUT>(
58
- iterable: Iterable<IN | PromiseLike<IN>>,
58
+ iterable: Iterable<IN>,
59
59
  mapper: AbortableAsyncMapper<IN, OUT>,
60
60
  opt: PMapOptions = {},
61
61
  ): Promise<OUT[]> {
62
- return new Promise<OUT[]>((resolve, reject) => {
63
- const { concurrency = Number.POSITIVE_INFINITY, errorMode = ErrorMode.THROW_IMMEDIATELY } = opt
62
+ const ret: (OUT | typeof SKIP)[] = []
63
+ // const iterator = iterable[Symbol.iterator]()
64
+ const items = [...iterable]
65
+ const itemsLength = items.length
66
+ if (itemsLength === 0) return [] // short circuit
67
+
68
+ const { concurrency = itemsLength, errorMode = ErrorMode.THROW_IMMEDIATELY } = opt
69
+
70
+ const errors: Error[] = []
71
+ let isSettled = false
72
+ let resolvingCount = 0
73
+ let currentIndex = 0
74
+
75
+ // Special cases that are able to preserve async stack traces
76
+
77
+ if (concurrency === 1) {
78
+ // Special case for concurrency == 1
79
+
80
+ for await (const item of items) {
81
+ try {
82
+ const r = await mapper(item, currentIndex++)
83
+ if (r === END) break
84
+ if (r !== SKIP) ret.push(r)
85
+ } catch (err) {
86
+ if (errorMode === ErrorMode.THROW_IMMEDIATELY) throw err
87
+ if (errorMode === ErrorMode.THROW_AGGREGATED) {
88
+ errors.push(err as Error)
89
+ }
90
+ // otherwise, suppress completely
91
+ }
92
+ }
93
+
94
+ if (errors.length) {
95
+ throw new AggregatedError(errors, ret)
96
+ }
97
+
98
+ return ret as OUT[]
99
+ } else if (!opt.concurrency || items.length <= opt.concurrency) {
100
+ // Special case for concurrency == infinity or iterable.length < concurrency
101
+
102
+ if (errorMode === ErrorMode.THROW_IMMEDIATELY) {
103
+ return (await Promise.all(items.map((item, i) => mapper(item, i)))).filter(
104
+ r => r !== SKIP && r !== END,
105
+ ) as OUT[]
106
+ }
107
+
108
+ ;(await Promise.allSettled(items.map((item, i) => mapper(item, i)))).forEach(r => {
109
+ if (r.status === 'fulfilled') {
110
+ if (r.value !== SKIP && r.value !== END) ret.push(r.value)
111
+ } else if (errorMode === ErrorMode.THROW_AGGREGATED) {
112
+ errors.push(r.reason)
113
+ }
114
+ })
115
+
116
+ if (errors.length) {
117
+ throw new AggregatedError(errors, ret)
118
+ }
64
119
 
65
- const ret: (OUT | typeof SKIP)[] = []
66
- const iterator = iterable[Symbol.iterator]()
67
- const errors: Error[] = []
68
- let isSettled = false
69
- let isIterableDone = false
70
- let resolvingCount = 0
71
- let currentIndex = 0
120
+ return ret as OUT[]
121
+ }
72
122
 
73
- const next = (skipped = false) => {
123
+ return new Promise<OUT[]>((resolve, reject) => {
124
+ const next = () => {
74
125
  if (isSettled) {
75
126
  return
76
127
  }
77
128
 
78
- const nextItem = iterator.next()
79
- const i = currentIndex
80
- if (!skipped) currentIndex++
81
-
82
- if (nextItem.done) {
83
- isIterableDone = true
129
+ const nextItem = items[currentIndex]!
130
+ const i = currentIndex++
84
131
 
132
+ if (currentIndex > itemsLength) {
85
133
  if (resolvingCount === 0) {
134
+ isSettled = true
86
135
  const r = ret.filter(r => r !== SKIP) as OUT[]
87
- if (errors.length && errorMode === ErrorMode.THROW_AGGREGATED) {
136
+ if (errors.length) {
88
137
  reject(new AggregatedError(errors, r))
89
138
  } else {
90
139
  resolve(r)
@@ -96,7 +145,7 @@ export async function pMap<IN, OUT>(
96
145
 
97
146
  resolvingCount++
98
147
 
99
- Promise.resolve(nextItem.value)
148
+ Promise.resolve(nextItem)
100
149
  .then(async element => await mapper(element, i))
101
150
  .then(
102
151
  value => {
@@ -114,7 +163,9 @@ export async function pMap<IN, OUT>(
114
163
  isSettled = true
115
164
  reject(err)
116
165
  } else {
117
- errors.push(err)
166
+ if (errorMode === ErrorMode.THROW_AGGREGATED) {
167
+ errors.push(err)
168
+ }
118
169
  resolvingCount--
119
170
  next()
120
171
  }
@@ -125,7 +176,7 @@ export async function pMap<IN, OUT>(
125
176
  for (let i = 0; i < concurrency; i++) {
126
177
  next()
127
178
 
128
- if (isIterableDone) {
179
+ if (isSettled) {
129
180
  break
130
181
  }
131
182
  }