@wowistudio/grit 0.0.1
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/LICENSE +21 -0
- package/README.md +147 -0
- package/dist/builder.d.ts +24 -0
- package/dist/builder.js +62 -0
- package/dist/exceptions.d.ts +11 -0
- package/dist/exceptions.js +20 -0
- package/dist/grit.d.ts +19 -0
- package/dist/grit.js +129 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/types.d.ts +21 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.js +50 -0
- package/package.json +27 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jeroen Huisman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Grit
|
|
2
|
+
|
|
3
|
+
> ⚠️ **Work in Progress**: This library is currently under active development. The API may change and some features may be incomplete.
|
|
4
|
+
|
|
5
|
+
A fluent API for configurable retries with error filtering and backoff strategies (fixed, exponential, random jitter)
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Using npm
|
|
11
|
+
npm install @wowistudio/grit
|
|
12
|
+
# Using pnpm
|
|
13
|
+
pnpm add @wowistudio/grit
|
|
14
|
+
# Using yarn
|
|
15
|
+
yarn add @wowistudio/grit
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage Examples
|
|
19
|
+
|
|
20
|
+
### Retry
|
|
21
|
+
|
|
22
|
+
Retry an operation a specified numbers of times. The total attempts will be 1 + n retries.
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { Grit } from '@wowistudio/grit';
|
|
26
|
+
|
|
27
|
+
class ValidationError extends Error {}
|
|
28
|
+
class NetworkError extends Error {}
|
|
29
|
+
class TimeoutError extends Error {}
|
|
30
|
+
|
|
31
|
+
// Basic
|
|
32
|
+
const result = await Grit.retry(3)
|
|
33
|
+
.attempt(fetchData);
|
|
34
|
+
|
|
35
|
+
console.log(result)
|
|
36
|
+
|
|
37
|
+
// Basic with access to attempt count
|
|
38
|
+
await Grit.retry(3)
|
|
39
|
+
.attempt((attemptCount) => {
|
|
40
|
+
console.log('Attempt:', attemptCount)
|
|
41
|
+
return apiCall()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// Retry only specific errors
|
|
45
|
+
await Grit.retry(5)
|
|
46
|
+
.onlyErrors([NetworkError, TimeoutError])
|
|
47
|
+
.attempt(() => makeNetworkRequest);
|
|
48
|
+
|
|
49
|
+
// Do not retry specific errors
|
|
50
|
+
await Grit.retry(3)
|
|
51
|
+
.skipErrors([ValidationError])
|
|
52
|
+
.attempt(apiCall);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Delay
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { Grit } from '@wowistudio/grit';
|
|
59
|
+
|
|
60
|
+
// Delay with fixed intervals (in ms)
|
|
61
|
+
await Grit.retry(3)
|
|
62
|
+
.withDelay(1000)
|
|
63
|
+
.attempt(apiCall);
|
|
64
|
+
|
|
65
|
+
// Delay with exponential backoff
|
|
66
|
+
// This setup generates delay: [2000, 4000, 8000]
|
|
67
|
+
await Grit.retry(3)
|
|
68
|
+
.withDelay({
|
|
69
|
+
delay: 2000,
|
|
70
|
+
factor: 2
|
|
71
|
+
})
|
|
72
|
+
.attempt(apiCall);
|
|
73
|
+
|
|
74
|
+
// Delay with randomness
|
|
75
|
+
await Grit.retry(4)
|
|
76
|
+
.withDelay({
|
|
77
|
+
minDelay: 3000,
|
|
78
|
+
maxDelay: 4000,
|
|
79
|
+
})
|
|
80
|
+
.attempt(apiCall);
|
|
81
|
+
|
|
82
|
+
// Delay with exact delays for each retry
|
|
83
|
+
await Grit.retry(3)
|
|
84
|
+
.withDelay([500, 1000, 1500])
|
|
85
|
+
.attempt(apiCall);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Timeout
|
|
89
|
+
|
|
90
|
+
> ⚠️ **Note**: JavaScript timeouts don't actually stop the execution of the underlying operation. The timeout will reject/throw an error after the specified duration, but the original operation may continue running in the background.
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
import { Grit } from '@wowistudio/grit';
|
|
94
|
+
|
|
95
|
+
// Operation timeout after specified amount of time
|
|
96
|
+
await Grit.retry(3)
|
|
97
|
+
.withTimeout(5000)
|
|
98
|
+
.attempt(apiCall);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Fallback
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { Grit } from '@wowistudio/grit';
|
|
105
|
+
|
|
106
|
+
const fastCacheLookup = async () => {}
|
|
107
|
+
const slowerDbFetch = async () => {}
|
|
108
|
+
|
|
109
|
+
// Attempt with fallback
|
|
110
|
+
await Grit.retry(0)
|
|
111
|
+
.withTimeout(100)
|
|
112
|
+
.fallback(slowerDbFetch)
|
|
113
|
+
.attempt(fastCacheLookup)
|
|
114
|
+
|
|
115
|
+
// Attempt return type is typesafe and will be a union of return type .fallback & .attempt
|
|
116
|
+
const result: number | string = await Grit.retry(0)
|
|
117
|
+
.withTimeout(100)
|
|
118
|
+
.fallback(() => 1)
|
|
119
|
+
.attempt(() => 'a')
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
## Reusable Builder Instances
|
|
124
|
+
|
|
125
|
+
You can define a builder instance once and reuse it. Each call to `.attempt()` creates a new execution context, so retry state is independent between calls:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import { Grit } from '@wowistudio/grit';
|
|
129
|
+
|
|
130
|
+
// Define a shared builder configuration
|
|
131
|
+
const grit = Grit.retry(3)
|
|
132
|
+
.withDelay(2000)
|
|
133
|
+
.withTimeout(5000)
|
|
134
|
+
.onlyErrors([NetworkError])
|
|
135
|
+
.beforeRetry((retryCount) => {
|
|
136
|
+
console.log(`Retrying (attempt ${retryCount})...`);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Use the same builder for multiple different operations
|
|
140
|
+
const userData = await grit.attempt(fetchUserData);
|
|
141
|
+
const orderData = await grit.attempt(fetchOrderData);
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
This project is licensed under the MIT License.
|
|
147
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { GritError } from "./exceptions.js";
|
|
2
|
+
import type { DelayConfig, FunctionToExecute, TimeoutConfig } from "./types.js";
|
|
3
|
+
declare class GritBuilder<FallbackType = never> {
|
|
4
|
+
#private;
|
|
5
|
+
private retryCount?;
|
|
6
|
+
_onlyErrors: (typeof GritError)[];
|
|
7
|
+
_skipErrors: (typeof GritError)[];
|
|
8
|
+
fn: FunctionToExecute<any> | undefined;
|
|
9
|
+
$delay: DelayConfig | undefined;
|
|
10
|
+
$timeout: TimeoutConfig | undefined;
|
|
11
|
+
$fallback: FunctionToExecute<any> | undefined;
|
|
12
|
+
beforeRetryFn?: (retryCount: number) => void;
|
|
13
|
+
logging: boolean;
|
|
14
|
+
constructor(retryCount: number);
|
|
15
|
+
onlyErrors(errors: (typeof GritError)[]): this;
|
|
16
|
+
skipErrors(errors: (typeof GritError)[]): this;
|
|
17
|
+
withLogging(enabled?: boolean): this;
|
|
18
|
+
withDelay(config: DelayConfig): this;
|
|
19
|
+
withTimeout(timeout: TimeoutConfig): this;
|
|
20
|
+
beforeRetry(fn: (retryCount: number) => void): this;
|
|
21
|
+
withFallback<T>(fallback: FunctionToExecute<T>): GritBuilder<T>;
|
|
22
|
+
attempt<T = FallbackType>(fn: FunctionToExecute<T>): Promise<FallbackType extends never ? T : T | FallbackType>;
|
|
23
|
+
}
|
|
24
|
+
export { GritBuilder };
|
package/dist/builder.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
2
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
3
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
4
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
5
|
+
};
|
|
6
|
+
var _GritBuilder_instances, _GritBuilder_build;
|
|
7
|
+
import { Grit } from "./grit.js";
|
|
8
|
+
class GritBuilder {
|
|
9
|
+
constructor(retryCount) {
|
|
10
|
+
_GritBuilder_instances.add(this);
|
|
11
|
+
this._onlyErrors = [];
|
|
12
|
+
this._skipErrors = [];
|
|
13
|
+
this.fn = undefined;
|
|
14
|
+
this.logging = false;
|
|
15
|
+
this.retryCount = retryCount;
|
|
16
|
+
}
|
|
17
|
+
onlyErrors(errors) {
|
|
18
|
+
this._onlyErrors = errors;
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
skipErrors(errors) {
|
|
22
|
+
this._skipErrors = errors;
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
25
|
+
withLogging(enabled = true) {
|
|
26
|
+
this.logging = enabled;
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
withDelay(config) {
|
|
30
|
+
this.$delay = config;
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
withTimeout(timeout) {
|
|
34
|
+
this.$timeout = timeout;
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
beforeRetry(fn) {
|
|
38
|
+
this.beforeRetryFn = fn;
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
withFallback(fallback) {
|
|
42
|
+
const newBuilder = this;
|
|
43
|
+
newBuilder.$fallback = fallback;
|
|
44
|
+
return newBuilder;
|
|
45
|
+
}
|
|
46
|
+
attempt(fn) {
|
|
47
|
+
return __classPrivateFieldGet(this, _GritBuilder_instances, "m", _GritBuilder_build).call(this).attempt(fn);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
_GritBuilder_instances = new WeakSet(), _GritBuilder_build = function _GritBuilder_build() {
|
|
51
|
+
return new Grit({
|
|
52
|
+
retryCount: this.retryCount,
|
|
53
|
+
onlyErrors: this._onlyErrors,
|
|
54
|
+
skipErrors: this._skipErrors,
|
|
55
|
+
delay: this.$delay,
|
|
56
|
+
timeout: this.$timeout,
|
|
57
|
+
beforeRetryFn: this.beforeRetryFn,
|
|
58
|
+
logging: this.logging,
|
|
59
|
+
fallback: this.$fallback,
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
export { GritBuilder };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
declare class GritError extends Error {
|
|
2
|
+
constructor(message: string);
|
|
3
|
+
}
|
|
4
|
+
declare class MaxRetriesError extends Error {
|
|
5
|
+
constructor(message: string);
|
|
6
|
+
}
|
|
7
|
+
export declare class TimeoutError extends Error {
|
|
8
|
+
name: string;
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
11
|
+
export { GritError, MaxRetriesError };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class GritError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "GritError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
class MaxRetriesError extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "MaxRetriesError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class TimeoutError extends Error {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'TimeoutError';
|
|
17
|
+
this.name = "TimeoutError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export { GritError, MaxRetriesError };
|
package/dist/grit.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { GritBuilder } from "./builder.js";
|
|
2
|
+
import type { FunctionToExecute, GritProps } from "./types.js";
|
|
3
|
+
declare class Grit<T> {
|
|
4
|
+
#private;
|
|
5
|
+
private retryCount?;
|
|
6
|
+
private attempts;
|
|
7
|
+
private retries;
|
|
8
|
+
private onlyErrors;
|
|
9
|
+
private skipErrors;
|
|
10
|
+
private delay;
|
|
11
|
+
private fallback;
|
|
12
|
+
private timeout;
|
|
13
|
+
private beforeRetryFn?;
|
|
14
|
+
private logging;
|
|
15
|
+
constructor(props: GritProps);
|
|
16
|
+
attempt(fn: FunctionToExecute<T>): Promise<T>;
|
|
17
|
+
static retry(retryCount: number): GritBuilder<never>;
|
|
18
|
+
}
|
|
19
|
+
export { Grit };
|
package/dist/grit.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
11
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
12
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
13
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
14
|
+
};
|
|
15
|
+
var _Grit_instances, _Grit_currentDelay_get, _Grit_handleBeforeRetry, _Grit_backoff, _Grit_withTimeout, _Grit_processError, _Grit_execute;
|
|
16
|
+
import { GritBuilder } from "./builder.js";
|
|
17
|
+
import { GritError, TimeoutError } from "./exceptions.js";
|
|
18
|
+
import { isObject, validateGritConfig } from "./utils.js";
|
|
19
|
+
class Grit {
|
|
20
|
+
constructor(props) {
|
|
21
|
+
_Grit_instances.add(this);
|
|
22
|
+
this.attempts = 1;
|
|
23
|
+
this.retries = 0;
|
|
24
|
+
this.logging = false;
|
|
25
|
+
const { retryCount, fn, onlyErrors, skipErrors, delay, timeout, beforeRetryFn, logging, fallback } = props;
|
|
26
|
+
this.retryCount = retryCount;
|
|
27
|
+
this.onlyErrors = onlyErrors || [];
|
|
28
|
+
this.skipErrors = skipErrors || [];
|
|
29
|
+
this.delay = delay;
|
|
30
|
+
this.fallback = fallback;
|
|
31
|
+
this.timeout = timeout;
|
|
32
|
+
this.beforeRetryFn = beforeRetryFn;
|
|
33
|
+
this.logging = logging || false;
|
|
34
|
+
validateGritConfig(delay, retryCount);
|
|
35
|
+
}
|
|
36
|
+
attempt(fn) {
|
|
37
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
38
|
+
return __classPrivateFieldGet(this, _Grit_instances, "m", _Grit_execute).call(this, fn);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
static retry(retryCount) {
|
|
42
|
+
return new GritBuilder(retryCount);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
_Grit_instances = new WeakSet(), _Grit_currentDelay_get = function _Grit_currentDelay_get() {
|
|
46
|
+
let delay = undefined;
|
|
47
|
+
if (typeof this.delay === "number") {
|
|
48
|
+
delay = this.delay;
|
|
49
|
+
}
|
|
50
|
+
else if (Array.isArray(this.delay)) {
|
|
51
|
+
delay = this.delay.shift();
|
|
52
|
+
}
|
|
53
|
+
else if (isObject(this.delay) && "delay" in this.delay) {
|
|
54
|
+
const { factor, delay: initialDelay } = this.delay;
|
|
55
|
+
delay = initialDelay * Math.pow(factor || 1, this.attempts - 2);
|
|
56
|
+
}
|
|
57
|
+
else if (isObject(this.delay) && "minDelay" in this.delay) {
|
|
58
|
+
const { minDelay, maxDelay, factor } = this.delay;
|
|
59
|
+
const randomDelay = Math.random() * (maxDelay - minDelay) + minDelay;
|
|
60
|
+
delay = factor ? randomDelay * Math.pow(factor, this.attempts - 2) : randomDelay;
|
|
61
|
+
}
|
|
62
|
+
if (!delay)
|
|
63
|
+
throw new GritError("No delay. Should never happen.");
|
|
64
|
+
if (this.logging)
|
|
65
|
+
console.debug("Delaying for", delay, "ms");
|
|
66
|
+
return delay;
|
|
67
|
+
}, _Grit_handleBeforeRetry = function _Grit_handleBeforeRetry() {
|
|
68
|
+
if (this.retries > 0 && this.beforeRetryFn)
|
|
69
|
+
this.beforeRetryFn(this.retries);
|
|
70
|
+
}, _Grit_backoff = function _Grit_backoff() {
|
|
71
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
72
|
+
return new Promise((resolve) => setTimeout(resolve, __classPrivateFieldGet(this, _Grit_instances, "a", _Grit_currentDelay_get)));
|
|
73
|
+
});
|
|
74
|
+
}, _Grit_withTimeout = function _Grit_withTimeout(promise) {
|
|
75
|
+
let timer = undefined;
|
|
76
|
+
const wrappedPromise = new Promise((resolve, reject) => {
|
|
77
|
+
// Use .then() instead of async IIFE to preserve stack traces
|
|
78
|
+
// eslint-disable-next-line promise/prefer-await-to-then, promise/prefer-catch
|
|
79
|
+
promise.then(resolve, reject);
|
|
80
|
+
if (this.timeout === Number.POSITIVE_INFINITY)
|
|
81
|
+
return;
|
|
82
|
+
// We create the error outside of `setTimeout` to preserve the stack trace.
|
|
83
|
+
const message = `Promise timed out after ${this.timeout} milliseconds`;
|
|
84
|
+
const timeoutError = new TimeoutError(message);
|
|
85
|
+
timer = setTimeout(() => {
|
|
86
|
+
reject(timeoutError);
|
|
87
|
+
}, this.timeout);
|
|
88
|
+
});
|
|
89
|
+
// eslint-disable-next-line promise/prefer-await-to-then
|
|
90
|
+
wrappedPromise.clear = () => {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
timer = undefined;
|
|
93
|
+
};
|
|
94
|
+
const cancelablePromise = wrappedPromise.finally(() => {
|
|
95
|
+
cancelablePromise.clear();
|
|
96
|
+
});
|
|
97
|
+
return cancelablePromise;
|
|
98
|
+
}, _Grit_processError = function _Grit_processError(error) {
|
|
99
|
+
if (error && typeof error === 'object' && 'constructor' in error) {
|
|
100
|
+
if (this.skipErrors.length > 0 && this.skipErrors.includes(error.constructor))
|
|
101
|
+
throw error;
|
|
102
|
+
if (this.onlyErrors.length > 0 && !this.onlyErrors.includes(error.constructor))
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
this.attempts++;
|
|
106
|
+
this.retries++;
|
|
107
|
+
}, _Grit_execute = function _Grit_execute(fn) {
|
|
108
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
109
|
+
try {
|
|
110
|
+
__classPrivateFieldGet(this, _Grit_instances, "m", _Grit_handleBeforeRetry).call(this);
|
|
111
|
+
console.log('this.timeout', this.timeout);
|
|
112
|
+
if (this.timeout)
|
|
113
|
+
return yield __classPrivateFieldGet(this, _Grit_instances, "m", _Grit_withTimeout).call(this, Promise.resolve(fn(this.attempts)));
|
|
114
|
+
return yield fn(this.attempts);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (this.retries >= this.retryCount) {
|
|
118
|
+
if (this.fallback)
|
|
119
|
+
return this.fallback(this.attempts);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
__classPrivateFieldGet(this, _Grit_instances, "m", _Grit_processError).call(this, error);
|
|
123
|
+
if (this.delay)
|
|
124
|
+
return __classPrivateFieldGet(this, _Grit_instances, "m", _Grit_backoff).call(this).then(() => __classPrivateFieldGet(this, _Grit_instances, "m", _Grit_execute).call(this, fn));
|
|
125
|
+
return yield __classPrivateFieldGet(this, _Grit_instances, "m", _Grit_execute).call(this, fn);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
export { Grit };
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type GritProps = {
|
|
2
|
+
retryCount?: number;
|
|
3
|
+
fn?: FunctionToExecute<any>;
|
|
4
|
+
fallback?: FunctionToExecute<any>;
|
|
5
|
+
onlyErrors?: (typeof Error)[];
|
|
6
|
+
skipErrors?: (typeof Error)[];
|
|
7
|
+
delay?: DelayConfig;
|
|
8
|
+
timeout?: TimeoutConfig;
|
|
9
|
+
beforeRetryFn?: (retryCount: number) => void;
|
|
10
|
+
logging?: boolean;
|
|
11
|
+
};
|
|
12
|
+
export type TimeoutConfig = number;
|
|
13
|
+
export type DelayConfig = number | number[] | {
|
|
14
|
+
factor?: number;
|
|
15
|
+
delay: number;
|
|
16
|
+
} | {
|
|
17
|
+
factor?: number;
|
|
18
|
+
minDelay: number;
|
|
19
|
+
maxDelay: number;
|
|
20
|
+
};
|
|
21
|
+
export type FunctionToExecute<T> = (attempts?: number) => (Promise<T> | T);
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { DelayConfig } from "./types.js";
|
|
2
|
+
export declare const isObject: (o: any) => o is Record<string, any>;
|
|
3
|
+
export declare const isPromise: <T>(value: any) => value is Promise<T>;
|
|
4
|
+
export declare const validateGritConfig: (config: DelayConfig | undefined, retryCount: number | undefined) => void;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { GritError } from "./exceptions.js";
|
|
2
|
+
export const isObject = (o) => {
|
|
3
|
+
return typeof o === 'object' && !Array.isArray(o) && o !== null;
|
|
4
|
+
};
|
|
5
|
+
export const isPromise = (value) => {
|
|
6
|
+
return value !== null && typeof value === "object" && typeof value.then === "function";
|
|
7
|
+
};
|
|
8
|
+
export const validateGritConfig = (config, retryCount) => {
|
|
9
|
+
if (!config)
|
|
10
|
+
return;
|
|
11
|
+
if (!retryCount)
|
|
12
|
+
throw new GritError("Missing retry config (Grit.retry(<count>))");
|
|
13
|
+
// validate array config
|
|
14
|
+
if (Array.isArray(config)) {
|
|
15
|
+
if (config.length !== retryCount)
|
|
16
|
+
throw new GritError("Delay array length must be equal to retry count");
|
|
17
|
+
for (const delay of config) {
|
|
18
|
+
if (delay <= 0)
|
|
19
|
+
throw new GritError("delay must be greater than 0");
|
|
20
|
+
}
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (typeof config === "number") {
|
|
24
|
+
if (config <= 0)
|
|
25
|
+
throw new GritError("delay must be greater than 0");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (!isObject(config))
|
|
29
|
+
throw new GritError("Invalid backoff config");
|
|
30
|
+
// validate delay config
|
|
31
|
+
if ("delay" in config) {
|
|
32
|
+
if (typeof config.delay !== "number")
|
|
33
|
+
throw new GritError("delay must be a number");
|
|
34
|
+
if (config.delay <= 0)
|
|
35
|
+
throw new GritError("delay must be greater than 0");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
// validate random delay config
|
|
39
|
+
const { minDelay, maxDelay } = config;
|
|
40
|
+
if (!minDelay || typeof minDelay !== "number")
|
|
41
|
+
throw new GritError("minDelay must be a number");
|
|
42
|
+
if (!maxDelay || typeof maxDelay !== "number")
|
|
43
|
+
throw new GritError("maxDelay must be a number");
|
|
44
|
+
if (minDelay >= maxDelay)
|
|
45
|
+
throw new GritError("minDelay must be less than maxDelay");
|
|
46
|
+
if (minDelay <= 0)
|
|
47
|
+
throw new GritError("minDelay must be greater than 0");
|
|
48
|
+
if (maxDelay <= 0)
|
|
49
|
+
throw new GritError("maxDelay must be greater than 0");
|
|
50
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wowistudio/grit",
|
|
3
|
+
"description": "A library for retry.",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"files": [
|
|
7
|
+
"/dist"
|
|
8
|
+
],
|
|
9
|
+
"version": "0.0.1",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./src/index.ts",
|
|
14
|
+
"default": "./src/index.ts"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"type": "module",
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:watch": "vitest"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"vitest": "^2.1.8",
|
|
25
|
+
"@types/node": "^20.9.0"
|
|
26
|
+
}
|
|
27
|
+
}
|