@naturalcycles/js-lib 14.80.0 → 14.83.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.
Files changed (49) hide show
  1. package/dist/decorators/asyncMemo.decorator.d.ts +22 -0
  2. package/dist/decorators/asyncMemo.decorator.js +96 -0
  3. package/dist/decorators/memo.decorator.d.ts +8 -0
  4. package/dist/decorators/memo.decorator.js +11 -6
  5. package/dist/decorators/memo.util.d.ts +32 -8
  6. package/dist/decorators/memo.util.js +17 -2
  7. package/dist/decorators/retry.decorator.js +1 -1
  8. package/dist/error/error.model.d.ts +6 -0
  9. package/dist/index.d.ts +5 -5
  10. package/dist/index.js +3 -2
  11. package/dist/json-schema/jsonSchemaBuilder.d.ts +2 -2
  12. package/dist/json-schema/jsonSchemaBuilder.js +4 -4
  13. package/dist/json-schema/jsonSchemas.d.ts +2 -2
  14. package/dist/promise/AggregatedError.d.ts +1 -1
  15. package/dist/promise/AggregatedError.js +2 -7
  16. package/dist/promise/pFilter.d.ts +2 -3
  17. package/dist/promise/pFilter.js +4 -4
  18. package/dist/promise/pMap.d.ts +1 -1
  19. package/dist/promise/pMap.js +67 -19
  20. package/dist/promise/pRetry.d.ts +17 -2
  21. package/dist/promise/pRetry.js +72 -40
  22. package/dist/types.d.ts +5 -5
  23. package/dist-esm/decorators/asyncMemo.decorator.js +104 -0
  24. package/dist-esm/decorators/memo.decorator.js +11 -5
  25. package/dist-esm/decorators/memo.util.js +15 -1
  26. package/dist-esm/decorators/retry.decorator.js +2 -2
  27. package/dist-esm/index.js +3 -3
  28. package/dist-esm/json-schema/jsonSchemaBuilder.js +4 -4
  29. package/dist-esm/promise/AggregatedError.js +2 -7
  30. package/dist-esm/promise/pFilter.js +4 -4
  31. package/dist-esm/promise/pMap.js +79 -19
  32. package/dist-esm/promise/pRetry.js +70 -39
  33. package/package.json +1 -1
  34. package/src/decorators/asyncMemo.decorator.ts +151 -0
  35. package/src/decorators/memo.decorator.ts +16 -5
  36. package/src/decorators/memo.util.ts +49 -10
  37. package/src/decorators/retry.decorator.ts +2 -2
  38. package/src/error/error.model.ts +7 -0
  39. package/src/index.ts +5 -3
  40. package/src/json-schema/jsonSchemaBuilder.ts +4 -4
  41. package/src/promise/AggregatedError.ts +3 -8
  42. package/src/promise/pFilter.ts +5 -14
  43. package/src/promise/pMap.ts +72 -21
  44. package/src/promise/pRetry.ts +105 -41
  45. package/src/types.ts +5 -5
  46. package/dist/promise/pBatch.d.ts +0 -7
  47. package/dist/promise/pBatch.js +0 -30
  48. package/dist-esm/promise/pBatch.js +0 -23
  49. package/src/promise/pBatch.ts +0 -31
@@ -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
  }
@@ -1,4 +1,5 @@
1
1
  import { _since, _stringifyAny, AnyFunction, CommonLogger } from '..'
2
+ import { TimeoutError } from './pTimeout'
2
3
 
3
4
  export interface PRetryOptions {
4
5
  /**
@@ -7,6 +8,13 @@ export interface PRetryOptions {
7
8
  */
8
9
  name?: string
9
10
 
11
+ /**
12
+ * Timeout for each Try, in milliseconds.
13
+ *
14
+ * Defaults to no timeout.
15
+ */
16
+ timeout?: number
17
+
10
18
  /**
11
19
  * How many attempts to try.
12
20
  * First attempt is not a retry, but "initial try". It still counts.
@@ -34,7 +42,7 @@ export interface PRetryOptions {
34
42
  *
35
43
  * @default () => true
36
44
  */
37
- predicate?: (err: unknown, attempt: number, maxAttempts: number) => boolean
45
+ predicate?: (err: Error, attempt: number, maxAttempts: number) => boolean
38
46
 
39
47
  /**
40
48
  * Log the first attempt (which is not a "retry" yet).
@@ -74,76 +82,132 @@ export interface PRetryOptions {
74
82
  * Default to `console`
75
83
  */
76
84
  logger?: CommonLogger
85
+
86
+ /**
87
+ * Defaults to true.
88
+ * If true - preserves the stack trace in case of a Timeout (usually - very useful!).
89
+ * It has a certain perf cost.
90
+ *
91
+ * @experimental
92
+ */
93
+ keepStackTrace?: boolean
77
94
  }
78
95
 
79
96
  /**
80
97
  * Returns a Function (!), enhanced with retry capabilities.
81
98
  * Implements "Exponential back-off strategy" by multiplying the delay by `delayMultiplier` with each try.
82
99
  */
83
- // eslint-disable-next-line @typescript-eslint/ban-types
84
- export function pRetry<T extends AnyFunction>(fn: T, opt: PRetryOptions = {}): T {
100
+ export function pRetryFn<T extends AnyFunction>(fn: T, opt: PRetryOptions = {}): T {
101
+ return async function pRetryFunction(this: any, ...args: any[]) {
102
+ return await pRetry(() => fn.call(this, ...args), opt)
103
+ } as any
104
+ }
105
+
106
+ export async function pRetry<T>(
107
+ fn: (attempt: number) => Promise<T>,
108
+ opt: PRetryOptions = {},
109
+ ): Promise<T> {
85
110
  const {
86
111
  maxAttempts = 4,
87
112
  delay: initialDelay = 1000,
88
113
  delayMultiplier = 2,
89
114
  predicate,
90
115
  logger = console,
91
- name = fn.name,
116
+ name,
117
+ keepStackTrace = true,
118
+ timeout,
92
119
  } = opt
93
120
 
121
+ const fakeError = keepStackTrace ? new Error('RetryError') : undefined
122
+
94
123
  let { logFirstAttempt = false, logRetries = true, logFailures = false, logSuccess = false } = opt
95
124
 
96
125
  if (opt.logAll) {
97
- logFirstAttempt = logRetries = logFailures = true
126
+ logSuccess = logFirstAttempt = logRetries = logFailures = true
98
127
  }
99
128
  if (opt.logNone) {
100
129
  logSuccess = logFirstAttempt = logRetries = logFailures = false
101
130
  }
102
131
 
103
- const fname = ['pRetry', name].filter(Boolean).join('.')
132
+ const fname = name || fn.name || 'pRetry function'
104
133
 
105
- return async function (this: any, ...args: any[]) {
106
- let delay = initialDelay
107
- let attempt = 0
134
+ let delay = initialDelay
135
+ let attempt = 0
136
+ let timer: NodeJS.Timeout | undefined
137
+ let timedOut = false
108
138
 
109
- return await new Promise((resolve, reject) => {
110
- const next = async () => {
111
- const started = Date.now()
139
+ return await new Promise((resolve, reject) => {
140
+ const rejectWithTimeout = () => {
141
+ timedOut = true // to prevent more tries
142
+ const err = new TimeoutError(`"${fname}" timed out after ${timeout} ms`)
143
+ if (fakeError) {
144
+ // keep original stack
145
+ err.stack = fakeError.stack!.replace('Error: RetryError', 'TimeoutError')
146
+ }
147
+ reject(err)
148
+ }
112
149
 
113
- try {
114
- attempt++
115
- if ((attempt === 1 && logFirstAttempt) || (attempt > 1 && logRetries)) {
116
- logger.log(`${fname} attempt #${attempt}...`)
117
- }
150
+ const next = async () => {
151
+ if (timedOut) return
118
152
 
119
- const r = await fn.apply(this, args)
153
+ if (timeout) {
154
+ timer = setTimeout(rejectWithTimeout, timeout)
155
+ }
120
156
 
121
- if (logSuccess) {
122
- logger.log(`${fname} attempt #${attempt} succeeded in ${_since(started)}`)
123
- }
124
- resolve(r)
125
- } catch (err) {
126
- if (logFailures) {
127
- logger.warn(
128
- `${fname} attempt #${attempt} error in ${_since(started)}:`,
129
- _stringifyAny(err, {
130
- includeErrorData: true,
131
- }),
132
- )
133
- }
157
+ const started = Date.now()
158
+
159
+ try {
160
+ attempt++
161
+ if ((attempt === 1 && logFirstAttempt) || (attempt > 1 && logRetries)) {
162
+ logger.log(`${fname} attempt #${attempt}...`)
163
+ }
164
+
165
+ const r = await fn(attempt)
166
+
167
+ clearTimeout(timer!)
134
168
 
135
- if (attempt >= maxAttempts || (predicate && !predicate(err, attempt, maxAttempts))) {
136
- // Give up
137
- reject(err)
138
- } else {
139
- // Retry after delay
140
- delay *= delayMultiplier
141
- setTimeout(next, delay)
169
+ if (logSuccess) {
170
+ logger.log(`${fname} attempt #${attempt} succeeded in ${_since(started)}`)
171
+ }
172
+
173
+ resolve(r)
174
+ } catch (err) {
175
+ clearTimeout(timer!)
176
+
177
+ if (logFailures) {
178
+ logger.warn(
179
+ `${fname} attempt #${attempt} error in ${_since(started)}:`,
180
+ _stringifyAny(err, {
181
+ includeErrorData: true,
182
+ }),
183
+ )
184
+ }
185
+
186
+ if (
187
+ attempt >= maxAttempts ||
188
+ (predicate && !predicate(err as Error, attempt, maxAttempts))
189
+ ) {
190
+ // Give up
191
+
192
+ if (fakeError) {
193
+ // Preserve the original call stack
194
+ Object.defineProperty(err, 'stack', {
195
+ value:
196
+ (err as Error).stack +
197
+ '\n --' +
198
+ fakeError.stack!.replace('Error: RetryError', ''),
199
+ })
142
200
  }
201
+
202
+ reject(err)
203
+ } else {
204
+ // Retry after delay
205
+ delay *= delayMultiplier
206
+ setTimeout(next, delay)
143
207
  }
144
208
  }
209
+ }
145
210
 
146
- void next()
147
- })
148
- } as any
211
+ void next()
212
+ })
149
213
  }
package/src/types.ts CHANGED
@@ -169,8 +169,8 @@ export type UnixTimestamp = number
169
169
  /**
170
170
  * Base interface for any Entity that was saved to DB.
171
171
  */
172
- export interface SavedDBEntity {
173
- id: string
172
+ export interface SavedDBEntity<ID = string> {
173
+ id: ID
174
174
 
175
175
  /**
176
176
  * unixTimestamp of when the entity was first created (in the DB).
@@ -189,10 +189,10 @@ export interface SavedDBEntity {
189
189
  * hence `id`, `created` and `updated` fields CAN BE undefined (yet).
190
190
  * When it's known to be saved - `SavedDBEntity` interface can be used instead.
191
191
  */
192
- export type BaseDBEntity = Partial<SavedDBEntity>
192
+ export type BaseDBEntity<ID = string> = Partial<SavedDBEntity<ID>>
193
193
 
194
- export type Saved<E> = Merge<E, SavedDBEntity>
195
- export type Unsaved<E> = Merge<E, BaseDBEntity>
194
+ export type Saved<E, ID = string> = Merge<E, SavedDBEntity<ID>>
195
+ export type Unsaved<E, ID = string> = Merge<E, BaseDBEntity<ID>>
196
196
 
197
197
  /**
198
198
  * Named type for JSON.parse / JSON.stringify second argument
@@ -1,7 +0,0 @@
1
- import { AbortableAsyncMapper, BatchResult } from '..';
2
- /**
3
- * Like pMap, but doesn't fail on errors, instead returns both successful results and errors.
4
- */
5
- export declare function pBatch<IN, OUT>(iterable: Iterable<IN | PromiseLike<IN>>, mapper: AbortableAsyncMapper<IN, OUT>, opt?: {
6
- concurrency?: number;
7
- }): Promise<BatchResult<OUT>>;
@@ -1,30 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.pBatch = void 0;
4
- const __1 = require("..");
5
- const pMap_1 = require("./pMap");
6
- /**
7
- * Like pMap, but doesn't fail on errors, instead returns both successful results and errors.
8
- */
9
- async function pBatch(iterable, mapper, opt) {
10
- try {
11
- const results = await (0, pMap_1.pMap)(iterable, mapper, {
12
- ...opt,
13
- errorMode: __1.ErrorMode.THROW_AGGREGATED,
14
- });
15
- return {
16
- results,
17
- errors: [],
18
- };
19
- }
20
- catch (err) {
21
- const { errors, results } = err;
22
- if (!errors || !results)
23
- throw err; // not an AggregatedError
24
- return {
25
- results,
26
- errors,
27
- };
28
- }
29
- }
30
- exports.pBatch = pBatch;
@@ -1,23 +0,0 @@
1
- import { ErrorMode } from '..';
2
- import { pMap } from './pMap';
3
- /**
4
- * Like pMap, but doesn't fail on errors, instead returns both successful results and errors.
5
- */
6
- export async function pBatch(iterable, mapper, opt) {
7
- try {
8
- const results = await pMap(iterable, mapper, Object.assign(Object.assign({}, opt), { errorMode: ErrorMode.THROW_AGGREGATED }));
9
- return {
10
- results,
11
- errors: [],
12
- };
13
- }
14
- catch (err) {
15
- const { errors, results } = err;
16
- if (!errors || !results)
17
- throw err; // not an AggregatedError
18
- return {
19
- results,
20
- errors,
21
- };
22
- }
23
- }
@@ -1,31 +0,0 @@
1
- import { AbortableAsyncMapper, BatchResult, ErrorMode } from '..'
2
- import { AggregatedError } from './AggregatedError'
3
- import { pMap } from './pMap'
4
-
5
- /**
6
- * Like pMap, but doesn't fail on errors, instead returns both successful results and errors.
7
- */
8
- export async function pBatch<IN, OUT>(
9
- iterable: Iterable<IN | PromiseLike<IN>>,
10
- mapper: AbortableAsyncMapper<IN, OUT>,
11
- opt?: { concurrency?: number },
12
- ): Promise<BatchResult<OUT>> {
13
- try {
14
- const results = await pMap(iterable, mapper, {
15
- ...opt,
16
- errorMode: ErrorMode.THROW_AGGREGATED,
17
- })
18
- return {
19
- results,
20
- errors: [],
21
- }
22
- } catch (err) {
23
- const { errors, results } = err as AggregatedError<OUT>
24
- if (!errors || !results) throw err // not an AggregatedError
25
-
26
- return {
27
- results,
28
- errors,
29
- }
30
- }
31
- }