@handy-common-utils/promise-utils 1.0.6 → 1.2.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.
package/README.md CHANGED
@@ -1,6 +1,12 @@
1
1
  # @handy-common-utils/promise-utils
2
2
 
3
- Promise related utilities
3
+ These Promise related utilities have 100% test coverage. The package is tiny because there is no dependency on any other package.
4
+ Functions provided are `repeat`, `withRetry`, `inParallel`, `delayedResolve`, `delayedReject`, `timoutResolve`, `timeoutReject`, `promiseState`, `synchronized`, etc.
5
+
6
+ [![Version](https://img.shields.io/npm/v/@handy-common-utils/promise-utils.svg)](https://npmjs.org/package/@handy-common-utils/promise-utils)
7
+ [![Downloads/week](https://img.shields.io/npm/dw/@handy-common-utils/promise-utils.svg)](https://npmjs.org/package/@handy-common-utils/promise-utils)
8
+ [![CI](https://github.com/handy-common-utils/promise-utils/actions/workflows/ci.yml/badge.svg)](https://github.com/handy-common-utils/promise-utils/actions/workflows/ci.yml)
9
+ [![codecov](https://codecov.io/gh/handy-common-utils/promise-utils/branch/master/graph/badge.svg?token=QBL6AB3CL5)](https://codecov.io/gh/handy-common-utils/promise-utils)
4
10
 
5
11
  ## How to use
6
12
 
@@ -1,4 +1,4 @@
1
- declare type InParrellelResult<T> = T extends void ? void : Array<T>;
1
+ export declare const FIBONACCI_SEQUENCE: number[];
2
2
  export declare enum PromiseState {
3
3
  Pending = "Pending",
4
4
  Fulfilled = "Fulfilled",
@@ -10,7 +10,7 @@ export declare abstract class PromiseUtils {
10
10
  * This function is useful for client side pagination.
11
11
  *
12
12
  * @example
13
- * const domainNameObjects = await Utils.repeat(
13
+ * const domainNameObjects = await PromiseUtils.repeat(
14
14
  * pagingParam => apig.getDomainNames({limit: 500, ...pagingParam}).promise(),
15
15
  * esponse => response.position? {position: response.position} : null,
16
16
  * (collection, response) => collection.concat(response.items!),
@@ -25,19 +25,43 @@ export declare abstract class PromiseUtils {
25
25
  * @param nextParameter The function for calculating next parameter from the operation result.
26
26
  * Normally the parameter controls paging,
27
27
  * This function should return null when next invocation of the operation function is not desired.
28
+ * If next invocation is desired, the return value of this function can be a Promise or not a Promise.
28
29
  * @param collect the function for merging operation result into the collection
29
30
  * @param initialCollection initial collection which would be the first argument passed into the first invocation of the collect function
30
31
  * @param initialParameter the parameter for the first operation
31
32
  * @returns Promise of collection of all the results returned by the operation function
32
33
  *
33
34
  */
34
- static repeat<Result, Param, Collection>(operation: (parameter: Partial<Param>) => Promise<Result>, nextParameter: (response: Result) => Partial<Param> | null, collect: (collection: Collection, result: Result) => Collection, initialCollection: Collection, initialParameter?: Partial<Param>): Promise<Collection>;
35
+ static repeat<Result, Param, Collection>(operation: (parameter: Partial<Param>) => Promise<Result>, nextParameter: (response: Result) => Partial<Param> | Promise<Partial<Param>> | null, collect: (collection: Collection, result: Result) => Collection, initialCollection: Collection, initialParameter?: Partial<Param>): Promise<Collection>;
36
+ /**
37
+ * Do an operation repeatedly until a criteria is met.
38
+ *
39
+ * @example
40
+ * const result = await PromiseUtils.withRetry(() => doSomething(), [100, 200, 300, 500, 800, 1000]);
41
+ *
42
+ * @template Result type of the operation result
43
+ * @template TError type of the possible error that could be generated by the operation
44
+ *
45
+ * @param operation a function that outputs a Promise result, normally the operation does not use its arguments
46
+ * @param backoff Array of retry backoff periods (unit: milliseconds) or function for calculating them.
47
+ * If retry is desired, before making next call to the operation the desired backoff period would be waited.
48
+ * If the array runs out of elements or the function returns `undefined` or either the array or the function returns a negative number,
49
+ * there would be no further call to the operation.
50
+ * The `attempt` argument passed into backoff function starts from 2 because only retries need to backoff,
51
+ * so the first retry is the second attempt.
52
+ * @param shouldRetry Predicate function for deciding whether another call to the operation should happen.
53
+ * If this argument is not defined, retry would happen whenever the operation rejects with an error.
54
+ * `shouldRetry` would be evaluated before `backoff`.
55
+ * The `attempt` argument passed into shouldRetry function starts from 1.
56
+ * @returns Promise of the operation result potentially with retries already applied
57
+ */
58
+ static withRetry<Result, TError = any>(operation: (attempt: number, previousResult: Result | undefined, previousError: TError | undefined) => Promise<Result>, backoff: Array<number> | ((attempt: number, previousResult: Result | undefined, previousError: TError | undefined) => number | undefined), shouldRetry?: (previousError: TError | undefined, previousResult: Result | undefined, attempt: number) => boolean): Promise<Result>;
35
59
  /**
36
60
  * Run multiple jobs/operations in parallel.
37
61
  *
38
62
  * @example
39
63
  * const topicArns = topics.map(topic => topic.TopicArn!);
40
- * await Utils.inParallel(5, topicArns, async topicArn => {
64
+ * await PromiseUtils.inParallel(5, topicArns, async topicArn => {
41
65
  * const topicAttributes = (await sns.getTopicAttributes({ TopicArn: topicArn }).promise()).Attributes!;
42
66
  * const topicDetails = { ...topicAttributes, subscriptions: [] } as any;
43
67
  * if (this.shouldInclude(topicArn)) {
@@ -53,41 +77,45 @@ export declare abstract class PromiseUtils {
53
77
  * This function is safe when there are infinite unknown number of elements in the job data.
54
78
  * @param operation the function that turns job data into result asynchronously
55
79
  * @returns Promise of void if the operation function does not return a value,
56
- * or promise of an arry containing results returned from the operation function.
80
+ * or promise of an array containing results returned from the operation function.
81
+ * In the array containing results, each element is either the fulfilled result, or the rejected error/reason.
57
82
  */
58
- static inParallel<Data, Result>(parallelism: number, jobs: Iterable<Data>, operation: (job: Data, index: number) => Promise<Result>): Promise<InParrellelResult<Result>>;
83
+ static inParallel<Data, Result, TError = Result>(parallelism: number, jobs: Iterable<Data>, operation: (job: Data, index: number) => Promise<Result>): Promise<Array<Result | TError>>;
59
84
  /**
60
85
  * Create a Promise that resolves after number of milliseconds specified
61
86
  * @param ms number of milliseconds after which the created Promise would resolve
62
- * @param result the result to be resolved for the Promise
87
+ * @param result the result to be resolved for the Promise, or a function that supplies the reuslt.
63
88
  * @returns the new Promise created
64
89
  */
65
- static delayedResolve<T>(ms: number, result?: T | PromiseLike<T> | undefined): Promise<T>;
90
+ static delayedResolve<T>(ms: number, result?: T | PromiseLike<T> | (() => (T | PromiseLike<T>))): Promise<T>;
66
91
  /**
67
- * Create a Promise that rejects after number of milliseconds specified
92
+ * Create a Promise that rejects after number of milliseconds specified.
68
93
  * @param ms number of milliseconds after which the created Promise would reject
69
- * @param reason the reason of the rejection for the Promise
94
+ * @param reason the reason of the rejection for the Promise, or a function that supplies the reason.
95
+ * If the reason ends up to be a rejected Promise, then the outcome (could be fulfilled or rejected) of it will be the reject reason of the Promise returned.
70
96
  * @returns the new Promise created
71
97
  */
72
- static delayedReject<T = never>(ms: number, reason: any): Promise<T>;
98
+ static delayedReject<T = never, R = any>(ms: number, reason: R | PromiseLike<R> | (() => R | PromiseLike<R>)): Promise<T>;
73
99
  /**
74
100
  * Apply timeout to an operation, in case timeout happens, resolve to the result specified.
75
101
  * If timeout does not happen, the resolved result or rejection reason of the original operation would be returned.
102
+ * If timeout does not happen and result is a function, the function won't be called.
76
103
  * @param operation the original operation that timeout would be applied
77
104
  * @param ms number of milliseconds for the timeout
78
- * @param result the result to be resolved in case timeout happens
105
+ * @param result the result to be resolved in case timeout happens, or a function that supplies the reuslt.
79
106
  * @return the new Promise that resolves to the specified result in case timeout happens
80
107
  */
81
- static timeoutResolve<T>(operation: Promise<T>, ms: number, result?: T | PromiseLike<T> | undefined): Promise<T>;
108
+ static timeoutResolve<T>(operation: Promise<T>, ms: number, result?: T | PromiseLike<T> | (() => (T | PromiseLike<T>)) | undefined): Promise<T>;
82
109
  /**
83
110
  * Apply timeout to an operation, in case timeout happens, reject with the reason specified.
84
111
  * If timeout does not happen, the resolved result or rejection reason of the original operation would be returned.
112
+ * If timeout does not happen and rejectReason is a function, the function won't be called.
85
113
  * @param operation the original operation that timeout would be applied
86
114
  * @param ms number of milliseconds for the timeout
87
- * @param rejectReason the reason of the rejection in case timeout happens
115
+ * @param rejectReason the reason of the rejection in case timeout happens, or a function that supplies the reason.
88
116
  * @return the new Promise that rejects with the specified reason in case timeout happens
89
117
  */
90
- static timeoutReject<T>(operation: Promise<T>, ms: number, rejectReason: any): Promise<T>;
118
+ static timeoutReject<T = never, R = any>(operation: Promise<T>, ms: number, rejectReason: R | PromiseLike<R> | (() => R | PromiseLike<R>)): Promise<T>;
91
119
  /**
92
120
  * Get the state of the Promise.
93
121
  * Please note that the returned value is a Promise, although it resolves immediately.
@@ -99,21 +127,23 @@ export declare abstract class PromiseUtils {
99
127
  /**
100
128
  * Equivalent of `synchronized` in Java.
101
129
  * In any situation there's no concurrent execution of any operation function associated with the same lock.
102
- * The operation function has access to the state (when `synchronized` is called), settledState (when the operation function is called), and result of the previous operation.
130
+ * The operation function has access to the state (when `synchronized` is called), settledState (when the operation function is called),
131
+ * and result (could be the fulfilled result or the rejected reason) of the previous operation.
103
132
  * In case there is no previous invocation, state, settledState and result would all be undefined.
104
133
  * @param lock the object (could be a string, a number, or `this` in a class) that is used to apply the lock
105
134
  * @param operation function for doing the computation and returning a Promise
106
135
  * @returns the result of the operation function
107
136
  */
108
- static synchronized<T>(lock: any, operation: (previousState: PromiseState | undefined, previousSettledState: PromiseState | undefined, previousResult: any) => Promise<T>): Promise<T>;
137
+ static synchronized<T>(lock: unknown, operation: (previousState: PromiseState | undefined, previousSettledState: PromiseState | undefined, previousResult: any) => Promise<T>): Promise<T>;
109
138
  }
110
139
  export declare const repeat: typeof PromiseUtils.repeat;
140
+ export declare const withRetry: typeof PromiseUtils.withRetry;
111
141
  export declare const inParallel: typeof PromiseUtils.inParallel;
112
142
  export declare const delayedResolve: typeof PromiseUtils.delayedResolve;
113
143
  export declare const delayedReject: typeof PromiseUtils.delayedReject;
114
144
  export declare const timeoutResolve: typeof PromiseUtils.timeoutResolve;
115
145
  export declare const timeoutReject: typeof PromiseUtils.timeoutReject;
116
146
  export declare const synchronized: typeof PromiseUtils.synchronized;
147
+ export declare const synchronised: typeof PromiseUtils.synchronized;
117
148
  export declare const promiseState: typeof PromiseUtils.promiseState;
118
- export {};
119
149
  //# sourceMappingURL=promise-utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"promise-utils.d.ts","sourceRoot":"","sources":["../src/promise-utils.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"promise-utils.d.ts","sourceRoot":"","sources":["../src/promise-utils.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
+ /* eslint-disable unicorn/no-null */
3
+ /* eslint-disable no-await-in-loop */
2
4
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.promiseState = exports.synchronized = exports.timeoutReject = exports.timeoutResolve = exports.delayedReject = exports.delayedResolve = exports.inParallel = exports.repeat = exports.PromiseUtils = exports.PromiseState = void 0;
5
+ exports.promiseState = exports.synchronised = exports.synchronized = exports.timeoutReject = exports.timeoutResolve = exports.delayedReject = exports.delayedResolve = exports.inParallel = exports.withRetry = exports.repeat = exports.PromiseUtils = exports.PromiseState = exports.FIBONACCI_SEQUENCE = void 0;
6
+ exports.FIBONACCI_SEQUENCE = [1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811];
4
7
  var PromiseState;
5
8
  (function (PromiseState) {
6
9
  PromiseState["Pending"] = "Pending";
@@ -13,7 +16,7 @@ class PromiseUtils {
13
16
  * This function is useful for client side pagination.
14
17
  *
15
18
  * @example
16
- * const domainNameObjects = await Utils.repeat(
19
+ * const domainNameObjects = await PromiseUtils.repeat(
17
20
  * pagingParam => apig.getDomainNames({limit: 500, ...pagingParam}).promise(),
18
21
  * esponse => response.position? {position: response.position} : null,
19
22
  * (collection, response) => collection.concat(response.items!),
@@ -28,6 +31,7 @@ class PromiseUtils {
28
31
  * @param nextParameter The function for calculating next parameter from the operation result.
29
32
  * Normally the parameter controls paging,
30
33
  * This function should return null when next invocation of the operation function is not desired.
34
+ * If next invocation is desired, the return value of this function can be a Promise or not a Promise.
31
35
  * @param collect the function for merging operation result into the collection
32
36
  * @param initialCollection initial collection which would be the first argument passed into the first invocation of the collect function
33
37
  * @param initialParameter the parameter for the first operation
@@ -42,16 +46,60 @@ class PromiseUtils {
42
46
  // eslint-disable-next-line no-await-in-loop
43
47
  const result = await operation(param);
44
48
  collection = collect(collection, result);
45
- param = nextParameter(result);
46
- } while (param !== null);
49
+ const paramOrPromise = nextParameter(result);
50
+ if (paramOrPromise === null) {
51
+ break;
52
+ }
53
+ param = await paramOrPromise;
54
+ } while (true);
47
55
  return collection;
48
56
  }
57
+ /**
58
+ * Do an operation repeatedly until a criteria is met.
59
+ *
60
+ * @example
61
+ * const result = await PromiseUtils.withRetry(() => doSomething(), [100, 200, 300, 500, 800, 1000]);
62
+ *
63
+ * @template Result type of the operation result
64
+ * @template TError type of the possible error that could be generated by the operation
65
+ *
66
+ * @param operation a function that outputs a Promise result, normally the operation does not use its arguments
67
+ * @param backoff Array of retry backoff periods (unit: milliseconds) or function for calculating them.
68
+ * If retry is desired, before making next call to the operation the desired backoff period would be waited.
69
+ * If the array runs out of elements or the function returns `undefined` or either the array or the function returns a negative number,
70
+ * there would be no further call to the operation.
71
+ * The `attempt` argument passed into backoff function starts from 2 because only retries need to backoff,
72
+ * so the first retry is the second attempt.
73
+ * @param shouldRetry Predicate function for deciding whether another call to the operation should happen.
74
+ * If this argument is not defined, retry would happen whenever the operation rejects with an error.
75
+ * `shouldRetry` would be evaluated before `backoff`.
76
+ * The `attempt` argument passed into shouldRetry function starts from 1.
77
+ * @returns Promise of the operation result potentially with retries already applied
78
+ */
79
+ static async withRetry(operation, backoff, shouldRetry = (previousError, _previousResult, _attempt) => previousError !== undefined) {
80
+ let attempt = 1;
81
+ const finalOutcome = await PromiseUtils.repeat((previousOutcome) => operation(attempt, previousOutcome.result, previousOutcome.error).then(result => ({ result })).catch(error => ({ error })), outcome => {
82
+ if (!shouldRetry(outcome.error, outcome.result, attempt)) {
83
+ return null;
84
+ }
85
+ const backoffMs = Array.isArray(backoff) ? backoff[attempt - 1] : backoff(attempt, outcome.result, outcome.error);
86
+ if (backoffMs == null || backoffMs < 0) {
87
+ return null;
88
+ }
89
+ attempt++;
90
+ return PromiseUtils.delayedResolve(backoffMs, outcome);
91
+ }, (_, outcome) => outcome, {});
92
+ if (finalOutcome.error !== undefined) {
93
+ throw finalOutcome.error;
94
+ }
95
+ return finalOutcome.result;
96
+ }
49
97
  /**
50
98
  * Run multiple jobs/operations in parallel.
51
99
  *
52
100
  * @example
53
101
  * const topicArns = topics.map(topic => topic.TopicArn!);
54
- * await Utils.inParallel(5, topicArns, async topicArn => {
102
+ * await PromiseUtils.inParallel(5, topicArns, async topicArn => {
55
103
  * const topicAttributes = (await sns.getTopicAttributes({ TopicArn: topicArn }).promise()).Attributes!;
56
104
  * const topicDetails = { ...topicAttributes, subscriptions: [] } as any;
57
105
  * if (this.shouldInclude(topicArn)) {
@@ -67,7 +115,8 @@ class PromiseUtils {
67
115
  * This function is safe when there are infinite unknown number of elements in the job data.
68
116
  * @param operation the function that turns job data into result asynchronously
69
117
  * @returns Promise of void if the operation function does not return a value,
70
- * or promise of an arry containing results returned from the operation function.
118
+ * or promise of an array containing results returned from the operation function.
119
+ * In the array containing results, each element is either the fulfilled result, or the rejected error/reason.
71
120
  */
72
121
  static async inParallel(parallelism, jobs, operation) {
73
122
  if (parallelism < 1) {
@@ -76,7 +125,7 @@ class PromiseUtils {
76
125
  const jobResults = new Array();
77
126
  let index = 0;
78
127
  const iterator = jobs[Symbol.iterator]();
79
- const promises = new Array(Math.floor(parallelism)).fill(0).map(async (_) => {
128
+ const promises = Array.from({ length: Math.floor(parallelism) }).fill(0).map(async (_) => {
80
129
  let iteratorResult;
81
130
  while (true) {
82
131
  iteratorResult = iterator.next();
@@ -86,54 +135,69 @@ class PromiseUtils {
86
135
  const job = iteratorResult.value;
87
136
  const jobIndex = index++;
88
137
  const jobResultPromise = operation(job, jobIndex);
89
- const jobResult = await jobResultPromise;
90
- if (jobResult !== undefined) {
91
- jobResults[jobIndex] = jobResult;
92
- }
138
+ jobResults[jobIndex] = await jobResultPromise.catch(error => error);
93
139
  }
94
140
  });
95
141
  await Promise.all(promises);
96
- return (jobResults.length > 0 ? jobResults : undefined);
142
+ return jobResults;
97
143
  }
98
144
  /**
99
145
  * Create a Promise that resolves after number of milliseconds specified
100
146
  * @param ms number of milliseconds after which the created Promise would resolve
101
- * @param result the result to be resolved for the Promise
147
+ * @param result the result to be resolved for the Promise, or a function that supplies the reuslt.
102
148
  * @returns the new Promise created
103
149
  */
104
150
  static delayedResolve(ms, result) {
105
- return new Promise(resolve => setTimeout(() => resolve(result), ms));
151
+ return new Promise(resolve => setTimeout(() => resolve(typeof result === 'function' ? result() : result), ms));
106
152
  }
107
153
  /**
108
- * Create a Promise that rejects after number of milliseconds specified
154
+ * Create a Promise that rejects after number of milliseconds specified.
109
155
  * @param ms number of milliseconds after which the created Promise would reject
110
- * @param reason the reason of the rejection for the Promise
156
+ * @param reason the reason of the rejection for the Promise, or a function that supplies the reason.
157
+ * If the reason ends up to be a rejected Promise, then the outcome (could be fulfilled or rejected) of it will be the reject reason of the Promise returned.
111
158
  * @returns the new Promise created
112
159
  */
113
160
  static delayedReject(ms, reason) {
114
- return new Promise((_resolve, reject) => setTimeout(() => reject(reason), ms));
161
+ return new Promise((_resolve, reject) => setTimeout(() => {
162
+ const r = typeof reason === 'function' ? reason() : reason;
163
+ Promise.resolve(r).catch(error => error).then(r => reject(r));
164
+ }, ms));
115
165
  }
116
166
  /**
117
167
  * Apply timeout to an operation, in case timeout happens, resolve to the result specified.
118
168
  * If timeout does not happen, the resolved result or rejection reason of the original operation would be returned.
169
+ * If timeout does not happen and result is a function, the function won't be called.
119
170
  * @param operation the original operation that timeout would be applied
120
171
  * @param ms number of milliseconds for the timeout
121
- * @param result the result to be resolved in case timeout happens
172
+ * @param result the result to be resolved in case timeout happens, or a function that supplies the reuslt.
122
173
  * @return the new Promise that resolves to the specified result in case timeout happens
123
174
  */
124
175
  static timeoutResolve(operation, ms, result) {
125
- return Promise.race([operation, PromiseUtils.delayedResolve(ms, result)]);
176
+ return Promise.race([
177
+ operation,
178
+ PromiseUtils.delayedResolve(ms, () => PromiseUtils.promiseState(operation)
179
+ .then(state => state === PromiseState.Pending ?
180
+ (typeof result === 'function' ? result() : result) :
181
+ {})),
182
+ ]);
126
183
  }
127
184
  /**
128
185
  * Apply timeout to an operation, in case timeout happens, reject with the reason specified.
129
186
  * If timeout does not happen, the resolved result or rejection reason of the original operation would be returned.
187
+ * If timeout does not happen and rejectReason is a function, the function won't be called.
130
188
  * @param operation the original operation that timeout would be applied
131
189
  * @param ms number of milliseconds for the timeout
132
- * @param rejectReason the reason of the rejection in case timeout happens
190
+ * @param rejectReason the reason of the rejection in case timeout happens, or a function that supplies the reason.
133
191
  * @return the new Promise that rejects with the specified reason in case timeout happens
134
192
  */
135
193
  static timeoutReject(operation, ms, rejectReason) {
136
- return Promise.race([operation, PromiseUtils.delayedReject(rejectReason, ms)]);
194
+ return Promise.race([
195
+ operation,
196
+ PromiseUtils.delayedReject(ms, () => PromiseUtils.promiseState(operation)
197
+ .then(state => state === PromiseState.Pending ?
198
+ (typeof rejectReason === 'function' ? rejectReason() : rejectReason) :
199
+ {})),
200
+ ]);
137
201
  }
138
202
  /**
139
203
  * Get the state of the Promise.
@@ -149,7 +213,8 @@ class PromiseUtils {
149
213
  /**
150
214
  * Equivalent of `synchronized` in Java.
151
215
  * In any situation there's no concurrent execution of any operation function associated with the same lock.
152
- * The operation function has access to the state (when `synchronized` is called), settledState (when the operation function is called), and result of the previous operation.
216
+ * The operation function has access to the state (when `synchronized` is called), settledState (when the operation function is called),
217
+ * and result (could be the fulfilled result or the rejected reason) of the previous operation.
153
218
  * In case there is no previous invocation, state, settledState and result would all be undefined.
154
219
  * @param lock the object (could be a string, a number, or `this` in a class) that is used to apply the lock
155
220
  * @param operation function for doing the computation and returning a Promise
@@ -164,9 +229,10 @@ class PromiseUtils {
164
229
  }
165
230
  switch (previousState) {
166
231
  case PromiseState.Pending: // concurrency
167
- resultPromise = previousResultPromise.then(result => operation(PromiseState.Pending, PromiseState.Fulfilled, result), reason => operation(PromiseState.Pending, PromiseState.Rejected, reason));
232
+ resultPromise = previousResultPromise.then(result => operation(PromiseState.Pending, PromiseState.Fulfilled, result), error => operation(PromiseState.Pending, PromiseState.Rejected, error));
168
233
  break;
169
234
  case undefined: // no concurrency and no history
235
+ // eslint-disable-next-line unicorn/no-useless-undefined
170
236
  resultPromise = operation(undefined, undefined, undefined);
171
237
  break;
172
238
  default: // no concurrency but with history
@@ -180,10 +246,12 @@ class PromiseUtils {
180
246
  exports.PromiseUtils = PromiseUtils;
181
247
  PromiseUtils.synchronizationLocks = new Map();
182
248
  exports.repeat = PromiseUtils.repeat;
249
+ exports.withRetry = PromiseUtils.withRetry;
183
250
  exports.inParallel = PromiseUtils.inParallel;
184
251
  exports.delayedResolve = PromiseUtils.delayedResolve;
185
252
  exports.delayedReject = PromiseUtils.delayedReject;
186
253
  exports.timeoutResolve = PromiseUtils.timeoutResolve;
187
254
  exports.timeoutReject = PromiseUtils.timeoutReject;
188
255
  exports.synchronized = PromiseUtils.synchronized;
256
+ exports.synchronised = PromiseUtils.synchronized;
189
257
  exports.promiseState = PromiseUtils.promiseState;
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@handy-common-utils/promise-utils",
3
- "version": "1.0.6",
3
+ "version": "1.2.0",
4
4
  "description": "Promise related utilities",
5
5
  "scripts": {
6
6
  "pretest": "eslint . --ext .ts",
7
- "test": "nyc mocha -r ts-node/register test/**/*.spec.ts",
7
+ "test": "nyc mocha",
8
8
  "prepare": "shx rm -rf dist && tsc && es-check",
9
9
  "preversion": "generate-api-docs-and-update-readme && git add README.md"
10
10
  },
@@ -15,10 +15,11 @@
15
15
  "main": "dist/promise-utils.js",
16
16
  "types": "dist/promise-utils.d.ts",
17
17
  "bin": {},
18
- "dependencies": {},
19
18
  "devDependencies": {
20
- "@handy-common-utils/dev-dependencies": "^1.0.11",
21
- "es-check": "^5.1.2"
19
+ "@handy-common-utils/dev-dependencies": "^1.0.21",
20
+ "@types/chai-as-promised": "^7.1.3",
21
+ "chai-as-promised": "^7.1.1",
22
+ "es-check": "^5.2.3"
22
23
  },
23
24
  "publishConfig": {
24
25
  "access": "public"